Skip to content

Latest commit

 

History

History
425 lines (350 loc) · 28.6 KB

File metadata and controls

425 lines (350 loc) · 28.6 KB

CME codebase audit

Assessment of the repository as of 2026-07-27, covering 11 Python modules (~180 KB, ~4,000 lines) plus 13 screenshots. Line references are to the pre-uplift master (6067f9f); the modules have since moved into src/cme/ and been renamed, so use the descriptions rather than the line numbers to locate anything.

Note

Most of this has now been addressed on the uplift/modernise-and-fix branch. See Resolution status at the end for what was fixed, what was found during the work, and what is still outstanding. The findings are kept in full because they are the rationale for the changes and the reference for the regression tests.

Verdict

CME is a well-scoped piece of scientific tooling — the pipeline it automates is genuinely intricate, and the tabbed workflow maps onto it sensibly. But as checked out it cannot run, and it has not been able to run on a stock environment for roughly a decade. The important structural problem is not the age of the dependencies; it is that every piece of science and every file-format writer is welded to a GUI event handler, so nothing can be tested, scripted, or reused without a display and a cluster.

The first decision is not technical: decide whether this repository is an archive or a project. The two paths diverge immediately, and most of the work below is only worth doing on the second one. Sections 1–5 are the findings; section 6 proposes a reorganisation and section 7 a prioritised plan.


1. Blocking issues — the code cannot execute

# Issue Evidence
1.1 The modules package is missing from the repository. Every module imports it, directly or via Common.py's star import. There is no vendored copy, submodule, or install instruction. Common.py:31-36, ics.py:5, contam.py:2, mergertree.py:2, gadgetrun.py:2
1.2 Python 2 only. 4 of 11 modules fail to parse on Python 3 (print statements, xrange). gadgetrun.py:291, halos.py:400, ics.py:255, install.py:50
1.3 Retired Traits namespace. The enthought.* namespace packages were dropped in ETS 4 (2011). No current release provides enthought.traits.api or enthought.traits.ui.wx.editor. Common.py:5-16
1.4 wxPython backend. matplotlib.backends.backend_wxagg and the NavigationToolbar2Wx API used here predate matplotlib 2.x; TraitsUI has since moved to Qt as its practical default. Common.py:2-3, Common.py:63
1.5 The Install tab builds from a lib/installs/ tree that is not in the repository. Every install button chdirs into a hardcoded relative path (./lib/installs/fftw-2.1.5/ etc.) that does not exist. install.py:49,95,141,157,170,184

Items 1.1 and 1.5 mean that even a perfect Python 2.7 + EPD environment would not get you a working application.


2. Correctness defects

These are independent of the environment problems — they are bugs in the logic as written. Grouped by confidence.

2.1 Certain (would raise, or silently do nothing)

# Defect Location
2.1.1 SLURM submission raises TypeError. SLURMcores, SLURMtime and SLURMmemory are integer traits concatenated directly with strings. The PBS path is the only known-good one. gadgetrun.py:320,323,325
2.1.2 The Gadget parameter sweep never executes. __init__ assigns self.nvir = [...] but the trait is nrvir; Traits silently creates a new attribute, leaving nrvir an empty list, so every for nrviri in self.nrvir loop body is skipped. gadgetrun.py:845 vs :103
2.1.3 Four state updates are no-ops== written where = was meant, so makeactive never changes and the affected controls stay disabled. contam.py:272,275,281,284
2.1.4 gethostid() raises UnboundLocalError when no host matches, but callers test if idhost: as though it returns a falsy value. halos.py:130-140, used at :188,215
2.1.5 gethalos_xy() raises NameError on the parent full-box path, because the 'pos' in halo_varx block reads idhost, which only exists in the zoom branch. The same function returns unbound x, y when dataexists is false. halos.py:220-224, :231
2.1.6 _plot_button_fired does nothing in parent mode. The entire plotting block is indented inside the elif self.parentorzoom == 'zoom' branch, so the parent branch computes dataexists and falls through. halos.py:234-285
2.1.7 The Odyssey cluster preset never applies. self.clustopt = 'odyssey' — typo for clusteropt; Traits accepts it as a new attribute, so the cluster stays at its default. header.py:75
2.1.8 Clear corrupts the ICs tab. self.main.mergertreetab.initstab = [] sets a stray attribute on the wrong object; the intent was self.main.initstab.haloid = []. candidates.py:148
2.1.9 Three MUSIC Poisson settings are never applied. __init__ writes self.pre_smooth, self.post_smooth and self.grad_order, but the traits are presmooth, postsmooth, gradorder. Compounding this, presmooth/postsmooth are declared as plain class integers rather than traits, yet the view builds Items for them. ics.py:698-699,705 vs :75-76,78
2.1.10 nhalo can be unbound, if the selected halo ID is absent from the candidate file. ics.py:533-535
2.1.11 determineboolstr returns unbound for any input that is not exactly True or False (e.g. a numpy bool). ics.py:889-895
2.1.12 A run script is opened before its directory is known to exist — the open(filepath + "/runscript") sits outside the else that guards on the path being found, so a missing path raises IOError after having already printed "PATH NOT FOUND". gadgetrun.py:290-314

