ChromIQ stores each profiling project as a folder under
~/ChromIQ/<target-name>/ (or the custom output path). The structure and all
path construction are owned by core/file_manager.py via three classes:
Project, Run, and Calibration.
<target-name>/ # = sanitised project name (spaces → hyphens)
project.json # manifest (schema_version 2 since #127)
Where are my files.txt # folder guide (from ui.file_guide)
cal/ # optional; one calibration shared by all runs
<target-name>-cal.ti1 / .ti2 / .cht / .ps
<target-name>-cal_NN.tif
<target-name>-cal.ti3 # calibration measurement
<target-name>-cal.cal # printcal output (stem-coupled to -cal.ti3)
<target-name>-cal.icc
<target-name>-cal.channels.json
exports/ # the cal chart's hand-off sidecars (#127)
meta.json
exports/ # Tools-menu exports (project-wide)
<target-name>-i1profiler.txt
<target-name>-i1profiler.pxf
runs/
run1/ # one folder per profile build
<target-name>.ti1 / .ti2 / .cht / .cie / .ps / .pdf / .channels.json / .strips.json
<target-name>_NN.tif # NN = page index (a real ordinal, not state)
<target-name>.ti3 # the measurement (chartread output)
<target-name>.icc # the profile (colprof output)
preconditioning.ti3 / .icc # role-named; seeded from a parent run
merged.ti3 / merged.icc # role-named; build-time refinement merge
calibrated.icc # role-named; applycal output
reads/ # present only when averaging is used
read1.ti3 / read2.ti3 / …
reports/ # #127: things ChromIQ tells the user
Quality_Check_N_<name>.txt / Refine_Strips_<name>.txt / report_*.json
exports/ # #127: hand-off files for other programs
<name>-colours.txt / <name>-i1profiler.txt / .pxf
cache/ # #127: tool intermediates — always safe to delete
*-patchbox.cht / *-sample.cht / *-diag.tif
meta.json
run2/ …
The v2 rule (#127): everything a user prints, installs or measures stays
at the run root (the Argyll tools are stem+cwd coupled there — chartread runs
with cwd = ti1.parent, colprof with cwd = ti3.parent, printtarg writes next
to the .ti1); the ChromIQ-only paperwork lives in three sub-folders with a
fixed vocabulary: reports/ (quality checks, refine lists, measurement
reports), exports/ (hand-off files, same name and meaning as the
project-level folder), cache/ (intermediates any tool can recreate —
deleting never loses data). External folders (a browsed .ti3, a scan
directory) get the same sub-folders via the module-level helpers
reports_subdir() / exports_subdir() / cache_subdir() +
ensure_subdir() (which falls back to the parent on read-only volumes).
The chart's own files (.ti1/.ti2/.ti3/.icc/page TIFFs/.cht/.ps/
.channels.json) carry the sanitised project name as their stem — so
printtarg stamps it on the printed sheet, the built ICC is self-identifying in
ColorSync and Finder, and the install path in the Check tab copies it to the
system folder as <target-name>.icc. The derived/intermediate files stay
role-named (reads/readN.ti3, preconditioning.*, merged.*,
calibrated.icc) — they never go on a printed sheet and the role name is
clearer at a glance than <target-name>_merged.ti3 would be.
Calibration uses <target-name>-cal so its printed sheet shows the project
name (you can tell which project the cal target belongs to) AND is
distinguishable from the profiling chart, which lives in runs/<id>/ under
the bare project name.
Role is encoded in the filename within a folder; the folder disambiguates
context. runs/run1/chart.ti3 and runs/run2/chart.ti3 are the same role in
different runs. There are no pre_/cal_ prefixes and no
_readN/_average/_merged suffixes — the folder boundary does that work.
This kills several whole bug classes that the old flat-folder + prefix/suffix scheme allowed:
- Cross-run averaging collision — run 2's
reads/cannot see run 1's, so averaged reads can never be double-counted into a refinement merge. - Suffix-stem collisions — no
set_ti3_pathstripping of_average/_readNto recover a.ti2;chart.ti3sits next tochart.ti2. .json-as-CGATS confusion — pre-conditioning data is a realpreconditioning.ti3, not CGATS hidden under a.jsonname.
chartread and colprof are stem-coupled: reading chart.ti2 writes
chart.ti3; reading chart.ti3 writes chart.icc. Naming the measurement
measurement.ti3 or the profile profile.icc would force a rename after every
tool — reintroducing exactly the stem fragility this layout removes. So the
canonical measurement is chart.ti3 and the profile is chart.icc. The
per-run folder supplies the role context the generic stem lacks.
A refinement merge is the exception that proves the rule: average -m writes a
throwaway merged.ti3, colprof builds merged.icc from it, and
Run.built_profile_icc() returns merged.icc when present (else chart.icc).
Check & Refine always works from the clean chart.ti3 (Architecture D).
| Class | Responsibility |
|---|---|
Project |
project.json manifest; create / load / create_or_load; current_run(), new_run(preconditioning_from=…), all_runs(); calibration, exports_dir |
Run |
every path in a run folder (chart_ti1, measurement_ti3 → chart.ti3, profile_icc → chart.icc, merged_ti3/_icc, calibrated_icc, preconditioning_ti3/_icc); reads(), next_read_path(), promote_measurement_to_read(), clear_reads(), reset_chart_artefacts(), built_profile_icc() |
Calibration |
the cal/ folder (cal_path, ti1/.ti2/.ti3/.icc, chart_tiffs(), exists(), reset()) |
Two entry points:
FileManager.project()— the current target'sProject(created on first access, cached untilset_target_namechanges).Run.for_dir(run_dir)— a project-lessRunbound to an explicit folder, for path operations where threading the wholeProjectthrough isn't worth it (e.g. the Measure tab deriving the run from the chart's.ti1parent).
Never build a working-folder path by string concatenation outside these
classes. Adding a new artefact means adding a property to Run (and, if it
should be wiped on chart regeneration, listing it in reset_chart_artefacts).
- First profile —
Project.createmakesrun1; chart_creator runs inruns/run1/with stemchart; chartread writeschart.ti3; colprof writeschart.icc. - Calibration target —
cal_target=Trueroutes chart_creator tocal/with stemcalibration; detection elsewhere is "ti3 lives incal/". - Averaging — "Measure again" moves
chart.ti3→reads/readN.ti3; "Average all reads" runsaverage reads/*.ti3back intochart.ti3. - Pre-conditioning refinement — "Use as pre-conditioning profile" makes
tab_chart capture the parent run id; at Generate-click,
Project.new_run(preconditioning_from=parent)copies the parent'schart.ti3/chart.iccinto the new run aspreconditioning.ti3/.iccand makes it current. Build-time merge →merged.ti3→merged.icc. - External
-cprofile —chart_creator._import_external_preconditioningcopies a vendor.icc(and sibling.ti3when refinement is on) into the run aspreconditioning.*. - Session restore — only
session_target_nameis persisted; every artefact path is re-derived fromproject.current_run().
project.json carries a schema_version; the current format is 2
(SCHEMA_VERSION in core/file_manager.py).
- 1 → 2 (#127):
Project.loadmigrates in place — creates thereports//exports//cache/sub-folders per run (andcal/exports/) and moves only files matching the exact patterns ChromIQ itself writes (_MIG_REPORTS/_MIG_CACHE+ the exact sidecar names). User files are never touched, nothing is renamed or deleted, the chart chain is explicitly protected (even for project names ending in a pattern tail, e.g. "X-sample"). Idempotent: a crash mid-migration is healed by loading again. The README is regenerated (the one deliberate overwrite). In-place loads from a.ti2/.txtalso migrate —_handle_inside's Continue branch runsProject.load. - Too-new schema: the project opens normally with
Project.schema_too_new = True(valuables sit in the same place in every format), and the main window shows a "please update ChromIQ" note at session restore. Never downgradeschema_version. - Pre-project-json folders (the pre-redesign flat layout): still no
migration — session restore skips them (the
project.jsonexistence check bails); the user starts fresh.
The full migration matrix is tested in tests/test_folder_layout_v2.py
against the frozen schema-1 fixture in tests/golden/project_v1/ (generated
by make_v1_fixture.py against the pre-#127 code — do not regenerate it with
current code). The golden folder-manifest tests in
tests/test_folder_layout_e2e.py pin the complete v2 tree per workflow
stage.
-
README backfill doesn't run on in-place loads.Fixed in #127 — the "Continue" branch of_handle_inside(both loaders) now callsProject.loadon the discovered root, which also runs the v1→v2 migration for in-place loads. -
.txtsaved into a project'sexports/folder. If a user saves an i1Profiler measurement.txtdirectly into<project>/exports/(instead of somewhere outside that ChromIQ then imports),resolve_txtsees it as "inside the project" and "Continue" runstxt2ti3inexports/— producing<stem>.ti3and (via Build Profile)<stem>.iccinexports/, outside the runs structure. Unlikely workflow (i1Profiler doesn't default to writing into ChromIQ'sexports/), but if it ever bites, the fix is to special-caseparent.name == "exports"in_handle_insideand route to the import flow instead.