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.
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.
| # | 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.
These are independent of the environment problems — they are bugs in the logic as written. Grouped by confidence.
| # | 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 |
| # | 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 |
main.py:98-99—_analysistab_defaultinstantiatesAnalysis, whose import is commented out atmain.py:8. Not currently reachable, but it is a latentNameError.halos.py:57—halo_typeoffers three values, but four branches are written;'subs of ID'can never be selected (halos.py:180,300,364).halos.py:54-55—boxtypeis declared twice; the second declaration silently wins.gadgetrun.py:226— the view buildsItem('clusteropt')but the trait declaration is commented out atgadgetrun.py:5; the attribute only appears via assignment at:927.candidates.py:44(upper),:17(id_select),Common.py:33(readids) are never used.
| # | 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 |
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.
| 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.
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.
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.
ics.py:564-601re-reads the parent snapshot's entirePOSandIDblocks, plus the IC file'sPOS/ID, once pernrvirvalue. 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-209rebuildsnp.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-237re-plots the full candidate set on every loop iteration, because the plotting block sits inside theforloop rather than after it.candidates.py:217grows the result array withnp.vstackper match — quadratic copying. Append to a list,np.array()once.
gadgetrun.pyis indented with 2 spaces; every other module uses 4.- Naming mixes
lowercase(haloid),camelCase(lagrPos),PascalCase(InitCondFile, mirroring Gadget's parameter names — defensible), andUPPER_CASEtraits (PMGRID). if x == True:throughout, rather thanif x:.- Deep view nesting:
halos.py:100-104reaches eightHGroup/VGroup/Grouplevels, 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.
| 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. |
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:
naming.py— one function for the run-directory convention. Removes ~150 lines and the whole class of drift bugs described in §4.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 malformedparam.txt.science/— functions over arrays, with the figure work left inui/. 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.clusters.toml— cluster profiles as data. Deletes theplatform.node()if-chains (header.py:55-99) and the hardcoded paths ingadgetrun.py:257,263, and makes the tool usable by someone who is not the author.
P0 — Decide the repository's status. Everything else depends on this.
If archiving (a reasonable choice for a 2014 research tool):
- The README now carries a status banner, documents the missing
modulesdependency, and records the directory-naming convention. ✔ - Add a
LICENSEso the code is legally reusable. - Tag a final release and mark the repository archived on GitHub.
- 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)
- Vendor or pin the
modulespackage (§1.1). Without it there is no way to test anything. - Python 2 → 3:
pyupgrade/2to3handlesprintandxrange; integer division anddict.keys()need review by hand. enthought.traits.*→traits/traitsui; wx → Qt (PySide6) for both TraitsUI and the matplotlib canvas inCommon.py:41-71.- Add
pyproject.tomlwith pinned versions. Verifypandas'.ixaccessor usage (halos.py:192,313,ics.py:537-541) — removed in pandas 1.0, replace with.loc. - Remove the
install.pytab, 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)
naming.py+test_naming.py; replace all 18 call sites.cosmology.pywith a single Hubble constant; replace all 6 literals.writers/+ golden-file tests, using a known-goodparam.txtand.conffrom an existing run directory as the reference.science/candidates.py+ a synthetic-catalogue test.- 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
shell.py: argument lists, noshell=True, no interpolated paths (§3.1–3.3).clusters.toml; strip the hardcoded identifiers in §3.4.withfor every file handle;os.path.existsfor existence tests.- Hoist the loop-invariant reads in §4.5 — the
ics.pyone is a several-GB-per-iteration win.
P5 — Polish
.gitignore; stop writingrungadget.shinto the repository root.- Delete the ~200 lines of commented-out code and the dead branches in §2.3.
- Normalise
gadgetrun.pyto 4-space indentation; runblackandruffacross the tree. - Docstrings on the
science/andwriters/functions — those are the reusable API. - Flatten the redundant single-child view groups.
| 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) |
Addressed on uplift/modernise-and-fix, in 17 commits. 171 tests now cover the
display-free library.
| 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. |
- Snapshot indices were zero-padded by concatenation (
"groups_0" + str(n)), correct only for two-digit numbers. Snapshot 5 looked forgroups_05where Gadget writesgroups_005, and the zoom tabs' default of 255 producedsnapdir_0255. - The contamination and halo tabs hardcoded
_Z127rather than reading their ownzinit, 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/H190897whenever the path trait lacked a trailing slash. for lmini in self.lminiterated an Enum holding a string, so a levelmin of 10 or above ran the sweep twice, with levelmin'1'and then'0'.HubbleParamwas 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.shemittedTOKEN# commentwith 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 calledHasTraits.__init__, so trait initialisation was skipped and kwargs were discarded._clusteropt_changedand_username_changedderived different path sets from the same inputs, so which paths you got depended on the order you touched the fields.reWriteICwas imported asre, shadowing the standard library module.
- The
modulesI/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. - The GUI has no test coverage. The library is well covered; the tabs are verified only
by
compilealland inspection, because exercising them needs a display and a real parent simulation. A Qt port would make headless widget tests feasible. - 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.
- 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. self.maincoupling (finding 4.4). Tabs still reach through a back-reference to mutate siblings, so construction order remains load-bearing.PARENT_SNAPSHOT/PARENT_ICSincme/ui/ics.pyare still hardcoded to the Caterpillar 512³ parent; they belong inclusters.toml.- The non-PLANCK cosmologies in
cme.cosmologywere taken from the cited papers, not from the originalgrifflib. PLANCK cross-checks exactly against the values that were hardcoded in the Gadget tab; the others should be diffed againstgrifflibif it is recovered, before being used for new science. - TraitsUI view declarations are still deeply nested (up to eight redundant group
levels) and unformatted, and
cme/ui/is excluded from theruff formatcheck.