(pdg_sim)=
```{eval-rst}
.. currentmodule:: MRzeroCore
```

# PDG Simulation

Phase Distribution Graphs split the magnetization into multiple states, forming a Graph over the duration of the sequence. More details can be found in the PDG Paper, once published.

The simulation runs in two passes:
- The pre-pass simulates a large number of states, but only roughly estimates the signal. This information is used to assess which states are important for the signal, and which can be skipped because they don't contribute anything.
- The main-pass uses the exact signal equation and takes all voxels into account, but simulates as few states as possible. It does so by using the information generated by the pre-pass.

The precision / speed tradeoff for both passes can be chosen as follows:

```python
# Default parameters: max 200 states per repetition
# with at leat 0.0001 of maximum possible magnetization
graph = mr0.compute_graph(seq, data, 200, 1e-4)
# Minimum emitted and latent signal of 0.01 (compared per repetition to strongest state)
signal = mr0.execute_graph(graph, seq, data, 0.01, 0.01)
```

The pre-pass limits the state count by both the maximum number (higher = more states / slower + more precise simulaiton) of states and the minimum magnetization (lower = more states). In practice, it turns out that the second parameter is less surprising in how it influences the quality of the simulation, which means that the first parameter should be set to a high number and the second parameter should be increased if the pre-pass is too slow (usually the main-pass is more likely to be the bottleneck).

The main-pass has two parameters which are used as threshold to determine which states are used in the signal equation and which states are simulated. Usually they are both set to the same number. Lower numbers mean that more states are above the thresold and will be simulated. It is not useful to set `min_emitted_signal` to a value lower than `min_latent_signal`, as this is trying to include to states into the signal equation that are not simulated.

Examples:
```python
# Very precise simulation
graph = mr0.compute_graph(seq, data, 10000, 1e-7)
signal = mr0.execute_graph(graph, seq, data, 1e-7, 1e-7)

# Average precision
graph = mr0.compute_graph(seq, data, 10000, 1e-4)
signal = mr0.execute_graph(graph, seq, data, 1e-3, 1e-4)

# FID only
graph = mr0.compute_graph(seq, data, 10000, 1e-3)
signal = mr0.execute_graph(graph, seq, data, 1, 0.1)
```

Optimal settings can always depend on the sequence itself. Some sequences are more demanding and require a very precise simulation, some are already decently described by only a few states.

(pre_pass)=
## Pre-Pass

Two functions are provided: {func}`compute_graph` and {func}`compute_graph_ext`. They are both wrappers around the actual prepass, which is written in rust. {func}`compute_graph` computes average values for $T_1$, $T_2$, $T_2'$ and $D$ and then calls {func}`compute_graph_ext`.

```{eval-rst}
.. autofunction:: compute_graph

.. autofunction:: compute_graph_ext
```

## Phase Distribution Graph

```{eval-rst}
.. autoclass:: MRzeroCore.simulation.pre_pass.Graph
    :members:
```

(main_pass)=
## Main-Pass

Takes the {class}`Sequence`, the {class}`SimData` and the {class} `Graph` produced by both in the [pre-pass](pre_pass) in order to calculate the measured ADC signal. Because of the work done by the [pre-pass](pre_pass), only the minimal work needed in order to achieve the desired precision is executed. This precision can be tuned by the {attr}`min_emitted_signal` and {attr}`min_latent_signal` thresholds. Higher values lead to less states being simulated, which improves speed and reduces accuracy. A value of 1 will mean that 2 states will be simulated (z0 and one +), resulting in the FID signal. A value of 0 means that everything will be simulated that somehow contributes to the signal.

```{eval-rst}
.. autofunction:: execute_graph
```