2.2 Wrong results (runs, but the output is incorrect)

# Defect Location
2.2.1 3D halo distribution plots y twice instead of z: z = tmphalos['posY']. Every full-box Mayavi scene is projected into a plane. halos.py:369
2.2.2 Reported host x-position is actually its z-position: self.hostposx = host['posZ']. halos.py:201
2.2.3 A phantom zero-mass candidate contaminates every selection. candidatearr is initialised to np.zeros((1,6)) and results are vstacked onto it, so row 0 is always [0,0,0,0,0,0] — it is plotted, counted in the array view, and accepted by the "add halo" membership test. candidates.py:314,217,124-125
2.2.4 Lagrangian pointer filenames embed a Python list literal. 'NRVIR' + str(self.nrvir) on a List trait yields H190897NRVIR['1'] — visible in screenshots/constructICs.png. The actual file is written with int(self.nrvir[0]), so the displayed and real paths disagree. ics.py:340-341,346-347,352-353 vs :449,665,675
2.2.5 The SLURM execute line is built from the PBS core count, so a SLURM job launches with PBSncores ranks. gadgetrun.py:333
2.2.6 _SLURMcores_changed has two identical branches, so enabling SUBFIND does not switch SLURM to the _sub binary and param_sub.txt the way the PBS path does. gadgetrun.py:433-437

2.3 Dead or unreachable code

  • main.py:98-99_analysistab_default instantiates Analysis, whose import is commented out at main.py:8. Not currently reachable, but it is a latent NameError.
  • halos.py:57halo_type offers three values, but four branches are written; 'subs of ID' can never be selected (halos.py:180,300,364).
  • halos.py:54-55boxtype is declared twice; the second declaration silently wins.
  • gadgetrun.py:226 — the view builds Item('clusteropt') but the trait declaration is commented out at gadgetrun.py:5; the attribute only appears via assignment at :927.
  • candidates.py:44 (upper), :17 (id_select), Common.py:33 (readids) are never used.

3. Security and robustness

# Issue Detail
3.1 Command injection / breakage via string-concatenated shell commands. 70 call sites use os.system or subprocess.call(..., shell=True) with GUI-supplied paths interpolated in. A path containing a space silently splits; one containing ; or $(...) executes. install.py (all buttons), ics.py:257,325, gadgetrun.py:302,306,309,362,368,370,406
3.2 rm commands built from unvalidated paths. f1.write("rm " + str(filepath) + "/*.e*\n") and rm wnoise* temp* run in a directory derived from GUI state. If a path trait is empty, the glob widens. gadgetrun.py:374-375, ics.py:322
3.3 A fragile offset into a third-party file. tail -n+96 .../Config.sh assumes the P-Gadget3 config has exactly 95 lines of preamble. Any upstream change silently corrupts the generated config. gadgetrun.py:359,365
3.4 Personal identifiers hardcoded in source — 19 occurrences: two cluster login names, four specific hostnames, three absolute paths under one user's home directory, and two email addresses. Also halos.py:131, an absolute path baked into a method body. header.py:55-99, gadgetrun.py:9-10,123,257,263, halos.py:131
3.5 Every file handle is unprotected by with. ~15 open()/close() pairs leak on exception, and _getcandidates_button_fired can leave out open on any error mid-loop. ics.py:666,676,743,837, gadgetrun.py:253,312,657,664, candidates.py:175
3.6 open() used as an existence test, in a try/except IOError, rather than os.path.exists. Race-prone and obscures intent. ics.py:374-378,450-451,515-519, gadgetrun.py:813-817

4. Structural problems

4.1 The science is trapped inside event handlers

