# Module feyn

```
Feyn is the main Python module to build and execute models that utilizes a QLattice.
A QLattice (short for Quantum Lattice) is a device which can be used to generate and explore a vast number of models linking a set of input observations to an output prediction. The actual QLattice runs on a dedicated computing cluster which is operated by Abzu. The `feyn.QLattice` class provides a client interface to communcate with, extract models from, and update the QLattice.
The QLattice stores and updates probabilistic information about the mathematical relationships (models) between observable quantities.
The workflow is typically:
# Connect to the QLattice
>>> ql = feyn.QLattice()
# Extract a QGraph
>>> qgraph = gl.get_qgraph(data.columns, output="out")
# Fit the QGraph to a local dataset
>>> qgraph.fit(data)
# Pick the best Graph from the QGraph
>>> graph = qgraph[0]
# Possibly update the QLattice with the graph to make the QLattice better at this kind of model in the future
>>> ql.update(graph)
# Or use the graph to make predictions
>>> predicted_y = graph.predict(new_data)
```

*class* QLattice

```
def __init__(
url: str = None,
api_token: str = None,
config: str = None
) -> QLattice
```

```
A QLattice (short for Quantum Lattice) is a device which can be used to generate and explore a vast number of models linking a set of input observations to an output prediction.
The QLattice stores and updates probabilistic information about the mathematical relationships (models) between observable quantities.
The actual QLattice runs on a dedicated computing cluster which is operated by Abzu. The `feyn.QLattice` class provides a client interface to communicate with, extract models from, and update the QLattice.
Most interactions with the QLattice takes place through the QGraph object which represents an infinite list of graphs (or models) for a specific problem.
So, while a QLattice is able to generate any graph linking any input to any output, a QGraph merely contains the graphs that relate a specified set of inputs to a specified output.
The workflow is typically:
1) extract a QGraph from the QLattice using `QLattice.get_qgraph`.
2) fit the QGraph to a local dataset using `QGraph.fit`.
3) choose one or more models from the QGraph using `QGraph[index]`.
4) potentially update the QLattice with new knowledge using `QLattice.update`.
Arguments:
url -- URL of your QLattice. (Should not to be used in combination with the config parameter).
api_token -- Authentication token for the communicating with this QLattice. (Should not to be used in combination with the config parameter).
config -- The configuration setting in your feyn.ini or .feynrc file to load the url and api_token from. These files should be located in your home folder.
```

*property* QLattice.registers

```
registers
```

```
The RegisterCollection of the QLattice
The RegisterCollection is used to find, create and remove registers from the QLattice.
```

*property* QLattice.snapshots

```
snapshots
```

```
Collection of snapshots for this QLattice
Use this collection to capture, list and restore the complete state of a QLattice.
```

*method* QLattice.get_qgraph

```
def get_qgraph(
self,
registers: List[str],
output: str,
stypes: Dict[str, str] = {},
max_depth: int = 5
)
```

```
Extract QGraph from inputs registers to an output register.
The standard workflow for QLattices is to extract a QGraph using this function. You specify a list of input registers corresponding to the input values you want to use for predictions, and a single output register corresponding to the output variable you want to predict.
Once the QGraph has been extracted from the QLattice, you'll typically use the `QGraph.fit()` function to fit the QGraph to an actual dataset.
Arguments:
registers -- List of register names to use in the QGraph.
output -- The output register name.
stypes -- An optional map from register names to semantic types.
max_depth -- The maximum depth of the graphs.
Returns:
QGraph -- The QGraph instance from the inputs to the output.
```

*method* QLattice.reset

```
def reset(
self
) -> None
```

```
Clear all learnings in this QLattice.
This is a potentially dangerous action. Think twice before calling this method.
```

*method* QLattice.update

```
def update(
self,
graph: feyn._graph.Graph
) -> None
```

```
Update QLattice with learnings from a `Graph`.
When updated, the QLattice learns to produce better QGraphs. This is how a QLattice evolves and narrows in on producing QGraphs with better and better models.
Without updating, the QLattice will not learn about good models and the QGraphs produced from the QLattice will not contain better models.
# Pick the best Graph from the QGraph and update the QLattice with it's learnings
>>> graph = qgraph[0]
>>> ql.update(graph)
Arguments:
graph -- Graph with learnings worth storing.
```

*class* QGraph

```
def __init__(
qlattice,
specs: List[str],
registers: Dict[str, str]
) -> QGraph
```

```
A QGraph is extracted from the QLattice with the `QLattice.get_qgraph()` method. It represents infinite list of graphs linking a set of input registers to an output register.
One way to think about a QGraph is as a partially ordered list of all possible graphs complying with some constraints. The head of the list contains the best graphs found for solving a problem at any given time.
A QGraph can be gradually sorted by fitting it to one or more datasets. Fitting is done using one of two methods, the `fit` method which searches further and further into the infinite list for better graphs, and the `sort` method which merely re-sorts the head of the list with new conditions.
Since the list is infinite, the sorting can never actually finish, but using the QLattice simulator, the QGraph can be extremely smart about how and where in the infinite list to search for better graphs.
It is possible to limit or constrain which graphs belong in the infinite list in several ways, most importantly using the `filter` method.
The constructor is for internal use. QGraphs should always be generated with the `QLattice.get_graph` method.
Arguments:
graph_dict -- A dictionary containing the QGraph descriptor.
```

