Initial Conditions & Initial State

Part of the running example, where stage 3 adds noise on one state variable.

The initial state of a simulation is the value of every state variable on every node where the integration opens. TVBO resolves it from three layered sources, each overriding the previous:

  1. Model defaults: state_variables.<name>.initial_value in the dynamics YAML.
  2. Per-node overrides: state: blocks on individual nodes.
  3. Random sampling: distribution on a state variable, activated for trial-based explorations.

A declared integration.transient_time is not one of these. It does not replace the initial condition: the run opens from the IC resolved above at \(t = -\text{transient\_time}\) and settles in-band, so \(t = 0\) is simply where the settle ends and the measured window begins. To start from a state some other run already settled, see Seeding from a settled state.

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.

Settling in-band

integration.transient_time marks a settling window at the head of one integration. It is not a second simulation and it does not touch the initial condition; the scan simply opens earlier:

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

res.integration.data       # the measured window, t in (0, duration]
res.integration.transient  # the settle,        t in (-2000, 0]
res.integration.full       # both, one buffer

Because the settle is part of the same window, an observation kernel that needs warm-up (a BOLD HRF, a phase memory, a running correlation) gets it from real signal, and its own settle-side output samples are dropped for you. Nothing needs to be declared for that to happen.

Seeding from a settled state

A settle that many runs reuse, the shared operating point a fit evaluates hundreds of candidates from, is a different thing, and it is declared as one:

initial_state:
  method: time_integration   # integrate forward to a settled start
  duration: 120000           # how long to settle for

That state is computed once and reused, where transient_time is re-integrated by every run that declares it. from_working_point and from_experiment are the other two ways to say “start from a state something else reached”.

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\)
initial_state.method global a declared seed (see above)

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.