Scripting

Every page of the app does its work by calling the tit Python API — it builds a config object, serialises it to JSON and runs the same module you can run yourself. This page walks the app page by page and shows, for each one, the call its Run button makes.

A runnable version of the same sequence is the example notebook.

Where scripts run

Scripting happens inside the container — that is where SimNIBS and tit live. Your project is mounted at /mnt/<project_name>/ and everything is pre-installed.

1. The Notebooks page, in the app. Open it and start typing; the kernel is the container’s SimNIBS Python with your project mounted.

2. docker exec, from your own terminal:

tit launch --project ~/datasets/000 --status   # prints the container's name
docker exec -it ti-toolbox-<hash>-tit-1 bash
simnibs_python my_script.py

Installing tit on your host (pip install tit) gives you the tit launch launcher and nothing else usable — the scripting API needs SimNIBS, which is in the image.

The project

In the GUI you open a project. In code, get_path_manager does the same: it is the object that knows where everything in a BIDS project lives, and every module uses it.

No data of your own yet? tit.examples.fetch_ernie(project) downloads both parts of the ernie dataset (its raw T1/T2 and its finished m2m_ernie head model, ~1.1 GB, GPL-3.0) into the project once and returns immediately when they are already there — the same call behind the Overview page’s Add example data button and python -m tit.examples --project DIR; tit.examples.fetch(dataset, part, project) takes any catalogue part id, such as fetch("mni152", "headmodel", project):

import os
from tit import get_path_manager
from tit.examples import fetch_ernie

PROJECT = os.environ.get("TIT_PROJECT_DIR") or "/path/to/your/project"
pm = get_path_manager(PROJECT)
fetch_ernie(PROJECT)
from tit import get_path_manager
from tit.pre import discover_subjects

pm = get_path_manager("/mnt/000")
print(discover_subjects("/mnt/000"))
Call GUI equivalent
get_path_manager(project_dir) Opening a project
discover_subjects(project_dir) The subject list
pm.m2m(sid) The head model of a subject
pm.simulations(sid) The Simulator’s results list
pm.flex_search(sid) / pm.ex_search(sid) The Optimizer’s results list

Pre-processing

In the GUI you tick the conversion and head-model boxes on the Pre-processing page and press Run. Those tick boxes are the keyword arguments of run_pipeline.

from tit.pre import run_pipeline, check_m2m_exists

if not check_m2m_exists("/mnt/000", "ernie"):
    run_pipeline(["ernie"], convert_dicom=True, create_m2m=True)
Argument GUI label Default
subject_ids Subject selection required
convert_dicom Convert DICOM to NIfTI False
create_m2m Create head model (charm) False
run_fastsurfer FastSurfer segmentation False
run_freesurfer FreeSurfer recon-all False
run_tissue_analysis Tissue analysis False
run_qsiprep / run_qsirecon DWI preprocessing / reconstruction False
extract_dti Extract DTI tensor False
skip_existing_outputs Skip completed steps False

In the GUI you fill the Optimizer ▸ Flex form — goal, current, electrode size, target ROI — and press Run. That form is FlexConfig.

from tit.opt import FlexConfig, run_flex_search

cfg = FlexConfig(
    subject_id="ernie",
    goal="mean",
    postproc="max_TI",
    current_mA=1.0,
    electrode=FlexConfig.ElectrodeConfig(shape="ellipse", dimensions=[8.0, 8.0]),
    roi=FlexConfig.SphericalROI(x=-37.0, y=-21.0, z=58.0, radius=10.0, use_mni=True),
)
result = run_flex_search(cfg)
print(result.best_value, result.output_folder)
Field GUI label Default
subject_id Subject required
goal Optimization goal (mean, max, focality, focality_tf) required
postproc Field post-processing (max_TI, dir_TI_normal, dir_TI_tangential) required
current_mA Current per channel (mA) required
electrode Electrode Parameters box required
roi Target ROI required
anisotropy_type Conductivity "scalar"
intensity_weight Intensity weight $w$ (focality_tf only) 0.0
eeg_net / enable_mapping Map result to an EEG net None / False
n_multistart Restarts 1
max_iterations, population_size Solver settings (blank = SimNIBS default) None
min_electrode_distance Minimum electrode distance (mm) 5.0
run_final_electrode_simulation Simulate the winning montage False

ROI types: FlexConfig.SphericalROI(x, y, z, radius, use_mni=, volumetric=, tissues=), FlexConfig.AtlasROI(atlas_path, label, hemisphere) for a cortical .annot region, and FlexConfig.SubcorticalROI(atlas_path, label, tissues, atlas_space) for a volumetric one. Each scalar field also accepts a list, which unions several regions into one target.

The focality_tf goal maximises a threshold-free contrast:

\[\frac{\overline{E}_{\mathrm{ROI}}^{\,1+w}}{p_{95}\!\left(E_{\mathrm{non\text{-}ROI}}\right)}\]

In the GUI you pick a leadfield, an ROI file and the electrodes to search on the Optimizer ▸ Ex page. That is ExConfig.

from tit.opt import ExConfig, run_ex_search

