Initial Conditions & Initial State

The initial state of a simulation is the value of every state variable on every node at \(t = 0\). TVBO resolves it from four layered sources, each overriding the previous:

  1. Model defaultsstate_variables.<name>.initial_value in the dynamics YAML.
  2. Per-node overridesstate: blocks on individual nodes.
  3. Random samplingdistribution on a state variable, activated for trial-based explorations.
  4. Transient warm-up — when integration.transient > 0, a pre-simulation is run and its final state replaces the IC.

The shape of state.initial_state.dynamics in the generated tvboptim code is (n_states, n_nodes).

1. Model defaults

Each state variable in the model YAML may carry initial_value. It is broadcast across all nodes.

state_variables:
  V:
    initial_value: 0.1
    domain: { lo: -2.0, hi: 4.0 }
  W:
    initial_value: 0.1
    domain: { lo: -6.0, hi: 6.0 }
from tvbo import Dynamics, SimulationExperiment, Network

dyn = Dynamics.from_db("Generic2dOscillator")
print({sv: dyn.state_variables[sv].initial_value for sv in dyn.state_variables})
{'V': 0.1, 'W': 0.1}

2. Per-node overrides

Attach a state: block to any node to override that node’s IC. Other nodes keep the model default.

net = Network.from_string("""
label: Visual Pathway
nodes:
  - id: 0
    label: V1
    state:
      V: {value: 0.5}
      W: {value: -0.2}
  - id: 1
    label: V2
    state:
      V: {value: 0.0}
  - {id: 2, label: MT}
edges: []
""")

Programmatic equivalent:

from tvbo.datamodel.tvbopydantic import StateVariable
exp.network.nodes[0].state = {
    'V': StateVariable(name='V', value=0.5),
    'W': StateVariable(name='W', value=-0.2),
}

The tvboptim template emits one at[<sv_idx>].set(jnp.array([...])) per overridden state variable.

3. Random / sampled initial conditions

Add a distribution to a state variable; tvboptim then samples ICs per trial when an exploration with n_trials > 1 is active.

YAML

state_variables:
  V:
    initial_value: 0.1
    distribution:
      name: Uniform           # Uniform | Gaussian (a.k.a. Normal)
      seed: 42
      domain: { lo: -2.0, hi: 2.0 }

For Gaussian, mean = \((lo+hi)/2\) and std = \((hi-lo)/4\) (clamped to the domain).

Programmatic

from tvbo.datamodel.tvbopydantic import Distribution, Domain, Exploration

exp.dynamics.state_variables['V'].distribution = Distribution(
    name='Uniform', domain=Domain(lo=-2.0, hi=2.0), seed=42
)
exp.dynamics.state_variables['W'].distribution = Distribution(
    name='Uniform', domain=Domain(lo=-6.0, hi=6.0), seed=42
)

# Activate sampling: n_trials samples are drawn via jax.vmap
exp.explorations['ic_trials'] = Exploration(name='ic_trials', n_trials=10)
res = exp.run()

Each trial draws an independent IC of shape (n_states, n_nodes) and runs the simulation in parallel via jax.vmap. Use average: trials on the exploration to reduce across trials.

Note

A plain exp.run() without an exploration ignores the distribution. Sampling only happens when at least one Exploration requests n_trials > 1. To add a random_initial_conditions flag for single runs, open an issue.

4. Transient warm-up

Set integration.transient to discard a settling period. The transient simulation honours the IC rules above, and its final state replaces the IC of the main run.

exp.integration.transient = 2000  # ms
res = exp.run()

In the generated code:

result_transient = model_fn_init(state_init)
# ...
state.dynamics.<sv> = result_transient.data[-1][<idx>]

Resolution order at a glance

Source Scope Triggered by
state_variables.initial_value global always (default)
network.nodes[i].state per-node state: block on a node
state_variables.distribution per-trial Exploration(n_trials=N) with \(N > 1\)
integration.transient global integration.transient > 0

Inspecting the generated code

print(exp.render('tvboptim'))

Search the output for initial_state.dynamics to see exactly how the IC is assembled for your experiment.