Skip to content

Commit ab4f0c8

Browse files
authored
Merge pull request #83 from PyAutoLabs/feature/cti-resurrection-phase0
CTI resurrection Phase 0: resurrect against the current PyAuto stack
2 parents e9be103 + c808cae commit ab4f0c8

35 files changed

Lines changed: 5004 additions & 4630 deletions

AGENTS.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# PyAutoCTI — Agent Instructions
2+
3+
Canonical, agent-agnostic instructions for this repo. `CLAUDE.md` imports this
4+
file; any tool that does not process `@`-imports should read this directly.
5+
6+
## What this repo is
7+
8+
**PyAutoCTI** (package `autocti`) is a Bayesian library for calibrating and
9+
modelling Charge Transfer Inefficiency (CTI) in CCD imaging: charge-injection
10+
imaging (`ImagingCI`) and 1D datasets (`Dataset1D`), trap/CCD models clocked
11+
through the C++ **arctic** code (`Clocker1D`/`Clocker2D` wrapping `arcticpy`),
12+
FPR/EPER extraction (`autocti/extract/`), and per-dataset `Fit*`/`Analysis*`
13+
classes. Heritage: Euclid VIS CTI calibration; also HST ACS
14+
(`autocti/instruments/acs`).
15+
16+
Dependency direction: autocti may import **autoarray** (data structures),
17+
**autofit** (model-fitting), and **autoconf** (config). Nothing in the PyAuto
18+
stack imports autocti — it is a leaf like PyAutoLens.
19+
20+
## Resurrection status (2026-07)
21+
22+
This repo was unmaintained for ~2 years and is being brought back into the
23+
ecosystem via the CTI resurrection epic
24+
([PyAutoCTI#82](https://github.com/PyAutoLabs/PyAutoCTI/issues/82)). Phase 0
25+
(importable + unit tests green on the current stack) is complete. **The
26+
visualization layer (`autocti/plot/`, `*/plot/*_plotters.py`,
27+
`*/model/plotter_interface.py`) is quarantined**: it still targets the removed
28+
autoarray Plotter API and is rewritten on the matplotlib function API
29+
(mirroring PyAutoGalaxy) in Phase 1. Until then `autocti.plot` is not
30+
importable, `Analysis` visualization no-ops with a logged warning, and the
31+
plot tests are skipped via `test_autocti/conftest.py`.
32+
33+
## arcticpy (read before installing)
34+
35+
`import autocti` requires **arcticpy** (pinned 2.6), which is deliberately not
36+
a pip dependency:
37+
38+
- Its PyPI sdist is **source-only C++** — it needs `libgsl-dev` headers and a
39+
toolchain to build.
40+
- Its own requirements **downgrade numpy below 2.0**, breaking a modern stack.
41+
42+
Install it after numpy is in place:
43+
44+
```bash
45+
pip install arcticpy==2.6 --no-build-isolation --no-deps
46+
```
47+
48+
If GSL headers are missing and you lack root, extract them locally
49+
(`apt-get download libgsl-dev && dpkg -x ...`) and point `CPPFLAGS`/`LDFLAGS`
50+
at them.
51+
52+
## Quick commands
53+
54+
```bash
55+
pip install -e ".[dev]" # install with dev/test extras
56+
python -m pytest test_autocti/ # full test suite
57+
python -m pytest test_autocti/extract/ # one focused directory
58+
```
59+
60+
In a sandboxed / restricted environment, point numba and matplotlib at
61+
writable caches:
62+
63+
```bash
64+
NUMBA_CACHE_DIR=/tmp/numba_cache MPLCONFIGDIR=/tmp/matplotlib python -m pytest test_autocti/
65+
```
66+
67+
## Related repos
68+
69+
- **Source siblings:** PyAutoConf, PyAutoArray, PyAutoFit (upstream).
70+
- **autocti_workspace** — runnable examples/tutorials (updated in epic Phase 4).
71+
- **autocti_workspace_test** — regression scripts + Euclid tvac/temporal
72+
heritage (rebuilt in epic Phase 5).
73+
- **Science context:** `PyAutoMemory/wiki/cti/` (trap physics, arctic
74+
algorithm, Euclid VIS / HST ACS heritage).
75+
76+
## Public API
77+
78+
The public surface is defined authoritatively in `autocti/__init__.py` — read
79+
it rather than trusting a hand-maintained table. Canonical import:
80+
81+
```python
82+
import autocti as ac
83+
```
84+
85+
## Key rules / footguns
86+
87+
- Import direction: autoarray / autofit / autoconf only — never autogalaxy or
88+
autolens.
89+
- Unit tests are numpy-only; there is no JAX in this library (arctic is C++).
90+
- Slicing an autoarray `Mask2D` returns a plain ndarray — rebuild a `Mask2D`
91+
with the parent's `pixel_scales` before constructing an `Array2D` from it
92+
(see `autocti/extract/two_d/abstract.py`).
93+
- Fits I/O goes through `autoconf.fitsable` (`ndarray_via_fits_from`,
94+
`output_to_fits`, `hdu_list_for_output_from`) — instance `.output_to_fits`
95+
methods no longer exist on autoarray structures.
96+
- All files use Unix line endings (LF, `\n`) — never `\r\n`.
97+
98+
## Working on issues
99+
100+
1. Read the issue description and any linked plan.
101+
2. Identify affected files and make the change.
102+
3. Run the full suite: `python -m pytest test_autocti/`.
103+
4. If you changed public API, say so explicitly — autocti_workspace may need
104+
updates.
105+
5. Ensure all tests pass before opening a PR.
106+
107+
## Never rewrite history
108+
109+
Never rewrite pushed history on any repo with a remote — no `git init` over a
110+
tracked repo, no force-push to `main`, no fresh-start "Initial commit", no
111+
`filter-repo` / `filter-branch` / `rebase -i` on pushed branches. To get a
112+
clean tree: `git fetch origin && git reset --hard origin/main && git clean -fd`.

CLAUDE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# PyAutoCTI — agent instructions
2+
The canonical, agent-agnostic instructions live in `AGENTS.md`. Claude Code loads them
3+
via the import below; if your tool does not process `@`-imports, open `AGENTS.md` in
4+
this directory and read it directly.
5+
@AGENTS.md

autocti/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,10 @@
7171
from .clocker.two_d import Clocker2D
7272
from . import aggregator as agg
7373
from . import util
74-
from . import plot
74+
75+
# `from . import plot` is quarantined: the Plotter object stack targets the
76+
# removed autoarray Plotter API and is rewritten on the new matplotlib function
77+
# API in Phase 1 of the CTI resurrection epic (PyAutoCTI#82).
7578
from . import mock as m # noqa
7679

7780
from autoconf import conf

autocti/aggregator/dataset_1d.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import autofit as af
55
import autoarray as aa
6+
from autoconf.fitsable import ndarray_via_hdu_from
67

78
from autocti.dataset_1d.dataset_1d.dataset_1d import Dataset1D
89

@@ -54,23 +55,28 @@ def _dataset_1d_list_from(
5455
for fit in fit_list:
5556
layout = fit.value(name=f"{folder}.layout")
5657

57-
data = aa.Array1D.from_primary_hdu(primary_hdu=fit.value(name=f"{folder}.data"))
58-
noise_map = aa.Array1D.from_primary_hdu(
59-
primary_hdu=fit.value(name=f"{folder}.noise_map")
60-
)
61-
pre_cti_data = aa.Array1D.from_primary_hdu(
62-
primary_hdu=fit.value(name=f"{folder}.pre_cti_data")
58+
hdu_list = fit.value(name=f"{folder}.dataset")
59+
60+
pixel_scales = hdu_list[0].header["PIXSCA"]
61+
62+
mask = aa.Mask1D(
63+
mask=ndarray_via_hdu_from(hdu_list[0]).astype("bool"),
64+
pixel_scales=pixel_scales,
6365
)
6466

67+
def values_from(hdu: int) -> aa.Array1D:
68+
return aa.Array1D.no_mask(
69+
values=ndarray_via_hdu_from(hdu_list[hdu]),
70+
pixel_scales=pixel_scales,
71+
)
72+
6573
dataset = Dataset1D(
66-
data=data,
67-
noise_map=noise_map,
68-
pre_cti_data=pre_cti_data,
74+
data=values_from(hdu=1),
75+
noise_map=values_from(hdu=2),
76+
pre_cti_data=values_from(hdu=3),
6977
layout=layout,
7078
)
7179

72-
mask = aa.Mask1D.from_primary_hdu(primary_hdu=fit.value(name=f"{folder}.mask"))
73-
7480
dataset_list.append(dataset.apply_mask(mask=mask))
7581

7682
return dataset_list

autocti/aggregator/imaging_ci.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from functools import partial
22

33
import autoarray as aa
4+
from autoconf.fitsable import ndarray_via_hdu_from
45
import autofit as af
56

67
from autocti.charge_injection.imaging.imaging import ImagingCI
@@ -56,33 +57,40 @@ def _imaging_ci_list_from(fit: af.Fit, use_dataset_full: bool = False):
5657
for fit in fit_list:
5758
layout = fit.value(name=f"{folder}.layout")
5859

59-
data = aa.Array2D.from_primary_hdu(primary_hdu=fit.value(name=f"{folder}.data"))
60-
noise_map = aa.Array2D.from_primary_hdu(
61-
primary_hdu=fit.value(name=f"{folder}.noise_map")
60+
hdu_list = fit.value(name=f"{folder}.dataset")
61+
62+
pixel_scales = (
63+
hdu_list[0].header["PIXSCAY"],
64+
hdu_list[0].header["PIXSCAX"],
6265
)
63-
pre_cti_data = aa.Array2D.from_primary_hdu(
64-
primary_hdu=fit.value(name=f"{folder}.pre_cti_data")
66+
67+
mask = aa.Mask2D(
68+
mask=ndarray_via_hdu_from(hdu_list[0]).astype("bool"),
69+
pixel_scales=pixel_scales,
6570
)
66-
try:
67-
cosmic_ray_map = aa.Array2D.from_primary_hdu(
68-
primary_hdu=fit.value(name=f"{folder}.cosmic_ray_map")
71+
72+
def values_from(hdu: int) -> aa.Array2D:
73+
return aa.Array2D.no_mask(
74+
values=ndarray_via_hdu_from(hdu_list[hdu]),
75+
pixel_scales=pixel_scales,
6976
)
70-
except AttributeError:
77+
78+
try:
79+
cosmic_ray_map = values_from(hdu=4)
80+
except IndexError:
7181
cosmic_ray_map = None
7282

7383
settings_dict = fit.value(name="dataset.settings_dict")
7484

7585
dataset = ImagingCI(
76-
data=data,
77-
noise_map=noise_map,
78-
pre_cti_data=pre_cti_data,
86+
data=values_from(hdu=1),
87+
noise_map=values_from(hdu=2),
88+
pre_cti_data=values_from(hdu=3),
7989
cosmic_ray_map=cosmic_ray_map,
8090
settings_dict=settings_dict,
8191
layout=layout,
8292
)
8393

84-
mask = aa.Mask2D.from_primary_hdu(primary_hdu=fit.value(name=f"{folder}.mask"))
85-
8694
dataset_list.append(dataset.apply_mask(mask=mask))
8795

8896
return dataset_list

0 commit comments

Comments
 (0)