cfg = ExConfig(
    subject_id="ernie",
    leadfield_hdf="ernie_leadfield_EEG10-20_Okamoto_2004.hdf5",
    roi_name="L-Insula.csv",
    electrodes=ExConfig.PoolElectrodes(electrodes=["Fp1", "Fp2", "C3", "C4", "Cz", "Pz"]),
)
run_ex_search(cfg)
Field GUI label Default
leadfield_hdf Leadfield required
roi_name ROI file required
electrodes PoolElectrodes (one list) or BucketElectrodes (four lists) required
total_current Total current (mA) 2.0
current_step Current step (mA) 0.5
channel_limit Per-channel limit (mA) None
roi_radius ROI radius (mm) 3.0
roi_coordinate_space ROI coordinates (subject/mni) "subject"
n_jobs Parallel workers (-1 = the global CPU limit, 70 % of the cores; larger values are capped at it) -1

MExConfig + run_m_ex_search are the same fields for the multipolar (4-pair) search, with current_mA in place of total_current/current_step.

Simulator

In the GUI you choose a montage from the dropdown, set the electrode parameters and press Run. The dropdown is the project’s montage list; load_montages reads exactly that file.

from tit.sim import SimulationConfig, run_simulation, load_montages, list_montage_names

print(list_montage_names("GSN-HydroCel-185.csv", mode="U"))
montages = load_montages(montage_names=["L_Insula"], eeg_net="GSN-HydroCel-185.csv")

cfg = SimulationConfig(
    subject_id="ernie",
    montages=montages,
    conductivity="scalar",
    intensities=[1.0, 1.0],
    output_fields=["TI_max"],
)
run_simulation(cfg)
Field GUI label Default
subject_id Subject required
montages Montage selection required
conductivity Conductivity (scalar, vn, dir, mc) "scalar"
intensities Current per pair (mA) [1.0, 1.0]
electrode_shape Electrode shape "ellipse"
electrode_dimensions Electrode dimensions (mm) [8.0, 8.0]
gel_thickness Gel thickness (mm) 4.0
rubber_thickness Rubber thickness (mm) 2.0
output_fields Output fields (TI_max, TI_avg, hf_peak, hf_sar) ["TI_max"]
map_to_surf / map_to_vol / map_to_mni / map_to_fsavg Output mapping checkboxes True / False / False / True

Two electrode pairs run a TI simulation, four or more run mTI — the API picks for you.

Analyzer

In the GUI you choose a simulation, a space and a region on the Analyzer page. Analyzer is that form, and its methods return the numbers the results table shows.

from tit.analyzer import Analyzer

analyzer = Analyzer(subject_id="ernie", simulation="L_Insula", space="mesh")
roi = analyzer.analyze_cortex(atlas="DK40", region="lh.insula")
print(roi.roi_mean, roi.roi_max, roi.roi_focality)
Field GUI label Default
subject_id Subject required
simulation Simulation required
space Space (mesh or voxel) "mesh"
tissue_type Tissue (voxel space only) "GM"
field Field file (blank = auto) None
analyze_cortex(atlas, region, visualize) Cortical (atlas region) analysis —
analyze_sphere(center, radius, coordinate_space, visualize) Spherical analysis —

run_group_analysis(subject_ids, simulation, space, analysis_type, ...) runs the same analysis across subjects, as the Analyzer’s multi-subject mode does.

Group statistics

In the GUI you define a comparison on the Statistics page and press Run. That is GroupComparisonConfig plus run_group_comparison.

from tit.stats import GroupComparisonConfig, run_group_comparison

cfg = GroupComparisonConfig(
    analysis_name="active_vs_sham",
    subjects=GroupComparisonConfig.load_subjects("subjects.csv"),
    test_type=GroupComparisonConfig.TestType.UNPAIRED,
)
res = run_group_comparison(cfg)
Field GUI label Default
analysis_name Analysis name required
subjects Subject table (or a CSV via load_subjects) required
test_type Test type (paired / unpaired / one-sample) UNPAIRED
alternative Alternative hypothesis TWO_SIDED
cluster_stat Cluster statistic (mass / size) MASS
cluster_threshold Cluster-forming threshold 0.05
n_permutations Permutations 1000
alpha Alpha 0.05
tissue_type Tissue GREY
space Space (MNI or fsaverage) MNI

The JSON config interface

The app never calls these functions in-process — it writes the serialised config to JSON and runs the module. You can run the exact same thing:

simnibs_python -m tit.sim        config.json
simnibs_python -m tit.opt.flex   config.json
simnibs_python -m tit.opt.ex     config.json
simnibs_python -m tit.opt.mex    config.json
simnibs_python -m tit.analyzer   config.json
simnibs_python -m tit.stats      config.json
simnibs_python -m tit.pre        config.json

Config files are written by tit.config_io.write_config_json().

AI coding agents

Full guide: AI Assistant.

If you write scripts with an AI assistant, install the TI-Toolbox agent plugin: it gives the assistant this wiki, the tit source and a read-only view of your project through an MCP server, so it stops guessing API fields. In Claude Code: /plugin marketplace add idossha/TI-Toolbox then /plugin install ti-toolbox@ti-toolbox.