catalog
tit.catalog ¶
Project catalog: subject and simulation discovery for the UI.
Built only on :class:tit.paths.PathManager (plus the existing per-domain
helpers it composes: :mod:tit.sim.utils montage I/O, :mod:tit.atlas
discovery, :mod:tit.opt.ex.roi/:mod:tit.opt.leadfield ROI and leadfield
helpers, :mod:tit.opt.flex.manifest); the server routes
(tit.server.routes.catalog, tit.server.routes.catalog_v1) serialise
these dicts as-is, so the BIDS / derivative layout rules are never
re-implemented outside tit.
v1 additions (this module is extended, not replaced, for Stage 1 / lane B2 —
see docs/dev/DECISIONS.md § 2026-08-27 (Electron and the job server) §3 "Catalog"): subject/simulation detail,
montage CRUD, EEG nets, atlases + regions, ROI CRUD, leadfields, flex/ex/mex
runs, analyses, reports, freehand configs, the project-level group catalog,
Quick Notes, and the subject-info presence matrix. A run/analysis directory
without the manifest file that marks it complete (flex_meta.json,
run_config.json, or the analysis.json+results.csv pair) is
ignored, so a cancelled or in-progress run never appears as a result.
subject_ids ¶
subject_ids(pm: PathManager) -> list[str]
Union of raw-BIDS, FastSurfer, legacy-FreeSurfer and m2m subjects.
Naturally sorted. derivatives/freesurfer is still unioned in so a
project processed before FastSurfer replaced recon-all keeps listing
its subjects.
Deliberately excludes subjects known only from sourcedata/ (DICOMs
staged, nothing converted yet) -- this is the onboarded set every other
catalog function gates on (montages, ROIs, EEG nets, ...), where a
subject with no derivative of any kind would be meaningless. See
:func:sourcedata_only_subject_ids and, for the two routes that do need
to see a not-yet-onboarded subject, :func:list_subjects /
:func:subject_detail.
Source code in tit/catalog.py
sourcedata_only_subject_ids ¶
sourcedata_only_subject_ids(pm: PathManager) -> list[str]
Subject ids with raw data staged under sourcedata/ and nowhere else.
:func:subject_ids would never mention these -- no BIDS directory, no
m2m, no FastSurfer/FreeSurfer -- so a project's own newly-arrived DICOMs
(Dataset 000's sub-102, before its DICOM-conversion stage has ever
run) were invisible to every page built on the catalog (lane FX5,
docs/dev/DECISIONS.md § 2026-09-03 (One Docker image and a real development loop)). Naturally sorted; a subject that
already appears in :func:subject_ids is never repeated here even if
sourcedata/ also holds a copy of its DICOMs.
Source code in tit/catalog.py
list_subjects ¶
list_subjects(pm: PathManager) -> list[dict]
One entry per subject: presence flags and the simulation count.
Also lists subjects known only from sourcedata/ (DICOMs staged, no
BIDS directory yet -- see :func:sourcedata_only_subject_ids): the
Subjects page and Pre-processing's own subjects table are exactly where
a project's newest subject needs to be plannable, before anything else
about them exists. An entry carries has_sourcedata: True when staged
raw data is present (so a client can tell "staged, not yet converted"
from has_raw False); the key is left out entirely otherwise -- the
overwhelming common case (a subject with no sourcedata copy at all), and
the shape every existing caller/test built before this field existed
stays byte-identical (/api/catalog/subjects is served with
response_model_exclude_unset, tit/server/routes/catalog.py).
Source code in tit/catalog.py
list_simulations ¶
list_simulations(pm: PathManager, sid: str) -> list[dict] | None
Simulations of sid (TI/ and mTI/ presence), None if unknown.
Source code in tit/catalog.py
output_files ¶
Every file under a run's output folder as {path, kind, label, bytes} rows.
The one filesystem read behind a finished job's Artifacts tab
(GET /api/jobs/{id}/artifacts): what is on disk, not what a runner remembered to
register. label is the path relative to root; bytes is the file size (None
if it cannot be stat'ed). Same bounded walk and project jail as :func:_dir_artifacts.
Source code in tit/catalog.py
classify_view_file ¶
What path is, as one of the seven kinds a scene understands — or None.
volume · label-volume · surface · mesh · annotation · morph ·
surface-data.
Decided from the name and its neighbours only. No file is opened: this runs once per row
of a menu that is redrawn on every keystroke, and a menu that reads its way through a
FreeSurfer surf/ directory is a menu that stutters. The one filesystem touch is
:func:_has_lut_sidecar, a single os.path.isfile on a sibling.
None means "not something a scene can use" and is returned for everything unrecognised —
.mat, .txt, .geo, .sigma, .label, .ctab, a log, and the derived FreeSurfer files
(lh.pial.T1, lh.smoothwm.K.crv) that are recon-all's bookkeeping rather than anyone's
input. Refusing to guess is the point: a file offered under the wrong kind fails at Open,
which is a much worse moment to find out than not being offered at all.
Source code in tit/catalog.py
surface_attachments ¶
surface_attachments(surface_path: str, *, extra_dirs: tuple[str, ...] = (), project_root: str | None = None) -> list[str]
Every annotation / morph / data-GIfTI file that belongs on surface_path.
Matched by hemisphere and nothing else, because that is the only correspondence FreeSurfer
actually promises: lh.thickness has one value per vertex of every lh.* surface, since
they all share a vertex numbering. Which lh. surface you hang it on is the viewer's choice,
not a fact about the file. (Tetravox's own worker checks the vertex count when the file is
attached, so a genuine mismatch is refused there with both counts named — this side does not
need to read a byte to be safe, only to be plausible.)
Searched in the surface's own directory plus extra_dirs — SimNIBS keeps the geometry in
m2m_<sid>/surfaces/ but writes the parcellations it made into m2m_<sid>/segmentation/,
two directories apart, which is exactly why nothing offered them before.
Source code in tit/catalog.py
subject_detail ¶
subject_detail(pm: PathManager, sid: str) -> dict | None
Full detail for one subject, None if unknown.
"Unknown" also admits a sourcedata-only subject (see
:func:sourcedata_only_subject_ids) -- the Subjects page fetches this
for every row :func:list_subjects returns, sourcedata-only rows
included, and a 404 there would just blank the detail pane rather than
error, but there is real information to return (has_sourcedata) so
it is returned rather than dropped.
Source code in tit/catalog.py
simulation_detail ¶
simulation_detail(pm: PathManager, sid: str, sim: str) -> dict | None
Full detail for one simulation, None if unknown.
Source code in tit/catalog.py
simulation_figures ¶
simulation_figures(pm: PathManager, sid: str, sim: str) -> list[dict] | None
Pictures a simulation run saved of itself; None if subject/simulation is unknown.
Today that is the montage visualisation tit.tools.montage_visualizer writes as
<sim>/<TI|mTI>/montage_imgs/<name>_highlighted_visualization.png -- the EEG net with
this run's electrodes highlighted. The Results pane shows it beside the channel chips,
which name the same montage in text, and in the run's Figures grid.
Not folded into SimulationDetail: that response has a Pydantic response_model
generated from the frozen v1 contract, so a new field there is a contract change. This is
the same shape as :func:electrode_overlays above -- a small presence query beside the
main read.
Source code in tit/catalog.py
electrode_overlays ¶
electrode_overlays(pm: PathManager, sid: str, sim: str) -> list[dict] | None
Electrode-overlay NIfTI presence for one simulation, per TI/mTI mode.
None if the subject/simulation is unknown. This is the listing half
of the electrode-overlay v1 gap documented in
tit/server/routes/viewers.py and pages/viewer/PARITY.md #1:
creating the overlay is a tools job
(tit.tools.electrode_overlay), and once it exists on disk,
:func:tit.viewspec._electrode_overlay_layer already picks it up for
kind=simulation ViewSpecs -- this endpoint just tells the Viewer page
whether that file is there yet (and its path), without a separate job
kind or catalog change for the overlay-building side.
Source code in tit/catalog.py
get_montages ¶
get_montages(pm: PathManager) -> dict
montage_list.json, reshaped to the v1 Montages schema.
Reads the explicitly selected project's montage file through the shared writer API.
Source code in tit/catalog.py
put_montage ¶
put_montage(pm: PathManager, net: str, kind: str, name: str, pairs: list[list[str]]) -> list[list[str]]
Create or overwrite one montage; returns pairs back.
Source code in tit/catalog.py
delete_montage ¶
delete_montage(pm: PathManager, net: str, kind: str, name: str) -> bool
Delete one montage; False if it did not exist.
Source code in tit/catalog.py
eeg_nets ¶
eeg_nets(pm: PathManager, sid: str) -> list[dict] | None
EEG nets available to sid, with electrode labels; None if unknown.
Source code in tit/catalog.py
atlases ¶
atlases(pm: PathManager, sid: str, space: str | None = None, kind: str | None = None) -> list[dict] | None
Atlases available to sid; None if the subject is unknown.
Every entry carries kind: "surface" for a FreeSurfer .annot
parcellation, "volume" for a label volume. For MNI space the list, its
order and each entry's kind, template and licence come from
resources/atlas/manifest.json (:mod:tit.atlas.manifest) rather than
from a filename list, so an atlas is only ever offered to the targeting mode
that can read it: the cortical mode asks for kind="cortical" and gets
surfaces, the subcortical mode asks for kind="subcortical" and gets
volumes. Before the manifest existed nothing recorded the kind of an MNI
atlas and the subcortical mode was handed every shipped MNI file whatever it
was.
Source code in tit/catalog.py
966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 | |
atlas_regions ¶
atlas_regions(pm: PathManager, sid: str, atlas_id: str, hemi: str | None = None) -> list[dict] | None
Regions of atlas_id for sid; None if the subject/atlas is unknown.
Per the reconciled v1 Region schema: for a cortical (surface/annotation)
atlas, id is the FreeSurfer .annot label index within that
region's own hemisphere file -- exactly the integer
FlexConfig.AtlasROI.label / ExConfig's equivalent needs, resolved
the same way :func:tit.opt.roi_spec.resolve_cortical_region_index_map
does -- and hemi is that region's hemisphere. For a subcortical
(volumetric) atlas, id is the voxel label value and hemi is
None (a region whose label cannot be parsed as an integer is dropped
rather than returned with a non-conforming id).
Source code in tit/catalog.py
1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 | |
nifti_labels ¶
nifti_labels(pm: PathManager, sid: str, path: str | None = None) -> list[dict] | None
Unique integer labels present in one label volume, named where a LUT applies.
The 3D Visual Exporter's sub-cortical mode asks the user for label numbers
(10,49); 2.5.0 answered that with a Qt dialog that parsed a FreeSurfer LUT
itself. This is that browser's data, from the toolbox's own segstats path
(:func:tit.atlas.segstats.compute_segstats +
:func:~tit.atlas.segstats.resolve_lut_for_atlas), which also writes the
<name>_labels.txt sidecar cache beside the volume -- so the first call on a
subject pays for the scan and every later one reads the cache, same as
:meth:VoxelAtlasManager.list_regions.
Parameters¶
pm : PathManager
sid : str
Subject whose m2m holds the default volume.
path : str or None
A specific label volume. None/empty means the subject's own
<m2m>/segmentation/labeling.nii.gz -- the sub-cortical mode's default,
and the only path the panel ever sends for an untouched form.
Returns¶
list of dict or None
[{"id": int, "name": str, "n_voxels": int}] sorted by id; None
for an unknown subject, a path outside the project jail, or a file that is
not there. The caller (routes/catalog_v1) turns None into a 404 --
deliberately one status for all three, so probing this route cannot tell a
file that exists outside the jail from one that does not exist at all.
Source code in tit/catalog.py
1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 | |
is_safe_name ¶
True if name is safe to use as a filename component.
Used for ROI names, free-hand config names, and montage names on every write path -- never trust a client-supplied string used to build a path.
Source code in tit/catalog.py
as_float ¶
Coerce value to float; raises ValueError naming the field on failure.
bool is rejected even though isinstance(True, int) is True in
Python -- a checkbox value has no business landing in a coordinate.
Source code in tit/catalog.py
list_rois ¶
list_rois(pm: PathManager, sid: str) -> list[dict] | None
Saved ROIs for sid; None if the subject is unknown.
Source code in tit/catalog.py
create_roi ¶
create_roi(pm: PathManager, sid: str, roi: dict) -> dict
Save a new spherical ROI centre for sid (subject-space CSV convention).
Mirrors :meth:tit.opt.ex.engine.ExSearchEngine.create_roi (a plain
x,y,z row plus a roi_list.txt entry) rather than importing it, so
this module stays the single writer of the CRUD shape the v1 contract
expects (radius/space are accepted but not persisted -- the CSV format
has never carried them; see this lane's final report).
Source code in tit/catalog.py
delete_roi ¶
delete_roi(pm: PathManager, sid: str, name: str) -> bool
Delete one saved ROI; False if it did not exist (also if name is unsafe).
Source code in tit/catalog.py
list_leadfields ¶
list_leadfields(pm: PathManager, sid: str) -> list[dict] | None
Precomputed leadfields for sid; None if the subject is unknown.
Source code in tit/catalog.py
flex_runs ¶
flex_runs(pm: PathManager, sid: str) -> list[dict] | None
Flex-search runs for sid; None if the subject is unknown.
Source code in tit/catalog.py
ex_runs ¶
ex_runs(pm: PathManager, sid: str, kind: str = 'ex') -> list[dict] | None
Ex/mEx-search runs for sid; None if the subject is unknown.
Source code in tit/catalog.py
ex_run_results ¶
ex_run_results(pm: PathManager, sid: str, kind: str, run: str) -> dict | None
Full final_output.csv of one ex/mEx run, as TableData.
Source code in tit/catalog.py
analyses ¶
analyses(pm: PathManager, sid: str, sim: str) -> list[dict] | None
Analyzer runs for sid/sim; None if either is unknown.
Source code in tit/catalog.py
analysis_summary ¶
analysis_summary(pm: PathManager, sid: str, sim: str, name: str) -> dict | None
results.csv of one analysis, as TableData.
name is what :func:analyses listed; a caller holding the run's own path relative
to the simulation (Analyses/Custom/run1) resolves too, so the one string a script
already has does not have to be translated into the listed form.
Source code in tit/catalog.py
reports ¶
reports(pm: PathManager, sid: str) -> list[dict] | None
Generated HTML reports for sid; None if the subject is unknown.
Source code in tit/catalog.py
find_report_path ¶
find_report_path(pm: PathManager, report_id: str) -> str | None
Resolve a reports() id back to its file path (used by /api/files/report).
Source code in tit/catalog.py
freehand_configs ¶
freehand_configs(pm: PathManager, sid: str) -> list[dict] | None
Saved free-hand electrode configs for sid; None if unknown.
Reads m2m_<id>/stim_configs/*.json. The on-disk format, written by the
free-hand electrode placement UI, is
{"name", "type": "U"|"M", "electrode_positions": {label: [x, y, z]}}.
The contract's FreehandConfig.type enum is [U, M] (unipolar/
multipolar), matching this on-disk value exactly (fixed from an earlier
xyz/label enum that matched nothing real -- see
contracts/CHANGES.md); type is passed through verbatim.
Source code in tit/catalog.py
put_freehand_config ¶
put_freehand_config(pm: PathManager, sid: str, name: str, config: dict) -> dict
Create or overwrite one free-hand electrode configuration.
Source code in tit/catalog.py
delete_freehand_config ¶
delete_freehand_config(pm: PathManager, sid: str, name: str) -> bool
Remove a saved placement definition, leaving simulation outputs intact.
Source code in tit/catalog.py
group_catalog ¶
group_catalog(pm: PathManager) -> dict
Project-level group catalog: stats runs, nilearn visuals, group analyses.
Directory conventions for the nilearn and group_analyses sections
are a best-effort guess (derivatives/ti-toolbox/{nilearn_visuals,
group_analysis}/) -- unverified against a real project, since Dataset
000 has neither populated. stats (derivatives/ti-toolbox/stats/
<type>/<name>/) is confirmed by :meth:PathManager.stats_output.
Source code in tit/catalog.py
group_stats_detail ¶
group_stats_detail(pm: PathManager, analysis_type: str, name: str) -> dict | None
One derivatives/ti-toolbox/stats/<type>/<name>/ run, read for the Results pane.
None when the run directory does not exist. status is "ok" once the run has
written something besides its log, and "empty" when it has not -- the state the
maintainer hit, where a failed 2-vs-1 group comparison left a bare .log and the pane
could say nothing about it. reason is then the log's own ERROR line, so the UI
reports why instead of showing an empty file list.
Source code in tit/catalog.py
2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 | |
read_notes ¶
read_notes(pm: PathManager) -> dict
Quick Notes content (derivatives/ti-toolbox/notes.txt).
Source code in tit/catalog.py
write_notes ¶
write_notes(pm: PathManager, text: str) -> dict
Replace Quick Notes content, atomically.
Source code in tit/catalog.py
subject_info_matrix ¶
subject_info_matrix(pm: PathManager) -> dict
Presence matrix (subject x data-stage) across the whole project.