*method* QGraph.copy

```
def copy(
self
)
```

```
Make a shallow copy of the QGraph.
Returns:
QGraph -- The copied QGraph.
```

*method* QGraph.filter

```
def filter(
self,
qfilter: feyn.filters.QGraphFilter
)
```

```
Create a new filtered QGraph which represents all graphs matching the specified filter.
A QGraph is an infinite list of graphs complying with some constraints. These constraints are specified as filters on the QGraph itself. Notice that when filtering an infinite list, the result is still infinite!
The search for graphs can be guided or limited by filtering a QGraph on for example depth, the number of edges, or required input features.
Arguments:
qfilter {feyn.filters.QGraphFilter} -- The filter to apply.
Raises:
ValueError: Raised when the qfilter could not be understood.
Returns:
QGraph -- The filtered QGraph
```

*method* QGraph.fit

```
def fit(
self,
data,
n_samples: Union[int, NoneType] = None,
loss_function='mean_squared_error',
show: Union[str, Callable[[feyn._graph.Graph, float], NoneType], NoneType] = 'graph',
threads: int = 1
) -> None
```

```
Fit `QGraph` with given data set. After the fitting, the head of the QGraph will be sorted by increasing loss.
Each call will further explore the infinite list of graphs, and may find better graphs that are then brought forward to their correct sorting order. Sometimes this leads to a new best graph making it to the first position in the list.
A call to fit() is comparable to an epoch in other frameworks. Every call will potentially lead to a new best graph being found. Since the list is infinite, there is always the possibility that a new superior graph is found, but in practice between 5 and 100 calls are usually sufficient.
The n_samples parameter controls how much each graph in the QGraph is trained during the process. The default behavior is to train each graph once with each sample in the dataset, unless the set is smaller than 10000, in which case the dataset will be upsampled to 10000 samples before fitting.
The samples are shuffled randomly before fitting to avoid issues with the Stochastic Gradient Descent algorithm.
The recommended workflow for fitting a QGraph involves several steps in a loop:
> for epoch in range(10):
> qgraph.fit(train) # fit the data to the training set
> qgraph.sort(validation) # sort it again on the validation set
> best = qgraph[0] # get the best performing graph on the validation set
> qlattice.update(best)
Arguments:
data -- Training data including both input and expected values. Can be either a dict mapping register names to value arrays, or a pandas.DataFrame.
n_samples -- Number of samples to fit on. The samples will be taken from `data`, possibly over- or undersampling the data. Passing `None` means use every sample in the dataset or 10000 samples, whichever is larger.
loss_function -- Name of the loss function or the function itself. This is the loss function to use for fitting. Can either be a string or one of the functions provided in `feyn.losses`.
show -- Controls status display. If specified, it should be either "graph", "text", or a user-defined callback function which receives the best graph found so far, along with it's loss.
threads -- Number of concurrent threads to use for fitting. Choose this number to match the number of CPU cores on your machine.
Returns:
QGraph -- The QGraph itself.
```

*method* QGraph.head

```
def head(
self,
n=5
)
```

```
Show the first `n` Graphs in the QGraph.
This only works in an interactive environment like Jupyter Notebook
Keyword Arguments:
n {int} -- Number of graphs to show. (default: {5})
```

*method* QGraph.sort

```
def sort(
self,
data,
loss_function='mean_squared_error',
threads: int = 1
)
```

```
Sort the head of the QGraph by loss of each graph against the provided data set. The difference between QGraph.fit and QGraph.sort is that while the fit function will bring completely new graphs to the head of the QGraph, the sort function will only sort the graphs that are already in the head.
# Fit and sort a graph from a qgraph
>>> qgraph.fit(training_data, loss_function=feyn.losses.mean_squared_error)
>>> qgraph.sort(some_other_data, loss_function=feyn.losses.mean_absolute_error)
# After this, the first graph in the QGraph will be the best performing graph on "some_other_data"
>>> best = qgraph[0]
Arguments:
data -- The data set to sort by. Can be either a dict mapping register names to value arrays, or a pandas.DataFrame.
loss_function -- The loss function to use when sorting graphs. Can either be a string or one of the functions provided in `feyn.losses`.
threads -- Number of concurrent threads to use for sorting. Choose this number to match the number of CPU cores on your machine.
Returns:
QGraph -- The QGraph itself
```

*class* Graph

```
def __init__(
size: int
) -> Graph
```

```
A Graph represents a single mathematical model which can be used used for predicting.
The constructor is for internal use. You will typically use `QGraph[ix]` to pick graphs from QGraphs, or load them from a file with Graph.load().
Arguments:
size -- The number of nodes this graph contains. The actual nodes must be added to the graph after construction.
```

*property* Graph.edge_count

```
edge_count
```

```
Get the total number of edges in this graph.
```

*property* Graph.depth

```
depth
```

