# Graph { #phasic.Graph }

```python
phasic.Graph(
    state_length=None,
    callback=None,
    parameterized=False,
    **kwargs,
)
```



## Methods

| Name | Description |
| --- | --- |
| [as_matrices](#phasic.Graph.as_matrices) | Convert the graph to its matrix representation. |
| [copy](#phasic.Graph.copy) | Returns a deep copy of the graph. |
| [discretize](#phasic.Graph.discretize) | Creates a graph for a discrete distribution from a continuous one. |
| [dph_pmf_batch](#phasic.Graph.dph_pmf_batch) | Compute discrete phase-type PMF at multiple jump counts with automatic parallelization. |
| [eliminate_to_dag](#phasic.Graph.eliminate_to_dag) | Perform symbolic graph elimination to create a reusable DAG structure. |
| [from_matrices](#phasic.Graph.from_matrices) | Construct a Graph from matrix representation. |
| [make_discrete](#phasic.Graph.make_discrete) | Takes a graph for a continuous distribution and turns |
| [moments_batch](#phasic.Graph.moments_batch) | Compute moments for multiple powers with automatic parallelization. |
| [moments_from_graph](#phasic.Graph.moments_from_graph) | Convert a parameterized Graph to a JAX-compatible function that computes moments. |
| [pdf_batch](#phasic.Graph.pdf_batch) | Compute PDF at multiple time points with automatic parallelization. |
| [plot](#phasic.Graph.plot) | Plots the graph using graphviz. See plot::plot_graph.py for more details. |
| [pmf_and_moments_from_graph](#phasic.Graph.pmf_and_moments_from_graph) | Convert a parameterized Graph to a function that computes both PMF/PDF and moments. |
| [pmf_from_cpp](#phasic.Graph.pmf_from_cpp) | Load a phase-type model from a user's C++ file and return a JAX-compatible function. |
| [pmf_from_graph](#phasic.Graph.pmf_from_graph) | Convert a Python-built Graph to a JAX-compatible function with full gradient support. |
| [pmf_from_graph_parameterized](#phasic.Graph.pmf_from_graph_parameterized) | Convert a parameterized Python graph builder to a JAX-compatible function. |
| [serialize](#phasic.Graph.serialize) | Serialize graph to array representation for efficient computation. |
| [svgd](#phasic.Graph.svgd) | Run Stein Variational Gradient Descent (SVGD) inference for Bayesian parameter estimation. |

### as_matrices { #phasic.Graph.as_matrices }

```python
phasic.Graph.as_matrices()
```

Convert the graph to its matrix representation.

Returns a NamedTuple containing the traditional phase-type distribution
matrices and associated information.

#### Returns {.doc-section .doc-section-returns}

<code>[]{.parameter-name} [:]{.parameter-annotation-sep} [[MatrixRepresentation](`phasic.MatrixRepresentation`)]{.parameter-annotation}</code>

:   NamedTuple with the following attributes: - states: np.ndarray of shape (n_states, state_dim), dtype=int32     State vector for each vertex - sim: np.ndarray of shape (n_states, n_states), dtype=float64     Sub-intensity matrix - ipv: np.ndarray of shape (n_states,), dtype=float64     Initial probability vector - indices: np.ndarray of shape (n_states,), dtype=int32     1-based indices for vertices (for use with vertex_at())

#### Examples {.doc-section .doc-section-examples}

```python
>>> g = Graph(1)
>>> start = g.starting_vertex()
>>> v1 = g.find_or_create_vertex([1])
>>> v2 = g.find_or_create_vertex([2])
>>> start.add_edge(v1, 1.0)
>>> v1.add_edge(v2, 2.0)
>>> g.normalize()
>>>
>>> matrices = g.as_matrices()
>>> print(matrices.sim)  # Sub-intensity matrix (attribute access)
>>> print(matrices.ipv)  # Initial probability vector
>>> # Can also use index access like a tuple
>>> states, sim, ipv, indices = matrices
```

### copy { #phasic.Graph.copy }

```python
phasic.Graph.copy()
```

Returns a deep copy of the graph.

### discretize { #phasic.Graph.discretize }

```python
phasic.Graph.discretize(reward_rate, skip_states=[], skip_slots=[])
```

Creates a graph for a discrete distribution from a continuous one.

        Creates a graph augmented with auxiliary vertices and edges to represent the discrete distribution. 

####         Parameters {.doc-section .doc-section---------parameters}

        reward_rate : 
            Rate of discrete events.
        skip_states : 
            Vertex indices to not add auxiliary states to, by default []
        skip_slots : 
            State vector indices to not add rewards to, by default []

####         Returns {.doc-section .doc-section---------returns}

        :
            A new graph and a matrix of rewards for computing marginal moments.

####         Examples {.doc-section .doc-section---------examples}


        >>> from phasic import Graph
        >>> def callback(state):
        ...     return [(state[0] + 1, [(state[0], 1)])]
        >>> g = Graph(callback)
        >>> g.discretize(0.1)
        >>> a = [1, 2, 3]
        >>> print([x + 3 for x in a])
        [4, 5, 6]
        >>> print("a
b")
        a
        b

### dph_pmf_batch { #phasic.Graph.dph_pmf_batch }

```python
phasic.Graph.dph_pmf_batch(jumps)
```

Compute discrete phase-type PMF at multiple jump counts with automatic parallelization.

Automatically uses pmap/vmap based on parallel configuration and batch size.
For single values, use the standard dph_pmf() method instead (no overhead).

#### Parameters {.doc-section .doc-section-parameters}

<code>[**jumps**]{.parameter-name} [:]{.parameter-annotation-sep} [[array_like](`array_like`)]{.parameter-annotation}</code>

:   Array of jump counts (integers) to evaluate PMF at

#### Returns {.doc-section .doc-section-returns}

<code>[]{.parameter-name} [:]{.parameter-annotation-sep} [[np](`numpy`).[ndarray](`numpy.ndarray`)]{.parameter-annotation}</code>

:   PMF values at each jump count

#### Examples {.doc-section .doc-section-examples}

```python
>>> import phasic as pta
>>> import numpy as np
>>>
>>> # Initialize parallel computing
>>> config = pta.init_parallel()
>>>
>>> # Build and discretize a model
>>> g = pta.Graph(1)
>>> # ... build model ...
>>> g_discrete, rewards = g.discretize(reward_rate=0.1)
>>> g_discrete.normalize()
>>>
>>> # Compute PMF at many jump counts (automatically parallelized)
>>> jumps = np.arange(0, 100)
>>> pmf_values = g_discrete.dph_pmf_batch(jumps)
```

#### Notes {.doc-section .doc-section-notes}

- Requires a discrete phase-type model (use discretize() first)
- Automatically parallelizes based on init_parallel() configuration

### eliminate_to_dag { #phasic.Graph.eliminate_to_dag }

```python
phasic.Graph.eliminate_to_dag()
```

Perform symbolic graph elimination to create a reusable DAG structure.

This method performs the O(n³) graph elimination algorithm ONCE and
returns a symbolic DAG where edges contain expression trees instead
of concrete values. The DAG can then be instantiated with different
parameters in O(n) time each.

This is the key optimization for SVGD and other inference methods
that require evaluating the same graph structure with many different
parameter vectors.

#### Returns {.doc-section .doc-section-returns}

<code>[]{.parameter-name} [:]{.parameter-annotation-sep} [[SymbolicDAG](`phasic.SymbolicDAG`)]{.parameter-annotation}</code>

:   Symbolic DAG that can be instantiated with parameters

#### Raises {.doc-section .doc-section-raises}

<code>[:]{.parameter-annotation-sep} [[RuntimeError](`RuntimeError`)]{.parameter-annotation}</code>

:   If the graph is not parameterized or elimination fails

#### Examples {.doc-section .doc-section-examples}

```python
>>> # Create parameterized graph
>>> g = Graph(1)
>>> v_a = g.create_vertex([0])
>>> v_b = g.create_vertex([1])
>>> v_c = g.create_vertex([2])
>>> v_a.add_edge_parameterized(v_b, 0.0, [1.0, 0.0, 0.0])
>>> v_b.add_edge_parameterized(v_c, 0.0, [0.0, 1.0, 0.0])
```

```python
>>> # Eliminate to symbolic DAG (once)
>>> dag = g.eliminate_to_dag()
>>> print(dag)  # SymbolicDAG(vertices=3, params=3, acyclic=True)
```

```python
>>> # Fast instantiation for SVGD (100-1000× faster!)
>>> for theta in particle_swarm:
...     g_concrete = dag.instantiate(theta)
...     log_prob = -g_concrete.expectation()  # Fast!
```

#### Performance {.doc-section .doc-section-performance}

- Elimination: O(n³) - performed once
- Instantiation: O(n) - performed per particle
- Expected speedup for SVGD: 100-1000×

#### See Also {.doc-section .doc-section-see-also}

SymbolicDAG : The returned symbolic DAG class
SymbolicDAG.instantiate : Create concrete graph from parameters

### from_matrices { #phasic.Graph.from_matrices }

```python
phasic.Graph.from_matrices(ipv, sim, states=None)
```

Construct a Graph from matrix representation.

#### Parameters {.doc-section .doc-section-parameters}

<code>[**ipv**]{.parameter-name} [:]{.parameter-annotation-sep} [[np](`numpy`).[ndarray](`numpy.ndarray`)]{.parameter-annotation}</code>

:   Initial probability vector, shape (n_states,)

<code>[**sim**]{.parameter-name} [:]{.parameter-annotation-sep} [[np](`numpy`).[ndarray](`numpy.ndarray`)]{.parameter-annotation}</code>

:   Sub-intensity matrix, shape (n_states, n_states)

<code>[**states**]{.parameter-name} [:]{.parameter-annotation-sep} [[np](`numpy`).[ndarray](`numpy.ndarray`)]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default}</code>

:   State vectors, shape (n_states, state_dim), dtype=int32 If None, uses default states [0], [1], [2], ...

#### Returns {.doc-section .doc-section-returns}

<code>[]{.parameter-name} [:]{.parameter-annotation-sep} [[Graph](`phasic.Graph`)]{.parameter-annotation}</code>

:   The reconstructed phase-type distribution graph

#### Examples {.doc-section .doc-section-examples}

```python
>>> ipv = np.array([0.6, 0.4])
>>> sim = np.array([[-2.0, 1.0], [0.0, -3.0]])
>>> g = Graph.from_matrices(ipv, sim)
>>> pdf = g.pdf(1.0)
```

### make_discrete { #phasic.Graph.make_discrete }

```python
phasic.Graph.make_discrete(mutation_rate, skip_states=[], skip_slots=[])
```

Takes a graph for a continuous distribution and turns
it into a descrete one (inplace). Returns a matrix of
rewards for computing marginal moments

### moments_batch { #phasic.Graph.moments_batch }

```python
phasic.Graph.moments_batch(powers)
```

Compute moments for multiple powers with automatic parallelization.

Automatically uses pmap/vmap based on parallel configuration and batch size.
For single values, use the standard moments() method instead (no overhead).

#### Parameters {.doc-section .doc-section-parameters}

<code>[**powers**]{.parameter-name} [:]{.parameter-annotation-sep} [[array_like](`array_like`)]{.parameter-annotation}</code>

:   Array of moment orders to compute (e.g., [1, 2, 3] for E[T], E[T^2], E[T^3])

#### Returns {.doc-section .doc-section-returns}

<code>[]{.parameter-name} [:]{.parameter-annotation-sep} [[np](`numpy`).[ndarray](`numpy.ndarray`)]{.parameter-annotation}</code>

:   Moment values for each power

#### Examples {.doc-section .doc-section-examples}

```python
>>> import phasic as pta
>>> import numpy as np
>>>
>>> # Initialize parallel computing
>>> config = pta.init_parallel()
>>>
>>> # Build a model
>>> g = pta.Graph(1)
>>> # ... build model ...
>>>
>>> # Compute multiple moments (automatically parallelized)
>>> powers = np.arange(1, 10)  # Moments 1 through 9
>>> moment_values = g.moments_batch(powers)
```

#### Notes {.doc-section .doc-section-notes}

- Automatically parallelizes based on init_parallel() configuration
- Each moment computation is independent and can be parallelized

### moments_from_graph { #phasic.Graph.moments_from_graph }

```python
phasic.Graph.moments_from_graph(graph, nr_moments=2, use_ffi=False)
```

Convert a parameterized Graph to a JAX-compatible function that computes moments.

This method creates a function that computes the first `nr_moments` moments of the
phase-type distribution: [E[T], E[T^2], ..., E[T^nr_moments]].

Moments are computed using the existing C++ `graph.moments(power)` method for efficiency.

#### Parameters {.doc-section .doc-section-parameters}

<code>[**graph**]{.parameter-name} [:]{.parameter-annotation-sep} [[Graph](`phasic.Graph`)]{.parameter-annotation}</code>

:   Parameterized graph built using the Python API with parameterized edges. Must have edges created with `add_edge_parameterized()`.

<code>[**nr_moments**]{.parameter-name} [:]{.parameter-annotation-sep} [[int](`int`)]{.parameter-annotation} [ = ]{.parameter-default-sep} [2]{.parameter-default}</code>

:   Number of moments to compute. For example: - 1: Returns [E[T]] (mean only) - 2: Returns [E[T], E[T^2]] (mean and second moment) - 3: Returns [E[T], E[T^2], E[T^3]]

<code>[**use_ffi**]{.parameter-name} [:]{.parameter-annotation-sep} [[bool](`bool`)]{.parameter-annotation} [ = ]{.parameter-default-sep} [False]{.parameter-default}</code>

:   If True, uses Foreign Function Interface approach.

#### Returns {.doc-section .doc-section-returns}

<code>[]{.parameter-name} [:]{.parameter-annotation-sep} [[callable](`callable`)]{.parameter-annotation}</code>

:   JAX-compatible function with signature: moments_fn(theta) -> jnp.array(nr_moments,) Returns array of moments: [E[T], E[T^2], ..., E[T^k]]

#### Examples {.doc-section .doc-section-examples}

```python
>>> # Create parameterized coalescent model
>>> def coalescent(state, nr_samples=2):
...     if len(state) == 0:
...         return [(np.array([nr_samples]), 1.0, [1.0])]
...     if state[0] > 1:
...         n = state[0]
...         rate = n * (n - 1) / 2
...         return [(np.array([n-1]), 0.0, [rate])]
...     return []
>>>
>>> graph = Graph(coalescent, nr_samples=3)
>>> moments_fn = Graph.moments_from_graph(graph, nr_moments=2)
>>>
>>> # Compute moments for given theta
>>> theta = jnp.array([0.5])
>>> moments = moments_fn(theta)  # [E[T], E[T^2]]
>>> print(f"Mean: {moments[0]}, Second moment: {moments[1]}")
>>>
>>> # Variance can be computed as: Var[T] = E[T^2] - E[T]^2
>>> variance = moments[1] - moments[0]**2
```

#### Notes {.doc-section .doc-section-notes}

- Requires graph to have parameterized edges (created with parameterized=True)
- Moments are raw moments, not central moments
- For variance, compute: Var[T] = E[T^2] - E[T]^2
- For standard deviation: std[T] = sqrt(Var[T])

### pdf_batch { #phasic.Graph.pdf_batch }

```python
phasic.Graph.pdf_batch(times, granularity=100)
```

Compute PDF at multiple time points with automatic parallelization.

Automatically uses pmap/vmap based on parallel configuration and batch size.
For single values, use the standard pdf() method instead (no overhead).

#### Parameters {.doc-section .doc-section-parameters}

<code>[**times**]{.parameter-name} [:]{.parameter-annotation-sep} [[array_like](`array_like`)]{.parameter-annotation}</code>

:   Array of time points to evaluate PDF at

<code>[**granularity**]{.parameter-name} [:]{.parameter-annotation-sep} [[int](`int`)]{.parameter-annotation} [ = ]{.parameter-default-sep} [100]{.parameter-default}</code>

:   Discretization granularity for PDF computation

#### Returns {.doc-section .doc-section-returns}

<code>[]{.parameter-name} [:]{.parameter-annotation-sep} [[np](`numpy`).[ndarray](`numpy.ndarray`)]{.parameter-annotation}</code>

:   PDF values at each time point

#### Examples {.doc-section .doc-section-examples}

```python
>>> import phasic as pta
>>> import numpy as np
>>>
>>> # Initialize parallel computing (once at notebook start)
>>> config = pta.init_parallel()
>>>
>>> # Build a simple model
>>> g = pta.Graph(1)
>>> start = g.starting_vertex()
>>> v1 = g.find_or_create_vertex([1])
>>> start.add_edge(v1, 2.0)
>>> g.normalize()
>>>
>>> # Compute PDF at many time points (automatically parallelized)
>>> times = np.linspace(0.1, 5.0, 1000)
>>> pdf_values = g.pdf_batch(times)
>>>
>>> # For single values, use pdf() instead:
>>> single_value = g.pdf(1.0)
```

#### Notes {.doc-section .doc-section-notes}

- Automatically parallelizes based on init_parallel() configuration
- Uses pmap across devices, vmap for vectorization, or serial execution
- No manual batching or parallelization code required

### plot { #phasic.Graph.plot }

```python
phasic.Graph.plot(*args, **kwargs)
```

Plots the graph using graphviz. See plot::plot_graph.py for more details.

#### Returns {.doc-section .doc-section-returns}

<code>[]{.parameter-name} [:]{.parameter-annotation-sep} []{.parameter-annotation}</code>

:   _description_

### pmf_and_moments_from_graph { #phasic.Graph.pmf_and_moments_from_graph }

```python
phasic.Graph.pmf_and_moments_from_graph(
    graph,
    nr_moments=2,
    discrete=False,
    use_ffi=False,
)
```

Convert a parameterized Graph to a function that computes both PMF/PDF and moments.

This is more efficient than calling `pmf_from_graph()` and `moments_from_graph()`
separately because it builds the graph once and computes both quantities.

#### Parameters {.doc-section .doc-section-parameters}

<code>[**graph**]{.parameter-name} [:]{.parameter-annotation-sep} [[Graph](`phasic.Graph`)]{.parameter-annotation}</code>

:   Parameterized graph built using the Python API with parameterized edges.

<code>[**nr_moments**]{.parameter-name} [:]{.parameter-annotation-sep} [[int](`int`)]{.parameter-annotation} [ = ]{.parameter-default-sep} [2]{.parameter-default}</code>

:   Number of moments to compute

<code>[**discrete**]{.parameter-name} [:]{.parameter-annotation-sep} [[bool](`bool`)]{.parameter-annotation} [ = ]{.parameter-default-sep} [False]{.parameter-default}</code>

:   If True, computes discrete PMF. If False, computes continuous PDF.

<code>[**use_ffi**]{.parameter-name} [:]{.parameter-annotation-sep} [[bool](`bool`)]{.parameter-annotation} [ = ]{.parameter-default-sep} [False]{.parameter-default}</code>

:   If True, uses Foreign Function Interface approach.

#### Returns {.doc-section .doc-section-returns}

<code>[]{.parameter-name} [:]{.parameter-annotation-sep} [[callable](`callable`)]{.parameter-annotation}</code>

:   JAX-compatible function with signature: model(theta, times) -> (pmf_values, moments)  Where: - pmf_values: jnp.array(len(times),) - PMF/PDF values at each time - moments: jnp.array(nr_moments,) - [E[T], E[T^2], ..., E[T^k]]

#### Examples {.doc-section .doc-section-examples}

```python
>>> # Create parameterized model
>>> graph = Graph(coalescent, nr_samples=3)
>>> model = Graph.pmf_and_moments_from_graph(graph, nr_moments=2)
>>>
>>> # Compute both PMF and moments
>>> theta = jnp.array([0.5])
>>> times = jnp.array([1.0, 2.0, 3.0])
>>> pmf_vals, moments = model(theta, times)
>>>
>>> print(f"PMF at times: {pmf_vals}")
>>> print(f"Moments: {moments}")  # [E[T], E[T^2]]
>>>
>>> # Use in SVGD with moment regularization
>>> svgd = SVGD(model, observed_pmf, theta_dim=1)
>>> svgd.fit_regularized(observed_times=data, nr_moments=2, regularization=1.0)
```

#### Notes {.doc-section .doc-section-notes}

- More efficient than separate calls to pmf_from_graph() and moments_from_graph()
- Required for using moment-based regularization in SVGD.fit_regularized()
- The moments are always computed from the same graph used for PMF/PDF

### pmf_from_cpp { #phasic.Graph.pmf_from_cpp }

```python
phasic.Graph.pmf_from_cpp(cpp_file, discrete=False)
```

Load a phase-type model from a user's C++ file and return a JAX-compatible function.

The C++ file should include 'user_model.h' and implement:

phasic::Graph build_model(const double* theta, int n_params) {
    // Build and return Graph instance
}

For efficient repeated evaluations with the same parameters without JAX overhead,
use load_cpp_builder() instead to get a builder function that creates Graph objects.

#### Parameters {.doc-section .doc-section-parameters}

<code>[**cpp_file**]{.parameter-name} [:]{.parameter-annotation-sep} [[str](`str`) or [Path](`Path`)]{.parameter-annotation}</code>

:   Path to the user's C++ file

<code>[**discrete**]{.parameter-name} [:]{.parameter-annotation-sep} [[bool](`bool`)]{.parameter-annotation} [ = ]{.parameter-default-sep} [False]{.parameter-default}</code>

:   If True, uses discrete phase-type distribution (DPH) computation. If False, uses continuous phase-type distribution (PDF).

#### Raises {.doc-section .doc-section-raises}

<code>[:]{.parameter-annotation-sep} [[ImportError](`ImportError`)]{.parameter-annotation}</code>

:   If JAX is not installed. Install with: pip install jax jaxlib

<code>[:]{.parameter-annotation-sep} [[FileNotFoundError](`FileNotFoundError`)]{.parameter-annotation}</code>

:   If the specified C++ file does not exist

#### Returns {.doc-section .doc-section-returns}

<code>[]{.parameter-name} [:]{.parameter-annotation-sep} [[callable](`callable`)]{.parameter-annotation}</code>

:   JAX-compatible function (theta, times) -> pmf_values that supports JIT, grad, vmap, etc.

#### Examples {.doc-section .doc-section-examples}

# JAX-compatible approach (default - for SVGD, gradients, optimization)

```python
>>> model = Graph.pmf_from_cpp("my_model.cpp")
>>> theta = jnp.array([1.0, 2.0])
>>> times = jnp.linspace(0, 10, 100)
>>> pmf = model(theta, times)
>>> gradient = jax.grad(lambda p: jnp.sum(model(p, times)))(theta)
```

# Discrete phase-type distribution

```python
>>> model = Graph.pmf_from_cpp("my_model.cpp", discrete=True)
>>> theta = jnp.array([1.0, 2.0])
>>> jumps = jnp.array([1, 2, 3, 4, 5])
>>> dph_pmf = model(theta, jumps)
```

# For direct C++ access without JAX (faster for repeated evaluations):

```python
>>> builder = load_cpp_builder("my_model.cpp")
>>> graph = builder(np.array([1.0, 2.0]))  # Build graph once
>>> pdf1 = graph.pdf(1.0)  # Use many times
>>> pdf2 = graph.pdf(2.0)  # No rebuild needed
```

### pmf_from_graph { #phasic.Graph.pmf_from_graph }

```python
phasic.Graph.pmf_from_graph(graph, discrete=False)
```

Convert a Python-built Graph to a JAX-compatible function with full gradient support.

This method automatically detects if the graph has parameterized edges (edges with
state vectors) and generates optimized C++ code to enable full JAX transformations
including gradients, vmap, and jit compilation.

For direct C++ access without JAX wrapping, use the Graph object's methods directly:
graph.pdf(time), graph.dph_pmf(jump), graph.moments(power), etc.

#### Raises {.doc-section .doc-section-raises}

<code>[:]{.parameter-annotation-sep} [[ImportError](`ImportError`)]{.parameter-annotation}</code>

:   If JAX is not installed. Install with: pip install jax jaxlib

#### Parameters {.doc-section .doc-section-parameters}

<code>[**graph**]{.parameter-name} [:]{.parameter-annotation-sep} [[Graph](`phasic.Graph`)]{.parameter-annotation}</code>

:   Graph built using the Python API. Can have regular edges or parameterized edges.

<code>[**discrete**]{.parameter-name} [:]{.parameter-annotation-sep} [[bool](`bool`)]{.parameter-annotation} [ = ]{.parameter-default-sep} [False]{.parameter-default}</code>

:   If True, uses discrete phase-type distribution (DPH) computation. If False, uses continuous phase-type distribution (PDF).

#### Returns {.doc-section .doc-section-returns}

<code>[]{.parameter-name} [:]{.parameter-annotation-sep} [[callable](`callable`)]{.parameter-annotation}</code>

:   If graph has parameterized edges:     JAX-compatible function (theta, times) -> pmf_values     Supports JIT, grad, vmap, etc. If graph has no parameterized edges:     JAX-compatible function (times) -> pmf_values     Supports JIT (backward compatible signature)

#### Examples {.doc-section .doc-section-examples}

# Non-parameterized graph (regular edges only)

```python
>>> g = Graph(1)
>>> start = g.starting_vertex()
>>> v0 = g.find_or_create_vertex([0])
>>> v1 = g.find_or_create_vertex([1])
>>> start.add_edge(v0, 1.0)
>>> v0.add_edge(v1, 2.0)  # fixed weight
>>>
>>> model = Graph.pmf_from_graph(g)
>>> times = jnp.linspace(0, 5, 50)
>>> pdf = model(times)  # No theta needed
```

# Parameterized graph (with edge states for gradient support)

```python
>>> g = Graph(1)
>>> start = g.starting_vertex()
>>> v0 = g.find_or_create_vertex([0])
>>> v1 = g.find_or_create_vertex([1])
>>> start.add_edge(v0, 1.0)
>>> v0.add_edge_parameterized(v1, 0.0, [2.0, 0.5])  # weight = 2.0*theta[0] + 0.5*theta[1]
>>>
>>> model = Graph.pmf_from_graph(g)
>>> theta = jnp.array([1.0, 3.0])
>>> pdf = model(theta, times)  # weight becomes 2.0*1.0 + 0.5*3.0 = 3.5
>>>
>>> # Full JAX support for parameterized graphs
>>> grad_fn = jax.grad(lambda t: jnp.sum(model(t, times)))
>>> gradient = grad_fn(theta)  # Gradients work!
```

# For direct C++ access (no JAX overhead), use graph methods:

```python
>>> pdf_value = g.pdf(1.5)  # Direct C++ call
>>> pmf_value = g.dph_pmf(3)  # Direct C++ call
```

### pmf_from_graph_parameterized { #phasic.Graph.pmf_from_graph_parameterized }

```python
phasic.Graph.pmf_from_graph_parameterized(graph_builder, discrete=False)
```

Convert a parameterized Python graph builder to a JAX-compatible function.

This allows users to define parameterized models where the graph structure
or edge weights depend on parameters.

#### Parameters {.doc-section .doc-section-parameters}

<code>[**graph_builder**]{.parameter-name} [:]{.parameter-annotation-sep} [[callable](`callable`)]{.parameter-annotation}</code>

:   Function (theta) -> Graph that builds a graph with given parameters

<code>[**discrete**]{.parameter-name} [:]{.parameter-annotation-sep} [[bool](`bool`)]{.parameter-annotation} [ = ]{.parameter-default-sep} [False]{.parameter-default}</code>

:   If True, uses discrete phase-type distribution (DPH) computation. If False, uses continuous phase-type distribution (PDF).

#### Returns {.doc-section .doc-section-returns}

<code>[]{.parameter-name} [:]{.parameter-annotation-sep} [[callable](`callable`)]{.parameter-annotation}</code>

:   JAX-compatible function (theta, times) -> pdf_values that supports JIT, grad, vmap, etc.

#### Examples {.doc-section .doc-section-examples}

```python
>>> def build_exponential(rate):
...     g = Graph(1)
...     start = g.starting_vertex()
...     v0 = g.find_or_create_vertex([0])
...     v1 = g.find_or_create_vertex([1])
...     start.add_edge(v0, 1.0)
...     v0.add_edge(v1, float(rate))
...     return g
>>>
>>> model = Graph.pmf_from_graph_parameterized(build_exponential)
>>> theta = jnp.array([1.5])
>>> times = jnp.linspace(0, 5, 50)
>>> pdf = model(theta, times)
```

### serialize { #phasic.Graph.serialize }

```python
phasic.Graph.serialize()
```

Serialize graph to array representation for efficient computation.

#### Returns {.doc-section .doc-section-returns}

<code>[]{.parameter-name} [:]{.parameter-annotation-sep} [[dict](`dict`)]{.parameter-annotation}</code>

:   Dictionary containing: - 'states': Array of vertex states (n_vertices, state_dim) - 'edges': Array of regular edges [from_idx, to_idx, weight] (n_edges, 3) - 'start_edges': Array of starting vertex regular edges [to_idx, weight] (n_start_edges, 2) - 'param_edges': Array of parameterized edges [from_idx, to_idx, x1, x2, ...] (n_param_edges, param_length+2) - 'start_param_edges': Array of starting vertex parameterized edges [to_idx, x1, x2, ...] (n_start_param_edges, param_length+1) - 'param_length': Length of parameter vector (0 if no parameterized edges) - 'state_length': Integer state dimension - 'n_vertices': Number of vertices

### svgd { #phasic.Graph.svgd }

```python
phasic.Graph.svgd(
    model,
    observed_data,
    prior=None,
    n_particles=50,
    n_iterations=1000,
    learning_rate=0.001,
    kernel='rbf_median',
    theta_init=None,
    theta_dim=None,
    return_history=False,
    seed=42,
    verbose=True,
)
```

Run Stein Variational Gradient Descent (SVGD) inference for Bayesian parameter estimation.

SVGD finds the posterior distribution p(theta | data) by optimizing a set of particles to
approximate the posterior. This method works with parameterized models created by
pmf_from_graph() or pmf_from_cpp() where the model signature is model(theta, times).

#### Parameters {.doc-section .doc-section-parameters}

<code>[**model**]{.parameter-name} [:]{.parameter-annotation-sep} [[callable](`callable`)]{.parameter-annotation}</code>

:   JAX-compatible parameterized model from pmf_from_graph() or pmf_from_cpp(). Must have signature: model(theta, times) -> values

<code>[**observed_data**]{.parameter-name} [:]{.parameter-annotation-sep} [[array_like](`array_like`)]{.parameter-annotation}</code>

:   Observed data points. For continuous models (PDF), these are time points where the density was observed. For discrete models (PMF), these are jump counts.

<code>[**prior**]{.parameter-name} [:]{.parameter-annotation-sep} [[callable](`callable`)]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default}</code>

:   Log prior function: prior(theta) -> scalar. If None, uses standard normal prior: log p(theta) = -0.5 * sum(theta^2)

<code>[**n_particles**]{.parameter-name} [:]{.parameter-annotation-sep} [[int](`int`)]{.parameter-annotation} [ = ]{.parameter-default-sep} [50]{.parameter-default}</code>

:   Number of SVGD particles. More particles = better posterior approximation but slower.

<code>[**n_iterations**]{.parameter-name} [:]{.parameter-annotation-sep} [[int](`int`)]{.parameter-annotation} [ = ]{.parameter-default-sep} [1000]{.parameter-default}</code>

:   Number of SVGD optimization steps

<code>[**learning_rate**]{.parameter-name} [:]{.parameter-annotation-sep} [[float](`float`)]{.parameter-annotation} [ = ]{.parameter-default-sep} [0.001]{.parameter-default}</code>

:   SVGD step size. Larger values = faster convergence but may be unstable.

<code>[**kernel**]{.parameter-name} [:]{.parameter-annotation-sep} [[str](`str`)]{.parameter-annotation} [ = ]{.parameter-default-sep} [\'rbf_median\']{.parameter-default}</code>

:   Kernel bandwidth selection method: - 'rbf_median': RBF kernel with median heuristic bandwidth (default) - 'rbf_adaptive': RBF kernel with adaptive bandwidth

<code>[**theta_init**]{.parameter-name} [:]{.parameter-annotation-sep} [[array_like](`array_like`)]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default}</code>

:   Initial particle positions (n_particles, theta_dim). If None, initializes randomly from standard normal.

<code>[**theta_dim**]{.parameter-name} [:]{.parameter-annotation-sep} [[int](`int`)]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default}</code>

:   Dimension of theta parameter vector. Required if theta_init is None.

<code>[**return_history**]{.parameter-name} [:]{.parameter-annotation-sep} [[bool](`bool`)]{.parameter-annotation} [ = ]{.parameter-default-sep} [False]{.parameter-default}</code>

:   If True, return particle positions throughout optimization

<code>[**seed**]{.parameter-name} [:]{.parameter-annotation-sep} [[int](`int`)]{.parameter-annotation} [ = ]{.parameter-default-sep} [42]{.parameter-default}</code>

:   Random seed for reproducibility

<code>[**verbose**]{.parameter-name} [:]{.parameter-annotation-sep} [[bool](`bool`)]{.parameter-annotation} [ = ]{.parameter-default-sep} [True]{.parameter-default}</code>

:   Print progress information

#### Returns {.doc-section .doc-section-returns}

<code>[]{.parameter-name} [:]{.parameter-annotation-sep} [[dict](`dict`)]{.parameter-annotation}</code>

:   Inference results containing: - 'particles': Final posterior samples (n_particles, theta_dim) - 'theta_mean': Posterior mean estimate - 'theta_std': Posterior standard deviation - 'history': Particle evolution over iterations (if return_history=True)

#### Raises {.doc-section .doc-section-raises}

<code>[:]{.parameter-annotation-sep} [[ImportError](`ImportError`)]{.parameter-annotation}</code>

:   If JAX is not installed

<code>[:]{.parameter-annotation-sep} [[ValueError](`ValueError`)]{.parameter-annotation}</code>

:   If model is not parameterized or theta_dim cannot be inferred

#### Examples {.doc-section .doc-section-examples}

```python
>>> import jax.numpy as jnp
>>> from phasic import Graph
>>>
>>> # Build parameterized coalescent model
>>> def coalescent_callback(state, nr_samples=3):
...     if len(state) == 0:
...         return [(np.array([nr_samples]), 1.0, [1.0])]
...     if state[0] > 1:
...         n = state[0]
...         rate = n * (n - 1) / 2
...         return [(np.array([n - 1]), 0.0, [rate])]
...     return []
>>>
>>> g = Graph.from_callback_parameterized(coalescent_callback, nr_samples=4)
>>> model = Graph.pmf_from_graph(g, discrete=False)
>>>
>>> # Generate synthetic observed data
>>> true_theta = jnp.array([2.0])
>>> times = jnp.linspace(0.1, 3.0, 15)
>>> observed_pdf = model(true_theta, times)
>>>
>>> # Run SVGD inference
>>> results = Graph.svgd(
...     model=model,
...     observed_data=observed_pdf,
...     theta_dim=1,
...     n_particles=30,
...     n_iterations=500,
...     learning_rate=0.01
... )
>>>
>>> print(f"True theta: {true_theta}")
>>> print(f"Posterior mean: {results['theta_mean']}")
>>> print(f"Posterior std: {results['theta_std']}")
```

#### Notes {.doc-section .doc-section-notes}

- SVGD requires a parameterized model. Non-parameterized models (signature: model(times))
  cannot be used for inference as there are no parameters to estimate.
- The likelihood is computed as sum(log(model(theta, observed_data)))
- For better results, ensure observed_data has sufficient information about the parameters
- Learning rate and number of iterations may need tuning for different problems