Every computation lives inside a _*_button_fired or _*_changed method that also clears a figure, sets axis labels, and calls wx.CallAfter. There is no function you can call to select candidates, extract a Lagrangian region, or emit a MUSIC config. Consequences:

  • Nothing can be unit tested — you need a display and a live parent simulation to exercise a mass cut.
  • Nothing can be batch-scripted. A 2,000-halo suite is driven by clicking.
  • Bugs like 2.1.6 (a mis-indented plotting block) are invisible, because the logic and the rendering are one blob.

This is the single highest-value thing to change, and it is the organising idea behind the layout proposed in section 6.

4.2 Massive duplication

Duplicated thing Occurrences Where
The H..._B..._Z..._P..._LN..._LX..._O..._NV... folder name 18 contam.py ×13, halos.py ×2, gadgetrun.py ×2, ics.py ×1
dircheck = ... + "/outputs/groups_0" + snapnum followed by an existence test 9 contam.py:209-284,426
The if self.projopt == 'xy' / 'xz' / 'yz' axis-selection block 3 ics.py:391-419,462-490,613-641
The 12-line softening cascade 2 gadgetrun.py:522-533,543-554
The mpirun -np ... ./P-Gadget3 ... execute-line builder 18 copies across 6 handlers, plus 2 in __init__ gadgetrun.py:415-453,928-929
The hardcoded Hubble parameter 0.6711 6 candidates.py:151,305, contam.py:93,316,385, halos.py:400
Near-identical _*_changed handlers that clear the figure 4 mergertree.py:85-123

contam.py is the clearest case: 8 of its 12 methods begin with the same 8-line folder-name construction followed by the same existence check. Extracting one run_dir_name() function and one outputs_exist() predicate removes roughly 150 lines and eliminates the class of bug where one copy is updated and the others are not.

The duplicated Hubble constant is a scientific correctness risk, not just untidiness: the value appears both as a trait (candidates.py:305) and as five hardcoded literals, one of which (halos.py:400) is inside a debug print.

4.3 Star imports and accidental dependencies

Common.py performs six star imports (enthought.traits.api, enthought.enable.api, matplotlib, grifflib, plus module-level numpy/wx/os) and every tab does from Common import *. Nothing declares what it actually uses.

The concrete hazard: ics.py uses patches.Rectangle (:422,495,646) without importing matplotlib.patches. It resolves only because from matplotlib import * at Common.py:19 runs after matplotlib.pyplot has been imported at :1, which populates matplotlib.patches as a side effect. Reorder those two lines and the ICs tab breaks.

Separately, ics.py:5 aliases a project module to re — shadowing the standard library re for that module.

4.4 Coupling through self.main

Each tab holds a back-reference to ApplicationMain and reaches through it to mutate siblings — candidates.py:135-139 writes into five other tabs. This makes the initialisation order load-bearing (main.py:78-105 must construct headertab before anything that reads headertab.datamasterpath, and candidatestab before the four tabs that copy its haloid list) and there is no mechanism enforcing it. Traits' own notification system (@on_trait_change, or a shared model object that tabs observe) is the idiomatic fix.

Note also that the halo sample is copied at construction time (self.haloidlist = self.main.candidatestab.haloid), which is why candidates.py has to manually push updates into each tab afterwards.

4.5 Performance

  • ics.py:564-601 re-reads the parent snapshot's entire POS and ID blocks, plus the IC file's POS/ID, once per nrvir value. For a 512³ parent that is ~4 GB of I/O per iteration, when one read outside the loop would serve all of them.
  • candidates.py:180-209 rebuilds np.array(np.float64(MWcand['posX'])) (and five siblings) inside the per-candidate loop, making the selection O(N²) in the base sample. Hoisting six lines makes it O(N).
  • candidates.py:223-237 re-plots the full candidate set on every loop iteration, because the plotting block sits inside the for loop rather than after it.
  • candidates.py:217 grows the result array with np.vstack per match — quadratic copying. Append to a list, np.array() once.