```
Get the depth of the graph
```

*property* Graph.edges

```
edges
```

```
Get the total number of edges in this graph.
```

*static method* Graph.load

```
def load(
file: Union[~AnyStr, pathlib.Path, TextIO]
) -> object
```

```
Load a `Graph` from a file.
Usually used together with `Graph.save`.
Arguments:
file -- A file-like object or a path to load the `Graph` from.
Returns:
Graph -- The loaded `Graph`-object.
```

*method* Graph.predict

```
def predict(
self,
X
) -> numpy.ndarray
```

```
Calculate predictions based on input values.
>>> graph.predict({ "age": [34, 78], "sex": ["male", "female"] })
[True, False]
Arguments:
X -- The input values. Can be either a dict mapping register names to value arrays, or a pandas.DataFrame.
Returns:
np.ndarray -- The calculated predictions.
```

*method* Graph.save

```
def save(
self,
file: Union[~AnyStr, pathlib.Path, TextIO]
) -> None
```

```
Save the `Graph` to a file-like object.
The file can later be used to recreate the `Graph` with `Graph.load`.
Arguments:
file -- A file-like object or path to save the graph to.
```

*class* Interaction

```
def __init__(
/,
*args,
**kwargs
) -> Interaction
```

```
A graph is a collection of Interactions linked together. Interactions can be either registers or pure mathematical functions such as 'tanh' or 'multiply'.
You normally get an interaction by accessing it from a Graph with the index operation. Interactions can be inspected in various ways to examine the behavior and properties of the graph.
Example usage. Get a Graph from a QGraph and print information about it's first interaction
>>> best_graph = qgraph.select(data)[0]
>>> interaction = best[0]
>>> print(interaction.name, interaction.type)
x, fixed
```

*property* Interaction.activation

```
activation
```

```
Get the latest activation(s) from this interaction.
This is the most recent value emitted by this interaction after each use - either fit or predict.
This property is useful to examine the graph's processing of individual samples.
>>> graph.predict({'x': [3,4]})
>>> graph[0].activation
.54
```

*property* Interaction.depth

```
depth
```

```
Get the depth of this interaction within it's graph.
```

*property* Interaction.name

```
name
```

```
Get the name of the interaction.
For register interactions this is the name of the register. For other interactions, it will normally be the same as the interaction type.
```

*property* Interaction.sources

```
sources
```

```
Get the indices of the predecessors of this interaction within the graph.
```

*property* Interaction.spec

```
spec
```

```
Get the spec string of the interaction, such as 'cell:tanh(i)->i', etc.
```

*property* Interaction.state

```
state
```

```
Get internal state of interaction, such as the weigths or other parameters.
The actual properties of the state depends on the interaction. You may examine them using dir() or though the InteractionState.__dict__ property.
Example:
>>> g = qgraph.select(data)[0]
>>> print(g[0].state.__dict__)
{'feature_min': -4.996334075927734, 'feature_max': 4.994193077087402, 'auto_adapt': True}
Or you can modify them dynamically:
>>> g[0].state.auto_adapt=False
```

*class* Snapshot

```
def __init__(
id: str,
when: datetime.datetime,
note: str
) -> Snapshot
```

```
A reference to a snapshot of a QLattice.
The constructor is for internal use. You will normally access snapshots from the `SnapshotCollection` on a QLattice:
>>> snapshot = qlattice.snapshots.capture("my snapshot")
>>> for snapshot in qlattice.snapshots:
>>> print(snapshot.id)
```

*property* Snapshot.id

```
id
```

```
A unique identifier for this snapshot
```

*property* Snapshot.note

```
note
```

```
An optional note provided by the user when the snapshot was taken
```

*property* Snapshot.when

```
when
```

```
A timestamp for when this snapshot was taken
```

*class* SnapshotCollection

```
def __init__(
qlattice
) -> SnapshotCollection
```

```
The SnapshotCollection is used to capture, list and restore the state of a QLattice.
Note: The actual snapshot is stored on the server side, so this collection manipulates references to these snapshots, including metadata about the snapshot.
Use this collection to capture snapshots of the state of the QLattice:
>>> snap = ql.snapshots.capture("Important lattice")
And restore it again later:
>>> ql.snapshots.restore(snap)
It is also possible to treat the snapshot collection as a list:
>>> for snap in ql.snapshots:
>>> print(snapshot.id, snapshot.when, snapshot.note)
1587645521.535264 2020-04-21 11:28:31.535264+02:00 Some lattice
1587645521.623532 2020-04-23 14:38:41.623532+02:00 Important lattice
```

*method* SnapshotCollection.capture

```
def capture(
self,
note: str
) -> feyn._snapshots.Snapshot
```

```
Capture a snapshot of this QLattice.
Arguments:
note -- A note which is stored along with the snapshot.
```

*method* SnapshotCollection.restore

```
def restore(
self,
snapshot: Union[str, feyn._snapshots.Snapshot]
)
```

```
Restore the QLattice to the state of a specific snapshot.
Arguments:
-- The snapshot te restore. Either a snapshot returned from snapshots.capture() or a string id.
```