One Jansen-Rit experiment, grown one block at a time
A single experiment that starts as one cortical column and ends as a drive sweep on a connectome — each block linked to the page that documents it.
2Specify
Every page in ② SPECIFY documents one block of a specification, and each stands on its own snippet so you can read it in isolation. This page is the thread that connects them: one experiment, grown six times, where each growth step is exactly the block one of those pages is about.
The model is Jansen-Rit, the cortical column whose rhythm depends on how hard it is driven. Every number below is computed when this page is built, so the claims are whatever the code actually produces.
Imports, plot style, and a spectrum helper
import warnings; warnings.filterwarnings("ignore")import numpy as npimport matplotlib.pyplot as pltimport bsplotfrom tvbo import SimulationExperimentbsplot.style.use("tvbo")DT =0.5# ms, the step size every stage below integrates atdef spectrum(x):"""Return (frequencies, node-averaged power) for a time-by-node array.""" x = np.asarray(x).squeeze() x = x[:, None] if x.ndim ==1else x freqs = np.fft.rfftfreq(x.shape[0], d=DT /1000.0) power = (np.abs(np.fft.rfft(x - x.mean(0), axis=0)) **2).mean(1)return freqs, powerdef peak_hz(x, lo=1.0, hi=40.0): freqs, power = spectrum(x) band = (freqs > lo) & (freqs < hi)returnfloat(freqs[band][power[band].argmax()])def grow(spec, old, new):"""Swap one block for another, refusing to pass a stale spec through silently."""if old notin spec:raiseAssertionError(f"block not found in the previous stage:\n{old}")return spec.replace(old, new)
Stage 1 — one cortical column
The smallest experiment that means anything: a model, a network of one node, and a clock. Nothing here is optional and nothing else is required.
* Owlready2 * Warning: ignoring cyclic subclass of/subproperty of, involving:
http://uri.interlex.org/tgbugs/uris/readable/atlas/Space
INFO [tvbo.run] [+0s] STEP 1: Running simulation...
INFO [tvbo.run] [+1s] Simulation period: 9000.0 ms, dt: 0.5 ms
INFO [tvbo.run] [+1s] Transient period: 1000.0 ms (settled on (-1000.0, 0], warm-started via update_history)
INFO [tvbo.run] [+1s] Simulation complete.
INFO [tvbo.run] [+1s] Experiment complete.
dims: {'time': 18000, 'variable': 6}
Stage 2 — declare what you measure
The quantity that maps onto EEG is the pyramidal membrane potential \(y_1 - y_2\), and computing it by hand after the run leaves it out of the specification. An observations block puts it back in, so the experiment records what it is about rather than everything it happens to hold.
An equation observation needs source: to name the state variables it reads. Without it TVB-O has no way to know that y1 - y2 is a two-variable expression rather than a label.
INFO [tvbo.run] [+0s] STEP 1: Running simulation...
INFO [tvbo.run] [+0s] Simulation period: 9000.0 ms, dt: 0.5 ms
INFO [tvbo.run] [+0s] Transient period: 1000.0 ms (settled on (-1000.0, 0], warm-started via update_history)
INFO [tvbo.run] [+0s] Simulation complete.
INFO [tvbo.run] [+0s] Experiment complete.
Figure 1: One column at its default drive. A clean limit cycle, and therefore a single sharp spectral line.
Stage 3 — drive it with noise
A deterministic column settles onto that limit cycle and stays there. Real cortex does not: Jansen and Rit drove the pyramidal population with a fluctuating input, and the broadened peak that produces is what makes the trace look like an EEG.
targets: puts the noise on one state variable rather than all six. y4 is the pyramidal population’s derivative state, which is where an afferent input enters.
INFO [tvbo.run] [+0s] STEP 1: Running simulation...
INFO [tvbo.run] [+1s] Simulation period: 9000.0 ms, dt: 0.5 ms
INFO [tvbo.run] [+1s] Transient period: 1000.0 ms (settled on (-1000.0, 0], warm-started via update_history)
INFO [tvbo.run] [+1s] Simulation complete.
INFO [tvbo.run] [+1s] Experiment complete.
Figure 2: The same column driven by noise. The line broadens into a band, and the amplitude fluctuates from cycle to cycle.
Stage 4 — sweep the drive
One run answers nothing about how the drive sets the rhythm. An explorations block turns the single experiment into a grid over the physiological input range, and record: says which observation to keep per cell so the result stays small.
DRIVES = [0.12, 0.17, 0.22, 0.27, 0.32]STAGE4 = STAGE3 +f"""explorations: drive: label: "Mean input to the pyramidal population" mode: product record: [eeg] space: mu: explored_values: {DRIVES}"""swept = SimulationExperiment.from_string(STAGE4).run()cells = np.asarray(swept.explorations.drive.observations["eeg"])column_hz = [peak_hz(cells[i]) for i inrange(cells.shape[0])]print({mu: round(hz, 2) for mu, hz inzip(DRIVES, column_hz)})
INFO [tvbo.run] [+0s] STEP 1: Running simulation...
INFO [tvbo.run] [+1s] Simulation period: 9000.0 ms, dt: 0.5 ms
INFO [tvbo.run] [+1s] Transient period: 1000.0 ms (settled on (-1000.0, 0], warm-started via update_history)
INFO [tvbo.run] [+1s] Simulation complete.
INFO [tvbo.run] [+1s] STEP 2: Running explorations...
INFO [tvbo.run] [+1s] > drive
INFO [tvbo.run] grid batch 1/1 (100%)
INFO [tvbo.run] [+1s] Explorations complete.
INFO [tvbo.run] [+1s] Experiment complete.
The column’s rhythm rides on its drive, which is Jansen and Rit’s own result: the same circuit sits in the theta band when weakly driven and climbs toward alpha as the input grows.
Stage 5 — put it on a connectome
Replace the one node with a real network and say how activity travels between nodes. Two things change: network gains an iri and a weight transform, and a coupling entry names the model’s own input port.
The coupling key is not a name you invent. Dynamics.from_db("JansenRit").coupling_inputs names it c_glob, and it enters the pyramidal equation exactly where mu does.
Figure 3: The same drive sweep, run on one column and on 87 coupled regions. Coupling supplies input the column cannot tell apart from its own drive, so the network’s rhythm stops tracking mu.
Two things that bite newcomers here
Normalize the connectome. The Desikan-Killiany matrix holds streamline counts in the hundreds of thousands. Fed in raw they swamp every intrinsic term. The transforms block rescales so the mean non-zero weight is \(1\), which puts the coupling gain on a scale you can reason about.
Jansen-Rit needs a saturating coupling function.tvbo:SigmoidalJansenRit bounds each edge’s contribution. Substituting tvbo:Linear on y1 makes the drive unbounded, and above a gain of roughly \(0.02\) on max-normalized weights the run diverges to inf rather than converging on anything.
Stage 6 — make it a study
A paper is not one experiment. A SimulationStudy holds several, shares blocks between them with YAML anchors, and declares the analyses and figures that turn results into the thing you publish.