4.6 Style inconsistencies

  • gadgetrun.py is indented with 2 spaces; every other module uses 4.
  • Naming mixes lowercase (haloid), camelCase (lagrPos), PascalCase (InitCondFile, mirroring Gadget's parameter names — defensible), and UPPER_CASE traits (PMGRID).
  • if x == True: throughout, rather than if x:.
  • Deep view nesting: halos.py:100-104 reaches eight HGroup/VGroup/Group levels, several of them redundant single-child wrappers.
  • ~200 lines of commented-out code, including whole blocks (header.py:35-49, gadgetrun.py:491-512, ics.py:776-807, halos.py:425-431). Git history serves this purpose; the comments only obscure the live logic.

5. Project hygiene

Missing Impact
requirements.txt / pyproject.toml No reproducible environment. The dependency list must be reverse-engineered from imports — which is how the README's list was produced.
Any test No way to verify a refactor. Nothing is currently testable anyway (§4.1).
CI configuration Nothing catches a syntax error on commit.
LICENSE All rights reserved by default, so the code is not legally reusable despite the README inviting collaborators.
.gitignore __pycache__, *.pyc, and generated .conf/param.txt/rungadget.sh artifacts are all untracked-but-unignored. gadgetrun.py:253 writes rungadget.sh into the repository root.
Docstrings One in the entire codebase (main.py:64).
CHANGELOG 30+ commits are "Update README.md"; there is no record of behavioural change.

6. Proposed reorganisation

The layout follows from §4.1: pure computation and pure text generation move out of the GUI, into modules that can be imported, tested and scripted without a display.

cme/
├── pyproject.toml              # deps + packaging (replaces "install EPD")
├── README.md
├── AUDIT.md
├── LICENSE
├── .gitignore
├── clusters.toml               # cluster profiles — replaces hostname if-chains
├── src/cme/
│   ├── app.py                  # was main.py — window assembly only
│   ├── naming.py               # run_dir_name(), lagr_paths(), outputs_exist()
│   │                           #   ← the 18 duplicated folder-name builders
│   ├── cosmology.py            # named cosmologies + ONE Hubble constant
│   ├── config.py               # loads clusters.toml; no hostnames in code
│   ├── io/                     # the former `modules` package, vendored & pinned
│   │   ├── snapshots.py        #   readsnap, readsnapHDF5
│   │   ├── halos.py            #   RSDataReader, readsubf
│   │   └── trees.py            #   MTCatalogue
│   ├── science/                # NO plotting, NO traits — plain functions on arrays
│   │   ├── candidates.py       #   select(catalogue, mass_range, zones) -> DataFrame
│   │   ├── lagrangian.py       #   region(parent, halo, nrvir) -> Region
│   │   ├── contamination.py    #   profile(catalogue, centre) -> (r, n, m)
│   │   └── softening.py        #   from_levelmax(boxsize, lmax) -> Softenings
│   ├── writers/                # pure string/file emitters — templates, not f.write chains
│   │   ├── music_conf.py       #   was ics.py:738-887
│   │   ├── gadget_param.py     #   was gadgetrun.py:653-769
│   │   ├── gadget_config.py    #   was gadgetrun.py:556-651
│   │   └── submit.py           #   PBS + SLURM from one template each
│   ├── shell.py                # subprocess wrapper: argument lists, no shell=True
│   └── ui/                     # thin HasTraits tabs — bind widgets, call the above
│       ├── common.py           #   explicit imports; no `import *`
│       ├── home.py  install.py  candidates.py  ics.py
│       └── gadget.py  halos.py  mergertree.py  contamination.py
├── tests/
│   ├── test_naming.py          # 18 duplications collapse to one tested function
│   ├── test_writers.py         # golden-file comparison against known-good configs
│   ├── test_candidates.py      # synthetic catalogue, known answer
│   └── test_cosmology.py
├── docs/
└── screenshots/

Four changes carry most of the benefit:

  1. naming.py — one function for the run-directory convention. Removes ~150 lines and the whole class of drift bugs described in §4.2.
  2. writers/ — the config emitters are already almost pure functions; they take scalars and produce text. Moved out and driven by templates, they become golden-file testable, which is the only practical defence against a silently malformed param.txt.
  3. science/ — functions over arrays, with the figure work left in ui/. This is what makes the pipeline scriptable for a large suite, and it is where bugs 2.2.1–2.2.3 would have been caught.
  4. clusters.toml — cluster profiles as data. Deletes the platform.node() if-chains (header.py:55-99) and the hardcoded paths in gadgetrun.py:257,263, and makes the tool usable by someone who is not the author.

7. Prioritised plan

P0 — Decide the repository's status. Everything else depends on this.

If archiving (a reasonable choice for a 2014 research tool):

  1. The README now carries a status banner, documents the missing modules dependency, and records the directory-naming convention. ✔
  2. Add a LICENSE so the code is legally reusable.
  3. Tag a final release and mark the repository archived on GitHub.
  4. Optionally record the science in a short docs/method.md — the selection criteria and contamination metric are the durable contribution, and they currently exist only as GUI callbacks.

Stop there. P1 onward is only worth it if the tool is to be used again.

If reviving:

P1 — Make it run (largest effort; nothing else can be verified until this is done)

  1. Vendor or pin the modules package (§1.1). Without it there is no way to test anything.
  2. Python 2 → 3: pyupgrade/2to3 handles print and xrange; integer division and dict.keys() need review by hand.
  3. enthought.traits.*traits / traitsui; wx → Qt (PySide6) for both TraitsUI and the matplotlib canvas in Common.py:41-71.
  4. Add pyproject.toml with pinned versions. Verify pandas' .ix accessor usage (halos.py:192,313, ics.py:537-541) — removed in pandas 1.0, replace with .loc.
  5. Remove the install.py tab, or point it at the system package manager. Hand-building FFTW2 in 2026 is not the right answer, and the source tree it expects is absent anyway.

P2 — Extract and test (do this before fixing bugs, so fixes are verifiable)

  1. naming.py + test_naming.py; replace all 18 call sites.
  2. cosmology.py with a single Hubble constant; replace all 6 literals.
  3. writers/ + golden-file tests, using a known-good param.txt and .conf from an existing run directory as the reference.
  4. science/candidates.py + a synthetic-catalogue test.
  5. GitHub Actions running the suite.

P3 — Fix the defects in §2, each with a regression test. Order: 2.1.1–2.1.3 and 2.1.6 (features that are simply inert), then 2.2.1–2.2.5 (wrong output), then the rest.

P4 — Harden

  1. shell.py: argument lists, no shell=True, no interpolated paths (§3.1–3.3).
  2. clusters.toml; strip the hardcoded identifiers in §3.4.
  3. with for every file handle; os.path.exists for existence tests.
  4. Hoist the loop-invariant reads in §4.5 — the ics.py one is a several-GB-per-iteration win.

P5 — Polish

  1. .gitignore; stop writing rungadget.sh into the repository root.
  2. Delete the ~200 lines of commented-out code and the dead branches in §2.3.
  3. Normalise gadgetrun.py to 4-space indentation; run black and ruff across the tree.
  4. Docstrings on the science/ and writers/ functions — those are the reusable API.
  5. Flatten the redundant single-child view groups.

Appendix: metrics

Metric Value
Python modules 11
Lines of Python ~4,000
Modules that fail to parse on Python 3 4 / 11
Shell invocations (os.system / shell=True) 70
Duplicated folder-name constructions 18
Hardcoded personal paths / usernames / emails 19
Hardcoded copies of the Hubble parameter 6
Tests 0
Docstrings 1
Largest modules gadgetrun.py (49 KB), ics.py (47 KB)

Resolution status

Addressed on uplift/modernise-and-fix, in 17 commits. 171 tests now cover the display-free library.

Fixed

Finding How
1.2 Python 2 only Ported: 35 print statements, 6 xrange, 21 pandas .ix.loc, five invalid escapes. All modules parse on 3.9-3.12.
1.3 Retired enthought.* namespace Mapped onto traits / traitsui / enable. wxPython kept as the toolkit.
2.1.1 SLURM TypeError cme.writers.submit formats rank counts with int(). Regression test.
2.1.2 Gadget sweep inert (nvir typo) Corrected to nrvir.
2.1.3 Four makeactive == no-ops Removed with the handler dedup in the contamination tab.
2.1.4 gethostid UnboundLocalError Returns None; tolerates a missing summary file.
2.1.5 gethalos_xy NameError / unbound return Restructured; returns empty arrays on a miss.
2.1.6 Plot button inert in parent mode Plotting block de-indented out of the zoom branch.
2.1.7 Odyssey preset never applied (clustopt typo) Replaced wholesale by clusters.toml.
2.1.8 Clear wrote to the wrong object One _publish_sample() for all five consumers.
2.1.9 Three MUSIC settings never applied Corrected names; presmooth/postsmooth are now real traits.
2.1.10 nhalo unbound Vectorised lookup that reports the miss.
2.1.11 determineboolstr unbound return Replaced by a total function in cme.writers.music.
2.1.12 Run script opened before its directory writers.write creates parents, called once the path is known.
2.2.1 3D distribution plotted y twice z = tmphalos['posZ'].
2.2.2 hostposx reported z host['posX'].
2.2.3 Phantom zero-mass candidate select() returns an empty array; the add-halo guard no longer tests for the sentinel.
2.2.4 NRVIR['1'] in displayed paths Routed through naming.lagr_region_path.
2.2.5 SLURM ranks from PBS core count One execute_command() per scheduler.
2.2.6 SUBFIND ignored on SLURM Same.
2.3 Dead code Analysis, duplicate boxtype, 3 unused imports, 5 unused buttons, 20 dead locals, ~40 lines of commented-out code removed.
3.1-3.3 Shell injection (70 sites) cme.shell runs argument lists; no shell=True or os.system remains. The tail -n+96 splice is done in Python.
3.4 19 personal identifiers Moved into clusters.toml.
3.5-3.6 Unprotected file handles No raw open() outside cme.writers; existence checks use os.path.exists.
4.1 Science trapped in event handlers cme.naming, cme.cosmology, cme.science, cme.writers are display-free and tested.
4.2 Duplication 18 folder-name builders → cme.naming; 6 Hubble literals → cme.cosmology; 18 mpirun strings → one method; softening cascade, projection block and 4 identical handlers all collapsed.
4.3 Star imports Explicit imports plus __all__ in cme/ui/common.py. The accidental patches dependency is now explicit.
4.5 Performance Parent snapshot reads hoisted out of the nrvir loop (~30 GB of I/O for a five-value sweep); candidate extraction made O(N); vstack-per-match replaced.
5 Hygiene pyproject.toml, LICENSE, .gitignore, CI, 171 tests, docstrings on the library.

Found during the work, also fixed

  • Snapshot indices were zero-padded by concatenation ("groups_0" + str(n)), correct only for two-digit numbers. Snapshot 5 looked for groups_05 where Gadget writes groups_005, and the zoom tabs' default of 255 produced snapdir_0255.
  • The contamination and halo tabs hardcoded _Z127 rather than reading their own zinit, so a run generated at another starting redshift was looked up in a directory that had never been created.
  • Base paths were joined as self.gadpath + 'halos/H', silently producing .../datahalos/H190897 whenever the path trait lacked a trailing slash.
  • for lmini in self.lmin iterated an Enum holding a string, so a levelmin of 10 or above ran the sweep twice, with levelmin '1' and then '0'.
  • HubbleParam was stored as H0 in a trait named for h and divided by 100 on the way out, so the value displayed in the GUI disagreed with the value written to disk.
  • Config.sh emitted TOKEN# comment with no separating space for tokens longer than the comment column.
  • The Gadget existence table hardcoded snapshot 63 regardless of the requested output count.
  • ApplicationMain.__init__ never called HasTraits.__init__, so trait initialisation was skipped and kwargs were discarded.
  • _clusteropt_changed and _username_changed derived different path sets from the same inputs, so which paths you got depended on the order you touched the fields.
  • reWriteIC was imported as re, shadowing the standard library module.

Outstanding

  1. The modules I/O package is still absent (finding 1.1). Nothing else in this list matters as much: the GUI cannot start without it. Vendor or pin it.
  2. The GUI has no test coverage. The library is well covered; the tabs are verified only by compileall and inspection, because exercising them needs a display and a real parent simulation. A Qt port would make headless widget tests feasible.
  3. wxPython → Qt (finding 1.4). Deliberately deferred: it is a large diff that cannot be verified here, and it would have obscured the behavioural fixes.
  4. The Install tab (finding 1.5) still expects a lib/installs/ source tree that is not in the repository. It is hardened but should probably be deleted in favour of the system package manager.
  5. self.main coupling (finding 4.4). Tabs still reach through a back-reference to mutate siblings, so construction order remains load-bearing.
  6. PARENT_SNAPSHOT / PARENT_ICS in cme/ui/ics.py are still hardcoded to the Caterpillar 512³ parent; they belong in clusters.toml.
  7. The non-PLANCK cosmologies in cme.cosmology were taken from the cited papers, not from the original grifflib. PLANCK cross-checks exactly against the values that were hardcoded in the Gadget tab; the others should be diffed against grifflib if it is recovered, before being used for new science.
  8. TraitsUI view declarations are still deeply nested (up to eight redundant group levels) and unformatted, and cme/ui/ is excluded from the ruff format check.