Skip to content

sim

tit.sim

TI/mTI simulation public API.

BaseSimulation

BaseSimulation(config: SimulationConfig, montage: Montage, logger)

Bases: ABC

Abstract base class for TI/mTI simulations.

Subclasses must implement :pyattr:_simulation_mode, :pyattr:_montage_type_label, :pyattr:_montage_imgs_key, :meth:_build_session, and :meth:_post_process.

Source code in tit/sim/base.py
def __init__(self, config: SimulationConfig, montage: Montage, logger):
    self.config = config
    self.montage = montage
    self.logger = logger
    self.pm = get_path_manager()
    self.m2m_dir = self.pm.m2m(config.subject_id)

run

run(simulation_dir: str) -> dict

Execute the full simulation pipeline. Returns a result dict.

Source code in tit/sim/base.py
def run(self, simulation_dir: str) -> dict:
    """Execute the full simulation pipeline. Returns a result dict."""
    montage_dir = self.pm.simulation(
        self.config.subject_id,
        self.montage.name,
    )
    dirs = setup_montage_directories(montage_dir, self._simulation_mode)
    create_simulation_config_file(
        self.config, self.montage, dirs["documentation"], self.logger
    )

    viz_pairs = None if self.montage.is_xyz else self.montage.electrode_pairs

    run_montage_visualization(
        montage_name=self.montage.name,
        simulation_mode=self._simulation_mode,
        eeg_net=self.montage.eeg_net,
        output_dir=dirs[self._montage_imgs_key],
        project_dir=self.config.project_dir,
        logger=self.logger,
        electrode_pairs=viz_pairs,
    )

    self.logger.info("SimNIBS simulation: Started")
    run_simnibs(self._build_session(dirs["hf_dir"]))
    self.logger.info("SimNIBS simulation: \u2713 Complete")

    output_mesh = self._post_process(dirs)
    self.logger.info(f"\u2713 {self.montage.name} complete")

    return {
        "montage_name": self.montage.name,
        "montage_type": self._montage_type_label,
        "status": "completed",
        "output_mesh": output_mesh,
    }

Montage dataclass

Montage(name: str, mode: MontageMode, electrode_pairs: list[tuple], eeg_net: str | None = None)

Unified montage: EEG-cap labels or 3-D XYZ coordinates.

parse_intensities

parse_intensities(s: str) -> list[float]

Parse comma-separated intensity string into list of floats.

Source code in tit/sim/config.py
def parse_intensities(s: str) -> list[float]:
    """Parse comma-separated intensity string into list of floats."""
    v = [float(x.strip()) for x in s.split(",")]
    n = len(v)
    if n == 1:
        return [v[0], v[0]]
    if n >= 2 and n % 2 == 0:
        return v
    raise ValueError(
        f"Invalid intensity format: expected 1 or an even number of values; got {n}: {s!r}"
    )

run_simulation

run_simulation(config: SimulationConfig, logger=None, progress_callback: Callable[[int, int, str], None] | None = None) -> list[dict]

Run TI/mTI simulations sequentially. Mode is auto-detected per montage. Montages are read from config.montages. Returns list of result dicts: montage_name, montage_type, status, output_mesh.

Source code in tit/sim/utils.py
def run_simulation(
    config: SimulationConfig,
    logger=None,
    progress_callback: Callable[[int, int, str], None] | None = None,
) -> list[dict]:
    """
    Run TI/mTI simulations sequentially. Mode is auto-detected per montage.
    Montages are read from ``config.montages``.
    Returns list of result dicts: montage_name, montage_type, status, output_mesh.
    """
    if logger is None:
        pm = get_path_manager()
        log_dir = pm.logs(config.subject_id)
        os.makedirs(log_dir, exist_ok=True)
        log_file = os.path.join(
            log_dir, f'Simulator_{time.strftime("%Y%m%d_%H%M%S")}.log'
        )
        logger = _make_file_logger("tit.sim", log_file)

    pm = get_path_manager()
    simulation_dir = pm.simulations(config.subject_id)

    from tit.sim.TI import TISimulation
    from tit.sim.mTI import mTISimulation

    montages = config.montages
    results = []
    total = len(montages)
    for idx, montage in enumerate(montages):
        logger.info(
            f"[{idx+1}/{total}] {montage.simulation_mode.value}: {montage.name}"
        )
        if progress_callback:
            progress_callback(idx, total, montage.name)
        cls = (
            TISimulation
            if montage.simulation_mode == SimulationMode.TI
            else mTISimulation
        )
        results.append(cls(config, montage, logger).run(simulation_dir))
    if progress_callback:
        progress_callback(total, total, "Complete")
    return results

list_montage_names

list_montage_names(project_dir: str, eeg_net: str, *, mode: str) -> list[str]

mode: 'U' or 'M'. Returns [] for missing nets.

Source code in tit/sim/utils.py
def list_montage_names(project_dir: str, eeg_net: str, *, mode: str) -> list[str]:
    """mode: 'U' or 'M'. Returns [] for missing nets."""
    data = load_montage_data(project_dir)
    net = data.get("nets", {}).get(eeg_net, {})
    key = "uni_polar_montages" if mode.upper() == "U" else "multi_polar_montages"
    return sorted(net.get(key, {}).keys())

ensure_montage_file

ensure_montage_file(project_dir: str) -> str

Return path to montage_list.json, creating it with default schema if absent.

Source code in tit/sim/utils.py
def ensure_montage_file(project_dir: str) -> str:
    """Return path to montage_list.json, creating it with default schema if absent."""
    path = _montage_list_path(project_dir)
    os.makedirs(os.path.dirname(path), exist_ok=True)
    if not os.path.exists(path):
        with open(path, "w") as f:
            json.dump({"nets": {}}, f, indent=4)
    return path

upsert_montage

upsert_montage(*, project_dir: str, eeg_net: str, montage_name: str, electrode_pairs: list[list[str]], mode: str) -> None

mode: 'U' → uni_polar_montages, 'M' → multi_polar_montages

Source code in tit/sim/utils.py
def upsert_montage(
    *,
    project_dir: str,
    eeg_net: str,
    montage_name: str,
    electrode_pairs: list[list[str]],
    mode: str,
) -> None:
    """mode: 'U' → uni_polar_montages, 'M' → multi_polar_montages"""
    data = load_montage_data(project_dir)
    net = data["nets"].setdefault(
        eeg_net, {"uni_polar_montages": {}, "multi_polar_montages": {}}
    )
    key = "uni_polar_montages" if mode.upper() == "U" else "multi_polar_montages"
    net[key][montage_name] = electrode_pairs
    save_montage_data(project_dir, data)