diff --git a/active/01_log_gaussian_with_limits_crash.md b/active/01_log_gaussian_with_limits_crash.md deleted file mode 100644 index 494a49fd..00000000 --- a/active/01_log_gaussian_with_limits_crash.md +++ /dev/null @@ -1,145 +0,0 @@ -# `@PyAutoFit` `LogGaussianPrior.with_limits` will crash on first call (and so will `_new_for_base_message`) - -Type: bug -Target: priors -Difficulty: medium -Autonomy: safe -Priority: normal -Status: formalised - -Found during the priors/messages audit (see -`PyAutoPrompt/autofit/priors_and_messages_math_audit.md`, finding A1). - -## Problem - -`@PyAutoFit/autofit/mapper/prior/log_gaussian.py:71-114` defines two -helpers that pass kwargs the constructor does not accept: - -```python -# log_gaussian.py:95-100 (with_limits) -return cls( - mean=(lower_limit + upper_limit) / 2, - sigma=upper_limit - lower_limit, - lower_limit=lower_limit, # <-- ctor does not accept this - upper_limit=upper_limit, # <-- ctor does not accept this -) - -# log_gaussian.py:109-114 (_new_for_base_message) -return LogGaussianPrior( - *message.parameters, - lower_limit=self.lower_limit, # self.lower_limit never set - upper_limit=self.upper_limit, # self.upper_limit never set - id_=self.instance().id, -) -``` - -`LogGaussianPrior.__init__` signature is `(mean, sigma, id_)` (lines 13-19). -Both helpers will raise `TypeError`. This is the same structural shape -as the original `LogUniformPrior` sign-convention bug — the LogGaussian -branch has clearly never run end-to-end. - -## Wider context — how these helpers are called - -`with_limits` is the standard prior-passing entry point. It is invoked -indirectly through model-mapper passing flows that look like: - -- `Prior.with_limits` is called when re-creating priors centred on the - posterior of a previous run. -- `_new_for_base_message` is called from message projection / EP code - paths when the underlying message has been refit and the wrapper - needs reconstructing. - -Both flows are dormant until a user runs a fit that produces a -`LogGaussianPrior` result and then chains a second fit that uses -prior-passing on that parameter. That combination has apparently -never been exercised — which is why the crash is latent. - -## Python reproducer - -```python -# Reproducer: log_gaussian_with_limits_crash.py -# Run: python log_gaussian_with_limits_crash.py -import traceback - -from autofit.mapper.prior.log_gaussian import LogGaussianPrior - -print("=== with_limits ===") -try: - p = LogGaussianPrior.with_limits(lower_limit=0.01, upper_limit=10.0) - print(f"Unexpected success: got {p!r}") -except TypeError as e: - print(f"TypeError as expected: {e}") - traceback.print_exc() - -print() -print("=== _new_for_base_message ===") -# Build any LogGaussianPrior, then attempt the helper path -p = LogGaussianPrior(mean=0.0, sigma=1.0) -try: - new = p._new_for_base_message(p.message.base_message) - print(f"Unexpected success: got {new!r}") -except (TypeError, AttributeError) as e: - print(f"Error as expected: {type(e).__name__}: {e}") - traceback.print_exc() -``` - -Expected (buggy) output: `TypeError: __init__() got an unexpected keyword -argument 'lower_limit'` for both calls (with an additional `AttributeError: -'LogGaussianPrior' object has no attribute 'lower_limit'` likely surfacing -before the TypeError on the second call). - -## Proposed fix - -Drop the unused kwargs in both helpers: - -```python -@classmethod -def with_limits(cls, lower_limit: float, upper_limit: float) -> "LogGaussianPrior": - return cls( - mean=(lower_limit + upper_limit) / 2, - sigma=upper_limit - lower_limit, - ) - -def _new_for_base_message(self, message): - return LogGaussianPrior( - *message.parameters, - id_=self.instance().id, - ) -``` - -There is no `lower_limit` / `upper_limit` for a true log-Gaussian (its -support is `(0, ∞)`), so this fix is the right semantic answer too — -the kwargs should never have been there. - -## What the agent picking this up should do - -1. Read `@PyAutoFit/autofit/mapper/prior/log_gaussian.py` end-to-end - (not just the lines quoted) to confirm the constructor signature - has not drifted since this prompt was written. -2. Run the reproducer above as a standalone script. Confirm both - helpers raise. -3. Sketch the fix in a scratch checkout (no commit) and re-run the - reproducer. Confirm both calls now succeed. -4. File the GitHub issue via `/create_issue priors/01_log_gaussian_with_limits_crash.md`. -5. **In the issue body, explicitly request that a collaborator with - probabilistic-programming background verify before any PR opens.** - The proposed fix is mechanical, but the audit was AI-generated and - we want a second pair of eyes. -6. **Stop. Do not call `/start_dev` until the issue has at least one - confirmation ack.** - - -## Fable verdict (2026-07-08, PyAutoFit main @ 0f26ff2d8; PyAutoFit#1330) - -**Verdict: CONFIRMED — fix now (severity: medium-high).** -`with_limits` raises `TypeError: __init__() got an unexpected keyword argument -'lower_limit'` as predicted. `_new_for_base_message` also crashes, but via a -different path than the audit predicted: `self.lower_limit` resolves through -`Prior.__getattr__` to the message, and the call then fails with -`AttributeError: 'TransformedMessage' object has no attribute 'instance'`. -Same conclusion — this branch has never run end-to-end. Note -`NormalMessage.from_mode` on current main now defensively pops -`lower_limit`/`upper_limit` kwargs, a workaround in the same family. -Proposed fix stands (drop the kwargs; a log-Gaussian has support (0, inf)). - - diff --git a/active/01_status_dashboard.md b/active/01_status_dashboard.md deleted file mode 100644 index 5edd2669..00000000 --- a/active/01_status_dashboard.md +++ /dev/null @@ -1,54 +0,0 @@ -# Cross-repo sync status dashboard - -> ⚠️ **Caveat — drafted from a stale repo state.** This prompt was drafted on 2026-04-27 during a forensic sweep that found local checkouts up to 101 commits behind origin. The trigger looked like a structural workflow flaw, but later analysis showed the drift was largely driven by **stale local checkouts being edited without `git pull` first**, not by missing tooling. Now that PyAutoPrompt is the canonical source-of-truth and `skills/install.sh` auto-discovers across both repos, some of the recommendations below may be over-engineered for the day-to-day case. Re-evaluate whether each measure is still warranted — the cheap habits (pull before edit, never rewrite history) buy most of the win. - -Drift across PyAuto repos is currently invisible — there's no single command -that shows whether each library and workspace is in sync with `origin/main`. -The 2026-04-27 audit found 12 repos with up to 101 commits of behind-origin -state, and we only noticed because of an unrelated investigation. Catching it -early needs a dashboard that's two keystrokes away. - -## What to build - -A shell function `pyauto-status` (loaded via `~/.bashrc` or `~/.local/bin/`) -that, for every git repo under `~/Code/PyAutoLabs/`, prints: - -- repo name -- current branch -- upstream tracking ref (or `NONE` if missing — a real failure mode that hid - `autolens_workspace_developer` for weeks) -- behind / ahead counts vs `@{u}` -- dirty file count -- a single-glyph flag column: `↓` for behind, `↑` for ahead, `*` for dirty - -Implementation notes: - -- Run `git fetch origin --quiet` per repo before counting (a stale dashboard is - worse than none). -- Skip directories that aren't git repos cleanly (warn, don't crash). -- Use the upstream branch from `git rev-parse --abbrev-ref @{u}`, not a - hardcoded `origin/main`. PyAutoFit recently moved from `main_build` → `main` - and a hardcoded version would have hidden that. - -A working draft is included in -`PyAutoPrompt/autoprompt/snippets/pyauto_status.sh` (see file). Test it; tune the -column widths; either inline it into `~/.bashrc` or symlink from `~/.local/bin/`. - -## Acceptance - -- Running `pyauto-status` from any directory shows all 12 repos in one screen. -- A repo on a non-default branch is visible (don't auto-checkout `main`). -- A repo with no upstream is flagged, not silently skipped. -- The whole sweep finishes in well under 10 seconds (parallel-friendly fetch). - -## Out of scope - -- Auto-pulling. This is a status command only — fixing drift is a separate - prompt (`05_sync_slash_command.md`). -- Pretty colors / TUI. Plain text is faster to read and pipe. - -## Files touched - -- `~/.bashrc` (or `~/.local/bin/pyauto-status` + `chmod +x`) -- Optionally: `PyAutoPrompt/scripts/status.sh` already prints prompt-registry - counts; consider adding a `--repos` flag that delegates to this function. diff --git a/active/02_gitignore_noise.md b/active/02_gitignore_noise.md deleted file mode 100644 index e9033237..00000000 --- a/active/02_gitignore_noise.md +++ /dev/null @@ -1,81 +0,0 @@ -# Workspace `.gitignore` cleanup - -> ⚠️ **Caveat — drafted from a stale repo state.** This prompt was drafted on 2026-04-27 during a forensic sweep that found local checkouts up to 101 commits behind origin. The trigger looked like a structural workflow flaw, but later analysis showed the drift was largely driven by **stale local checkouts being edited without `git pull` first**, not by missing tooling. Now that PyAutoPrompt is the canonical source-of-truth and `skills/install.sh` auto-discovers across both repos, some of the recommendations below may be over-engineered for the day-to-day case. Re-evaluate whether each measure is still warranted — the cheap habits (pull before edit, never rewrite history) buy most of the win. - -Generated artifacts are routinely polluting `git status` in the workspace repos, -to the point where the user (and agents) stop reading `git status` because -"of course it's dirty". On 2026-04-27 this caused 101-commit drift on -PyAutoGalaxy to go unnoticed for weeks: the workspaces showed 8+ "dirty" files -each, all generated, so the actual modified files (real PyAutoJAX→PyAutoLabs -renames) were buried. - -Examples of the noise pattern, all observed in working trees: - -- `image.fits` at workspace root (a script wrote a literal default filename) -- `path/`, `scripts/path/` (a script took `"path"` as a literal arg) -- `scripts/scripts/` (a script ran from inside `scripts/` with `cd scripts/`) -- `output_path/` (similar literal) -- `root.log` (logger output) -- `__pycache__/` directories everywhere - -Some of these reflect actual bugs in scripts (passing `"path"` as a positional -arg). The bugs should be fixed at source where reasonable, but `.gitignore` is -the backstop that prevents the pollution from compounding. - -## What to ship - -Add to the `.gitignore` of every workspace repo (`autofit_workspace`, -`autogalaxy_workspace`, `autolens_workspace`, the `*_test` and `*_developer` -variants): - -```gitignore -# Generated artifacts — never check in -image.fits -path/ -scripts/path/ -scripts/scripts/ -output_path/ -root.log -*.log - -__pycache__/ -*.pyc -.codex/ -``` - -Where the literal noise comes from a real script bug (e.g. `image.fits` from a -plotter that defaulted to that filename), file an issue or fix the source while -you're there. - -## Acceptance - -- After pulling the updated `.gitignore` into a clean workspace and running the - smoke tests, `git status` is clean. -- The dataset/* simulator outputs that workspaces actually want to track - (`dataset/imaging/.../data.fits` etc.) remain tracked — only the literal-string - accidents are ignored. - -## How to handle existing tracked files matching the new patterns - -`git rm --cached ` (don't use plain `git rm`, that deletes content) then -commit the `.gitignore` and the `--cached` removal in the same PR. - -## Out of scope - -- Source-side fixes for the scripts that write `path/` etc. — file follow-up - issues but don't bundle the fixes here. -- Library `.gitignore` updates (PyAutoConf/PyAutoFit/PyAutoArray/PyAutoGalaxy/ - PyAutoLens already have reasonable `.gitignore`s; the noise problem is - workspace-specific). - -## Files touched - -One PR per workspace repo: - -- `autofit_workspace/.gitignore` -- `autogalaxy_workspace/.gitignore` -- `autolens_workspace/.gitignore` -- `autofit_workspace_test/.gitignore` -- `autolens_workspace_test/.gitignore` -- `autofit_workspace_developer/.gitignore` -- `autolens_workspace_developer/.gitignore` diff --git a/active/02_uniform_logpdf_array_handling.md b/active/02_uniform_logpdf_array_handling.md deleted file mode 100644 index d0580b7e..00000000 --- a/active/02_uniform_logpdf_array_handling.md +++ /dev/null @@ -1,131 +0,0 @@ -# `@PyAutoFit` `UniformPrior.logpdf` does not handle array inputs - -Type: bug -Target: priors -Difficulty: large -Autonomy: supervised -Priority: normal -Status: formalised - -Found during the priors/messages audit (see -`PyAutoPrompt/autofit/priors_and_messages_math_audit.md`, finding A7). - -## Problem - -`@PyAutoFit/autofit/mapper/prior/uniform.py:100-116`: - -```python -def logpdf(self, x): - # TODO: handle x as a numpy array - if x == self.lower_limit: - x += epsilon - elif x == self.upper_limit: - x -= epsilon - return self.message.logpdf(x) -``` - -The `if x == self.lower_limit:` test is scalar-only. The `# TODO` -acknowledges the gap. Passing a numpy array raises -`ValueError: The truth value of an array with more than one element is -ambiguous`. - -## Wider context — how `logpdf` is called - -`Prior.logpdf` is the user-facing way to evaluate a prior's log density. -It's invoked from: - -- Plotting / visualisation code that vectorises across a grid. -- Diagnostic notebooks comparing priors. -- Anywhere the user wants to ask "what is p(x) under this prior" - without going through the full search machinery. - -The internal NonLinearSearch / Fitness machinery uses -`log_prior_from_value` (which is correctly array-safe for the JAX path, -see `uniform.py:167-179`). So this bug doesn't break inference — it -breaks user-facing density evaluation. The fact that the `# TODO` has -sat there suggests the array path was never used in earnest. - -The boundary epsilon-snap exists because `UniformNormalMessage.logpdf` -goes to `-inf` exactly at the limits (`phi_transform` is `Φ⁻¹`, and -`Φ⁻¹(0) = -inf`). The fix needs to preserve that behaviour for arrays. - -## Python reproducer - -```python -# Reproducer: uniform_logpdf_array_handling.py -import numpy as np -import autofit as af - -prior = af.UniformPrior(lower_limit=0.0, upper_limit=1.0) - -# Scalar works -print(f"scalar logpdf(0.5) = {prior.logpdf(0.5)}") - -# Boundary scalar works (epsilon-snap path) -print(f"scalar logpdf(0.0) = {prior.logpdf(0.0)} (lower edge)") - -# Array breaks -try: - print(prior.logpdf(np.array([0.2, 0.5, 0.7]))) -except ValueError as e: - print(f"ARRAY: ValueError as expected: {e}") - -# Array including boundaries (what we'd want post-fix) -try: - print(prior.logpdf(np.array([0.0, 0.5, 1.0]))) -except ValueError as e: - print(f"ARRAY w/ boundaries: ValueError as expected: {e}") -``` - -Expected (buggy) output: scalar calls succeed, array calls raise -`ValueError: The truth value of an array with more than one element is -ambiguous`. - -## Proposed fix - -Replace the scalar boundary-snap with a vectorised np.where (mirroring -the pattern already in `log_prior_from_value`): - -```python -def logpdf(self, x): - x = np.asarray(x) - x = np.where(x == self.lower_limit, x + epsilon, x) - x = np.where(x == self.upper_limit, x - epsilon, x) - return self.message.logpdf(x) -``` - -Open question for the reviewer: do we want `logpdf` to return `-inf` -for x outside `[lower_limit, upper_limit]` (like `log_prior_from_value` -on the JAX path), or do we want to keep delegating unconditionally to -the message (which extrapolates via the `phi_transform`)? Current -scalar behaviour silently delegates — array behaviour should match -that, but the reviewer should confirm. - -## What the agent picking this up should do - -1. Read `@PyAutoFit/autofit/mapper/prior/uniform.py` end-to-end. -2. Read `@PyAutoFit/autofit/messages/composed_transform.py` to - understand what the underlying `UniformNormalMessage.logpdf` does - for out-of-support values — the fix must not change that behaviour - accidentally. -3. Run the reproducer. Confirm scalar works and array raises. -4. Sketch the fix above in a scratch checkout (no commit). Re-run the - reproducer to confirm array calls now succeed and match - `scipy.stats.uniform.logpdf` over the support. -5. File the GitHub issue via `/create_issue priors/02_uniform_logpdf_array_handling.md`. -6. **In the issue body, ask the reviewer to weigh in on the - out-of-support semantics question above.** Also ask whether the - `epsilon = 1e-14` snap is still load-bearing or whether a clean - `np.where(in_bounds, message.logpdf, -np.inf)` would be cleaner. -7. **Stop. Do not implement until the open question is resolved.** - - -## Fable verdict (2026-07-08, PyAutoFit main @ 0f26ff2d8; PyAutoFit#1330) - -**Verdict: CONFIRMED — fix now (severity: low-medium).** -Scalar path works; array input raises `ValueError` (ambiguous truth value) -exactly as described; the `# TODO` is still in the source. Vectorised -`np.where` fix stands; the out-of-support semantics question remains open -and should be settled alongside prompt 07's convention decision. - - diff --git a/active/03_gamma_from_mode_wrong_formula.md b/active/03_gamma_from_mode_wrong_formula.md deleted file mode 100644 index 9af165ff..00000000 --- a/active/03_gamma_from_mode_wrong_formula.md +++ /dev/null @@ -1,177 +0,0 @@ -# `@PyAutoFit` `GammaMessage.from_mode` produces a Gamma that matches neither the requested mode nor variance - -Type: bug -Target: priors -Difficulty: too-large -Autonomy: supervised -Priority: normal -Status: formalised - -Found during the priors/messages audit (see -`PyAutoPrompt/autofit/priors_and_messages_math_audit.md`, finding A3). - -## Problem - -`@PyAutoFit/autofit/messages/gamma.py:75-81`: - -```python -@classmethod -def from_mode(cls, mode, covariance, **kwargs): - m, V = cls._get_mean_variance(mode, covariance) - alpha = 1 + m ** 2 * V # m=mode, V=variance → units: variance² - beta = alpha / m - return cls(alpha, beta, **kwargs) -``` - -The Gamma distribution with rate parameterisation has: - -- `mode = (α - 1) / β` (for α ≥ 1) -- `variance = α / β²` - -The formula `alpha = 1 + m² * V` does not satisfy either equation. It -is also dimensionally wrong: `m²` has units of (variance), `V` is -variance, so `m² * V` has units of variance². `α` is dimensionless. - -The "1 +" suggests someone intended `α = 1 + m²/V` (the mean-matching -closed form that holds when mean ≈ mode for large α), but the operator -is wrong. - -## Wider context — how `from_mode` is used - -`from_mode` is the standard way to construct a message from a point -estimate. Sister methods: - -- `NormalMessage.from_mode` (`@PyAutoFit/autofit/messages/normal.py:296-327`) - — for a Gaussian, mode == mean, so `from_mode(m, V)` just constructs - `NormalMessage(mode, sqrt(V))`. Correct. -- `TruncatedNormalMessage.from_mode` — same pattern, correct. - -For asymmetric distributions like Gamma, mode != mean, so "from_mode" -has to solve a system. The expected invariants the reviewer should -confirm: - -- **Interpretation A** (match mode + variance exactly): solve - `mode = (α-1)/β` and `var = α/β²` jointly → quadratic in α. -- **Interpretation B** (match mean to mode, match variance): use - `mean = α/β = m` and `var = α/β² = V` → `α = m²/V`, `β = m/V`. The - resulting Gamma has mean=m, var=V, and mode = m - V/m (only equals m - when V → 0). -- **Interpretation C** (current code's apparent intent — buggy): - `α = 1 + m²/V` (close to Interpretation B but with the "+1" shift - used in some texts to keep α ≥ 1). - -The audit doc proposes the current line should be `α = 1 + m²/V`, but -the reviewer should confirm whether the call sites expect mean-matching -or mode-matching behaviour. Search for callers of `GammaMessage.from_mode` -to find out — the audit found none in the test suite, which is itself -a finding. - -## Python reproducer - -```python -# Reproducer: gamma_from_mode_wrong_formula.py -import numpy as np - -from autofit.messages.gamma import GammaMessage - -np.random.seed(0) - -target_mode, target_var = 2.0, 1.0 - -print(f"Target: mode = {target_mode}, var = {target_var}") -print() - -g = GammaMessage.from_mode(target_mode, target_var) -print(f"Constructed: alpha = {g.alpha:.4f}, beta = {g.beta:.4f}") - -# What the resulting Gamma actually has: -analytic_mean = g.alpha / g.beta -analytic_var = g.alpha / g.beta ** 2 -analytic_mode = (g.alpha - 1) / g.beta if g.alpha >= 1 else 0.0 -print(f"Resulting analytic mean = {analytic_mean:.4f}") -print(f"Resulting analytic var = {analytic_var:.4f}") -print(f"Resulting analytic mode = {analytic_mode:.4f}") -print() - -# Sample-based sanity check -samples = np.random.gamma(g.alpha, scale=1 / g.beta, size=200_000) -print(f"Sampled mean = {samples.mean():.4f}") -print(f"Sampled var = {samples.var():.4f}") -print() - -# Compare to the two plausible intended formulas -def gamma_from_mode_intended_match_mean(m, V): - """Interpretation B: mean=mode, var=V.""" - alpha = m ** 2 / V - beta = m / V - return alpha, beta - -def gamma_from_mode_audit_proposed(m, V): - """Audit's proposed fix: alpha = 1 + m^2/V.""" - alpha = 1 + m ** 2 / V - beta = alpha / m - return alpha, beta - -aB, bB = gamma_from_mode_intended_match_mean(target_mode, target_var) -print(f"If we'd used alpha=m²/V (match mean): " - f"alpha={aB:.4f}, beta={bB:.4f}, " - f"mean={aB/bB:.4f}, var={aB/bB**2:.4f}, " - f"mode={(aB-1)/bB:.4f}") - -aC, bC = gamma_from_mode_audit_proposed(target_mode, target_var) -print(f"If we'd used alpha=1+m²/V (audit's guess): " - f"alpha={aC:.4f}, beta={bC:.4f}, " - f"mean={aC/bC:.4f}, var={aC/bC**2:.4f}, " - f"mode={(aC-1)/bC:.4f}") -``` - -Expected (buggy) output: the constructed Gamma's mean / var / mode do -not match the requested target. The two intended formulas land much -closer (in different ways). - -## Proposed fix - -**Needs design input.** The audit's first-pass guess is -`alpha = 1 + m**2 / V`, but the reviewer should confirm which invariant -`from_mode` is meant to maintain. Possible fixes, in order of likelihood: - -1. `alpha = 1 + m**2 / V; beta = alpha / m` — matches the intent - suggested by the literal `"1 +"` in the current code. -2. `alpha = m**2 / V; beta = m / V` — mean-matching (sister classes - `NormalMessage.from_mode` are mean-matching, so this is consistent - with the family). -3. Solve the quadratic to match mode + var exactly — only worth doing - if a caller actually depends on the mode being preserved. - -## What the agent picking this up should do - -1. Read `@PyAutoFit/autofit/messages/gamma.py` and - `@PyAutoFit/autofit/messages/normal.py:296-327` (sister - `from_mode`) end-to-end. -2. Grep for `GammaMessage.from_mode` and `Gamma.*from_mode` across - `@PyAutoFit` and the workspaces — list every call site so the - reviewer can see what invariant callers depend on. -3. Run the reproducer. Confirm the three formulas produce three - different Gammas. -4. File the GitHub issue via `/create_issue priors/03_gamma_from_mode_wrong_formula.md`. -5. **In the issue body, ask the reviewer to pick between options 1 / 2 - / 3 above based on what `from_mode` is supposed to mean for this - family.** This is a math + API design call, not a mechanical fix. -6. **Stop. Do not implement until the reviewer specifies the desired - invariant.** - - -## Fable verdict (2026-07-08, PyAutoFit main @ 0f26ff2d8; PyAutoFit#1330) - -**Verdict: CONFIRMED, sharpened — fix after invariant decision (severity: medium, latent).** -Correction to the audit: the mean DOES match the requested mode exactly -(beta = alpha/m forces mean = m). The variance is what's wrong, and it is -*inversely* wrong: `from_mode(2, V=0.25)` yields var = 2.0; `from_mode(2, V=4)` -yields var = 0.235 — `alpha = 1 + m**2 * V` has V in the numerator where it -belongs in the denominator. Reproducer note: V=1 cannot discriminate -`1 + m**2*V` from `1 + m**2/V`; test at V != 1. Interpretation B -(`alpha = m**2/V`, `beta = m/V`; matches mean+variance, consistent with the -Normal family) remains the leading candidate. No callers found; latent until -Gamma messages enter an EP projection. - - diff --git a/active/03_history_rewrite_guard.md b/active/03_history_rewrite_guard.md deleted file mode 100644 index aa858ea9..00000000 --- a/active/03_history_rewrite_guard.md +++ /dev/null @@ -1,85 +0,0 @@ -# "Never rewrite history" guard in CLAUDE.md / AGENTS.md - -> ⚠️ **Caveat — drafted from a stale repo state.** This prompt was drafted on 2026-04-27 during a forensic sweep that found local checkouts up to 101 commits behind origin. The trigger looked like a structural workflow flaw, but later analysis showed the drift was largely driven by **stale local checkouts being edited without `git pull` first**, not by missing tooling. Now that PyAutoPrompt is the canonical source-of-truth and `skills/install.sh` auto-discovers across both repos, some of the recommendations below may be over-engineered for the day-to-day case. Re-evaluate whether each measure is still warranted — the cheap habits (pull before edit, never rewrite history) buy most of the win. - -The single most damaging drift mechanism on 2026-04-27 was **independent `git init` -"fresh start" rewrites** on different machines. The three workspace repos ended -up with **no merge base at all** with origin — local and origin had each gone -through "Initial commit — fresh start for AI workflow" workflows, producing -identical content under entirely different SHAs. 41 commits had to be discarded. - -The cause is a class of operations that humans and AI agents both find tempting: - -- `rm -rf .git && git init` to "start clean" -- Squashing entire histories into a single "Initial commit" -- `git push --force` to main when local diverges from origin -- Cherry-picking onto a freshly-init'd local branch - -Each of these is sometimes the right tool. None of them is the right tool when -the goal is "I want a clean working tree" — `git fetch && git reset --hard -origin/main && git clean -fd` does the same thing without breaking shared history. - -## What to ship - -Add a `## Never rewrite history` section to every PyAuto repo's `CLAUDE.md` -(and `AGENTS.md` if one exists), worded for both humans and AI agents: - -```markdown -## Never rewrite history - -NEVER perform these operations on any repo with a remote: - -- `git init` in a directory already tracked by git -- `rm -rf .git && git init` -- Commit with subject "Initial commit", "Fresh start", "Start fresh", "Reset - for AI workflow", or any equivalent message on a branch with a remote -- `git push --force` to `main` (or any branch tracked as `origin/HEAD`) -- `git filter-repo` / `git filter-branch` on shared branches -- `git rebase -i` rewriting commits already pushed to a shared branch - -If the working tree needs a clean state, the **only** correct sequence is: - - git fetch origin - git reset --hard origin/main - git clean -fd - -This applies equally to humans, local Claude Code, cloud Claude agents, Codex, -and any other agent. The "Initial commit — fresh start for AI workflow" pattern -that appeared independently on origin and local for three workspace repos is -exactly what this rule prevents — it costs ~40 commits of redundant local work -every time it happens. -``` - -Also add to the `## General Rules` of any repo where one exists, a single line: - -```markdown -- Before any `git init`, `git push --force`, or destructive history operation, - stop and confirm with the user. These rules are non-negotiable. -``` - -## Acceptance - -- The section is present in every PyAuto* and *_workspace* repo's CLAUDE.md. -- A casual `grep "Never rewrite history" -- '**/CLAUDE.md'` over the whole - ecosystem returns hits in every repo. -- Cloud and local agent invocations from now on respect the rule by default - (verify by reading agent transcripts post-deployment for any "fresh start" - language). - -## Out of scope - -- A pre-commit hook that blocks "Initial commit"-style messages on remote-tracked - branches. Useful but more brittle than CLAUDE.md discipline; can be a follow-up. -- Renaming existing fresh-start commits in history. They're already in. - -## Files touched - -One PR per repo, just `CLAUDE.md` (and `AGENTS.md` where present): - -- PyAutoConf, PyAutoFit, PyAutoArray, PyAutoGalaxy, PyAutoLens -- autofit_workspace, autogalaxy_workspace, autolens_workspace -- autofit_workspace_test, autolens_workspace_test -- autofit_workspace_developer, autolens_workspace_developer -- HowToLens (if it has CLAUDE.md) -- admin_jammy -- PyAutoPrompt diff --git a/active/04_truncated_normal_log_partition_incomplete.md b/active/04_truncated_normal_log_partition_incomplete.md deleted file mode 100644 index 5328cd30..00000000 --- a/active/04_truncated_normal_log_partition_incomplete.md +++ /dev/null @@ -1,185 +0,0 @@ -# `@PyAutoFit` `TruncatedNormalMessage` pdf does not integrate to 1 via the generic interface - -Type: bug -Target: priors -Difficulty: large -Autonomy: supervised -Priority: normal -Status: formalised - -Found during the priors/messages audit (see -`PyAutoPrompt/autofit/priors_and_messages_math_audit.md`, finding A4). - -## Problem - -`@PyAutoFit/autofit/messages/truncated_normal.py:29-49`: - -```python -def log_partition(self, xp=np) -> float: - from scipy.stats import norm - a = (self.lower_limit - self.mean) / self.sigma - b = (self.upper_limit - self.mean) / self.sigma - Z = norm.cdf(b) - norm.cdf(a) - return xp.log(Z) if Z > 0 else -xp.inf -``` - -This returns only the truncation normaliser `log Z`. It does **not** -include the standard Gaussian log-partition `A_gauss(η) = -η₁²/(4η₂) - 0.5·log(-2η₂)`. - -The class inherits `MessageInterface.logpdf` (no override), which -computes: - -``` -logpdf(x) = log_base_measure + η·T(x) - log_partition() -``` - -With: -- `log_base_measure = -0.5·log(2π)` (`truncated_normal.py:51`) -- `η = [μ/σ², -1/(2σ²)]` (`truncated_normal.py:163-192`) -- `T(x) = [x, x²]` (`truncated_normal.py:218-236`) -- `log_partition = log Z` ← missing the Gaussian piece - -Expanding: -``` -logpdf(x) = -0.5·log(2π) + μx/σ² - x²/(2σ²) - log Z - = -0.5·log(2π) - (x-μ)²/(2σ²) + μ²/(2σ²) - log Z -``` - -The correct truncated-normal log pdf is: -``` -log p(x) = -0.5·log(2π) - log σ - (x-μ)²/(2σ²) - log Z for x in [a, b] -``` - -So the generic-interface logpdf is wrong by `+μ²/(2σ²) + log σ` (i.e. -missing `-log σ - μ²/(2σ²)`). Consequently `truncated_message.pdf(x)` -does not integrate to 1. - -`NormalMessage.log_partition` *does* include the Gaussian piece -(`@PyAutoFit/autofit/messages/normal.py:49-63`), so the bug is -specifically in the truncated subclass. - -## Wider context — why this matters and why it's latent - -The class also provides direct paths that are correct: - -- `_normal_gradient_hessian` (`truncated_normal.py:349-407`) computes - `logpdf` directly with the `-log σ` and `-log Z` terms. -- `log_prior_from_value` (`truncated_normal.py:481-522`) computes the - full normalised density. - -So: - -- **Sampling** (uses `value_for`) — correct. -- **Inference figure-of-merit** (uses `log_prior_from_value` via - `Fitness._call`) — correct. -- **EP / variational machinery** that calls `.logpdf` via the generic - interface — wrong. -- **User-facing `.pdf(x)`** — wrong (gives a density that integrates - to `σ · exp(μ²/(2σ²))` over the full normal support). - -The latency is because `TruncatedNormalMessage` is rarely fed through -the EP machinery in practice. But anyone calling `.pdf()` or `.factor()` -on it gets a silently wrong answer. - -## Python reproducer - -```python -# Reproducer: truncated_normal_log_partition_incomplete.py -import numpy as np -from scipy.integrate import quad -from scipy.stats import truncnorm - -from autofit.messages.truncated_normal import TruncatedNormalMessage - -# Pick μ != 0 and σ != 1 so both missing terms (-log σ and -μ²/(2σ²)) -# are visible. Bounds are wide so Z is close to 1 and we can see the -# generic-interface error as a clean ~σ·exp(μ²/(2σ²)) factor. -mean, sigma = 1.0, 2.0 -lo, hi = -5.0, 5.0 -msg = TruncatedNormalMessage(mean=mean, sigma=sigma, lower_limit=lo, upper_limit=hi) - -# Path A: MessageInterface.logpdf (inherited, generic exponential family) -def pdf_via_interface(x): - return float(np.exp(msg.logpdf(np.asarray(float(x))))) - -# Path B: scipy.stats.truncnorm as ground truth -a, b = (lo - mean) / sigma, (hi - mean) / sigma -def pdf_correct(x): - return float(truncnorm.pdf(x, a, b, loc=mean, scale=sigma)) - -# Path C: log_prior_from_value (the direct path, should be correct) -def pdf_via_log_prior(x): - return float(np.exp(msg.log_prior_from_value(float(x)))) - -I_interface, _ = quad(pdf_via_interface, lo, hi) -I_correct, _ = quad(pdf_correct, lo, hi) -I_log_prior, _ = quad(pdf_via_log_prior, lo, hi) - -print(f"∫ pdf dx via MessageInterface.logpdf (BUGGY): {I_interface:.4f}") -print(f"∫ pdf dx via log_prior_from_value (correct): {I_log_prior:.4f}") -print(f"∫ pdf dx via scipy.stats.truncnorm (truth): {I_correct:.4f}") -print() -print(f"Expected ratio buggy / correct = σ · exp(μ²/(2σ²)) " - f"= {sigma * np.exp(mean**2 / (2 * sigma**2)):.4f}") -print(f"Observed ratio = " - f"{I_interface / I_correct:.4f}") -``` - -Expected (buggy) output: `I_interface ≈ 2.27`, `I_log_prior ≈ 1.0`, -`I_correct ≈ 1.0`. The ratio matches `σ·exp(μ²/(2σ²)) = 2·exp(0.125)`. - -## Proposed fix - -Add the Gaussian log-partition to the truncation log-partition: - -```python -def log_partition(self, xp=np) -> float: - from scipy.stats import norm - a = (self.lower_limit - self.mean) / self.sigma - b = (self.upper_limit - self.mean) / self.sigma - Z = norm.cdf(b) - norm.cdf(a) - log_Z = xp.log(Z) if Z > 0 else -xp.inf - - # Same as NormalMessage.log_partition: -η₁²/(4η₂) - 0.5·log(-2η₂) - eta1, eta2 = self.natural_parameters(xp=xp) - A_gauss = -(eta1**2) / 4 / eta2 - xp.log(-2 * eta2) / 2 - - return A_gauss + log_Z -``` - -Reviewer should also confirm whether `TruncatedNaturalNormal` (same -file, lines 569-) has the same bug — it inherits `log_partition` from -`TruncatedNormalMessage` so the fix should automatically propagate, -but it's worth a quick numerical check. - -## What the agent picking this up should do - -1. Read `@PyAutoFit/autofit/messages/truncated_normal.py` and - `@PyAutoFit/autofit/messages/normal.py` (for the - sister `log_partition` that *is* correct) and - `@PyAutoFit/autofit/messages/interface.py:47-87` (the generic - `logpdf` path that consumes `log_partition`). -2. Run the reproducer. Confirm the three integrals differ as predicted. -3. Sketch the fix in a scratch checkout (no commit). Re-run. Confirm - `I_interface` now ≈ 1.0. -4. Repeat for `TruncatedNaturalNormal` to confirm the fix propagates. -5. File the GitHub issue via `/create_issue priors/04_truncated_normal_log_partition_incomplete.md`. -6. **In the issue body, ask a reviewer with exponential-family / EP - background to verify the math. Note explicitly that this affects - only the generic interface path; `log_prior_from_value` and - sampling are unaffected.** -7. **Stop. Do not implement until the math is confirmed.** - - -## Fable verdict (2026-07-08, PyAutoFit main @ 0f26ff2d8; PyAutoFit#1330) - -**Verdict: CONFIRMED, numerically exact — highest-priority math fix (severity: high for EP).** -Integral of pdf via the generic `MessageInterface.logpdf` = 2.2663, matching -the predicted error factor sigma*exp(mu^2/(2 sigma^2)) = 2.2663 to 4 d.p.; -`log_prior_from_value` and scipy truncnorm both = 1.0000. The generic -exponential-family path is precisely what `autofit/graphical` consumes, so -this is the first fix to land ahead of the EP statistics review (Phase 1 of -`research/graphical_ep/ep_framework_review.md`). Proposed fix (add the -Gaussian log-partition term) verified correct analytically. - - diff --git a/active/05_inv_beta_suffstats_clamp_noop.md b/active/05_inv_beta_suffstats_clamp_noop.md deleted file mode 100644 index 4a886f82..00000000 --- a/active/05_inv_beta_suffstats_clamp_noop.md +++ /dev/null @@ -1,166 +0,0 @@ -# `@PyAutoFit` `inv_beta_suffstats` negative-clamp branch is a no-op - -Type: bug -Target: priors -Difficulty: large -Autonomy: supervised -Priority: normal -Status: formalised - -Found during the priors/messages audit (see -`PyAutoPrompt/autofit/priors_and_messages_math_audit.md`, finding A2). - -## Problem - -`@PyAutoFit/autofit/messages/beta.py:51-111` solves for `(α, β)` of a -Beta distribution given log-sufficient-statistics, by Newton-Raphson. -The post-NR guard for negative parameters is broken: - -```python -# beta.py:96-110 -if np.any(ab < 0): - warnings.warn( - "invalid negative parameters found for inv_beta_suffstats, " - "clampling value to 0.5", - RuntimeWarning - ) - b = np.clip(ab, 0.5, None) # ← writes to LOCAL `b`, not to `ab` - -shape = np.shape(lnX) -if shape: - a = ab[:, 0].reshape(shape) # ← unpacks from `ab`, which was never updated - b = ab[:, 1].reshape(shape) # ← overwrites the clamp's local `b` -else: - a, b = ab[0, :] # ← same: ignores the clamp - -return a, b -``` - -The `np.clip` result is bound to local `b` and immediately overwritten -two lines later when `a` and `b` are unpacked from the unmodified `ab`. -Net effect: when the warning fires, the returned `(a, b)` are still -negative. - -## Wider context — how this function is called - -`inv_beta_suffstats` is called from -`@PyAutoFit/autofit/messages/beta.py:241-258` — -`BetaMessage.invert_sufficient_statistics`, which is the projection -step that turns sample moments back into Beta parameters during EP / -variational updates. - -Returning a negative `α` or `β` to that pipeline means: - -- The reconstructed `BetaMessage(alpha=neg, beta=neg)` has an - ill-defined PDF (Beta requires α, β > 0). -- Downstream sampling (`np.random.beta(neg, neg, ...)`) raises. -- `logpdf` evaluates to nonsense. - -Whether this is reachable in practice depends on whether the NR loop -ever fails to converge. The test suite never exercises this branch -(see audit doc). The warning would fire if it ever happens, but the -intended clamp would not actually rescue the values — the next -operation downstream would still see negative parameters and crash or -silently produce garbage. - -## Python reproducer - -This is a Python source bug rather than a numerical one — the clearest -demonstration is reading the source and showing the local variable -mismatch. The cleanest reproducer monkey-patches `np.linalg.solve` to -force the NR loop into the negative region: - -```python -# Reproducer: inv_beta_suffstats_clamp_noop.py -import inspect -import warnings - -import numpy as np - -from autofit.messages import beta as beta_mod - -# 1) Source inspection — see the bug directly -print("=== Source of inv_beta_suffstats ===") -src = inspect.getsource(beta_mod.inv_beta_suffstats) -print(src) -print() -print("Observe: `b = np.clip(ab, 0.5, None)` writes to LOCAL `b`,") -print("then a/b are re-unpacked from `ab` (never updated) below the clamp.") -print() - -# 2) Numerical demonstration: force NR off the rails so ab goes negative -print("=== Numerical demo ===") -real_solve = np.linalg.solve - -def bad_solve(A, rhs): - """Force a giant negative step so ab becomes negative.""" - return -np.ones_like(rhs) * 100.0 - -np.linalg.solve = bad_solve -try: - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - a, b = beta_mod.inv_beta_suffstats(-1.0, -1.0) - - print(f"Warning fired? {any('clampling' in str(x.message) for x in w)}") - print(f"Returned a = {a}") - print(f"Returned b = {b}") - print(f"Both >= 0.5 (would be true if clamp worked)? {a >= 0.5 and b >= 0.5}") - print(f"Both >= 0 (Beta requires positive params)? {a >= 0 and b >= 0}") -finally: - np.linalg.solve = real_solve -``` - -Expected (buggy) output: the warning fires (so the branch is taken), -but the returned `a` and `b` are deeply negative — the clamp had no -effect. - -## Proposed fix - -Assign the clamp back to `ab`: - -```python -if np.any(ab < 0): - warnings.warn( - "invalid negative parameters found for inv_beta_suffstats, " - "clamping value to 0.5", # also: typo "clampling" → "clamping" - RuntimeWarning, - ) - ab = np.clip(ab, 0.5, None) -``` - -The reviewer should consider whether silently clamping is actually the -right behaviour or whether this should raise — a Beta projection that -needed clamping to escape negative territory probably indicates a -failed fit, and silently substituting `0.5` could mask a real problem. - -## What the agent picking this up should do - -1. Read `@PyAutoFit/autofit/messages/beta.py` end-to-end. -2. Grep for callers of `inv_beta_suffstats` and - `BetaMessage.invert_sufficient_statistics` across `@PyAutoFit` and - the workspaces — list every site so the reviewer can judge whether - "clamp" or "raise" is the right behaviour for downstream code. -3. Run the reproducer. Confirm the warning fires and the returned - values are negative. -4. Sketch the fix above (assign `ab =`) in a scratch checkout. Re-run. - Confirm clamp now works. -5. File the GitHub issue via `/create_issue priors/05_inv_beta_suffstats_clamp_noop.md`. -6. **In the issue body, ask the reviewer to choose between - (a) silently clamping (preserves current intent) and (b) raising - (more honest about a failed projection).** Either is a one-line - change; the reviewer's preference matters. -7. **Stop. Do not implement until the clamp-vs-raise question is - resolved.** - - -## Fable verdict (2026-07-08, PyAutoFit main @ 0f26ff2d8; PyAutoFit#1330) - -**Verdict: CONFIRMED — fix now (severity: medium).** -Forcing NR off the rails: warning fires, returned a = b = -498.8 — the clamp -is a no-op (local `b` overwritten two lines later). One-line fix. On -clamp-vs-raise: lean **raise** — a projection that went negative is a failed -fit and silently substituting 0.5 masks it (house rule: no silent guards; -fix/expose the producer). - - diff --git a/active/05_sync_slash_command.md b/active/05_sync_slash_command.md deleted file mode 100644 index de992e0e..00000000 --- a/active/05_sync_slash_command.md +++ /dev/null @@ -1,80 +0,0 @@ -# `/sync` slash command — one-shot multi-repo sync - -> ⚠️ **Caveat — drafted from a stale repo state.** This prompt was drafted on 2026-04-27 during a forensic sweep that found local checkouts up to 101 commits behind origin. The trigger looked like a structural workflow flaw, but later analysis showed the drift was largely driven by **stale local checkouts being edited without `git pull` first**, not by missing tooling. Now that PyAutoPrompt is the canonical source-of-truth and `skills/install.sh` auto-discovers across both repos, some of the recommendations below may be over-engineered for the day-to-day case. Re-evaluate whether each measure is still warranted — the cheap habits (pull before edit, never rewrite history) buy most of the win. - -A reproducible version of what was done by hand on 2026-04-27 to pull all 12 -repos in line with origin/main. Drift recovery should be a one-command, -one-minute operation so doing it weekly is cheap and drift never compounds. - -## What to ship - -A new skill `PyAutoPrompt/skills/sync/SKILL.md` (or `sync.md` as a flat command -file — match whichever convention the existing prompt-coupled skills use). - -Behavior, deterministic per-repo: - -``` -for repo in : - if no .git: skip with warning - elif no upstream: warn, list (don't auto-set; surfaces real bugs) - elif merge-base = NONE: surface for confirmation BEFORE any reset - elif behind > 0 and ahead = 0: ff-pull (auto, after handling dirty) - elif behind > 0 and ahead > 0: audit local-ahead commits per origin/main: - - merge commits → check PR state via gh - - non-merge commits → grep origin/main for subject match - If all duplicates, reset --hard with confirm. - If any unique → present diff, ask user. - elif ahead > 0 only: warn, don't push (pushing is user-driven) - elif clean: OK -``` - -Dirty-file handling: - -- Untracked files byte-identical to upstream: silently delete (the "lost" - fits/npz fixtures pattern that blocked pulls on 2026-04-27). -- Tracked-modified files whose diff is fully present in upstream: stash, pull, - drop stash if redundant (the PyAutoJAX→PyAutoLabs rename pattern). -- Anything else: stash with named label, pull, surface for user. - -Output: a single dashboard at the end identical to `pyauto-status` (see -`01_status_dashboard.md`) so the user sees clear before/after. - -## Implementation notes - -- The forensic audit on 2026-04-27 has a working sequence — start from that - conversation transcript or the commands recorded in skill development logs. -- For the "are local-ahead commits all duplicated" check, the heuristic that - worked: subject-match against `git log origin/main --oneline | grep -F - "$subject"`, plus PR-merged check via `gh pr view --json state`. False - positives are rare in practice but always confirm before resetting. -- Workspaces with `merge-base = NONE` are the dangerous case — never auto-reset - these; always show the user the local-ahead commit list and the comparison - with origin's, exactly as the 2026-04-27 audit did. - -## Acceptance - -- `/sync` run on a clean tree (all repos at origin/main) prints the dashboard - and exits in <30s with no changes. -- `/sync` run on a synthetically-staled tree (one repo `git reset --hard HEAD~5`) - fast-forwards it and prints clean dashboard. -- `/sync` run on a tree with truly-divergent local commits (one repo with - unique work) prompts before any destructive action and aborts cleanly on "no". -- `/sync` doesn't touch `admin_jammy` or `PyAutoPrompt` differently — same - rules apply, but those repos see fewer noisy false positives because they're - small. - -## Dependencies - -- `01_status_dashboard.md` (the dashboard format the skill prints) -- `pyauto-status` shell function or equivalent - -## Out of scope - -- Auto-pushing local-ahead commits. Push is always user-initiated. -- Sync of node_modules / pip installs after pulling. That's `06_repo_health_audit.md`. - -## Files touched - -- `PyAutoPrompt/skills/sync/SKILL.md` (new) -- `admin_jammy/skills/install.sh` — add `sync` to the SKILLS array (if it has - a SKILL.md — it should) diff --git a/active/06_normal_message_sigma_negative_unchecked.md b/active/06_normal_message_sigma_negative_unchecked.md deleted file mode 100644 index 9d3b0426..00000000 --- a/active/06_normal_message_sigma_negative_unchecked.md +++ /dev/null @@ -1,189 +0,0 @@ -# `@PyAutoFit` `NormalMessage` silently accepts negative sigma; `TruncatedNormalMessage` rejects it - -Type: bug -Target: priors -Difficulty: large -Autonomy: supervised -Priority: low -Status: formalised - -Found during the priors/messages audit (see -`PyAutoPrompt/autofit/priors_and_messages_math_audit.md`, finding A6). - -## Problem - -`@PyAutoFit/autofit/messages/normal.py:26-46` defines a helper -`assert_sigma_non_negative` with a JAX-aware branch: - -```python -def assert_sigma_non_negative(sigma, xp=np): - is_negative = sigma < 0 - if xp.__name__.startswith("jax"): - import jax - return jax.lax.cond( - is_negative, - lambda _: (_ for _ in ()).throw(ValueError("Sigma cannot be negative")), - lambda _: None, - operand=None, - ) - else: - if bool(is_negative): - raise ValueError("Sigma cannot be negative") -``` - -Two problems: - -1. The call site is commented out (`normal.py:103`): - - ```python - # assert_sigma_non_negative(sigma, xp=xp) - ``` - - So negative sigma silently constructs a `NormalMessage` with a - non-physical variance. Most downstream math (logpdf, gradient, - `1/sigma²`) accidentally still gives a finite number — it's just - wrong. - -2. The JAX branch wouldn't behave as intended even if uncommented. - `lambda _: (_ for _ in ()).throw(...)` creates a generator that - throws when iterated, but `jax.lax.cond` doesn't iterate the lambda - in trace mode — it traces both branches. Under `jit` the exception - is suppressed/rewritten. - -Meanwhile `TruncatedNormalMessage` *does* enforce this -(`@PyAutoFit/autofit/messages/truncated_normal.py:85-86`): - -```python -if (np.array(sigma) < 0).any(): - raise exc.MessageException("Sigma cannot be negative") -``` - -So the two classes disagree on what counts as a valid input. - -## Wider context — why the asymmetry matters - -The inconsistency is load-bearing because `GaussianPrior` wraps -`NormalMessage` directly (not the truncated version), so anything that -produces a negative sigma upstream and feeds it into a `GaussianPrior` -goes undetected. - -The audit identified two upstream sources that can produce negative -sigma: - -- **`RelativeWidthModifier(value).__call__(mean)` → `value * mean`** - (`@PyAutoFit/autofit/mapper/prior/width_modifier.py:78-81`). If - `mean < 0`, the resulting sigma is negative. This is finding A9 in - the audit and gets its own prompt (08). -- **Prior-passing `GaussianPrior.with_limits(lower, upper)` → - `sigma = upper - lower`**. If a user inverts the limits or passes - identical limits, the constructed prior has `sigma <= 0`. No check - in either method. - -So `NormalMessage` is the last line of defence. Fixing it here is the -high-leverage move because every other path eventually flows through -this constructor. - -## Python reproducer - -```python -# Reproducer: normal_message_sigma_negative_unchecked.py -import numpy as np -import autofit as af - -from autofit.messages.normal import NormalMessage -from autofit.messages.truncated_normal import TruncatedNormalMessage - -print("=== NormalMessage(0, sigma=-1) ===") -try: - n = NormalMessage(mean=0.0, sigma=-1.0) - print(f" constructs silently: {n!r}") - print(f" variance = {n.variance}") # = 1.0, deceptively finite - print(f" logpdf(0) = {n.logpdf(np.asarray(0.0))}") -except ValueError as e: - print(f" raised ValueError as expected: {e}") - -print() -print("=== TruncatedNormalMessage(0, sigma=-1) ===") -try: - t = TruncatedNormalMessage(mean=0.0, sigma=-1.0) - print(f" constructs silently: {t!r}") -except Exception as e: - print(f" raised {type(e).__name__}: {e}") - -print() -print("=== GaussianPrior with collapsed limits ===") -try: - p = af.GaussianPrior.with_limits(lower_limit=5.0, upper_limit=5.0) - print(f" constructs silently with sigma=0: {p!r}") - print(f" prior.value_for(0.5) = {p.value_for(0.5)}") - # sigma=0 → degenerate, value_for collapses to mean -except Exception as e: - print(f" raised {type(e).__name__}: {e}") -``` - -Expected (buggy) output: `NormalMessage(0, -1)` constructs without -complaint and returns a finite `variance = 1.0`. `TruncatedNormalMessage(0, -1)` -raises `MessageException`. `GaussianPrior.with_limits(5, 5)` constructs -with sigma=0. - -## Proposed fix - -Two small changes, both straightforward: - -1. **Enforce `sigma > 0` in `NormalMessage.__init__`** to match - `TruncatedNormalMessage`. Either uncomment and fix the existing - `assert_sigma_non_negative` helper, or just inline a NumPy check - that doesn't try to be JAX-aware (the JAX path can defer to NaN - propagation): - - ```python - if (np.asarray(sigma) < 0).any(): - raise exc.MessageException("Sigma cannot be negative") - ``` - -2. **Decide whether `sigma == 0` is also illegal.** A degenerate - Gaussian has support {μ}, which breaks `logpdf`. Most users - wouldn't construct it deliberately. Either: - - reject (`sigma <= 0`), simpler and stricter, or - - allow (`sigma < 0` rejected, `sigma == 0` permitted as a - "point-mass-like" thing). - -The audit recommends `sigma <= 0` → reject, but the reviewer should -confirm whether any current call site depends on `sigma == 0` -constructing. - -## What the agent picking this up should do - -1. Read `@PyAutoFit/autofit/messages/normal.py`, - `@PyAutoFit/autofit/messages/truncated_normal.py:80-95`, - `@PyAutoFit/autofit/mapper/prior/gaussian.py:69-96` (the - `with_limits` upstream), and - `@PyAutoFit/autofit/mapper/prior/width_modifier.py` (the other - upstream that can produce negative sigma). -2. Run the reproducer. Confirm the three asymmetric outcomes. -3. Sketch the fix in a scratch checkout (no commit). Re-run. Confirm - `NormalMessage(0, -1)` now raises. -4. Check whether any test in `@PyAutoFit/test_autofit` deliberately - constructs `NormalMessage` with `sigma <= 0`. If yes, list those - tests for the reviewer (they may be using degenerate priors for - point-mass behaviour — and the fix would break them). -5. File the GitHub issue via `/create_issue priors/06_normal_message_sigma_negative_unchecked.md`. -6. **In the issue body, ask the reviewer to choose between - `sigma < 0` rejected (allows `sigma == 0`) and `sigma <= 0` - rejected. Note this prompt is the prerequisite for prompt 08 - (RelativeWidthModifier safety), which leans on this check.** -7. **Stop. Do not implement until the strict-vs-permissive question - is settled.** - - -## Fable verdict (2026-07-08, PyAutoFit main @ 0f26ff2d8; PyAutoFit#1330) - -**Verdict: CONFIRMED — fix now (severity: medium; prerequisite for 08).** -`NormalMessage(0, sigma=-1)` constructs silently with deceptive -`variance = 1.0`; `TruncatedNormalMessage` raises `MessageException`; -`GaussianPrior.with_limits(5, 5)` constructs with sigma = 0. The -`assert_sigma_non_negative` call is still commented out and its JAX branch -is still broken as described. Lean **sigma <= 0 rejected** (strict), with a -numpy-only check; JAX path defers to NaN propagation. - - diff --git a/active/06_repo_health_audit.md b/active/06_repo_health_audit.md deleted file mode 100644 index 723e421a..00000000 --- a/active/06_repo_health_audit.md +++ /dev/null @@ -1,70 +0,0 @@ -# Monthly repo-health audit - -> ⚠️ **Caveat — drafted from a stale repo state.** This prompt was drafted on 2026-04-27 during a forensic sweep that found local checkouts up to 101 commits behind origin. The trigger looked like a structural workflow flaw, but later analysis showed the drift was largely driven by **stale local checkouts being edited without `git pull` first**, not by missing tooling. Now that PyAutoPrompt is the canonical source-of-truth and `skills/install.sh` auto-discovers across both repos, some of the recommendations below may be over-engineered for the day-to-day case. Re-evaluate whether each measure is still warranted — the cheap habits (pull before edit, never rewrite history) buy most of the win. - -`pyauto-status` (prompt 01) catches commit-count drift but doesn't catch -*structural* problems — repos on dead branches, repos with no remote configured, -directories that look like repos but aren't, stale stashes, generated noise that -slipped past `.gitignore`. These compound silently. The 2026-04-27 audit found: - -- PyAutoFit checked out on `main_build` (a defunct branch), 91 commits behind. - No automation noticed because everything still imported. -- `autolens_workspace_developer` had no `origin` remote at all despite having a - GitHub repo — committing into a void for weeks. -- `autofit_workspace_developer` had no `.git` at all — a directory that looked - like a git repo to humans but wasn't. - -A monthly cron / scheduled run catches these classes structurally. - -## What to ship - -A skill (or a plain script) `PyAutoPrompt/scripts/audit.sh` that, for every -directory under `~/Code/PyAutoLabs/`, reports: - -1. **Branch on a non-default ref.** `git branch --show-current` vs the - `origin/HEAD` symbolic ref. PyAutoFit on `main_build` would have lit up here. -2. **No remote configured.** `git remote -v` empty. Caught autolens_workspace_developer. -3. **No `.git` directory.** Caught autofit_workspace_developer. -4. **Working-tree files older than 30 days that aren't tracked.** Most often - generated junk (`output_path/`, `image.fits`, etc.) that survived because - `.gitignore` doesn't cover it yet. -5. **Stash entries older than 14 days.** Drift-from-stash is a real failure mode - (this run almost fell into one when an early stash pop conflict was left - sitting). Old stashes either matter (recover) or don't (drop). -6. **Branches local-only (no upstream) that haven't been touched in 30 days.** - Likely abandoned feature branches. -7. **Any tracked file matching the noise patterns from `02_gitignore_noise.md`.** - Means the gitignore patches haven't shipped yet for that repo. - -Output format: per-repo block with severity-tagged findings (`ERROR` / `WARN` / -`INFO`), one block per repo, exit code = number of ERRORs. - -Schedule via the existing `/schedule` skill or a plain cron — monthly is enough, -findings are slow-changing. - -## Acceptance - -- Running on the current tree finds zero ERRORs (everything was cleaned up - manually on 2026-04-27). -- Synthetically introducing each failure mode (e.g. `git checkout -b dead-branch` - in PyAutoFit, `rm -rf .git/refs/remotes` in another) makes the audit flag it. -- Output is short enough to read in 30 seconds — don't dump every untracked file, - summarize. - -## Companion config: snooze list - -A `PyAutoPrompt/scripts/audit_snooze.txt` for things known-acceptable -(e.g. PyAutoConf intentionally on `feature/speed-up-unit-tests`). Format: -`::`. Keep it short — if the snooze list grows -beyond ~10 lines, that's a smell. - -## Out of scope - -- Auto-fixing findings. Audit reports; user decides. -- Disk-usage / pack-size audits (separate concern). - -## Files touched - -- `PyAutoPrompt/scripts/audit.sh` (new) -- `PyAutoPrompt/scripts/audit_snooze.txt` (new, possibly empty initially) -- Optionally a `/schedule` entry to run it monthly diff --git a/active/07_log_prior_normalisation_convention.md b/active/07_log_prior_normalisation_convention.md deleted file mode 100644 index 9eca2f67..00000000 --- a/active/07_log_prior_normalisation_convention.md +++ /dev/null @@ -1,188 +0,0 @@ -# `@PyAutoFit` `log_prior_from_value` convention is inconsistent across priors - -Type: bug -Target: priors -Difficulty: too-large -Autonomy: supervised -Priority: normal -Status: formalised - -Found during the priors/messages audit (see -`PyAutoPrompt/autofit/priors_and_messages_math_audit.md`, finding A5). - -## Problem - -After the LogUniform sign-convention fix (`e95295b83`), every prior's -`log_prior_from_value` returns "density form" — but the choice of which -additive constants to drop is inconsistent: - -| Prior / message | `log_prior_from_value(x)` returns | Dropped constants | -|---|---|---| -| `UniformPrior` | `0.0` | `-log(b - a)` | -| `LogUniformPrior` | `-log x` | `-log log(b/a)` | -| `NormalMessage` (used by `GaussianPrior` via `__getattr__`) | `-(x-μ)² / (2σ²)` | `-log σ - 0.5·log(2π)` | -| `LogGaussianPrior` | `-(log x - μ)²/(2σ²) - log x` | `-log σ - 0.5·log(2π)` | -| `TruncatedNormalMessage` | full normalised density including `-log σ`, `-0.5·log(2π)`, `-log Z` | **none** | - -The TruncatedNormal is the odd one out — it returns the full normalised -density. Everything else drops at least one constant. - -## Wider context — what this affects - -For the existing inference machinery this is fine: - -- **MCMC** (`Emcee`, `Zeus`) — only ratios of log-posterior matter, - additive constants cancel. -- **MLE** (`LBFGS`, `BFGS`, `Drawer`) — minimises `-2 · figure_of_merit`, - additive constants are dropped from the gradient. -- **Nested sampling** (`Dynesty`, `Nautilus`) — uses `prior_transform`, - not `log_prior_from_value`, so this column is bypass-able. - -So *posterior shape* and *MAP location* are correct under the current -inconsistency. But it's a foot-gun for: - -1. **Marginal likelihood / Bayes-factor calculations** that try to read - `log_likelihood + sum(log_priors)` as an actual log-evidence. The - missing constants make cross-prior-family comparisons silently - wrong. -2. **Anyone calling `prior.logpdf(x)` and expecting a proper density**. - Some priors give a normalised density, some don't — depends on - which class. -3. **Future contributors** following the existing pattern when adding - a new prior. The current code teaches the wrong lesson (mix of - conventions). - -The recent fix-commit (`e95295b83`) deliberately picked "drop constants" -for the Gaussian-family fixes, on the grounds that the constants are -"a true constant in the prior, irrelevant to posterior shape". But it -didn't propagate that decision to TruncatedNormal, and didn't put the -convention in a docstring anyone could find. - -## Python reproducer - -```python -# Reproducer: log_prior_normalisation_convention.py -import numpy as np -from scipy.integrate import quad - -import autofit as af -from autofit.messages.truncated_normal import TruncatedNormalMessage - -def integrate_exp_log_prior(prior, lo, hi, **kwargs): - """Numerically integrate exp(log_prior_from_value(x)) over [lo, hi].""" - def f(x): - return float(np.exp(prior.log_prior_from_value(float(x)))) - I, _ = quad(f, lo, hi, **kwargs) - return I - -# 1. Uniform -u = af.UniformPrior(lower_limit=0.0, upper_limit=2.0) -I_u = integrate_exp_log_prior(u, 0.0, 2.0) -print(f"Uniform(0, 2): ∫ exp(log_prior) dx = {I_u:.4f} (drops -log(b-a)=-log 2)") - -# 2. LogUniform -lu = af.LogUniformPrior(lower_limit=0.1, upper_limit=10.0) -I_lu = integrate_exp_log_prior(lu, 0.1, 10.0) -print(f"LogUniform(0.1, 10): ∫ exp(log_prior) dx = {I_lu:.4f} (drops -log log(b/a))") - -# 3. Gaussian (delegated to NormalMessage via __getattr__) -g = af.GaussianPrior(mean=0.0, sigma=1.0) -I_g = integrate_exp_log_prior(g, -10.0, 10.0) -print(f"Gaussian(0, 1): ∫ exp(log_prior) dx = {I_g:.4f} (drops -log σ - 0.5·log 2π)") - -# 4. LogGaussian -lg = af.LogGaussianPrior(mean=0.0, sigma=1.0) -I_lg = integrate_exp_log_prior(lg, 1e-6, 100.0) -print(f"LogGaussian(0, 1): ∫ exp(log_prior) dx = {I_lg:.4f} (drops -log σ - 0.5·log 2π)") - -# 5. TruncatedNormal — the odd one out -tn_msg = TruncatedNormalMessage(mean=0.0, sigma=1.0, lower_limit=-2.0, upper_limit=2.0) -I_tn = integrate_exp_log_prior(tn_msg, -2.0, 2.0) -print(f"TruncatedNormal: ∫ exp(log_prior) dx = {I_tn:.4f} (full normalised pdf — ≈1.0)") - -print() -print("Convention is inconsistent: TruncatedNormal returns a normalised pdf,") -print("everything else returns up-to-additive-constant.") -``` - -Expected (buggy) output: the five integrals are all different. The -TruncatedNormal one is ≈ 1.0; the others are not. - -## Proposed fix — needs a decision, not code - -Two coherent options: - -### Option A: drop constants everywhere (audit's first-pass recommendation) - -Pros: matches existing Gaussian/LogGaussian/Uniform/LogUniform behaviour -(four out of five). Fastest path. Matches the *spirit* of the existing -fix commit. Lower runtime cost (no `log(Z)` for truncated normal in the -inference loop). - -Cons: ` prior.logpdf` is no longer a proper density. Marginal-likelihood -calculations require a separate "log normaliser" hook per prior class. - -Change required: `TruncatedNormalMessage.log_prior_from_value` drops the -constants — only return `-0.5·(x-μ)²/σ²` plus the in-bounds mask. - -### Option B: keep full normalised density everywhere - -Pros: `prior.logpdf(x)` is always a proper density, the value is -meaningful in isolation, evidence calculations are correct without -extra hooks. Matches `scipy.stats` convention. - -Cons: bigger change (every prior is touched). Slight runtime cost in -the MCMC/MLE inner loop from computing constants that get cancelled -anyway. Need to ensure JAX paths are also normalised consistently. - -### Decision criteria - -The audit recommends asking the reviewer: - -- Do any downstream consumers (workspace examples, `analysis` plotting, - third-party code in `autoarray`/`autogalaxy`/`autolens`) read - `prior.log_prior_from_value(x)` and treat it as a proper density? -- Is there appetite to expose a separate `prior.log_normaliser` - property that returns the dropped constants on demand? This would - let Option A coexist with evidence calculations. - -Once chosen, the change is: - -- Update every prior subclass to match the convention. -- Add a docstring on `Prior.log_prior_from_value` (the base class) - stating the convention as a hard contract. -- Add a property-based test (see prompt 09) that asserts the convention - on every subclass. - -## What the agent picking this up should do - -1. Read every prior's `log_prior_from_value` in - `@PyAutoFit/autofit/mapper/prior/` and - `@PyAutoFit/autofit/messages/`. Make a fresh version of the table - above to confirm the audit's claims have not drifted. -2. Grep across `@PyAutoFit`, `@autofit_workspace`, - `@PyAutoArray`, `@PyAutoGalaxy`, `@PyAutoLens` for callers of - `log_prior_from_value` and `prior.logpdf` to identify downstream - consumers. List each one in the issue body. -3. Run the reproducer. Confirm the five integrals differ. -4. File the GitHub issue via `/create_issue priors/07_log_prior_normalisation_convention.md`. -5. **In the issue body, frame the choice (Option A vs B) clearly and - ask the reviewer to pick.** Provide the list of downstream callers - so the reviewer can see which choice is least disruptive. -6. **Stop. This is a design decision, not a bug fix.** Implementation - proceeds only after the reviewer signs off on the convention. - - -## Fable verdict (2026-07-08, PyAutoFit main @ 0f26ff2d8; PyAutoFit#1330) - -**Verdict: CONFIRMED — design decision still open (Option A vs B).** -Integrals of exp(log_prior_from_value): Uniform(0,2) = 2.000, -LogUniform(0.1,10) = 4.605, Gaussian(0,1) = 2.507, LogGaussian(0,1) = 2.507, -TruncatedNormalMessage = 1.000 — four-vs-one inconsistency exactly as -audited. Current main's `NormalMessage.log_prior_from_value` docstring now -documents the drop-constants convention, strengthening Option A's claim to -be the de-facto standard. Recommend settling this inside Phase 2 of the EP -framework review (`research/graphical_ep/ep_framework_review.md`), whose -formal-equations documentation must state the convention either way. - - diff --git a/active/08_relative_width_modifier_safety.md b/active/08_relative_width_modifier_safety.md deleted file mode 100644 index fd53e0ee..00000000 --- a/active/08_relative_width_modifier_safety.md +++ /dev/null @@ -1,188 +0,0 @@ -# `@PyAutoFit` `RelativeWidthModifier` produces zero / negative sigma for parameters near 0 - -Type: bug -Target: priors -Difficulty: too-large -Autonomy: supervised -Priority: normal -Status: formalised - -Found during the priors/messages audit (see -`PyAutoPrompt/autofit/priors_and_messages_math_audit.md`, finding A9 -and C7). - -> **Prerequisite:** prompt 06 (`NormalMessage` sigma check) should -> land first. With negative sigma rejected by the constructor, this -> bug becomes loud — `RelativeWidthModifier` × negative mean would -> raise immediately. Without that fix, this bug is silent. - -## Problem - -`@PyAutoFit/autofit/mapper/prior/width_modifier.py:78-81`: - -```python -class RelativeWidthModifier(WidthModifier): - def __call__(self, mean): - return self.value * mean -``` - -This is the prior-passing width used when the user has not specified -an absolute floor. For a parameter posterior centred near 0 or -crossing 0: - -- `mean = 0` → new prior sigma = 0 → degenerate Gaussian. -- `mean < 0` → new prior sigma < 0 → currently accepted silently by - `NormalMessage` (see prompt 06), which then produces a Gaussian - whose `variance = sigma² > 0` but whose `value_for`, gradient and - `sample` propagate the negative scale (sometimes flipping signs). - -## Wider context — how this is reached in practice - -Prior passing flow (paraphrased from -`@PyAutoFit/autofit/mapper/prior_model/abstract.py` and the -`GaussianPrior.with_limits` family): - -1. A search finishes. Each parameter gets a posterior median `m̂`. -2. For each parameter, look up the `WidthModifier` configured for - that class+attribute in `conf.instance.prior_config`. -3. If `RelativeWidthModifier(0.5)`, the next prior has - `sigma = 0.5 · m̂`. -4. If no width-modifier is configured, the system defaults to - `RelativeWidthModifier(0.5)` (see `width_modifier.py:57-72`). - -So this default path is reachable any time: - -- A parameter is roughly centred near zero (common: pixel offsets, - ellipticity components, perturbations). -- A parameter can take negative values (common: shifts, log-residuals). - -The default is silently dangerous in either case. - -## Python reproducer - -```python -# Reproducer: relative_width_modifier_safety.py -import autofit as af - -from autofit.mapper.prior.width_modifier import RelativeWidthModifier - -mod = RelativeWidthModifier(0.5) - -print("=== Various means through RelativeWidthModifier(0.5) ===") -for m in [10.0, 1.0, 0.1, 0.0, -0.1, -1.0]: - sigma = mod(m) - print(f" mean={m:>6.2f} → sigma={sigma:>6.3f}", end=" ") - if sigma <= 0: - print("← PROBLEMATIC (sigma must be > 0 for Gaussian)") - else: - print() - -print() -print("=== Downstream: build a GaussianPrior with the bad sigma ===") -sigma_bad = mod(-1.0) -try: - p = af.GaussianPrior(mean=0.0, sigma=sigma_bad) - print(f" GaussianPrior(mean=0, sigma={sigma_bad}) constructed silently: {p!r}") - print(f" value_for(0.5) = {p.value_for(0.5)}") - print(f" value_for(0.84) = {p.value_for(0.84)} (should be ~mean+sigma)") -except Exception as e: - print(f" raised {type(e).__name__}: {e} (expected after prompt-06 lands)") - -print() -print("=== Zero-sigma degeneracy ===") -try: - p0 = af.GaussianPrior(mean=0.0, sigma=mod(0.0)) - print(f" GaussianPrior with sigma=0 constructed: {p0!r}") - print(f" value_for(0.1) = {p0.value_for(0.1)}") - print(f" value_for(0.9) = {p0.value_for(0.9)}") - print(f" (collapsed: every unit value maps to the mean)") -except Exception as e: - print(f" raised {type(e).__name__}: {e}") -``` - -Expected (buggy) output: negative and zero sigmas pass through the -modifier without complaint. With prompt 06 still unfixed, the -downstream `GaussianPrior` also constructs silently. - -## Proposed fix — needs a design decision - -Three options, in increasing scope: - -### Option A: minimal safety floor in `RelativeWidthModifier` - -```python -class RelativeWidthModifier(WidthModifier): - def __init__(self, value, absolute_floor=None): - super().__init__(value) - self.absolute_floor = absolute_floor # optional, defaults None - - def __call__(self, mean): - sigma = self.value * abs(mean) - if self.absolute_floor is not None: - sigma = max(sigma, self.absolute_floor) - return sigma -``` - -- Uses `abs(mean)` so sign is preserved as a width. -- Lets users opt into a floor per attribute via YAML config. -- Backwards-compatible default (no floor). - -Pro: smallest change. Con: doesn't fix the default-zero-floor case. - -### Option B: mandatory floor with sensible default - -Always require an `absolute_floor` (e.g. default `1e-8`), so the -modifier *cannot* return zero. Existing YAML configs need a one-time -audit to add explicit floors where needed. - -Pro: defensive by default. Con: config migration work. - -### Option C: redesign per finding C7 in the audit - -Replace `WidthModifier` subclasses with a single -`PriorPassWidth(relative=..., absolute_floor=..., absolute_cap=...)` -that always sigmoids between bounds. YAML schema migrates accordingly. - -Pro: cleanest long-term API. Con: largest change, breaks config files. - -### Decision criteria - -The reviewer should consider: - -- How many YAML `prior_config` entries currently use the implicit - `RelativeWidthModifier(0.5)` default? (Grep workspace configs.) -- Are any of those for parameters that can cross zero? -- Is `abs(mean)` the right thing? Some users may rely on negative - sigma silently flipping the prior — though no test covers this. - -## What the agent picking this up should do - -1. **Wait for prompt 06 to be acked and merged first.** This prompt - leans on `NormalMessage` rejecting negative sigma; otherwise the - reproducer's "downstream effect" is silent and harder to argue. -2. Read `@PyAutoFit/autofit/mapper/prior/width_modifier.py` and the - YAML `prior_config` files in `@PyAutoFit/autofit/config/priors/` - and the workspaces. -3. Grep across `@PyAutoFit`, `@PyAutoArray`, `@PyAutoGalaxy`, - `@PyAutoLens`, `@autofit_workspace`, `@autogalaxy_workspace`, - `@autolens_workspace` for `RelativeWidthModifier` and - `width_modifier:` to list every config that relies on the default. -4. Run the reproducer. Confirm zero / negative sigma escapes the - modifier. -5. File the GitHub issue via `/create_issue priors/08_relative_width_modifier_safety.md`. -6. **In the issue body, present options A/B/C and ask the reviewer - which is the right scope.** Include the config audit list so the - reviewer can judge migration cost. -7. **Stop. Do not implement until the reviewer chooses A, B, or C.** - - -## Fable verdict (2026-07-08, PyAutoFit main @ 0f26ff2d8; PyAutoFit#1330) - -**Verdict: CONFIRMED — fix after prompt 06 (severity: medium).** -`RelativeWidthModifier(0.5)(-1.0) = -0.5` flows into `GaussianPrior` -silently; `value_for(0.84) = -0.497` (scale flipped). Zero mean gives -sigma = 0 degenerate prior. Implicit default `RelativeWidthModifier(0.5)` -still in place. Options A/B/C decision still required; prompt 06 landing -first turns this from silent to loud as designed. - - diff --git a/active/08_test_summary.md b/active/08_test_summary.md deleted file mode 100644 index 4c6d2d00..00000000 --- a/active/08_test_summary.md +++ /dev/null @@ -1,11 +0,0 @@ -currently, when smoke tests run they display only on that prompt but the ifnormation is not permenant and often lost. -Can you make it so smoke test runs store their summary to a file which then displays when pyauto-summary runs, -so I can see the state of all smoke tests when I activeate my venv? Can you make this text green or red depenending -on success or failure. - -Can you also do a similar thing with the latest build and release, e.g. what version is the software, -whene did we last do a release and what is a summary (keep it quite concise, just say how many failures not -which ones) were there when we did the full autobuild release which runs all test workspaces? Again, -this probably means files need creating when they run/ - -I dont want loading the venv to be too slow so try and make sure this stays fast. \ No newline at end of file diff --git a/active/10_fixed_message_cache_growth.md b/active/10_fixed_message_cache_growth.md deleted file mode 100644 index f1349e9e..00000000 --- a/active/10_fixed_message_cache_growth.md +++ /dev/null @@ -1,139 +0,0 @@ -# `@PyAutoFit` `FixedMessage.logpdf_cache` is an unbounded class-level dict - -Type: bug -Target: priors -Difficulty: large -Autonomy: supervised -Priority: high -Status: formalised - -Found during the priors/messages audit (see -`PyAutoPrompt/autofit/priors_and_messages_math_audit.md`, finding A8). - -## Problem - -`@PyAutoFit/autofit/messages/fixed.py:57-62`: - -```python -class FixedMessage(AbstractMessage): - ... - logpdf_cache = {} # class-level mutable dict, lives forever - - def logpdf(self, x: np.ndarray) -> np.ndarray: - if x.shape not in FixedMessage.logpdf_cache: - FixedMessage.logpdf_cache[x.shape] = np.zeros_like(x) - return FixedMessage.logpdf_cache[x.shape] -``` - -Every distinct shape ever seen during the process lifetime is memoised -on the class. In a long-running fit that evaluates `logpdf` on arrays -of varying shape (per-iteration sample chunks, varying batch sizes, -JAX traces, etc.) the cache grows unbounded. - -There is also a subtle aliasing risk: the cached zero-array is -returned by reference, so any caller that mutates the result mutates -the shared cache entry. - -## Wider context — how `FixedMessage` is used - -`FixedMessage` represents a delta / point-mass / fixed-value -"distribution" used as a placeholder in EP graphs when a variable is -clamped. Its logpdf is mathematically undefined (a delta is not a -density), so the class returns `0` as a no-op that doesn't perturb -the EP message-passing arithmetic. - -The class is rarely the bottleneck of a fit, so the cache hasn't -caused user-visible problems. But: - -- Long-running services (e.g. always-on EP solver) leak. -- Test suites that run thousands of fits in one process leak. -- Returning a shared mutable array is the kind of latent bug that - shows up as "fix in this place produces a regression in that place". - -Not a math bug. A correctness-of-Python bug with low blast radius. - -## Python reproducer - -```python -# Reproducer: fixed_message_cache_growth.py -import numpy as np -from autofit.messages.fixed import FixedMessage - -msg = FixedMessage(value=1.0) - -# Before -print(f"cache size before: {len(FixedMessage.logpdf_cache)}") - -# Generate a logpdf call for many distinct shapes -for n in range(1, 1001): - _ = msg.logpdf(np.zeros(n)) - -print(f"cache size after 1000 distinct shapes: {len(FixedMessage.logpdf_cache)}") -print() - -# Aliasing demonstration -a = msg.logpdf(np.zeros(5)) -b = msg.logpdf(np.zeros(5)) -print(f"Same shape returns the SAME object? {a is b}") -a[0] = 99.0 # mutates the shared cache entry -print(f"After mutating a: b[0] = {b[0]} (should be 0.0 — but shared cache)") -``` - -Expected (buggy) output: cache grows to 1000 entries; `a is b` is -True; mutating `a[0]` corrupts `b[0]`. - -## Proposed fix - -Either: - -1. **Compute on demand** (simplest, no cache): - - ```python - def logpdf(self, x): - return np.zeros_like(x) - ``` - -2. **Compute on demand, return a fresh array** to also kill the - aliasing — same as (1), the cache was the only thing aliasing. - -3. **Keep a tiny LRU cache** if profiling actually shows allocation - pressure (unlikely for `np.zeros_like`): - - ```python - from functools import lru_cache - @staticmethod - @lru_cache(maxsize=32) - def _zeros(shape, dtype): - return np.zeros(shape, dtype=dtype) - def logpdf(self, x): - return self._zeros(x.shape, x.dtype).copy() - ``` - -The audit recommends option 1: `np.zeros_like(x)` is cheap, the cache -optimises nothing important, and removing it eliminates both the leak -and the aliasing. - -## What the agent picking this up should do - -1. Read `@PyAutoFit/autofit/messages/fixed.py` end-to-end. -2. Grep for `FixedMessage` usage to confirm no caller relies on the - "shared mutable zero array" behaviour. If any test or code does, it - was almost certainly accidental; flag for the reviewer. -3. Run the reproducer. Confirm cache growth and aliasing. -4. Sketch the fix (option 1) in a scratch checkout. Re-run the - reproducer. Confirm `a is b` is now False and the cache is empty. -5. File the GitHub issue via `/create_issue priors/10_fixed_message_cache_growth.md`. -6. **In the issue body, ask the reviewer whether the cache was ever - intentional** (i.e. is there a known hot path that benefits from - it?). If not, option 1 is the right answer. -7. **Stop. Do not implement until acked.** - - -## Fable verdict (2026-07-08, PyAutoFit main @ 0f26ff2d8; PyAutoFit#1330) - -**Verdict: CONFIRMED — fix now (severity: low-medium; one-line).** -Cache grew 0 -> 500 over 500 distinct shapes; same-shape calls return the -same object and mutating one corrupts the other (aliasing verified). -Option 1 (`np.zeros_like(x)`, no cache) remains the right fix. - - diff --git a/active/10_sigma_crit_jax.md b/active/10_sigma_crit_jax.md deleted file mode 100644 index 1bd2062e..00000000 --- a/active/10_sigma_crit_jax.md +++ /dev/null @@ -1,22 +0,0 @@ -# Weak lensing FitWeak upgrades: per-galaxy sigma_crit scaling + JAX support - -Type: feature -Target: weak -Difficulty: medium -Autonomy: supervised -Priority: high -Status: formalised - -Two follow-ups from the completed weak series (epic z_features/weak_shear.md), combined because both rework FitWeak internals: - -(1) Per-galaxy sigma_crit (lensing-efficiency) scaling on the redshift storage shipped in 7a: when -WeakDataset.redshifts is present, FitWeak scales the model shear per galaxy by beta_i/beta_ref where -beta = D_ls/D_s (angular diameter distances from the tracer's cosmology; z_ref = the tracer's source-plane -redshift; galaxies at z <= z_lens get zero signal). For is_reduced datasets both gamma AND kappa scale: -g_i = (s_i*gamma)/(1 - s_i*kappa). Datasets without redshifts are unchanged (single effective source plane). - -(2) JAX/pytree support for FitWeak, mirroring AnalysisPoint: register_instance_pytree(FitWeak, -no_flatten=(constants)), thread xp through the fit statistics (LensCalc hessian methods already take xp), -AnalysisWeak(use_jax=True) path registers pytrees in fit_from. Default stays use_jax=False. Library unit -tests NumPy-only per repo rules; JAX validation via an autolens_workspace_test parity script -(fitness._vmap per the standing rule), which also un-parks the old fast_visualization D.2.b.iii item. diff --git a/active/10_solver_over_under_prediction.md b/active/10_solver_over_under_prediction.md deleted file mode 100644 index 8a75394f..00000000 --- a/active/10_solver_over_under_prediction.md +++ /dev/null @@ -1,44 +0,0 @@ -# Point-source solver over/under-prediction handling + workspace guide - -Type: feature -Target: cluster -Difficulty: large -Autonomy: supervised -Priority: normal -Status: formalised - -Careful treatment of multiple-image over- and under-prediction in the point-source solver/likelihood, plus a workspace guide documenting the choices. - -The cluster/point-source image-plane likelihood must handle the case where the model tracer predicts -*more* images than observed (extra images — often demagnified centrals or artefacts of a wrong mass -model) and *fewer* images than observed (under-prediction — the model cannot reproduce an observed -image). The library currently offers three pairing schemes (Pair / PairAll / PairRepeat, Hungarian -assignment — see scripts/cluster/likelihood_function.py which documents the too-many/too-few image -pathology) but the behaviour under mismatched image counts needs deliberate design rather than -incidental behaviour: - -- Audit what each pairing scheme actually does today when n_model != n_observed, including the - likelihood penalty (or silent absence of one) in each direction. Compare against how LensTool - handles it (chi^2 penalty terms for missing images; treatment of predicted-but-unobserved images, - which observers often justify as "below detection limit"). -- Decide and implement the default: e.g. explicit penalty terms for unmatched observed images - (under-prediction should always be penalized hard) and a configurable policy for extra model - images (penalize / ignore-with-warning / magnification-threshold filter for demagnified centrals). - No silent guards — a model that cannot produce an observed image should be loudly bad, not - quietly fine. -- Verify solver robustness feeding this: PointSolver grid resolution vs missed images (a real - image missed by a too-coarse solver grid must not masquerade as model under-prediction). -- Write a guide at `autolens_workspace/scripts/guides/` (matching the existing guides' style) - documenting: the pairing schemes, the over/under-prediction policies and how to choose, solver - settings that matter at cluster scale, and source-plane vs image-plane chi^2 trade-offs — the - reference the flagship LensTool example links to for likelihood choices. - -Relevant prior finding: cluster source-plane chi^2 has a PointSolver precision-floor issue -(magnification-amplified, ~8e7 at truth — see cluster-test-workspace notes / likelihood_sanity.py); -this prompt's audit should keep that in mind since penalty-term magnitudes interact with the floor. - -Scope: PyAutoLens (point-source fit/solver) + autolens_workspace (guide + cluster scripts prose). -Should land before or alongside the flagship LensTool example, whose real-data fit will hit these -cases immediately. - - diff --git a/active/11_small_datasets_cluster.md b/active/11_small_datasets_cluster.md deleted file mode 100644 index d908e030..00000000 --- a/active/11_small_datasets_cluster.md +++ /dev/null @@ -1,64 +0,0 @@ -# PYAUTO_SMALL_DATASETS support for cluster lensing (fast workspace test runs) - -Type: feature -Target: cluster -Difficulty: medium -Autonomy: supervised -Priority: normal -Status: formalised - -Add PYAUTO_SMALL_DATASETS support for cluster lensing, so workspace test runs of the cluster -scripts run fast the way other datasets do. - -Original request (verbatim): "Can you add a PyAutoMind prompt to add PYAUTO_SMALL_DATASET support -for cluster lensing, which makes it so workspace test runs run fast and is used for other -datasets. This may already be there due to point source models but is worth a task." - -Grounding (2026-07-09 session): - -- PYAUTO_SMALL_DATASETS already exists and covers parts of the path: PointSolver has a documented - smoke-test short-circuit (autolens/point/solver/point_solver.py:87-111), and PyAutoArray - downsizes masks/grids/over-sampling (mask_2d.py, uniform_2d.py, dataset_util.py, - over_sample_util.py). So the point-source *solve* is already fast-mode aware. -- Despite that, cluster/start_here.py and cluster/modeling.py exceed 500 s under - PYAUTO_TEST_MODE=2 even on clean main (control-verified 2026-07-08) and are parked in - PyAutoBuild no_run.yaml:32-33 ("test mode breaks it"). PyAutoBuild env_vars.yaml has NO cluster - entries, so the flag is never even applied to them in CI. The un-parking of these two scripts is - the concrete success criterion for this task. -- Suspected remaining hot spots to profile and fix under the flag: (a) the cluster simulator - auto-regeneration (JAX PointSolver JIT compile + full-resolution imaging sim at 0.1"/px over an - arcminute field) — small-mode should shrink the imaging grid and/or skip the imaging leg; - (b) the 149-profile lenstool example scripts (scripts/cluster/lenstool/): data.py downloads a - 96 MB mosaic (small-mode should skip the cutout leg) and modeling.py's tracer operations scale - with profile count; (c) tracer/critical-curve visualization on multi-plane cluster tracers - (known ~10 min/plane in numpy — the same cost that forced tracer.png regeneration to be dropped - in autolens_workspace#238 and MAKE_FIGURES gating in #240). -- Beware the known env-flag footguns: PYAUTO_SMALL_DATASETS mutates Grid2D.uniform inside - decorators, so trace the full call path when extending it (memory: smoke-env grid mutations); - and fixes belong in config/build/env_vars.yaml overrides, never os.environ mutation in scripts. -- Deliverables: (1) extend/verify small-dataset behaviour across the cluster path (simulator, - modeling, start_here, lenstool example, csv_api/likelihood_function as needed); (2) add cluster - entries to PyAutoBuild env_vars.yaml; (3) un-park cluster/start_here + cluster/modeling from - no_run.yaml with a timed CI-green run as evidence; (4) consider promoting the new lenstool - example scripts into the smoke set (small curated subset rule — only if genuinely fast). - - - -__Addendum (user direction, 2026-07-09): standardize the auto-simulation guard__ - -Data-consuming workspace scripts should use the canonical auto-simulation pattern, as the imaging -scripts do: - - if al.util.dataset.should_simulate(str(dataset_path)): - import subprocess, sys - subprocess.run([sys.executable, "scripts/cluster/simulator.py"], check=True) - -The cluster scripts currently use hand-rolled existence checks (`modeling.py` / `start_here.py` -test data.fits + scaling_galaxies.csv manually) and `lenstool/data.py` uses its own download -caching. As part of this task, migrate the cluster scripts to `al.util.dataset.should_simulate` -— which also carries the PYAUTO_SMALL_DATASETS delete-and-regenerate semantic that makes the -small-mode dataset actually get built small (see autoarray/util/dataset_util.py). The weak-lensing -twin of this migration is already in flight (weak-viz-profiles, PyAutoLens#581, plus -feature/weak/9_small_datasets.md) — mirror its conventions so imaging, weak and cluster all tell -the same auto-sim story. For lenstool/data.py, downloads are not simulations: keep the download -caching, but gate the expensive legs (96 MB mosaic/cutout) off under PYAUTO_SMALL_DATASETS. diff --git a/active/1_autonomy_contract.md b/active/1_autonomy_contract.md deleted file mode 100644 index 70a896ec..00000000 --- a/active/1_autonomy_contract.md +++ /dev/null @@ -1,61 +0,0 @@ -# The Brain autonomy contract — make the Autonomy header load-bearing - -Type: feature -Target: autonomy -Repos: -- PyAutoBrain -- PyAutoMind -Difficulty: medium -Autonomy: human-required -Priority: high -Status: draft - -## Why - -The Mind prompt header already blesses `Autonomy: safe | supervised | -human-required` (@PyAutoMind/README.md "Prompt file format"), and the Intake -Agent persists it via the sizing faculty — but **nothing consumes it**. Every -workflow run stops at the same human checkpoints regardless of the value. This -prompt makes the field load-bearing: it is the doctrine every later task in -`feature/autonomy/` keys off. - -## What - -Write `@PyAutoBrain/AUTONOMY.md` — one canonical page, linked from -`WORKFLOW.md` and the Mind README (no duplicated prose), that: - -1. **Enumerates every human checkpoint** in the dev workflow today: Plan-Mode - approval in `start_dev`, ship PR sign-off (`## API Changes` / `## Scripts - Changed`), Heart YELLOW acknowledgement, the merge/close prompt, the - `pre_build` minor-version ask, post-merge cleanup confirmation. -2. **Defines behaviour per level at each checkpoint**: - - `safe` → proceed and log (plan written to the issue, not held). - - `supervised` → proceed, but batch questions to the issue - (checkpoint-and-continue, see `5_checkpoint_and_continue.md`). - - `human-required` → today's behaviour, unchanged. -3. **Per-work-type autonomy caps** — a prompt's header can never exceed the - cap: `refactor`/`test`/`maintenance` may run `safe`; `feature`/`bug` are - capped at `supervised` until the calibration log (below) justifies raising; - `release` is always `human-required`. -4. **Activation rule** — autonomy levels only take effect when the human - launches with an explicit `--auto` (or equivalent). Default runs behave - exactly as today. Autonomy is opt-in per invocation, never ambient. -5. **Calibration log** — a small append-only record (in Mind, e.g. - `autonomy_log.md`): for each autonomous run, was the PR merged unchanged, - amended, or rejected? This is the evidence for later raising/lowering caps. -6. **Model doctrine refresh** — update `WORKFLOW.md`'s "Opus plans, Sonnet - executes" split to the Fable era: Fable (or the strongest available model) - for orchestration/judgment and tutorial prose; the split must be stated - model-agnostically (strongest-available / mid / fast tiers) so nothing - breaks if Fable access lapses. - -## Boundaries (adversarial findings, keep these) - -- The contract is **doctrine only** — no skill behaviour changes in this task; - `4_auto_dev_mode.md` implements consumption. One PR, prose + pointers. -- Sizing (a model) assigns Autonomy; skipping approval purely on the model's - own estimate is circular. The caps + explicit `--auto` + calibration log are - the mitigations — they are not optional extras. -- Merging a PR stays a human act at every level (standing preference). - -Blocked-by: nothing. Everything else in `feature/autonomy/` is blocked by this. diff --git a/active/1_workspace_visualization.md b/active/1_workspace_visualization.md deleted file mode 100644 index 5ce1d7d8..00000000 --- a/active/1_workspace_visualization.md +++ /dev/null @@ -1,17 +0,0 @@ -Step 1 of the ellipse-JAX series. The end goal is to make `@PyAutoGalaxy/autogalaxy/ellipse/model/analysis.py`, `AnalysisEllipse.log_likelihood_function` JAX-compatible (analogous to `AnalysisImaging` in `@PyAutoGalaxy/autogalaxy/imaging/model/analysis.py`). Before any of that, we need to lock down the existing numpy behaviour with integration tests in `@autogalaxy_workspace_test/scripts`, so when later prompts rewrite the gnarly bits we can spot regressions immediately. - -This prompt covers the **ellipse visualization** integration test. The follow-up `2_workspace_jax_likelihood.md` covers the likelihood-function script. - -Please: - -1. Add `@autogalaxy_workspace_test/scripts/visualization.py`. Pattern it on the existing `@autogalaxy_workspace/scripts/ellipse/fit.py` walkthrough but trimmed to the visualization side: load (or auto-simulate) a small dataset, fit a single `Ellipse` (and an `Ellipse + EllipseMultipole`), then exercise every public plotter path through `@PyAutoGalaxy/autogalaxy/ellipse/model/plotter.py` (`PlotterEllipse.imaging`, `PlotterEllipse.fit_ellipse`) and `aplt.FitEllipsePlotter` if one exists. Use `PYAUTO_OUTPUT_MODE=1` semantics — the script just has to run end-to-end without raising. - -2. Cover the multipole code path too: a fit with `multipole_list=[ag.EllipseMultipole(m=4, multipole_comps=(0.05, 0.0))]` and a fit with `EllipseMultipoleScaled` from `@PyAutoGalaxy/autogalaxy/ellipse/ellipse/ellipse_multipole.py`. - -3. Cover the masked-data code path: apply a `Mask2D` that the ellipse partially overlaps, so `FitEllipse.points_from_major_axis_from`'s 300-iteration mask-rejection loop in `@PyAutoGalaxy/autogalaxy/ellipse/fit_ellipse.py:81-134` actually fires. Without this, prompt 6 has nothing to compare against. - -4. Use the workspace docstring style (`"""..."""` blocks with `__Section Name__` headers, no `#` comments) — see `@autogalaxy_workspace/scripts/ellipse/fit.py` for examples. - -5. Test bar: the script runs cleanly under `bash run_all_scripts.sh` from `@autogalaxy_workspace_test/`, and produces output PNGs under `output_mode/visualization/` when `PYAUTO_OUTPUT_MODE=1` is set. - -This is numpy-only — no JAX yet. The point is to have a regression target before we touch anything underneath. diff --git a/active/2_csv.md b/active/2_csv.md deleted file mode 100644 index a7978086..00000000 --- a/active/2_csv.md +++ /dev/null @@ -1,99 +0,0 @@ -For autolens_workspace/scripts/cluster/simulator.py, can we make it so that all parameters are in .csv form -and loaded from there, to establish that the base way to interact with the autolens API for clusters -is via csv. - -I thin kthe way to make this work is to write a guide, autolens_workspace/scripts/cluster/csv_api.py, -which illustrates how to set up lens models using the normal autolens API and then output them to csv, -and showing how all those featres linked together. - -This csv file can then act as an "Auto Simulate" type siutation for simulator.py, which loads the csv outputsof this file. -The simulator will put the csv files int he lens it simulates at the end, meaning other scripts only need the -auto simulate performed here. - -Things I am still unclear on that this guide could help with are: - -Shouild main galaxies, extra galaxies and scaling galaies use their own csv files or can they all be combined into one? -I think it would be good if they could all be combined into one, but hiswould mean the .csv needs to know a lot more -than just parameters, but feasible mass profile class, lens name (e.g. when its used to name light and mass profiles in the Galaxy), redshift and -others. I think I like the idea of a single .csv API being used for all cluster interfaces. - -The flip side is this could get complicated because if the same galaxy has light and mass profiles then the notion of column -heads breaks down, so maybe the rule is "one csv file per light or mass profile", and the reuse of light profile names, mass profile -names and galaxy names is exploited when building the model? Most cluster models will apply the same thing over loads -of galaxies so I think that works, so we can just build it in an extensible way. - -This would mean it also needs the galaxy names, even though in simulator.py galaxies are not named when used in a Tracer -these names would be used for performing model composition, again the csv_api.py script could explain and cover this. - -I would then go so far as to make it so that this guide also explains point_datasets.csv, which functionally looks a lot -more complete to me and just needs an explanation. I would explain this before doing galaxy API, as convention is normally load -dataset before modeling. Note that point_datasets.csv itself is made by simulator.py, thus I think csv_api.py -can just make an example one which is not paired to the model in the guide making it clear its for illustrative purposes -but that simultor.py makes the actual one. - -There are also no .csv's used at all for defining the point source model, which obviously need to be paired -with the point dataset. - -This: - -""" -__Source Galaxies__ - -The 2 background sources at *different* redshifts. Each carries a `SersicCore` light profile (used only -for visual confirmation of the lensed arcs — the cored profile changes gradually in the centre so explicit -source-plane over-sampling is unnecessary) and a `Point` model component whose multiple-image positions -we solve for and use as the modeling data. - -Each source's redshift is taken from ``source_redshifts``, so source 0 sits at ``z = 1.0`` and source 1 -at ``z = 2.0``. The `Tracer` ray-traces multi-plane through both planes automatically. -""" -source_galaxies = [] -for i, (centre, src_z) in enumerate(zip(source_centres, source_redshifts)): - bulge = al.lp.SersicCore( - centre=centre, - ell_comps=al.convert.ell_comps_from(axis_ratio=0.8, angle=60.0 + 30.0 * i), - intensity=2.0, - effective_radius=0.3, - sersic_index=1.0, - ) - point = al.ps.Point(centre=centre) - source_galaxies.append( - al.Galaxy(redshift=src_z, bulge=bulge, **{f"point_{i}": point}) - ) - - -And this: - -_source_models = [ - af.Model( - al.Galaxy, - redshift=src_z, - bulge=af.Model( - al.lp.SersicCore, - centre=src_centre, - ell_comps=al.convert.ell_comps_from(axis_ratio=0.8, angle=60.0 + 30.0 * i), - intensity=2.0, - effective_radius=0.3, - sersic_index=1.0, - ), - **{ - f"point_{i}": af.Model(al.ps.Point, centre=src_centre), - }, - ) - for i, (src_centre, src_z) in enumerate(zip(source_centres, source_redshifts)) -] - -Shoiuld all be put into two csv files (source_point_models.csv and source_light_models.csv) again making the whole -cluster experience a first-class csv experience. - -I guess at this point users should easily not just load .csv's into models but also be able to print the csv -contents in python / Notebook cells and have clear print statements of the loaded objects showing how their -csv load parameters link to autolens objects, I guess the csv_api does that. - -Finally, do a quick scan through autolens_workspace of other csv uses but I think at the moment its just scaling galaxies -which are already implemented wellk.. - -Do deep research when coming to this csv API and once you're happy with it make csv interface the version used on all 3 cluster -scripts that exist. This is a huge issue -- the csv interface defines cluster modeling throughout, so dont be afraid -to ask hard questions about balancing the need for modeling large amoiunts of gaalxies to making a user friendly API. -Feel free to ask if some of this work would benefit going into the source code more so than it alredy has. \ No newline at end of file diff --git a/active/2_modeling_cluster.md b/active/2_modeling_cluster.md deleted file mode 100644 index b29c75fc..00000000 --- a/active/2_modeling_cluster.md +++ /dev/null @@ -1,108 +0,0 @@ -Cluster modeling: minimal first-pass rewrite of `modeling.py`. - -The cluster simulator at `@autolens_workspace/scripts/cluster/simulator.py` has long since been -rewritten to a small multi-plane cluster (2 main lens galaxies + standalone NFW host halo + 2 -sources at distinct redshifts z=1.0 and z=2.0, with point-source positions written to a combined -`point_datasets.csv` carrying a per-source `redshift` column). The paired -`@autolens_workspace/scripts/cluster/modeling.py` was **never updated** to match — it still loads -five separate `point_dataset_{i}.json` files, hardcodes source `redshift=1.0`, and composes the -pre-rewrite "10 extra galaxies on a scaling relation + 5 sources" model. `cluster/modeling` and -`cluster/start_here` are both parked in `autolens_workspace/config/build/no_run.yaml`. - -This prompt is a **minimal first pass**: rewrite `modeling.py` to actually pair to the current -simulator. `start_here.py` stays parked; a CSV-driven lens/source-galaxy interface is deferred to a -later prompt. - -__Required changes (autolens_workspace/scripts/cluster/modeling.py)__ - -1. **CSV-load the point datasets.** Replace the `for i in range(5)` JSON loop (and the docstring - example block that currently *describes* CSV loading) with the real call: - - ```python - dataset_list = al.list_from_csv(file_path=dataset_path / "point_datasets.csv") - ``` - - Each `PointDataset` exposes `.positions`, `.positions_noise_map`, and `.redshift` — use all - three downstream. Do not retain a JSON fallback. - -2. **Load the centre files the simulator actually writes.** Replace - `extra_galaxies_centre_list.json` + `extra_galaxies_luminosities.json` (these no longer exist) - with: - - - `main_lens_centres.json` — `Grid2DIrregular` of the 2 main lens galaxy centres. - - `host_halo_centre.json` — `Grid2DIrregular` of the 1 host halo centre. - - See `@autolens_workspace/scripts/cluster/simulator.py` lines ~511–522 for the exact filenames - and how they're written. - -3. **Compose the lens model paired to the simulator.** The truth model has three categories. Each - maps to a model component as follows: - - - **2 main lens galaxies** at `redshift_lens = 0.5`. Each uses `al.mp.dPIEMassSph` with centre - fixed from `main_lens_centres.json[i]`. Free parameters per galaxy: `ra`, `rs`, `b0`. See - `simulator.py` lines ~72–86 for parameter meaning and physical ranges — anchor priors there - (e.g. `ra` ~ 0.05–0.1", `rs` ~ 10–30", `b0` log-uniform around the truth Einstein-scale). - - - **1 host halo galaxy** at `redshift = 0.5`, with `al.mp.NFWMCRLudlowSph` mass, centre fixed - from `host_halo_centre.json[0]`, `mass_at_200` a free `LogUniformPrior` bracketing `10**15.3`. - The `redshift_object` and `redshift_source` plumbing must match the simulator — - `redshift_object = 0.5`, `redshift_source = max(source_redshifts)` so the concentration is - anchored to the furthest source plane. - - - **2 source galaxies**, one per `PointDataset` in `dataset_list`. The key change is that the - **source redshift comes from the dataset, not a literal**: - - ```python - for i, dataset in enumerate(dataset_list): - point = af.Model(al.ps.Point) - positions = np.atleast_2d(dataset.positions) - point.centre_0 = af.GaussianPrior(mean=float(np.mean(positions[:, 0])), sigma=3.0) - point.centre_1 = af.GaussianPrior(mean=float(np.mean(positions[:, 1])), sigma=3.0) - source = af.Model(al.Galaxy, redshift=dataset.redshift, **{f"point_{i}": point}) - ``` - -4. **Drop the entire `extra_galaxies` + scaling-relation block** (the old 10-satellite - `dPIEMassSph` scaling block + `extra_galaxies=af.Collection(**extra_galaxies_dict)` wiring). - The current simulator does not emit a scaling-relation member population; that's a follow-up. - -5. **Rewrite the `__Model__` docstring.** It currently claims "ten extra lens galaxies with - `DPIEPotentialSph`", "five source galaxies", and "N=22" — all wrong. Replace with an accurate - description of the 2-main + halo + 2-source model and its actual free-parameter count. - -6. **Reactivate the script.** Remove `cluster/modeling` from - `autolens_workspace/config/build/no_run.yaml`. Leave `cluster/start_here` parked (a separate - prompt will rewrite it later). - -__Verification__ - -Before opening the PR, confirm the script runs end-to-end under test mode from -`autolens_workspace`: - -```bash -PYAUTO_TEST_MODE=2 PYAUTO_SKIP_FIT_OUTPUT=1 PYAUTO_SKIP_VISUALIZATION=1 \ - python scripts/cluster/modeling.py -``` - -It should compose the model, call the analysis log-likelihood once, and exit cleanly. The -canonical workspace smoke run (`/smoke_test` over the modeling.py path) must also pass. - -__Out of scope (do NOT touch in this prompt)__ - -- **No new lens/source-galaxy CSV API.** A `lens_galaxies.csv` / `source_galaxies.csv` interface - mirroring `al.galaxy_table_from_csv` is the natural next step but is *deferred* — for 2 main - galaxies the existing JSON+per-galaxy code path is fine and lets us see the rewritten modeling - work in isolation. The reference pattern (when we eventually do this work) lives in - `@autolens_workspace/scripts/imaging/features/scaling_relation/modeling.py` lines ~180–215. -- **No `start_here.py` rewrite.** Stays in `no_run.yaml`. -- **No library changes.** All edits land in the workspace. -- **No `extra_galaxies` scaling-relation work.** Adding scaling-relation satellite members back - in is its own follow-up prompt, paired with a simulator extension that emits them. - -__Reference files__ - -- `@autolens_workspace/scripts/cluster/simulator.py` — truth model, parameter ranges, redshift - conventions, JSON+CSV output filenames. -- `@autolens_workspace/scripts/point_source/start_here.py` — current galaxy-scale point-source - modeling baseline (priors, `PointSolver` setup, `AnalysisPoint` wiring). -- `@autolens_workspace/scripts/imaging/features/scaling_relation/modeling.py` — reference for the - deferred lens/source CSV API. Do not adopt it here; just preserve as a pointer for next round. diff --git a/active/2_review_faculty.md b/active/2_review_faculty.md deleted file mode 100644 index 27b766bb..00000000 --- a/active/2_review_faculty.md +++ /dev/null @@ -1,49 +0,0 @@ -# Review faculty — an automatic branch-review verdict conductors consult - -Type: feature -Target: autonomy -Repos: -- PyAutoBrain -Difficulty: large -Autonomy: supervised -Priority: high -Status: draft - -## Why - -"Do hard tasks autonomously, rely on testing and review, human validates at the -end" only works if review is a first-class automatic gate — symmetric with how -the vitals faculty wraps Heart. Today code review happens ad-hoc in the human's -session; nothing machine-checkable stands between an autonomous implementation -and its PR. - -## What - -Add `@PyAutoBrain/agents/faculties/review/` — a read-only faculty that, given a -task worktree / feature branch: - -1. Runs the harness code-review capability (`/code-review` at high effort) plus - a verification pass (drive the affected flow, not just tests) over the diff - against the target branch. -2. Returns a verdict: **CLEAN** (ship), **FINDINGS** (list, ranked; autonomous - runs must resolve or downgrade to a human checkpoint), **BLOCKED** (could - not review — treat as human-required). -3. Follows the faculty shape: `AGENTS.md` with a `Tier:` line, deterministic - entrypoint, never dispatches or mutates. Its "sensor" is the diff + the - review tooling, the way vitals' sensor is Heart. - -Wire the consult into the ship path so `ship_*` in autonomous mode requires -`review == CLEAN` alongside the Heart gate (the gate composition itself is -defined in `3_autonomous_ship_gate.md`). - -## Boundary decision (adversarial finding — settled, record it in the AGENTS.md) - -A diff review is a **side-effect-free opinion**, which is the definition of a -faculty — it does **not** belong in Heart. Heart is the organism-state -observer (repo state, CI, PRs, deep install checks on main); it never looks at -feature branches and stays the sole authority on *release* readiness. The -review faculty gates the **dev workflow's** ship step only. Do not extend -Heart, and do not let this faculty grow release opinions. - -Blocked-by: 1_autonomy_contract.md (verdict semantics must match the contract's -checkpoint table). diff --git a/active/2_scaling_relation.md b/active/2_scaling_relation.md deleted file mode 100644 index bfc331d3..00000000 --- a/active/2_scaling_relation.md +++ /dev/null @@ -1,11 +0,0 @@ -The API for the placement of galaxies on a mass light scaling relation is given autolens_workspace/scripts/group/scaling_relation. - -For group scale lenses, this is an optional feature which group scale modeling odes not necessarily neeed. - -For cluster lenses, I want it to be the default, with all default cluster scripts having 10 scaling galaxies -whose masses are lower than the main lens and dark matter halo. I want them in that regime of have to be modeled collectively, -but dont contribute much individually. - -Can you upddate the 3 cluster examples on the autolens workspace to this effect, always using the .csv -interface for scaling galaxies. Can you make sure the simulator still produces multiple images in the right -positions/ \ No newline at end of file diff --git a/active/2_workspace_jax_likelihood.md b/active/2_workspace_jax_likelihood.md deleted file mode 100644 index b50dcc51..00000000 --- a/active/2_workspace_jax_likelihood.md +++ /dev/null @@ -1,22 +0,0 @@ -Step 2 of the ellipse-JAX series. Step 1 (`1_workspace_visualization.md`) added the visualization integration test. This prompt adds the **likelihood-function** integration tests, still on the numpy path. They lock in the reference numbers we'll later assert against once the JAX path lands in `7_analysis_ellipse_jax.md`. - -The pattern to mirror is `@autogalaxy_workspace_test/scripts/jax_likelihood_functions/imaging/lp.py`, which (a) auto-simulates a small dataset on first run, (b) builds a model + analysis, (c) computes a baseline `log_likelihood` on the numpy path, and (d) compares the JIT path with `np.testing.assert_allclose(rtol=1e-4)`. For this prompt, only build steps (a)-(c) — leave a `# TODO(7_analysis_ellipse_jax.md)` placeholder where the JIT comparison will go. - -Please: - -1. Add `@autogalaxy_workspace_test/scripts/jax_likelihood_functions/ellipse/__init__.py`. - -2. Add `@autogalaxy_workspace_test/scripts/jax_likelihood_functions/ellipse/simulator.py` modelled on `@autogalaxy_workspace_test/scripts/jax_likelihood_functions/imaging/simulator.py` — small grid (e.g. shape_native=(50, 50), pixel_scales=0.2), single Sersic galaxy, written into `dataset/ellipse/jax_test/`. - -3. Add `@autogalaxy_workspace_test/scripts/jax_likelihood_functions/ellipse/fit.py`: - - Load (or auto-simulate via `simulator.py`) the dataset. - - Build an `af.Collection(ellipses=af.Collection(ellipse_0=af.Model(ag.Ellipse, major_axis=...)))`-style model (consult `@autogalaxy_workspace/scripts/ellipse/modeling.py` for the exact composition shape). - - Build `analysis = ag.AnalysisEllipse(dataset=dataset)` (today this defaults to `use_jax=False`). - - Compute `fit_np = analysis.fit_list_from(instance=model.instance_from_prior_medians())` and print every component — `log_likelihood`, `chi_squared`, `noise_normalization`, `figure_of_merit` — to capture the reference numbers. - - Leave a `# TODO(7_analysis_ellipse_jax.md): jax.jit(analysis.fit_from) round-trip` placeholder block at the end. - -4. Add `@autogalaxy_workspace_test/scripts/jax_likelihood_functions/ellipse/multipoles.py`: same as `fit.py` but with `multipole_list=[ag.EllipseMultipole(m=4, multipole_comps=(0.05, 0.0))]` per ellipse. This locks in the multipole code path which has its own JAX-incompatible `while` loops in `EllipseMultipole.get_shape_angle` (handled in prompt 5). - -5. Use the workspace docstring style throughout (see prompt 1 for reference). - -6. Test bar: both scripts run cleanly under `bash run_all_scripts.sh` and the printed reference numbers are stable to ~1e-6 across runs. No JAX imports yet — `import jax` should not appear in any of these files. diff --git a/active/3_autonomous_ship_gate.md b/active/3_autonomous_ship_gate.md deleted file mode 100644 index 14e0f980..00000000 --- a/active/3_autonomous_ship_gate.md +++ /dev/null @@ -1,48 +0,0 @@ -# Autonomous-ship gate — audit and define what an unattended ship must verify - -Type: feature -Target: autonomy -Repos: -- PyAutoBrain -- PyAutoHeart -Difficulty: medium -Autonomy: supervised -Priority: high -Status: draft - -## Why - -Heart's readiness verdict measures **organism state** — repo state, CI status, -open PRs in the cheap tick; verify_install-class deep checks on demand -(@PyAutoHeart/AGENTS.md). It never looks at a feature branch. Branch-level -signal today comes from `ship_*`'s own worktree pytest + smoke run, and the gap -is papered over by the human eyeballing the diff at PR sign-off. Remove the -human and the gap is live: an autonomous run could ship on "Heart GREEN" that -says nothing about the change being shipped. - -## What - -1. **Audit** what an unattended `ship_library` / `ship_workspace` run actually - verifies on the branch today: which test scope (changed-repo pytest? full?), - which smoke subset, what the Heart verdict does and does not cover, where - "never modify code to make tests pass" is enforced. -2. **Define the autonomous-ship gate** in the autonomy contract's terms — - proposal to validate or amend: - `worktree pytest (affected repos, full suite) AND smoke subset AND review - faculty CLEAN AND Heart GREEN` — all four, no substitutions; YELLOW is a - human checkpoint at every autonomy level. -3. **Fix the gaps found** — e.g. if smoke coverage keys off main rather than - the worktree, or if the tested repo set misses downstream dependents of the - changed API. - -## Boundaries - -- Heart stays untouched as the release authority; if any branch-level check is - genuinely a health check, it still runs from the dev workflow, not Heart - (Heart is an observer of the organism, not of task branches). -- Smoke tests remain the small curated subset — do not mass-promote - integration scripts to make the gate feel stronger. -- Keep the gate definition in `AUTONOMY.md` (one place), with `ship_*` skills - pointing at it. - -Blocked-by: 1_autonomy_contract.md, 2_review_faculty.md. diff --git a/active/3_fit.md b/active/3_fit.md deleted file mode 100644 index 00e16485..00000000 --- a/active/3_fit.md +++ /dev/null @@ -1,14 +0,0 @@ -We are now going to add weak lensing Fit class - -First, we need to create the fit.py module, so inspect @autolens_workspace/scripts/weak and -@PyAutoLens/autolens/imaging/fit modules . We are basically going to make everything weak does from here a "mirror" of -the imaging model API (and also interferoter.) - -So, set up a FitWeak module which compute the same key quantities as other Fit objects, such as residuals, chi -squared and log likelihood. Put unit tests in following the imaging unit test stucture, baring in mind there should -be far fewer as there are no variants like linear light profiles of pixelizations. Have me eyeball a few unit tests -so I can see they make sense. - -In a second phase, inspect @PyAutoLens/autolens/plot and set up the FitWeak weak_plots.py, you may need to do -some research on how best to plot these quantities, it may be we want to plot the dataset values via quiver on top -of the model ones. \ No newline at end of file diff --git a/active/3_test_workspace.md b/active/3_test_workspace.md deleted file mode 100644 index 1f34368d..00000000 --- a/active/3_test_workspace.md +++ /dev/null @@ -1,12 +0,0 @@ -Lets set up autolens_workspace_test to have dediciated cluster functionality, in particular: - -1) Write a scripts/cluster/csv_api.py, which follows the autolens_workspace/scripts/cluster/csv_api.py and therefore includes a full suite of mains, extra, scaling, source galaxies/ -1) Write a scripts/cluster/simulator.py, which uses the autolens_workspace/scripts/cluster/simulator.py and uses the csv exa,ple but outputs its point dataset csv, source light profile .csv and all other data. -2) move scripts/imaging/visualization_cluster.py to scripts/cluster/visualization.py, run against the simulator.py output. -3) Set up a likelihood sanity check, which uses the simulated cluster (will need a scritps/cluster/simulator file) and the does sanity checks on its source plane chi-squared (using Point objects with FitPositionsSource) and image plane chi squared (using FitPositionsImagePair). I think the sensible way to do this is to perturb the mass model parameter inputs to the simulator script by a small amount (e.g. 0.1%) and medium amount (e.g. 1%) and large amount (e.g. 5%) and make sure the chi-squared is near zero for the first and that ther log likelihood decreases. The point is that we havent really done full end to end tests tthat the likelihood function gives sensible results to build up confidence cluster modeling is ok. This should ue the simulator made in the test workspace. -4) Build on 3. to do an example where the simulated cluster has sources at different redshifts, and we only get the maximum likelihood soltion when the redshifts are right, and test therefore for that we must get redshifts right. For small changes in redshift (that are still multiplane) make sure we get small likelihood decreases. -5) Can you pair the example in 4 with fits which uses Imaging and light profiles (the values used in the simulator to make images with arfcs and again make sure the likelihood patterns hold. -6) Produce good visualization of 4 and 5 so we can see where the positions end up compared to the data and what the residuals of the light profiles for the cluster set up look like. -7) Now make scripts/jax_likelihood_functions/cluster/single_plane.py and multi_plane.py that has numerical assertions against it using the above test case. - -The whole idea behind this task is to stress test cluster likelihood function as much as possible and look for bugs, edge cases and really anything that can (and will go wrong). So be persistent, be nit-picking and look for obvious issues with our implementation. \ No newline at end of file diff --git a/active/3_unit_tests_masked_loop.md b/active/3_unit_tests_masked_loop.md deleted file mode 100644 index 968edf5c..00000000 --- a/active/3_unit_tests_masked_loop.md +++ /dev/null @@ -1,23 +0,0 @@ -Step 3 of the ellipse-JAX series. The 300-iteration mask-rejection loop in `FitEllipse.points_from_major_axis_from` (`@PyAutoGalaxy/autogalaxy/ellipse/fit_ellipse.py:81-134`) is the single nastiest piece of imperative code in the ellipse module — Python `for` loop, dynamic shape changes via `points = points[unmasked_indices]`, scipy interpolator calls, and a `raise ValueError` after 300 iterations. Prompt 6 will rewrite it for JAX. Before that we need unit tests in `@PyAutoGalaxy/test_autogalaxy/ellipse/test_fit_ellipse.py` that pin the current behaviour, so when the rewrite goes in we know nothing has shifted. - -Please: - -1. Add (or extend) tests in `@PyAutoGalaxy/test_autogalaxy/ellipse/test_fit_ellipse.py` covering the four exit paths of the loop: - - - **Zero-masked**: a mask that doesn't overlap the ellipse points. Assert `points.shape[0] == ellipse.total_points_from(pixel_scale)` and that the loop exits on the first iteration via the `total_points_required == total_points - total_points_masked` branch. - - - **Under-masked (trim path)**: a mask that drops a small fraction of points (e.g. ~10%). Assert the returned `points` has exactly `total_points_required` rows and that the trimmed indices are the latest ones in `unmasked_indices` (the slicing is `unmasked_indices[number_of_extra_points:]`). - - - **Over-masked (extra-points path)**: a mask that drops enough points to force a re-call of `ellipse.points_from_major_axis_from(..., n_i=i)` with a higher angular resolution. Assert the loop runs for at least one iteration and that the final `points.shape[0] == total_points_required`. - - - **Unreachable (`ValueError`)**: a degenerate mask where no `i ≤ 300` satisfies the constraint. Use `pytest.raises(ValueError)` and assert the error message matches the existing wording in `fit_ellipse.py`. - -2. Use small `Mask2D` shapes (e.g. 30×30) so the tests run fast. Build the mask explicitly via `aa.Mask2D(...)` rather than `Mask2D.circular`, so the masked region is deterministic and the tests are robust to changes in the circular-mask helper. - -3. Cover both with and without `multipole_list`. The multipole branch goes through the inner `for multipole in self.multipole_list:` block at lines 113-120 — add at least one test that hits this with `EllipseMultipole(m=4, multipole_comps=(0.05, 0.0))`. - -4. Pin numerical reference values for at least one mask configuration: capture the returned `points` array via `np.testing.assert_allclose(points, expected, rtol=1e-12)` against a hard-coded reference. This is the strongest regression test — when prompt 6 swaps the loop for a JAX-friendly oversample-then-mask approach, the new path should reproduce these numbers (or the test must be re-pinned with a written justification). - -5. Follow `@PyAutoGalaxy/CLAUDE.md` "Never use JAX in unit tests" — these tests stay numpy-only. The cross-numpy/JAX parity check happens in the workspace_test scripts from prompt 2. - -6. Test bar: `python -m pytest test_autogalaxy/ellipse/test_fit_ellipse.py -v` passes, including the new tests. diff --git a/active/4_auto_dev_mode.md b/active/4_auto_dev_mode.md deleted file mode 100644 index ac465869..00000000 --- a/active/4_auto_dev_mode.md +++ /dev/null @@ -1,52 +0,0 @@ -# --auto mode through start_dev → ship_* — consume the Autonomy header - -Type: feature -Target: autonomy -Repos: -- PyAutoBrain -- PyAutoMind -Difficulty: large -Autonomy: supervised -Priority: high -Status: draft - -## Why - -This is the payoff task: the dev workflow gains an explicit autonomous mode so -`Autonomy: safe` tasks run start-to-PR without interactive checkpoints, and the -human's job moves to validating an open PR instead of approving every step. - -## What - -Add `--auto` to the `start_dev → start_library/start_workspace → ship_*` -lifecycle (skill bodies + any `bin/pyauto-brain` plumbing): - -1. **Activation** — only when the human launches with `--auto`; the task's - `Autonomy:` header (subject to the per-work-type caps in `AUTONOMY.md`) - then selects the behaviour. Default invocations are byte-for-byte today's - flow. -2. **`safe` behaviour** — the plan is written to the GitHub issue (and Mind - `active.md` summary) instead of being held for Plan-Mode approval; - implementation proceeds; ship is gated by the autonomous-ship gate - (`3_autonomous_ship_gate.md`); the run ends at **PR open**, with the PR body - carrying the plan, the review-faculty verdict, test/smoke counts, and a - short validation checklist for the human. -3. **Stop-at-PR is hard** — merge and issue-close remain human acts regardless - of level (standing preference). An explicit additional flag may extend to - merge later; do not build it in this task. -4. **Calibration hook** — every `--auto` run appends its outcome row to the - calibration log defined in `1_autonomy_contract.md`. -5. **Failure behaviour** — a failed gate (tests, review FINDINGS, Heart - YELLOW/RED) downgrades the run to a human checkpoint: state written to the - issue, session ends cleanly, nothing force-shipped. Never modify code to - make tests pass remains absolute. - -## Boundaries - -- `supervised` behaviour (checkpoint-and-continue) is `5_checkpoint_and_continue.md`, - not this task — here `supervised` simply means today's interactive flow. -- No new conductor: this is the existing dev-workflow skills consuming the - contract, per the growth rule (no new agents without demonstrated need). - -Blocked-by: 1_autonomy_contract.md, 2_review_faculty.md, 3_autonomous_ship_gate.md. -First consumer: `6_refactor_conductor.md` work-type runs. diff --git a/active/4_jax_interp_2d.md b/active/4_jax_interp_2d.md deleted file mode 100644 index 9c1bcbfe..00000000 --- a/active/4_jax_interp_2d.md +++ /dev/null @@ -1,27 +0,0 @@ -Step 4 of the ellipse-JAX series. `DatasetInterp` in `@PyAutoGalaxy/autogalaxy/ellipse/dataset_interp.py` uses `scipy.interpolate.RegularGridInterpolator` for the data, noise-map, and mask. scipy is numpy-only, so this is the first hard JAX blocker for `AnalysisEllipse.log_likelihood_function`. There is no JAX-compatible 2D interpolator anywhere in the codebase — the only precedent is the 1D `_interp1d_jax` in `@PyAutoArray/autoarray/inversion/mesh/interpolator/rectangular_spline.py:86-106`. We need a 2D analogue. - -Please: - -1. Add a 2D regular-grid bilinear interpolator helper to PyAutoArray. Suggested location: `@PyAutoArray/autoarray/numerics/interp_2d.py` (create the `numerics/` subpackage if it doesn't already exist; check `@PyAutoArray/autoarray/__init__.py` for the right import surface). Two paths: - - - `_interp_2d_numpy(points, x_axis, y_axis, values, fill_value=0.0)` — matches the current `RegularGridInterpolator(bounds_error=False, fill_value=0.0)` semantics in `dataset_interp.py`. A direct call to `scipy.interpolate.RegularGridInterpolator` is fine here. - - `_interp_2d_jax(points, x_axis, y_axis, values, fill_value=0.0)` — uses `jax.scipy.ndimage.map_coordinates(values, coords, order=1, cval=fill_value)`. Translate `(y, x)` world coordinates to pixel-fractional coordinates using `x_axis`, `y_axis` (assume regularly spaced — the existing scipy `points_interp` is built from `mask.derive_grid.all_false`, which is regular). - - Public dispatcher `interp_2d(points, x_axis, y_axis, values, fill_value=0.0, xp=np)` that picks the path. Mirror the dispatch style in `rectangular_spline.py`. - -2. Unit tests in `@PyAutoArray/test_autoarray/numerics/test_interp_2d.py`: - - Random `(N, 2)` query points inside the grid: assert numpy and JAX paths agree to `rtol=1e-6`. - - Out-of-bounds query points: assert both paths return `fill_value` for those rows. - - Single-point query: assert shape is `(1,)` not `()`. - - `xp=np` is the default — JAX-only tests gated by `pytest.importorskip("jax")` per `@PyAutoArray/CLAUDE.md` testing conventions. - -3. Wire `DatasetInterp` to the new helper. In `@PyAutoGalaxy/autogalaxy/ellipse/dataset_interp.py`: - - Drop the cached `data_interp`, `noise_map_interp`, `mask_interp` properties that return `RegularGridInterpolator` instances. - - Replace with methods `data_interp(points, xp=np)`, `noise_map_interp(points, xp=np)`, `mask_interp(points, xp=np)` that call `aa.numerics.interp_2d(...)` directly. The interp axes (`points_interp`) can stay cached. - - Keep the existing call sites in `fit_ellipse.py` working: `self.interp.data_interp(self._points_from_major_axis)` continues to take a `(N, 2)` array. - -4. Do **not** touch `FitEllipse.points_from_major_axis_from`'s 300-iteration loop in this prompt. That's prompt 6. The mask interpolation calls inside the loop continue to use the numpy path because the surrounding code is still numpy-only — pass `xp=np` explicitly at those call sites. - -5. Test bar: - - `python -m pytest test_autoarray/numerics/test_interp_2d.py -v` passes. - - `python -m pytest test_autogalaxy/ellipse/ -v` still passes (no behavioural change on the numpy path — same `fill_value=0.0`, same regular-grid semantics). - - The reference numbers from prompt 2's workspace_test scripts are unchanged to `rtol=1e-10`. diff --git a/active/4_likelihood_function.md b/active/4_likelihood_function.md deleted file mode 100644 index 781fee5e..00000000 --- a/active/4_likelihood_function.md +++ /dev/null @@ -1,10 +0,0 @@ -autolens_workspace has many examples of step-by-step likelihood functions (see pretty much all sub packages in scripts for style or writing, -how code breaks it down, amount of text in comments compared to code, etc.) - -Can you produce this file for autolens_workspace/scritps/cluster/likelihood_function.py, documenting first the source -plane chi squared and then moving on to the image plane chi squared. - -Before we begin, go through the source code to produce your view of this step by step guide and I will then -refine with you to what level of grannularity of extra steps to include. - -Asume the sources are at different redshifts, but all lenses the same redshift, the standard cluster model. \ No newline at end of file diff --git a/active/4_modeling.md b/active/4_modeling.md deleted file mode 100644 index fb481491..00000000 --- a/active/4_modeling.md +++ /dev/null @@ -1,27 +0,0 @@ -# We are now going to add weak lensing modeling - -Type: feature -Target: weak -Difficulty: large -Autonomy: supervised -Priority: high -Status: formalised - -We are now going to add weak lensing modeling. - -First, we need to create the analysis.py module, so inspect @autolens_workspace/scripts/weak and -@PyAutoLens/autolens/imaging/model . We are basically going to make everything weak does from here a "mirror" of -the imaging model API (and also interferoter.) - -The log_likelihod_function is in particular important, we already have the codw which creates the shear field -show in the @autolens_workspace/scripts/weak/simulator.py file and we can turn it into a fit in -@PyAutoLens/autolens/imaging/fit.py, so shouldnt be hard to work out the pattern and whats needed here. - -I believe everything else (plotter.py, result.py, visualizer.py) should be relatively straight forward to work -out by simply copying the patterns and logic from the imaging module, but keep an eye out for unexpected tricky -parts. - -Finally, read @autolens_workspace/scripts/imaging/modeling.py and then make an equivalent for -weak lensing. - - diff --git a/active/5_checkpoint_and_continue.md b/active/5_checkpoint_and_continue.md deleted file mode 100644 index 96355bf1..00000000 --- a/active/5_checkpoint_and_continue.md +++ /dev/null @@ -1,45 +0,0 @@ -# Checkpoint-and-continue — supervised runs batch questions instead of blocking - -Type: feature -Target: autonomy -Repos: -- PyAutoBrain -Difficulty: medium -Autonomy: supervised -Priority: normal -Status: draft - -## Why - -The proven pattern already exists: `register_and_iterate`'s autonomy contract -runs unattended except at named judgment gates, where it "writes a clear -question and stops" and auto-advances between tasks. Generalising it changes -the human's interaction model from answering interrupts live to reviewing a -batch of questions and PRs — the single biggest reduction in required input -for `supervised` (i.e. most) tasks. - -## What - -For `--auto` runs on tasks whose effective level is `supervised`: - -1. At a judgment gate (per the `AUTONOMY.md` checkpoint table), write the - question — with enough context to answer cold — to the task's GitHub issue, - set the task's `active.md` status to `awaiting-input`, and **continue**: to - the next independent step if one exists, else to the next queued task. -2. Keep questions **conversational and infrequent** — one batched comment per - pause, not a trickle (consistent with the user-facing-issue update style). -3. On resume (human answers on the issue / relaunches), the run picks up from - the recorded state — `active.md` is already the shared cross-environment - task state, so no new state store. - -## Boundaries - -- Ship sign-off and merge remain checkpoints that *stop* the task (they park - it as `awaiting-input`); checkpoint-and-continue never bypasses the gate, - it only stops the human's *session* from being held hostage. -- No new registry or daemon — the issue + `active.md` are the whole mechanism. -- Genuine hard blockers still write up the blocker and park, exactly as - `register_and_iterate` does today. - -Blocked-by: 1_autonomy_contract.md. Pairs with 4_auto_dev_mode.md; consumed -wholesale by 7_queue_runner.md. diff --git a/active/5_ellipse_xp.md b/active/5_ellipse_xp.md deleted file mode 100644 index 0ec86900..00000000 --- a/active/5_ellipse_xp.md +++ /dev/null @@ -1,33 +0,0 @@ -Step 5 of the ellipse-JAX series. With the 2D interpolator in place from prompt 4, the next blocker is the geometry math in `@PyAutoGalaxy/autogalaxy/ellipse/ellipse/ellipse.py` and `@PyAutoGalaxy/autogalaxy/ellipse/ellipse/ellipse_multipole.py`. Every routine on `Ellipse` uses bare `np.*`, and `EllipseMultipole.get_shape_angle` uses Python `while` loops to wrap an angle into `[-180/m, 180/m]` — both incompatible with `jax.jit` tracing. Convert these to the `xp=np` pattern documented in `@PyAutoGalaxy/CLAUDE.md` "JAX Support" section. - -Please: - -1. Add `xp=np` as a keyword argument to every method in `@PyAutoGalaxy/autogalaxy/ellipse/ellipse/ellipse.py` that returns a numerical array: - - `Ellipse.angles_from_x0_from` - - `Ellipse.ellipse_radii_from_major_axis_from` - - `Ellipse.x_from_major_axis_from` - - `Ellipse.y_from_major_axis_from` - - `Ellipse.points_from_major_axis_from` - - Replace bare `np.*` with `xp.*` inside the function bodies (`xp.linspace`, `xp.sin`, `xp.cos`, `xp.divide`, `xp.add`, `xp.sqrt`, `xp.stack`). The `total_points_from` method stays numpy — its return type is a Python `int` and it's used to set static shapes outside the JIT trace. - - Special case in `points_from_major_axis_from`: the `idx = np.logical_or(np.isnan(x), np.isnan(y)); if np.sum(idx) > 0: raise NotImplementedError()` guard is JAX-incompatible (Python `if` on a traced value). Replace with `if xp is np:` around the guard — under JAX, NaNs propagate through downstream `nansum`/`nanmean` and we'd rather see them than crash inside a JIT trace. - -2. Same treatment for `EllipseMultipole.points_perturbed_from` and `EllipseMultipoleScaled.points_perturbed_from` in `@PyAutoGalaxy/autogalaxy/ellipse/ellipse/ellipse_multipole.py`. Add `xp=np`, swap `np.*` for `xp.*`. The `multipole_comps_from` and `multipole_k_m_and_phi_m_from` helpers from `@PyAutoGalaxy/autogalaxy/convert.py` are called outside the math loop on Python tuples — leave those as-is unless they trip JIT (verify by tracing). - -3. **Replace the `while` loops** in `EllipseMultipole.get_shape_angle` (`@PyAutoGalaxy/autogalaxy/ellipse/ellipse/ellipse_multipole.py:66-69`) with arithmetic that JAX can trace. The intent is "wrap `angle` into the open interval `(-180/m, 180/m]`". A direct replacement using `xp.mod` works: - - ```python - period = 360.0 / self.m - angle = xp.mod(angle + period / 2.0, period) - period / 2.0 - ``` - - This produces values in `[-period/2, period/2)` rather than `(-period/2, period/2]`, which is a tiny boundary-case difference. Verify against the existing tests in `@PyAutoGalaxy/test_autogalaxy/ellipse/` and add a test pinning the new behaviour at the boundary (`angle = period/2.0`) so future changes don't drift unnoticed. - -4. The existing call sites in `FitEllipse` and elsewhere don't pass `xp` — they get the numpy default and behaviour is unchanged. Don't thread `xp` through the call sites in this prompt; that happens in prompt 6 and 7 where it actually matters. - -5. Add unit tests in `@PyAutoGalaxy/test_autogalaxy/ellipse/test_ellipse.py` that for one fixed `Ellipse` and one fixed `EllipseMultipole`, the `xp=np` and `xp=jnp` paths produce numerically identical points to `rtol=1e-6`. Gate the JAX side with `pytest.importorskip("jax")`. - -6. Test bar: - - `python -m pytest test_autogalaxy/ellipse/ -v` passes. - - The reference numbers from prompt 2's workspace_test scripts are unchanged on the numpy path. diff --git a/active/5_likelihood_function.md b/active/5_likelihood_function.md deleted file mode 100644 index fe70489e..00000000 --- a/active/5_likelihood_function.md +++ /dev/null @@ -1,18 +0,0 @@ -# The autolens_workspace/scripts/imaging/likelihood_function.py file gives a step-by-step guide of the imaging - -Type: feature -Target: weak -Difficulty: medium -Autonomy: safe -Priority: normal -Status: formalised - -The autolens_workspace/scripts/imaging/likelihood_function.py file gives a step-by-step guide of the imaging -CCD likelihood function. - -Can you make autolens_workspace/scripts/weak/likelihood_function.py which will do the exact same thing but -give a step by step guide of the weak lesning likelihood function. This will require you to go throuhg the -source code, work out the steps and documnet them following the same style and level of detail as the other -likelihood_function.py examples in the workspace. - - diff --git a/active/5_profiling.md b/active/5_profiling.md deleted file mode 100644 index 22c85442..00000000 --- a/active/5_profiling.md +++ /dev/null @@ -1,19 +0,0 @@ -# autolens_workspace/scritps/cluster/likelihood_function.py gives a step by step guide of the likelihood - -Type: feature -Target: cluster -Difficulty: large -Autonomy: supervised -Priority: normal -Status: formalised - -autolens_workspace/scritps/cluster/likelihood_function.py gives a step by step guide of the likelihood function for a source plane chi squared -and image plane chi squared. - -autolens_profiling/likelihood gives step-by-step timing of likelihood functions for all types of models. - -Write two scripts in autolens_profiling/likelihood/cluster for this profiling break down for source chi squared and image chi squared. - -Asume the sources are at different redshifts, but all lenses the same redshift, the standard cluster model. - - diff --git a/active/6_dpie_lenstool_parameterization.md b/active/6_dpie_lenstool_parameterization.md deleted file mode 100644 index d2f0ac25..00000000 --- a/active/6_dpie_lenstool_parameterization.md +++ /dev/null @@ -1,64 +0,0 @@ -# LensTool-native dPIE parameterization and numerical parity validation - -Type: feature -Target: cluster -Difficulty: large -Autonomy: supervised -Priority: high -Status: formalised - -Add a LensTool-native parameterization of the dPIE mass profile and validate numerical parity against LensTool. - -`autogalaxy/profiles/mass/total/dual_pseudo_isothermal_mass.py` (`dPIEMass` / `dPIEMassSph`) is -already a direct port of LensTool's C code, parameterized as (ra, rs, b0) in arcsec with the -pseudo-elliptical radius r_em^2 = x^2/(1+eps)^2 + y^2/(1-eps)^2 (LensTool's convention, not -intermediate-axis). The docstring already records the conversion b0 = 4*pi*sigma_0^2/c^2 * D_LS/D_S -and the Eliasdottir-2007 E0 relation. - -LensTool users parameterize the dPIE as (sigma [km/s], r_core [arcsec or kpc], r_cut [arcsec or kpc]), -so a "same model in both codes" workflow needs: - -1. A LensTool-facing constructor or profile — e.g. `dPIEMass.from_lenstool(sigma=..., r_core=..., r_cut=..., redshift_object=..., redshift_source=..., cosmology=...)` - (or a thin `dPIEMassLensTool` wrapper) that converts (sigma, r_core, r_cut) → (b0, ra, rs), - handling the sigma vs sigma_dPIE (Eliasdottir Table A.1) subtlety explicitly. Beware LensTool's - historical factor conventions (the 4pi vs 6pi issue between Kassiola & Kovner 1993, Eliasdottir 2007 - and the LensTool implementation) — read Eliasdottir et al. 2007 (arXiv:0710.5636) Appendix A and - Limousin et al. 2005 before fixing the conversion. -2. Ellipticity convention mapping: LensTool .par files give ellipticity + position angle; map to - ell_comps, and document whether LensTool's eps is (a^2-b^2)/(a^2+b^2) (potential ellipticity) vs - (a-b)/(a+b), since dPIE in LensTool is pseudo-elliptical in the potential for some code paths. -3. Numerical parity validation: convergence, potential and deflection angles of the converted profile - against reference values — either by running LensTool itself, using published values, or using - an independent implementation (e.g. the `lenstronomy` dPIE or Galan's work) as a cross-check. - A parity script belongs in autolens_workspace_test (cross-package checks live there, not library - unit tests); library unit tests cover the pure conversion math numpy-only. - -Outcome: a documented, tested parameter mapping that the later "PyAutoLens for LensTool users" -workspace example (and any real LensTool .par ingestion) can rely on. This is the foundation prompt -of the LensTool-parity series — file findings on any irreconcilable convention differences -prominently, since they determine how close the flagship example can get. - - - -__Research findings (deep-research pass, 2026-07-08)__ - -- **Velocity-dispersion chain (confirmed):** LensTool's .par `v_disp` is the *fiducial* - sigma_LT, related to the dPIE central velocity dispersion by sigma_0 = sqrt(3/2) * sigma_LT - (Bergamini et al. 2019, arXiv:1905.13236; Eliasdottir et al. 2007 App. A). sigma_LT is - Eliasdottir's sigma_dPIE. Chain to autolens: E0(Eliasdottir) = 6*pi*(sigma_LT/c)^2 * D_LS/D_S, - and b0 = E0 * (rs^2 - ra^2)/rs^2 = 4*pi*(sigma_0/c)^2 * D_LS/D_S in the rs->inf limit — both - relations already in the dPIEMass docstring. `from_lenstool` should take sigma_LT (what users - read out of .par files / papers) and document the sqrt(3/2) trap loudly. -- **Ellipticity (the main open trap):** the port's `_ellip()` uses |ell_comps| directly as the - Kassiola & Kovner eps in r_em^2 = x^2/(1+eps)^2 + y^2/(1-eps)^2, i.e. eps = (a-b)/(a+b) — - consistent with PyAuto's standard ell_comps magnitude. LensTool .par files and papers quote - ellipticity as e = (a^2-b^2)/(a^2+b^2) (verify against LensTool source `ci05`/`piemd` — the - wiki page projets.lam.fr/projects/lenstool/wiki/piemd was unreachable during research; use the - public git repo git-cral.univ-lyon1.fr or github mirror). The converter must map - e_par -> q -> eps=(1-q)/(1+q) and be validated numerically, including position-angle convention - (LensTool angle is counter-clockwise from x-axis? — verify) vs PyAuto's phi from north. -- **Radius units:** LensTool .par `core_radius` / `cut_radius` are in arcsec (variants - `core_radius_kpc` / `cut_radius_kpc` in kpc); support both in the converter via cosmology. -- **Parity references:** M(R) closed form for ra=0: M(R) = (pi*sigma_0^2/G)(R + r_cut - - sqrt(r_cut^2 + R^2)) (Bergamini 2019 App. C) — good analytic anchor for the mass/convergence - parity test alongside lenstronomy's PJAFFE/dPIE as an independent implementation. diff --git a/active/6_fit_ellipse_masked_jax.md b/active/6_fit_ellipse_masked_jax.md deleted file mode 100644 index bd29d71e..00000000 --- a/active/6_fit_ellipse_masked_jax.md +++ /dev/null @@ -1,26 +0,0 @@ -Step 6 of the ellipse-JAX series. Prompts 4 and 5 cleared the way: the data/noise/mask interpolator now has a JAX path, and `Ellipse` / `EllipseMultipole` accept `xp=jnp`. The remaining JAX blocker on `FitEllipse` is the 300-iteration mask-rejection loop in `points_from_major_axis_from` (`@PyAutoGalaxy/autogalaxy/ellipse/fit_ellipse.py:81-134`). The loop does dynamic-shape slicing (`points = points[unmasked_indices]`) and a Python `for` with an early `continue` based on traced values — none of that traces under `jax.jit`. The unit tests added in prompt 3 are the regression target for this rewrite. - -The core idea for the JAX path: **oversample with a fixed shape, mask invalid points with NaN, and let downstream `nansum`/`nanmean` reductions in `chi_squared` / `residual_map` do the right thing**. Downstream is already NaN-aware (see `@PyAutoGalaxy/autogalaxy/ellipse/fit_ellipse.py:240, 296` — `np.nanmean`, `np.nansum`), so this strategy doesn't change the reduction layer. - -Please: - -1. Add `xp=np` to `FitEllipse.points_from_major_axis_from`. Add `use_jax: bool = False` (or read it off the dataset / a class attribute — match whatever pattern emerges from prompt 7's `AnalysisEllipse` wiring; pick whichever is least intrusive). - -2. Keep the existing numpy loop **unchanged** under `if xp is np:`. The unit tests from prompt 3 must still pass byte-for-byte. Do not delete or reformat the loop body. - -3. Add a JAX path under the `else:` branch: - - Compute `total_points_required = ellipse.total_points_from(pixel_scale)` (still a Python `int`, fine to pass as a static argument to JIT). - - Choose a fixed oversample factor `K` (suggest `K = 4`; tune with the workspace_test multipoles script if needed). Compute `oversample_total = total_points_required * K`. - - Call `ellipse.points_from_major_axis_from(pixel_scale=..., n_i=oversample_total - total_points_required, xp=xp)` to get an `(oversample_total, 2)` array. - - Apply multipole perturbations the same way (already JAX-safe after prompt 5). - - Evaluate the mask interpolator: `mask_values = self.interp.mask_interp(points, xp=xp)`. Build `keep = mask_values == 0` as a boolean array. - - Replace the `points = points[unmasked_indices]` dynamic-shape slice with `xp.where(keep[:, None], points, xp.nan)`. Output shape stays `(oversample_total, 2)`. - -4. Update downstream call sites in `FitEllipse` so the `(oversample_total, 2)` JAX array flows through `data_interp` / `noise_map_interp` correctly. The `data_interp` JAX path (added in prompt 4) propagates NaNs through `map_coordinates`, but the masked output value is `fill_value=0.0` for OOB — that's fine for the masked rows because we already overwrote those positions with `nan` before the interp call. Sanity-check by setting up a tiny example and verifying `chi_squared` matches the numpy path to `rtol=1e-4`. - -5. The 300-iteration safety raise in the numpy path stays under `if xp is np:` — JAX has no analogue of "the loop ran out". Document the JAX path's failure mode in a single docstring line: *"With xp=jnp, masked points are dropped via NaN propagation; if `K=4` is insufficient, the chi-squared will be biased by missing perimeter samples — increase K and re-pin the workspace_test reference numbers."* - -6. Test bar: - - `python -m pytest test_autogalaxy/ellipse/test_fit_ellipse.py -v` passes (prompt 3's tests, all numpy-path). - - Add one new test in the same file that compares the numpy-path output against the JAX-path output for a non-trivial mask, asserting `nansum(chi_squared)` agrees to `rtol=1e-4`. Skip with `pytest.importorskip("jax")`. - - The reference numbers from prompt 2's workspace_test scripts are unchanged on the numpy path. diff --git a/active/6_refactor_conductor.md b/active/6_refactor_conductor.md deleted file mode 100644 index ea07df7e..00000000 --- a/active/6_refactor_conductor.md +++ /dev/null @@ -1,53 +0,0 @@ -# Refactor conductor — the first default-auto agent - -Type: feature -Target: autonomy -Repos: -- PyAutoBrain -Difficulty: medium -Autonomy: supervised -Priority: normal -Status: draft - -## Why - -Demonstrated need, not symmetry: a "refactor agent" bullet has sat in -`@PyAutoMind/ideas.md` since before this series was conceived, and `/refactor` -today is merely `start_dev` pre-tagged with the work-type. Refactoring is also -the **most autonomy-friendly work-type** — behaviour-preserving by definition, -so the test suite + review faculty form a near-complete gate — which makes it -the right proving ground for the `--auto` machinery before feature/bug work -earns the same treatment. - -## What - -Add `@PyAutoBrain/agents/conductors/refactor/` following the Build Agent's -shape (Tier line, `AGENTS.md`, deterministic entrypoint, capability audit): - -1. **Plans no-behaviour-change work**: selects the next `refactor/*` Mind task - (or plans a named one), emits a `RefactorDecision` mirroring the - FeatureDecision shape, with an explicit behaviour-preservation argument - (what invariant, which tests witness it). -2. **Mines candidates**: can sweep review-faculty FINDINGS, - simplification-review output, and `ideas.md` refactor bullets into proposed - `refactor//` prompts (via intake — it files, it does not bypass). -3. **Runs at `safe` by default** under `--auto`, per the work-type cap in - `AUTONOMY.md` — the first conductor whose normal mode is autonomous, ending - at PR-open with the review verdict attached. -4. Re-point the `/refactor` verb from the work-type-entry shim to the - conductor; update the routing table in `PyAutoBrain/AGENTS.md` and - `PyAutoMind/ROUTING.md`. - -## Boundaries - -- Consults faculties only (sizing, review, memory when it lands, vitals for - risky work) — never another conductor. -- A refactor that changes any public API or observable behaviour is - misclassified: the conductor re-routes it to feature/bug rather than - proceeding at `safe`. -- Whether an "optimize agent" (the adjacent ideas.md bullet) is this - conductor's second mode or its own thing is a scope decision for the plan — - do not silently absorb it. - -Blocked-by: 4_auto_dev_mode.md (and transitively 1–3). Mark the ideas.md -bullet `[formalised -> feature/autonomy/6_refactor_conductor.md]` when issued. diff --git a/active/6_visualization_profiles.md b/active/6_visualization_profiles.md deleted file mode 100644 index 3cb9dea2..00000000 --- a/active/6_visualization_profiles.md +++ /dev/null @@ -1,12 +0,0 @@ -# Weak lensing shear profile and convergence map visualization - -Type: feature -Target: weak -Difficulty: medium -Autonomy: supervised -Priority: high -Status: formalised - -Weak lensing shear profile and convergence map visualization. Add the two canonical cluster weak-lensing diagnostics missing from autolens/weak/plot: (1) an azimuthally averaged tangential and cross shear radial profile helper (binned gamma_t / gamma_x vs radius with error bars, cross-shear as the standard B-mode null test) computed about a chosen centre from a WeakDataset or FitWeak; (2) a Kaiser-Squires style convergence map reconstruction plotted from the shear field. Re-export into aplt alongside the existing nine quiver helpers and demo both in autolens_workspace/scripts/weak (extend simulator.py / fit.py outputs). Tangential shear profiles are THE standard observable in cluster weak lensing (Oguri 2012 SGAS; Medezinski 2016 A2744). - - diff --git a/active/7_analysis_ellipse_jax.md b/active/7_analysis_ellipse_jax.md deleted file mode 100644 index 51ba435e..00000000 --- a/active/7_analysis_ellipse_jax.md +++ /dev/null @@ -1,30 +0,0 @@ -Step 7 of the ellipse-JAX series — the keystone. Prompts 4-6 made every piece JAX-traceable; this prompt wires `AnalysisEllipse` so `jax.jit(analysis.fit_from)(instance)` works end to end. The template is `AnalysisImaging` in `@PyAutoGalaxy/autogalaxy/imaging/model/analysis.py:30-187`, which has all the moving parts (`use_jax: bool = True`, `_register_fit_imaging_pytrees()`, `super().__init__(use_jax=use_jax)`). - -Please: - -1. In `@PyAutoGalaxy/autogalaxy/ellipse/model/analysis.py`: - - Add `use_jax: bool = True` to `AnalysisEllipse.__init__` and pass it through `super().__init__(use_jax=use_jax)`. Default `True` matches `AnalysisImaging`. - - Add a `fit_from(instance: af.ModelInstance) -> FitEllipse` method (today only `fit_list_from` exists). It should mirror `AnalysisImaging.fit_from`: build the `FitEllipse` (or list of `FitEllipse` collapsed into a sum-figure-of-merit wrapper), call `_register_fit_ellipse_pytrees()` once when `self._use_jax`, return the resulting `FitEllipse`. - - Update `log_likelihood_function` to call `self.fit_from(instance).figure_of_merit` (or sum the list, matching the existing logic). The existing `fit_list_from` stays — it's used by `VisualizerEllipse.visualize` in `@PyAutoGalaxy/autogalaxy/ellipse/model/visualizer.py:64`. - -2. Implement `_register_fit_ellipse_pytrees()` modelled on `AnalysisImaging._register_fit_imaging_pytrees()` (lines 168-187). Register: - - `FitEllipse` with `no_flatten=("dataset",)`. The `interp` cached property reconstructs from `dataset` so it's safe to skip flattening. - - `Ellipse` (generic flatten via `register_instance_pytree`). - - `EllipseMultipole` and `EllipseMultipoleScaled` (generic flatten). - - Reuse the helper from `autoarray.abstract_ndarray.register_instance_pytree`. Make the function idempotent — match the registry-guard pattern in the imaging analysis. - - Place a thin shim `@PyAutoGalaxy/autogalaxy/analysis/jax_pytrees.py::register_ellipses_pytree()` if useful to mirror `register_galaxies_pytree`, but it's optional — generic registration may be enough since `Ellipse`s are stored on `instance.ellipses` as a list, not a custom container. - -3. Flip the workspace_test scripts from prompt 2 (`@autogalaxy_workspace_test/scripts/jax_likelihood_functions/ellipse/{fit.py, multipoles.py}`) to exercise the JIT path: - - Replace the `# TODO(7_analysis_ellipse_jax.md)` placeholder with the actual JIT round-trip block, modelled on `@autogalaxy_workspace_test/scripts/jax_likelihood_functions/imaging/lp.py:107-129` — `analysis_jit = ag.AnalysisEllipse(dataset=dataset, use_jax=True); fit_jit_fn = jax.jit(analysis_jit.fit_from); fit = fit_jit_fn(instance)`. - - Assert `np.testing.assert_allclose(float(fit.log_likelihood), float(fit_np.log_likelihood), rtol=1e-4)` against the numpy reference computed earlier in the script. - - Assert `isinstance(fit.log_likelihood, jnp.ndarray)`. - - Add a `fitness._vmap` batch-evaluation block too, mirroring `imaging/lp.py:74-98`. This catches issues that only surface under `jax.vmap`. - -4. Add a unit test in `@PyAutoGalaxy/test_autogalaxy/ellipse/test_analysis.py` that constructs `AnalysisEllipse(dataset, use_jax=False)` and asserts the existing numpy `log_likelihood_function` value is unchanged for a known instance. The JAX-path checks live in the workspace_test scripts — `@PyAutoGalaxy/CLAUDE.md` "Never use JAX in unit tests". - -5. Test bar: - - `python -m pytest test_autogalaxy/ -v` passes (no regressions in the imaging/interferometer paths). - - The two workspace_test scripts run cleanly and the JIT path matches the numpy reference to `rtol=1e-4`. - - `bash run_all_scripts.sh` from `@autogalaxy_workspace_test/` is green. - -After this lands, ellipse modeling can run inside `Drawer` / `Nautilus` / any other JAX-compatible search the same way `AnalysisImaging` does today. Note that `Drawer` itself still needs a small fix to pass `use_jax_jit=True` through to `Fitness` (out of scope for this series — see the `z_features/ellipse_fitting_jax.md` "see also" note). diff --git a/active/7_queue_runner.md b/active/7_queue_runner.md deleted file mode 100644 index 1484f2d4..00000000 --- a/active/7_queue_runner.md +++ /dev/null @@ -1,56 +0,0 @@ -# Queue runner — generalize register_and_iterate into a work-type-agnostic loop - -Type: feature -Target: autonomy -Repos: -- PyAutoBrain -- PyAutoMind -Difficulty: large -Autonomy: supervised -Priority: normal -Status: draft - -## Why - -The organism already has a working autonomous orchestrator: -`register_and_iterate` takes a queue of Mind prompts, drives -`start_dev → ship_*` per prompt, pauses only at named judgment gates, and -auto-advances — but it is welded to the pytree PoC (scaffold pattern, -classification heuristic, registration loop). Extracting the loop gives the -"orchestrator of multiple agents" capability generically, at low risk, because -the pattern is production-proven. - -## What - -1. Extract the generic loop into a new skill (working name `/run_queue`): - read a queue file (default `PyAutoMind/queue.md`, or an explicit prompt - list), and per entry run the dev lifecycle **at that task's effective - autonomy level** — `safe` tasks straight to PR-open, `supervised` tasks via - checkpoint-and-continue, `human-required` tasks skipped with a note. -2. Keep the blessed queue conventions: processed in order, done entries - prepended `# DONE `, never deleted. -3. **Batch report** at the end of a run: per task — outcome (PR URL / parked - question / blocker), test+review verdicts, calibration-log rows appended. -4. Re-base `register_and_iterate` on the generic loop (its pytree-specific - scaffold/classification becomes the task-type plugin), so there is one loop - implementation, not two. - -## Boundary decision (adversarial finding — settled) - -The queue runner is a **skill**, not a new conductor. ORGANISM.md's consult -DAG forbids a conductor consulting conductors, and the precedent -(`register_and_iterate`) is a skill that drives the same lifecycle the human -would. Filing it as a conductor would force an amendment to the organism -doctrine for no gain. If during planning a genuine conductor-shaped need -emerges (a human-meaningful verb with its own decision object), stop and -re-plan rather than quietly promoting it. - -## Boundaries - -- Worktree conflicts: the runner must respect `active.md` claims - (`worktree_check_conflict`) and run tasks serially unless claims are - provably disjoint. -- Context/cost: one task per session-scale unit of work; the runner is a loop - over sessions' worth of work, not one monster context. - -Blocked-by: 4_auto_dev_mode.md, 5_checkpoint_and_continue.md. diff --git a/active/7_real_data.md b/active/7_real_data.md deleted file mode 100644 index 8112037b..00000000 --- a/active/7_real_data.md +++ /dev/null @@ -1,12 +0,0 @@ -# Weak lensing real data example on Abell 2744. Reproduce a - -Type: feature -Target: weak -Difficulty: large -Autonomy: supervised -Priority: high -Status: formalised - -Weak lensing real data example on Abell 2744. Reproduce a not-too-complex published cluster weak-lensing analysis end to end on a public shear catalog, as the first PyAutoLens weak-lensing result on real data. Target: Abell 2744 (HST Frontier Fields cluster) — public catalogs include the Medezinski et al. 2016 Subaru/Suprime-Cam analysis (arXiv:1507.03992) and the JWST UNCOVER pyRRG-JWST shear catalog; pick whichever is cleanest to obtain and document provenance. Requires: (1) a WeakDataset catalog loader (from_fits / from_csv: positions, e1/e2, weights, optional source redshifts); (2) reduced shear support — real catalogs measure g = gamma/(1-kappa), not shear, so FitWeak/simulator need a reduced-shear mode, plus sigma_crit / lensing-efficiency scaling for a source redshift distribution; (3) an autolens_workspace/scripts/weak/real_data example fitting an NFW (or NFW+substructures) mass model and reproducing the published tangential shear profile and mass within errors (Oguri 2012 SGAS-style analysis). Depends on AnalysisWeak from feature/weak/4_modeling.md. - - diff --git a/active/7_scaling_relation_lenstool_convention.md b/active/7_scaling_relation_lenstool_convention.md deleted file mode 100644 index 80d6fb34..00000000 --- a/active/7_scaling_relation_lenstool_convention.md +++ /dev/null @@ -1,62 +0,0 @@ -# Cluster scaling relation: LensTool convention (reference-anchored, fixed exponent, r_cut scaling) - -Type: feature -Target: cluster -Difficulty: medium -Autonomy: supervised -Priority: high -Status: formalised - -Reparameterize the cluster scaling-relation tier to the LensTool / referee-endorsed convention: reference-anchored normalization, fixed exponent, and truncation-radius scaling. - -Current state (`autolens_workspace/scripts/cluster/modeling.py` + `start_here.py`, and -`scripts/group/features/scaling_relation/`): each scaling-tier member gets -`b0 = scaling_factor * luminosity ** scaling_exponent` with `scaling_factor ~ U(0,1)` and -`scaling_exponent ~ U(0,2)` both free, while `ra` and `rs` are held fixed for the whole tier. - -A referee/collaborator comment on a paper using this parameterization is correct about standard -practice, and the workspace should move to it: - -1. **Anchor the normalization to a reference galaxy** (BCG/BGG or L*): parameterize - `b0_i = b0_ref * (L_i / L_ref)^beta` so the free parameter is the Einstein-radius-like strength - of the reference galaxy — physically interpretable, easy to set a prior range on, and - dimensionally clean (the current `scaling_factor` has units arcsec/L^beta that change with beta). -2. **Fix beta by default.** LensTool convention derives from Faber-Jackson: - sigma_0 = sigma_0* (L/L*)^(1/4), and since b0 ∝ sigma^2 this gives beta = 0.5 for b0. - Default beta fixed at 0.5, with prose explaining it can be freed or set from fundamental-plane / - velocity-dispersion fits. -3. **Scale the truncation radius too**: LensTool scales r_cut = r_cut* (L/L*)^(1/2) (and r_core - similarly, though r_core is usually fixed negligible). Currently `rs` is fixed at 10.0" for the - whole tier — add `rs_i = rs_ref * (L_i/L_ref)^0.5` with `rs_ref` free or fixed, documented. - -Scope: update `scripts/cluster/modeling.py`, `scripts/cluster/start_here.py`, -`scripts/cluster/simulator.py` (truth relation should be generated in the new convention so -recovered parameters are interpretable), `scaling_galaxies.csv` schema if needed (a reference-galaxy -row or normalization column), and the group-scale feature docs -`scripts/group/features/scaling_relation/modeling.py` + `modeling_for_luminosities.py` prose so both -tiers tell the same story. Keep the old free-(alpha, beta) form documented as an option — it is -mathematically equivalent for alpha and some users will want beta free — but the default and the -tutorial prose should follow the LensTool convention. Note the luminosity input convention: papers -often compute L from the MGE decomposition (L = sum_i 2*pi*sigma_i^2 I_i / q_i); the prose should -state luminosities are relative (only L_i/L_ref matters), which sidesteps unit questions. - -Depends on nothing; should land before the "PyAutoLens for LensTool users" flagship example, which -will use this convention. - - - -__Research findings (deep-research pass, 2026-07-08)__ - -- LensTool convention operates on sigma and r_cut, anchored to a reference magnitude/luminosity - (usually the BCG or L*): sigma_LT_i = sigma_LT_ref * (L_i/L_0)^alpha, - r_cut_i = r_cut_ref * (L_i/L_0)^beta_cut. Canonical Faber-Jackson: alpha = 0.25 -> since - b0 ∝ sigma^2, the b0 exponent is 2*alpha = 0.5 (the value to fix as default). -- Measured values (Bergamini et al. 2019, MUSE kinematics of cluster members): alpha = 0.27-0.28 - (b0 exponent ≈ 0.55); beta_cut = gamma - 2*alpha + 1 with fundamental-plane gamma = 0.2, - giving beta_cut ≈ 0.64-0.66 (vs 0.5 canonical). Prose should present 0.5 as the default with - the Bergamini kinematic calibration as the documented refinement, and note the mass-follows-light - degeneracy: r_cut_ref and sigma_ref are strongly degenerate (total member mass ∝ sigma^2 * r_cut), - which is why beta_cut is never sampled freely. -- Reference anchoring in papers is via magnitudes: L_i/L_0 = 10^(0.4*(m_0 - m_i)); the CSV schema - should accept either luminosities or magnitudes, and the reference row should be identifiable - (BCG/BGG flag or explicit L_0 value). diff --git a/active/8_lenstool_users_example.md b/active/8_lenstool_users_example.md deleted file mode 100644 index a9391bf5..00000000 --- a/active/8_lenstool_users_example.md +++ /dev/null @@ -1,47 +0,0 @@ -# Flagship 'PyAutoLens for LensTool users' example on real cluster data (SMACS J0723) - -Type: docs -Target: cluster -Difficulty: large -Autonomy: supervised -Priority: high -Status: formalised - -Flagship "PyAutoLens for LensTool users" end-to-end cluster example on real data, reproducing a published LensTool model as closely as possible. - -Most cluster strong-lensing modelers use LensTool. A workspace example that says "if you use -LensTool, this script does the same thing in PyAutoLens" — same profile (dPIE), same scaling -relation convention, same multiple-image positional likelihood, on real public data with a -published LensTool model to compare against — would be extremely valuable for adoption. - -Candidate cluster: **SMACS J0723.3-7327** (the first JWST cluster). It is relatively regular -(single BCG), has public HST (RELICS) + JWST imaging, and has multiple published LensTool models -with parameter tables to reproduce and compare against: the RELICS/Sharon HST model (Mahler et al., -arXiv:2208.08483; model products public on MAST via doi:10.17909/T9SP45), Mahler et al. 2022 -(arXiv:2207.05007, HST model with MUSE spectroscopic redshifts), and Caminha et al. 2022. -Alternatives if SMACS0723 proves too complex: a relaxed CLASH cluster (e.g. Abell 383, Richard et -al. 2011) or a group-scale lens; final choice is part of this task — criteria are: few multiple-image -families (~5), spectroscopic redshifts available, published LensTool parameter table, public imaging. - -The example should: -- Ingest the published multiple-image catalogue (positions + redshifts) via the cluster CSV API - (point_datasets.csv), and the cluster-member catalogue into scaling_galaxies.csv. -- Compose the same mass model as the published LensTool work: cluster-scale dPIE halo(s) + BCG + - scaling-relation members (LensTool convention: fixed exponents, reference-anchored normalization, - r_cut scaling), using the LensTool-native dPIE parameterization (from_lenstool conversion). -- Fit with the source-plane chi^2 (LensTool's default) and show the image-plane chi^2 as the - rigorous upgrade, with prose mapping every PyAutoLens concept to its LensTool equivalent - (.par file section -> CSV row / model component; sigma,r_core,r_cut -> b0,ra,rs; image-plane vs - source-plane optimization; Bayesian evidence vs chi^2/RMS). -- Compare recovered parameters and image-position RMS against the published LensTool values, and - state honestly where conventions prevent exact parity. - -Location: `autolens_workspace/scripts/cluster/` (e.g. `lenstool.py` or a `lenstool/` subfolder if -the data-prep stage warrants a second script). Real-data download/prep must be reproducible -(documented MAST fetch or a packaged dataset under `autolens_workspace/dataset/cluster/`). - -Depends on: the dPIE LensTool parameterization/parity prompt and the scaling-relation -reparameterization prompt. There is a real prospective user available for back-and-forth beta -testing once a draft exists — plan for an iteration loop with them. - - diff --git a/active/8_memory_faculty.md b/active/8_memory_faculty.md deleted file mode 100644 index b198e7e1..00000000 --- a/active/8_memory_faculty.md +++ /dev/null @@ -1,48 +0,0 @@ -# Memory faculty — one read-only consult for PyAutoMemory + autolens_assistant - -Type: feature -Target: autonomy -Repos: -- PyAutoBrain -Difficulty: medium -Autonomy: supervised -Priority: normal -Status: draft - -## Why - -Feature, bug and intake conductors each describe "consult PyAutoMemory" in -prose, and WORKFLOW.md tells every skill to do it ad-hoc — the same -duplication vitals was created to remove for Heart. A memory faculty makes the -consult uniform and machine-invokable, and it is the prerequisite for the -science→task pipeline (`9_scholar_intake.md`): scholar mode needs a -disciplined read surface, not bulk-loading wikis. - -## What - -Add `@PyAutoBrain/agents/faculties/memory/`: - -1. Given a topic/question (e.g. "prior work on delaunay pixelization - regularization"), return a **cited digest**: relevant PyAutoMemory sub-wiki - pages, autolens_assistant skill/wiki pages, prior Mind `complete.md` - entries — pointers plus a short synthesis, never wholesale page dumps. -2. Follow the faculty shape: read-only, judges and stops, sensor organs are - Memory and the assistant workspaces. Do not couple to Memory's internal - layout — resolve sub-wikis at query time (the standing rule). -3. Rewire the consumers: Feature/Bug conductors and the WORKFLOW.md "consult - Memory before substantial planning" step point at the faculty instead of - restating the procedure. - -## Boundaries - -- **Privacy seam**: PyAutoMemory is personal. The faculty's digests flow into - Mind prompts and issues on private/organism repos — fine — but anything that - later lands in public user-facing repos (workspace tutorials, docs) must not - carry PyAutoMemory references. State this in the faculty's AGENTS.md. -- Operational history stays Mind's (complete.md, issues); the faculty may read - it but the boundary prose in ORGANISM.md is unchanged. -- Not a RAG build-out — no indexes, no embeddings, no new infra. Grep + the - wikis' own structure, same as a careful human session. - -Blocked-by: 1_autonomy_contract.md only (independent of 2–7; can run in -parallel with them). diff --git a/active/8_strong_lensing.md b/active/8_strong_lensing.md deleted file mode 100644 index bbf1f466..00000000 --- a/active/8_strong_lensing.md +++ /dev/null @@ -1,12 +0,0 @@ -# Combined strong plus weak lensing example - -Type: feature -Target: weak -Difficulty: medium -Autonomy: supervised -Priority: high -Status: formalised - -Combined strong plus weak lensing example. Add autolens_workspace/scripts/weak/features/strong_lensing/ with dedicated simulator.py, fit.py and modeling.py showing weak-lensing shear constraints combined with strong lens modeling of the same mass distribution — the science mode where weak shear is the extra large-radius signal around strong-lens clusters (Niemiec 2020 hybrid-Lenstool, arXiv:2002.04635) and strong-lens groups (Oguri 2012 Sloan Giant Arcs Survey, arXiv:1109.2594), not cosmic shear or galaxy-galaxy lensing. simulator.py simulates the SAME tracer into an imaging dataset (strong) and a surrounding WeakDataset shear field (weak); fit.py fits both with a shared tracer; modeling.py combines AnalysisImaging + AnalysisWeak via PyAutoFit analysis summing to show the joint fit constraining the mass profile better than either alone (parametric core + large-radius shear, the hybrid-Lenstool insight). Depends on AnalysisWeak from feature/weak/4_modeling.md. - - diff --git a/active/9_cluster_visualization.md b/active/9_cluster_visualization.md deleted file mode 100644 index 4c66e4c9..00000000 --- a/active/9_cluster_visualization.md +++ /dev/null @@ -1,43 +0,0 @@ -# Cluster-scale visualization: multi-plane critical curves/caustics, large-FoV plots, aplt promotion - -Type: feature -Target: cluster -Difficulty: large -Autonomy: supervised -Priority: normal -Status: formalised - -Dedicated cluster-scale visualization: large-field plots, all critical curves and caustics, promoted into the library aplt interfaces. - -Cluster-scale lenses need visualization that is qualitatively different from the galaxy-scale plots -in place: fields of view are arcminutes not arcseconds, there are tens of mass components, multiple -source planes each with their own critical curves and caustics, and multiple-image families that need -per-source identification at a glance. - -A prototype already exists — `autolens_workspace_test/scripts/cluster/visualization.py` (moved from -scripts/imaging/visualization_cluster.py) produces overlaid positions, per-source grids and -cluster-tuned critical curves — and z_features/cluster_lensing.md has a deferred "aplt plotter -promotion" item this prompt subsumes. Promote and extend into first-class library support: - -- **All critical curves / caustics**: for a multi-plane tracer, compute and plot the tangential and - radial critical curves for every source-plane redshift (they differ per plane), and the - corresponding caustics in each source plane, with per-plane colouring and a legend. Current - galaxy-scale defaults assume one source plane. -- **Large-image handling**: sensible defaults for arcminute-scale FoVs — critical-curve resolution - (the marching grid must be fine enough to resolve member-galaxy-scale features without an - intractable grid), position markers sized for the FoV, and zoom-grid subplots per multiple-image - family. -- **Per-source colouring** of observed vs model-predicted positions, matched by pairing, so - over/under-prediction is visible immediately. -- Promote via the standard `aplt` / `Visuals2D` / `Include2D` interfaces (e.g. Include2D flags for - per-plane critical curves; a `TracerPlotter`/point-fit subplot tuned for clusters), so the cluster - workspace scripts and the flagship LensTool example get these plots without bespoke matplotlib. -- Watch JAX interplay: critical-curve computation on big grids has known pitfalls - (ZeroSolver vmap incompatibility; use the jit-friendly path where relevant) — numpy-path-only is - acceptable for visualization, stated explicitly. - -Scope is library (PyAutoLens/PyAutoGalaxy aplt) + workspace (cluster scripts consume the new -plotters; update visualization script). The flagship LensTool example and real-user beta testing -will exercise these plots, so this should land before or alongside that example. - - diff --git a/active/9_scholar_intake.md b/active/9_scholar_intake.md deleted file mode 100644 index e3800050..00000000 --- a/active/9_scholar_intake.md +++ /dev/null @@ -1,49 +0,0 @@ -# Scholar intake — science analysis and machine sources feed the Mind - -Type: feature -Target: autonomy -Repos: -- PyAutoBrain -- PyAutoMind -- autolens_assistant -Difficulty: medium -Autonomy: supervised -Priority: normal -Status: draft - -## Why - -The missing pairing: scientific analysis (autolens_assistant sessions, -PyAutoMemory reading, profiling results) currently produces insight that dies -in the session unless the human hand-carries it into `ideas.md`. The organism -should conceive tasks from its own scientific work — with the human as -*editor* of a proposed batch, not *author* of every prompt. - -## What - -1. **Scholar mode for `/research`**: a research run over autolens_assistant / - PyAutoMemory (via the memory faculty) ends by emitting candidate task - bullets into `PyAutoMind/ideas.md`, each with provenance — "motivated by - ". Bullets only: the - existing `intake ideas` sweep then formalises them into headed prompts, - and the human reviews the IntakeDecisions before `--apply`. -2. **Extend the intake sweep to machine sources**: beyond `ideas.md`, let - `intake` propose prompts from accumulating machine output — Heart-filed - issues, profiling result summaries, review-faculty findings that outgrew - their PR — same dry-run-first, human-approves-batch contract. - -## Boundaries (adversarial findings) - -- **Never write prompts or planned.md directly** from scholar mode — ideas.md - is the staging area precisely so intake's existing conception discipline - (classify, size, human review, `--apply`) is reused, not bypassed. Noise - tasks die cheaply at the bullet stage. -- **Provenance stays private**: PyAutoMemory citations live in Mind (private - organism repo). They must never leak into public workspace/docs output that - a formalised task later produces. -- Intake's own rules hold: it files, it never starts dev; low-confidence - classifications land in `triage/`; raw bullets are marked - `[formalised -> …]`, never deleted. - -Blocked-by: 8_memory_faculty.md (the read surface), 1_autonomy_contract.md. -Independent of the 4–7 execution track. diff --git a/active/9_small_datasets.md b/active/9_small_datasets.md deleted file mode 100644 index ac47cf39..00000000 --- a/active/9_small_datasets.md +++ /dev/null @@ -1,12 +0,0 @@ -# Weak lensing PYAUTO_SMALL_DATASETS support - -Type: feature -Target: weak -Difficulty: small -Autonomy: safe -Priority: normal -Status: formalised - -Weak lensing PYAUTO_SMALL_DATASETS support. Extend the PYAUTO_SMALL_DATASETS=1 smoke-mode mechanism (autoarray/util/dataset_util.py — 15x15 array cap + should_simulate delete-and-regenerate, honoured by Mask2D.circular / Grid2D.uniform) to weak-lensing datasets so workspace-test smoke runs of scripts/weak/ are fast. Weak datasets are catalogue-shaped (N background galaxies, not pixels), so the lever is the galaxy count: cap the number of positions drawn by SimulatorShearYX.via_tracer_random_positions_from (e.g. 200 -> ~25) when the env var is active, mirroring the imaging cap convention and constants in aa.util.dataset. NOTE (2026-07-09, user direction): the should_simulate auto-simulation pattern for scripts/weak/{fit,modeling}.py ships with series step 6 (weak-viz-profiles, PyAutoLens#581) — this task only needs to verify the env-var path regenerates a SMALL catalogue through that pattern. Keep the cap OUT of via_tracer_from (explicit user grid) and note the weak path uses Grid2DIrregular so existing uniform-grid caps never touch it. Gotcha from prior work: these env-var mutations can bite inside decorators — trace the full call path before assuming where the cap lands. Env var wiring for smoke runs already exists via PyAutoBuild config/build/env_vars.yaml; no os.environ mutation in scripts. - - diff --git a/active/Kernel2D.md b/active/Kernel2D.md deleted file mode 100644 index d941c300..00000000 --- a/active/Kernel2D.md +++ /dev/null @@ -1,26 +0,0 @@ -This error likes to crop up in autolens assistant can you work out why: - -│ ... first 5 lines hidden (Ctrl+O to show) ... │ -│ y than library releases, so version mismatches are expected and not actionable for `main`-branch users. │ -│ │ -│ You can also set the environment variable PYAUTO_SKIP_WORKSPACE_VERSION_CHECK=1 to disable temporarily. │ -│ warnings.warn(_missing_version_warning(root, library_version)) │ -│ Traceback (most recent call last): │ -│ File "", line 1, in │ -│ File "/home/jammy/Code/PyAutoLabs/PyAutoLens/autolens/__init__.py", line 156, in __getattr__ │ -│ raise AttributeError(f"module {__name__!r} has no attribute {name!r}") │ -│ AttributeError: module 'autolens' has no attribute 'Kernel2D' - -I wonder if it was built against old code base by my collaborator and thus we need to reuse the tools -that build some aspects of autolens assistant? - -Heres a SLACK chat: - -Jam  [10:23 AM] -Does autolens_assistant need any kind of rebuild or whatnot done when I update workspaces or do a release -Rich  [10:25 AM] -There's a skill that should check the API but we might want to nail it down better -[10:25 AM]It could possibly keep a hash of the AutoLens release and then diff so the agent can see API updates - - -Note that Kernel2D was removed like 6 months ago so this is a very strange old API issue. \ No newline at end of file diff --git a/active/adapt_images_pytree_fix.md b/active/adapt_images_pytree_fix.md deleted file mode 100644 index c5e10754..00000000 --- a/active/adapt_images_pytree_fix.md +++ /dev/null @@ -1,83 +0,0 @@ -Fix `AdaptImages.galaxy_image_dict` Galaxy-identity mismatch across `jax.jit` boundary in -@PyAutoGalaxy, and re-enable the three autogalaxy_workspace_test scripts that this blocks. - -__Problem__ - -When `jax.jit(analysis.fit_from)(instance)` returns a `FitImaging` via the pytree registration -added in PyAutoGalaxy PR #364, accessing `fit.log_likelihood` post-unflatten fails for any model -that uses `AdaptImages`: - -``` -AttributeError: 'NoneType' object has no attribute 'array' - File .../rectangular_adapt_image.py", line 93, in mesh_weight_map_from - mesh_weight_map = adapt_data.array -``` - -__Root cause__ - -`FitImaging` is registered with `no_flatten=("dataset", "adapt_images", "settings")`, so -`adapt_images` rides across the pytree boundary as aux — its `galaxy_image_dict` keys are the -**trace-time** `ag.Galaxy` instances. `self.galaxies` is registered as a pytree (dynamic), so -post-unflatten it contains **fresh** `Galaxy` instances built via `autofit.Model.instance_unflatten` -→ `self.cls(*constructor_arguments)`, each with a new `.id`. `hash(galaxy)` returns `int(self.id)`, -so the fresh Galaxy doesn't match any key in `adapt_images.galaxy_image_dict`. The lookup at -`PyAutoGalaxy/autogalaxy/galaxy/to_inversion.py:555` raises `KeyError` → `adapt_galaxy_image = None` -→ `mesh.mesh_weight_map_from(adapt_data=None)` blows up. - -The analogous fix on the autolens side that solved a similar dict-keyed-by-instance problem is -tracked at `@PyAutoPrompt/autolens/linear_light_profile_intensity_dict_pytree.md`. - -Note that autolens's jax_likelihood_functions/imaging/rectangular.py currently passes in -autolens_workspace_test despite apparently having the same Galaxy-identity issue — worth checking -what autolens does differently (e.g. a shared fix in autoarray / autofit, or a subtly different -FitImaging inversion path that bypasses the `galaxy_image_dict` lookup for the adapt mesh). That -diff may reveal the minimal fix for autogalaxy, or a broader pattern that should be lifted into -autoarray. - -__Scripts blocked__ - -From @autogalaxy_workspace_test/scripts/jax_likelihood_functions/imaging/, these were deferred in -the initial task 3/9 ship (PyAutoGalaxy PR #364, workspace PR on autogalaxy_workspace_test): - -- `rectangular_mge.py` — MGE bulge + `ag.mesh.RectangularAdaptImage` + `ag.reg.Adapt` -- `delaunay.py` — `ag.mesh.Delaunay` + `ag.image_mesh.Hilbert` (or `Overlay`, which still wires - the image-plane mesh grid via `adapt_images.galaxy_name_image_plane_mesh_grid_dict`) -- `delaunay_mge.py` — MGE bulge + Delaunay - -The initial ship used `ag.mesh.RectangularUniform` + `ag.reg.Constant` for `rectangular.py` (no -adapt dependency), which does pass. After this fix lands, re-port the three scripts above using -the proper adapt-image autolens references at -`@autolens_workspace_test/scripts/jax_likelihood_functions/imaging/{rectangular,rectangular_mge, -delaunay,delaunay_mge}.py`. - -__Deliverables__ - -1. **PyAutoGalaxy library fix** for the Galaxy-identity issue across the JIT boundary. - Candidate approaches (pick whichever is cleanest): - - Key `AdaptImages.galaxy_image_dict` by the galaxy's path-tuple (e.g. `('galaxies', 'galaxy')`) - instead of the Galaxy instance — stable across unflatten. - - Look up by `galaxy.id` via an identity map that is rebuilt during `fit_from` (not carried as - aux). - - Register `Galaxy` with a custom pytree that preserves `.id` through unflatten so hashes match. - - Move the adapt-image lookup inside `fit_from` so it runs during tracing (before the pytree - boundary) and stores `adapt_galaxy_image` on the mapper directly rather than looking it up - lazily. -2. **Unit test** in `test_autogalaxy/` exercising the fix **without importing JAX** (follow the - numpy-only unit test convention — cross-xp checks live in workspace_test). -3. **Re-port the three deferred scripts** into autogalaxy_workspace_test using the autolens - references, and re-enable them in `smoke_tests.txt` alongside the existing jax_likelihood_ - functions/imaging/ entries. -4. Add `jax_likelihood_functions/imaging/delaunay_mge.py` commented out in `smoke_tests.txt` with - the exact jax-0.7 regression comment from autolens's smoke_tests.txt. - -__Dependencies__ - -- PyAutoGalaxy PR #364 (pytree registration scaffold) must be merged first. -- Cross-check: if the fix in autoarray/autofit applies to autolens too, include an autolens test - update in the same library PR. - -__Umbrella__ - -Follow-up from PyAutoLabs/autogalaxy_workspace_test#8 (epic #5 task 3/9). Same issue may re-surface -in task 4/9 (`jax_likelihood_interferometer`) and task 5/9 (`jax_likelihood_multi`) — either fix -once here, or carry the same deferral pattern into those tasks. diff --git a/active/add_a_formalise_mode_to_the_pyautobrain.md b/active/add_a_formalise_mode_to_the_pyautobrain.md deleted file mode 100644 index 546830e8..00000000 --- a/active/add_a_formalise_mode_to_the_pyautobrain.md +++ /dev/null @@ -1,15 +0,0 @@ -# Add a formalise mode to the PyAutoBrain intake agent - -Type: feature -Target: PyAutoBrain -Repos: -- PyAutoBrain -- PyAutoMind -Difficulty: medium -Autonomy: supervised -Priority: normal -Status: formalised - -Add a formalise mode to the PyAutoBrain intake agent. It walks the PyAutoMind prompts the census flags as headerless or with missing header fields and retroactively formalises them: classify and size each prompt body via the shared sizing faculty, then insert or complete the light metadata header in place, preserving the original text verbatim. This is the planned follow-up previously codenamed repair; formalise is the better word because raw prompts are intended word-vomit awaiting conception, not defects. The taxonomy folder stays authoritative for Type/Target; where the classifier disagrees with the folder, report a re-home suggestion but never move or delete files. Dry-run proposes, --apply writes. Touches intake agent code in PyAutoBrain and rewrites prompt files in PyAutoMind. - - diff --git a/active/add_a_reconcile_mode_to_the_pyautobrain.md b/active/add_a_reconcile_mode_to_the_pyautobrain.md deleted file mode 100644 index 769d7ec9..00000000 --- a/active/add_a_reconcile_mode_to_the_pyautobrain.md +++ /dev/null @@ -1,15 +0,0 @@ -# Add a reconcile mode to the PyAutoBrain intake agent - -Type: feature -Target: PyAutoBrain -Repos: -- PyAutoBrain -- PyAutoMind -Difficulty: small -Autonomy: supervised -Priority: low -Status: formalised - -Add a reconcile mode to the PyAutoBrain intake agent. It audits the PyAutoMind backlog for prompts describing already-shipped work whose status has gone stale, because a prompt's Status header is not a reliable completeness signal (formalise preserves an existing Status verbatim, so a shipped task can still read Status: planned). For each backlog prompt, cross-reference against complete.md prompt-path references and section headers, issued/ basenames, and optionally the target repo git log / merged PRs, then report a confidence-ranked list of suspected-complete prompts for a human to retire — never move or delete files automatically. Complements census/dashboard/formalise. Touches intake agent code in PyAutoBrain and reads PyAutoMind. - - diff --git a/active/add_census_and_dashboard_modes_to_the.md b/active/add_census_and_dashboard_modes_to_the.md deleted file mode 100644 index d46617af..00000000 --- a/active/add_census_and_dashboard_modes_to_the.md +++ /dev/null @@ -1,15 +0,0 @@ -# Add census and dashboard modes to the PyAutoBrain intake agent - -Type: feature -Target: PyAutoBrain -Repos: -- PyAutoBrain -- PyAutoMind -Difficulty: medium -Autonomy: supervised -Priority: normal -Status: formalised - -Add census and dashboard modes to the PyAutoBrain intake agent. A census mode inventories every filed PyAutoMind prompt (work-type, target, difficulty, status), and a dashboard mode renders that census as a clean summary page committed to the PyAutoMind repo. Touches the intake agent code in PyAutoBrain plus a generated page in PyAutoMind. - - diff --git a/active/adopt_the_lenstool_reference_magnitude_mag0_conv.md b/active/adopt_the_lenstool_reference_magnitude_mag0_conv.md deleted file mode 100644 index bec4c8ff..00000000 --- a/active/adopt_the_lenstool_reference_magnitude_mag0_conv.md +++ /dev/null @@ -1,31 +0,0 @@ -# Adopt the LensTool reference-magnitude (mag0) convention for the scaling-relation tier - -Type: docs -Target: workspaces -Repos: -- autolens_workspace -Difficulty: medium -Autonomy: supervised -Priority: normal -Status: formalised - -Adopt the LensTool reference-magnitude (mag0) convention for the scaling-relation tier across the autolens_workspace examples, replacing the non-standard max(sample-luminosity) anchor. Referee point (Limousin 2005/2007, Eliasdottir 2007, Bergamini 2019): the scaling-relation normalization should be anchored to a FIXED reference magnitude / reference luminosity (LensTool's mag0, conventionally the BCG magnitude), NOT the max luminosity of the scaling sample; the exponent should be FIXED (Faber-Jackson / fundamental plane), not fitted. DECISION (user-selected): use an explicit fixed reference-luminosity/magnitude CONSTANT documented as settable to the BCG (a fiducial L* where no BCG photometry exists), so einstein_radius_ref/b0_ref = the Einstein radius / lens strength of a galaxy at the reference magnitude. This is a strict reparameterization (alpha_new = alpha_old*(L_ref_new/L_ref_old)**exponent), so simulated member physics MUST stay identical and priors must still bracket the reparameterized truth. - -THREE example families are affected (verified by grep on current main): -1. cluster/ (modeling.py ~L373-390, start_here.py ~L300-302, likelihood_function.py ~L150, simulator.py ~L332-333): dPIE; b0 = b0_ref*(L/L_ref)**0.5; exponent ALREADY fixed 0.5; rs ALREADY scaled ~L**0.5. ONLY change the anchor (max -> explicit reference) + docstrings. DO NOT re-free the exponent or touch rs scaling. Note the fixed rs_ref may need a numeric rescale to keep member rs identical under the new anchor. -2. group/features/scaling_relation/ (modeling.py ~L262-280, fit.py ~L237-238, likelihood_function.py ~L166-167, simulator.py): Isothermal (no truncation radius); theta_E = theta_E_ref*(L/L_ref)**0.5; exponent ALREADY fixed 0.5. ONLY change the anchor + docstrings. Group BCG has NO photometry in the dataset (centre only) -> use a documented fiducial L*. -3. imaging/features/scaling_relation/ (modeling.py ~L280-294, fit.py ~L226, likelihood_function.py ~L172, simulator.py hardcodes the truth via einstein_radius=0.3*L**1.0): the ODD ONE — theta_E = scaling_factor * L**scaling_exponent with scaling_factor~U(0,0.5) AND scaling_exponent~U(0,2) BOTH free and NO reference ratio (raw luminosity). OPEN QUESTION to resolve before editing: is this example deliberately a FREE-exponent teaching case (demonstrating the general fit) that should stay as-is, with only cluster+group re-anchored? Or should it also be made LensTool-like (introduce reference-magnitude ratio + fix exponent 0.5)? - -4. **SLaM pipelines (VERIFIED inventory miss — added after adversarial grep; ChatGPT flagged, Claude confirmed against the tree):** group/slam.py (TWO sites: L314/330 and L791/807) and group/features/linear_light_profiles/slam.py (TWO sites: L261/276 and L724/739). All four use the OLD form: scaling_factor~U(0,0.5) * total_luminosity**scaling_relation with scaling_relation~U(0,2.0) (FREE exponent, raw luminosity, no reference ratio) — i.e. exactly the referee-objected parameterization, in PRODUCTION SLaM pipelines that users actually run. These were missed by the original prompt. Recommendation: re-anchor these to the standard fixed-0.5 reference-magnitude convention regardless of how the imaging-toy pedagogy question resolves (SLaM is production, not a teaching demo). Note each file has the pattern at TWO source-lp stages. - -VERIFIED FALSE POSITIVES (do NOT touch): guides/advanced/multi_plane.py:201 — `scaling_factor` there is `cosmology.scaling_factor_between_redshifts_from` (a cosmological scale factor, pure name collision). group/modeling.py, group/features/multi_gaussian_expansion/modeling.py, imaging|interferometer/features/extra_galaxies/modeling.py, cluster/csv_api.py, modeling_for_luminosities.py — all docstring cross-references only, no instantiation. - -REPARAMETERIZATION MATH (correct form — ChatGPT's numbers are right but its symbolic formula is written backwards): to keep simulated member physics identical when L_ref changes, p_ref_new = p_ref_old * (L_ref_new / L_ref_old)**0.5 (the ref value INCREASES when the fiducial L* > old max). Worked truths for fiducial L*=1.0: group theta_E_ref 0.135 -> 0.201; cluster b0_ref 0.12 -> 0.190, rs_ref 10.0 -> 15.8. TRAP: make the SIMULATOR and MODEL share the SAME explicit reference luminosity so members stay consistent by construction — otherwise the cluster's fixed rs values mismatch (simulator on max=0.4 vs model on L*=1.0). - -CONSTRAINTS: beta is already fixed at 0.5 in cluster+group and the cluster tier already scales the truncation radius rs~L**0.5 — do NOT regress these (the stale-note trap). Regenerate notebooks after editing scripts. - -RESOLVED DECISIONS (user, 2026-07-10): (a) dPIE core radius `ra` — SCALE it ra~L**0.5 for full LensTool PIEMD fidelity (add ra_ref; scale like rs; update the cluster simulator truth for ra too). (b) Imaging toy AND SLaM — RE-ANCHOR both to the standard fixed-0.5 reference-magnitude convention (one convention everywhere; no free-exponent variant retained). (c) Bergamini kinematic exponents (sigma~L^0.27) — mention in docs as the "when kinematics exist" refinement, not the default. Net scope: uniform LensTool reference-magnitude convention with fixed exponent 0.5 and full r_core/r_cut/b0 luminosity scaling across ALL sites — 3 feature families (cluster, group/scaling_relation, imaging/scaling_relation) + 2 SLaM pipelines (group/slam.py, group/features/linear_light_profiles/slam.py; 4 sites) + their simulators/fit/likelihood + regenerated notebooks. - -GATE: user reviewed convention with ChatGPT (Fable usage exhausted); adversarial verification done; decisions resolved. Ready for /start_dev. Files under autolens_workspace/scripts/; notebooks regenerated via PyAutoBuild. - - diff --git a/active/aggregator_output_contracts.md b/active/aggregator_output_contracts.md deleted file mode 100644 index 83925100..00000000 --- a/active/aggregator_output_contracts.md +++ /dev/null @@ -1,30 +0,0 @@ -# Fix release aggregator and generated-output contracts - -## Context - -Seven release failures involve results that are absent, shorter than examples assume, -or written to a different location. Several pass in a stateful local checkout, so the -first task is a clean, directory-ordered reproduction. Primary owners are @PyAutoFit, -@autogalaxy_workspace, @autolens_workspace, and @autolens_workspace_test. - -## Scripts - -- `autogalaxy_workspace/scripts/guides/results/start_here.py` -- `autogalaxy_workspace/scripts/guides/results/aggregator/samples_via_aggregator.py` -- `autolens_workspace/scripts/guides/results/start_here.py` -- `autolens_workspace/scripts/guides/results/aggregator/galaxies_fits.py` -- `autolens_workspace/scripts/guides/results/aggregator/samples_via_aggregator.py` -- `autolens_workspace/scripts/guides/results/workflow/csv_make.py` -- `autolens_workspace_test/scripts/imaging/convolution.py` - -## Required work - -1. Run each parent directory in CI order from a clean output tree under the release - profile, then rerun each script independently to document prerequisites. -2. Determine whether failures are library output/aggregator regressions, invalid script - assumptions about sample counts, or missing directory creation for generated files. -3. Fix PyAutoFit when valid completed searches are not discoverable. Otherwise update - scripts minimally while preserving their teaching narrative. -4. Ensure scripts create their own output directories and do not depend on unrelated - earlier legs or developer-local artifacts. -5. Add the narrowest appropriate tests and rerun all seven release-profile scripts. diff --git a/active/aggregator_output_png.md b/active/aggregator_output_png.md deleted file mode 100644 index 7f7d0a81..00000000 --- a/active/aggregator_output_png.md +++ /dev/null @@ -1,30 +0,0 @@ - Title: AggregateImages.output_to_folder — subplots from different source images with different grid sizes produce mismatched panel sizes - - Problem: - - When combining subplots from two different source images that have different grid layouts, the extracted panels end up at different physical sizes in the final composite PNG. - - Concrete example: I'm combining panels from rgb.png (a 2x2 grid) and subplot_fit.png (a 4x3 grid). The RGB panel is extracted at its native size from the 2x2 grid, while the fit panels are extracted from - the larger 4x3 grid. Because each source image has a different total size and grid cell count, the individual panels end up at different pixel dimensions. When they're stitched side-by-side in the output, - the RGB panel appears visibly smaller (roughly half the height/width) compared to the fit panels. - - Expected behaviour: - - All panels in the final composite should appear at the same size, regardless of which source image or grid layout they came from. Either: - - 1. AggregateImages should automatically rescale all extracted panels to a common size before compositing, or - 2. There should be a user-facing parameter (e.g. panel_size=(width, height) or normalize_panel_size=True) that controls this. - - Where to look: - - - AggregateImages class and its output_to_folder method — this is where panels from different source images are stitched together. - - The subplot extraction logic that crops panels from the source PNGs using the Enum grid positions — this is where the panel pixel dimensions diverge because each source image has a different grid. - - The final compositing/concatenation step where extracted panels are placed side-by-side. - - Suggested fix: - - After extracting all panels (from whichever source images), resize them to a common target size before concatenation. The target could be the max height/width across all panels, or user-specified. PIL's - Image.resize() with LANCZOS resampling would preserve quality. - - --- - Want me to open this as a GitHub issue on the autofit repo? \ No newline at end of file diff --git a/active/alma_datacube.md b/active/alma_datacube.md deleted file mode 100644 index ea1965fe..00000000 --- a/active/alma_datacube.md +++ /dev/null @@ -1,221 +0,0 @@ -My acollaborators Aris and Hannah want to be able to do interferometer analysis of ALMA data cubes, which are -basically Interferometer objects but lists of them across channels. - -Heres hannah's initial issue: - -https://github.com/PyAutoLabs/PyAutoPrompt/issues/18 - -Heres our SLACK conversion: - -Jam  [8:43 AM] -If you can open an issue on here describing what you think the code should do using natural language and example python snippets, I can sort that for you: https://github.com/PyAutoLabs/PyAutoPrompt -PyAutoLabs/PyAutoPromptStarting point of the PyAuto workflow: prompt registry and prompt-coupled Claude Code skills.LanguageShellLast updated11 hours agoAdded by GitHubHannah Stacey  [1:34 PM] -Hey @Aris I made an issue in PyAutoPrompt do you have some code as an example for how you are doing the per channel modelling at the moment? -[1:35 PM]https://github.com/PyAutoLabs/PyAutoPrompt/issues/18 -Jam  [1:44 PM] -Ok, put anything in the issue you can, at 7pm I get my next wave of Claude tokens, I'll put it to work on the issue, so prepare to have your mind blown. Any information / context you can put in the issue that you think might help please do, especially an existing python snipper or example :slightly_smiling_face: -Hannah Stacey  [1:49 PM] -I had a go at creating an optical 3D modelling script using Cursor but it didn't really work :sweat_smile: -Aris  [2:00 PM] -I have an idea of how we can make it a bit lot more efficient. - -For a single inteferometer object you will input the visibilities, uv_wavelengths and noise_map (i.e. visibility errors). For a cube you will have a list of interferometer objects each with their own set of visibilities, uv_wavelengths and noise_map. - -The most expensive calculation in the code is, L^T * W_tilde * L, where L is the lensing operator (fixed for a set of lens parameters) and what we call the w_tilde matrix, D^T C^{-1} D, which only depends on uv_wavelengths and noise_map. - -However, for channel-to-channel the uv_wavelengths change very little which makes the fourier transform almost identical. The noise_map does change from channel to channel but unless the emission line is at the edge of a spectral window it shouldnt change that much, so we can also assume that it is the same for all channels. - -So to make things more efficient we can have a list of interferometer objects but only the visibilities will be different for each object and they will all share the same uv_Wavelengths and noise_map. - -So when we compute the likelihood for the cube, all objects have the same L^T * W_tilde * L (which we compute once) and then we solve the linear system for each intereferometer object to compute the source-pixel values. So we end up with a list of reconstructions (this is so much cheaper compared to calculating, L^T * W_tilde * L). - -Finally, the total likelihood is the sum of all likelihoods for each interferometer object. - -Note that the dirty_image of each interferometer is different but that depends on visibilities (as well as uv_wavelengths, noise_map which we assume are the same) (edited)  -Hannah Stacey  [2:01 PM] -is there a reason why to have a list of interferometer objects instead of having like a boolean parameter to decide whether to use the channel information or collapse it? -[2:02 PM]or even, this would simplify things more, to just have an input uvfits that contains all of this information in a single file? -Aris  [2:03 PM] -a lot of things depend on the structure of the dataset object, e.g. all calculations of the likelihood. If we were to create a new type of dataset, i.e. a 3D cube, then we will have to do a big restructure -Hannah Stacey  [2:03 PM] -would you really? or can it just be interpreted as multi-band? -Aris  [2:04 PM] -I am not that familiar with the details of the multi-dataset modelling in autolens, but I assume it creates different dataset object (in this case imaging datasets). -Hannah Stacey  [2:04 PM] -wouldn't it make your life easier to have a single uvfits so you don't need to do that extra data preparation? -Aris  [2:05 PM] -the amount of wotk for this extra data prep step is nothing compared to having to re-structure the code to expect 3D arrays instead of 2D -Hannah Stacey  [2:05 PM] -ok -[2:06 PM]but will you then still have to manually input 50 lots of interferometer objects -Aris  [2:06 PM] -I had to do it for the PyLensKin package, but it was simpler because its only parametric source mdoels -[2:08 PM]having said that the the suggestion I gave above where all objects share the same w_tilde matrix might not play well with a single L^T * W_tilde * L operation, cause this does not occur at the dataset level -Hannah Stacey  [2:10 PM] -i see what you're saying about assuming sigma and lambda are the same but i'm not sure i like it.. it seems like it would be better to get it correct from the start. I guess it depends what Claude is capable of. We could try the heavy option and see what Claude does? I guess the whole idea is to reduce the amount of manual labour of restructuring the whole code -[2:10 PM]what do you think? is it worth trying? -[2:11 PM]i'm happy to test it on my side to see if it works -Aris  [2:11 PM] -This operation, L^T * W_tilde * L, can take more than 1min for the highest res dataset. Multiple this by the number of channels you are fitting to get an estimate of a single likelihood evaluaiton -[2:12 PM]on CPUs -[2:13 PM]so if W_tilde doesnt change to a level that affects things then we should defo do what I suggested. -[2:13 PM]if you want your runs to end before the end of this year -Hannah Stacey  [2:17 PM] -hmm well obviously i want things to finish, but at MPA we could do 3D modelling of something like SPT-0418 in a day, and I'm sure that autolens could do just as well. Another argument in favour of the ''long way" is that if you go to lower frequencies the fractional bandwidth is larger and you have more per-channel flagging due to RFI, so the assumption might become a problem. If you ever wanted to do VLBI with autolens it would be necessary (edited)  -[2:18 PM]James what is your opinion -Aris  [2:18 PM] -I think the Dataset3D class should be a list of of datasets still but we can add helper fuctions like from_fits_3D where it will recongnise that it should load arrays of shape (n_channels, n_visibilities, 2). That will solve your issue of data prep -Hannah Stacey  [2:18 PM] -we could also try both ways? -Aris  [2:18 PM] -The problem will be later on in the code -[2:19 PM]where we perform the reconstructions -Jam  [2:19 PM] -I will have the first pass at claude do it via Aris's simplification, because its good to get to a point wheere something runs end-to-end, and it sounds like doing it the "proper" way isnt much more than making it so that the list of interferometer objects each have their own uv_wavelengrhs and call their NUFFT indepedently, which will be an easy Claude follow up issue. - -The GPU code is 50x faster than the CPU code Aris is used to, I think, so I am not worried about run times. but its good to break the problem down into smaller steps and get ech one running with Claude rather than do everything at once -Hannah Stacey  [2:21 PM] -ok maybe we try that then - we can suggest it a from_fits_3d helper that Aris suggested (maybe also a from_uvfits_3d for visibilities?) then use the simpler approach, see if that works -Aris  [2:22 PM] -ok, so who will make the .md with all that? -Jam  [2:23 PM] -The hardest part is gonna be going from where we are now (lots of context / descriptions but no Python code) to a working example. So I'll instruct claude to get us to the simpler case Aris describes. Once we're there I think the more advanced stuff Claude will be able to do it without much guidance. -Hannah Stacey  [2:23 PM] -either you have to tell me what to do or someone else does it :sweat_smile: -Jam  [2:23 PM] -I will just copy and paste this SLACK chat into Claude and I think we'll get there. If you have any Python code or snippets I think that's the last bit of context we need. -[2:24 PM]doesnt need to be end-to-end Python but a few instrutive snippets on the interface at these key points -Aris  [2:38 PM] -class Interferometer3D: - - def __init__( - self, - data: list, - noise_map: VisibilitiesNoiseMap, - uv_wavelengths: np.ndarray, - real_space_mask: Mask2D, - transformer_class=TransformerNUFFT, - sparse_operator: Optional[InterferometerSparseOperator] = None, - raise_error_dft_visibilities_limit: bool = True, - ): - pass - - list_of_interferometers = [] - for i in range(uv_wavelengths.shape[0]): - list_of_interferometers.append( - Interferometer( - data: list, - noise_map: list, - uv_wavelengths: list, - real_space_mask: Mask2D, - transformer_class=TransformerNUFFT, - ) - ) - - self.transformer = transformer_class( - uv_wavelengths=uv_wavelengths, - real_space_mask=real_space_mask, - ) - - def from_fits_3D( - data_path, - noise_map_path, - uv_wavelengths_path, - real_space_mask, - visibilities_hdu=0, - noise_map_hdu=0, - uv_wavelengths_hdu=0, - transformer_class=TransformerNUFFT, - ): - - - visibilities = ndarray_via_fits_from( - file_path=data_path, - hdu=visibilities_hdu - ) - if is_3D(visibilities): - list_of_visibilities = [ - Visibilities(visibilities[i, :, :]) - for i in range(visibilities.shape[0]) - ] - else: - raise NotImplementedError() - - noise_map = ndarray_via_fits_from( - file_path=noise_map_path, - hdu=noise_map_hdu - ) - if is_3D(noise_map): - noise_map_mean = np.mean(noise_map, axis=0) # NOTE: NOT SURE THIS IS THE BEST WAY - - - uv_wavelengths = ndarray_via_fits_from( - file_path=uv_wavelengths_path, - hdu=uv_wavelengths_hdu - ) - if is_3D(uv_wavelengths): - uv_wavelengths_mean = np.mean(uv_wavelengths, axis=0) - - Interferometer3D( - visibilities=list_of_visibilities, - noise_map=VisibilitiesNoiseMap(noise_map) - uv_wavelengths=uv_wavelengths_mean, - ) - - def is_3D(array): - if len(array.shape) == 3: - return True - else: - return FalseInterferomter3D could be something like this -Jam  [2:39 PM] -Cool, I think I've got enough. Final question, is there any reason we need Interferometer3D rather than just a list of Interferometer objects? It doesnt feel to me like we need it to be its own Python class -[2:40 PM]I guess its because you want the Inversion to reuse the NUFFT once across all channels, and the list dataset API wouldnt do that naturally. Ok -Hannah Stacey  [2:41 PM] -Maybe this is more a general computing question, but is there any memory advantage to a list of nxm arrays as opposed to an nxmxl array -Jam  [2:42 PM] -Ok, I'll prob actually get the slower implementation working which doesnt reuse NUFFT and we can build from there. Thats a question for Claude at this point -Aris  [2:43 PM] -that would be the simpler thing to implement by far. You will jsut reuse existing functionality for everything -Aris  [2:44 PM] -i guess start testing from there, have a feeling for run times, and then we move to the sligtly more complex implementation -Hannah Stacey  [2:45 PM] -Maybe you could eventually have a boolean switch to choose which version to use - -Here are a few threads: - -Hannah Stacey  [2:41 PM] -Maybe this is more a general computing question, but is there any memory advantage to a list of nxm arrays as opposed to an nxmxl array -Jam  [2:47 PM] -No, the primary motivation of using lists is that many of the Python objects which do the computation (e.g. AnalysisInterferometer, FitInterferometer can naturally be iterated over a lists, and so we can set this up reusing a lot of code. - -I think most Astronomers would of started by defining a datacube as a 3d ndarray (x, y, channel) but this would mean most the downstream code needs specific functionality to h andle the third dimension. - -In terms of memory, JAX will convert all this to special array types before modeling begins so it doesnt matter what Python objects we use - -Aris, Hannah Stacey Aris and youAris  [2:44 PM] -i guess start testing from there, have a feeling for run times, and then we move to the sligtly more complex implementation -Jam  [2:45 PM] -Yeah exactly, it wont be hard to implmenet your speed up but better to have Claude do it from a point where things are stable and working - -Aris, Hannah Stacey Aris, Hannah Stacey, and youAris  [2:18 PM] -I think the Dataset3D class should be a list of of datasets still but we can add helper fuctions like from_fits_3D where it will recongnise that it should load arrays of shape (n_channels, n_visibilities, 2). That will solve your issue of data prep -Hannah Stacey  [2:19 PM] -yeah that could work -Jam  [2:20 PM] -Yeah I think we'll end up with a Dataset3D object like this, which reuses all the FitInterferomter / AnalysisInterferometer objects internally so avoid code restructuring / redevelopment -Aris  [2:21 PM] -perhaps we can have like a preload fitting class where if the operation, L^T * W_tilde * L, is performed once then it is reused for all channels. - -This will require the least development and potential to breakt he code - -Also work a read through of @autolens_workspace/scritps/interferometer/features/pixelization/likelihood_function.py for a step-by-step guide about what we're doing. - - -My take is the following: - -1) We will end up with lists of Interferometer objects, one for each cube, prioritizing the computationally-expensive but implmenetation-simple approach of doing a NUFFT for each channel initially. -2) We will do all initial work on autolens_workspace_developer/datacube, and work through to integration workspaces from there. -3) We first want a good, representative autolens_workspace_developer/datacube, followed by the kind of step-by-step JAX likelihood fucntion we have in scripts like autolens_workspace_developer/jax_profiling/interferometer. -4) We will then make some autolens_workspace scripts, primnarily a simulator.py and modeling.py. - -Obviously this is a pretty huge feature and thus do deep research before you give me a plan. Think critically about design, but work towards getting the simplest (but could be slower) implementation going first. Make absolutely certain resuing all code ande existing API makes sense over making bespoke source code (e.g. Datacube3D object,AnalysisDataCube3D class, etc.) \ No newline at end of file diff --git a/active/alma_interferometer_support.md b/active/alma_interferometer_support.md deleted file mode 100644 index 7444a2b2..00000000 --- a/active/alma_interferometer_support.md +++ /dev/null @@ -1,285 +0,0 @@ -# ALMA interferometer support in PyAutoReduce - -Type: feature -Target: PyAutoReduce -Difficulty: medium -Autonomy: safe -Priority: normal -Status: formalised - -Original request (verbatim): - -Can we do ALMA in PyAutoReduce, noting that here I have lots of feedback and direction from a user who does ALMA modeling we can follow so a lot less research should be required (but do a bit). - -## Slack conversation with Aris (ALMA modeler, 2026-07-09) - -Aris [12:20 PM]: yeah I dont mind. -[12:21 PM] but just so you know one doesnt need to run the data reduction pipeline anymore, you can now download the reduced data from the ALMA archive (you used to download the raw and run the pipeline yourself) -[12:22 PM] what my custom codes do is take the calibrated data and extract visibilities, uv_wavelengths, etc... in a format that autolens wants -[12:22 PM] this is what I can send you - -Jam [12:22 PM]: yeah i will tell claude to set up both in PyAutoReduce with download the default - -Aris [12:31 PM]: This is an example script that reads .ms.split.cal - -ms -> this stands for measurement set. This is how ALMA data are delivered. - -If multiple execution blocks are carried out (either on the same day or different days - think of it as different exposures), you get multiple measurement sets. These have different names (or rather ids). In this example there are 2: - -uids = ["A002_Xb9b1b9_X3046", "A002_Xb99cbd_X2456"] - -Then for each measurement set you get 4 spectral windows (spw). You can extract all of them or however many you like (one spw might have an emission line so in this case you disregard it). In this example only two spw are extracted: - -spws = ["1", "2"] - -Then you select how many channels in each spw to collapse. For continuum modelling I usually collapse the whole spw (different spw might have different number of channels - for emission lines the observer usually chooses more channels). The parameter that controls how many channels are collapsed is width. In this example width = 240. - -Finally, main_func(uid=uid, field="G09v1.40", spws=spws, width=width, directory=".", clean=False) takes all these inputs and outputs visibilities per uid per spw (which will have shape [2, Nvis, 2], the first 2 is the polarization). - -autolens wants visibilities and uv_wavelengths in shape (Nvis, 2). You can concatenate them accordingly before feeding into autolens. - -[12:32 PM] This script runs in CASA -[12:32 PM] so on the terminal type casa and a new window pops up. There you can do execute("name_of_the_file") -[12:33 PM] you can probably write a python script and have python open and execute in a CASA env in the background, but I have never managed to make this work - -## Aris's example script (main_2016.1.00282.S_G09v1.40.py, runs inside CASA) - -```python -import os, sys -import numpy as np - -try: - from astropy import ( - units, - constants, - ) - from astropy.io import fits - astropy_is_imported = True -except: - astropy_is_imported = False - - -def getcol_wrapper(ms, table, colname): - if os.path.isdir(ms): - tb.open("{}/{}".format(ms, table)) - col = np.squeeze(tb.getcol(colname)) - tb.close() - else: - raise IOError("{} does not exist".format(ms)) - return col - - -def get_num_chan(ms): - return getcol_wrapper(ms=ms, table="SPECTRAL_WINDOW", colname="NUM_CHAN") - - -def get_spw_ids(ms): - return getcol_wrapper(ms=ms, table="DATA_DESCRIPTION", colname="SPECTRAL_WINDOW_ID") - - -def get_visibilities(ms): - if os.path.isdir(ms): - data = getcol_wrapper(ms=ms, table="", colname="DATA") - else: - raise IOError("{} does not exist".format(ms)) - visibilities = np.stack(arrays=(data.real, data.imag), axis=-1) - return visibilities - - -def export_visibilities(ms, filename): - if os.path.isfile(filename): - print("{} already exists".format(filename)) - else: - visibilities = get_visibilities(ms=ms) - print("shape (visibilities):", visibilities.shape) - if astropy_is_imported: - fits.writeto(filename=filename + ".fits", data=visibilities, overwrite=True) - else: - with open(filename + ".numpy", 'wb') as file: - np.save(file, visibilities) - - -def convert_array_to_wavelengths(array, frequency): - if astropy_is_imported: - array_converted = ((array * units.m) * (frequency * units.Hz) / constants.c).decompose().value - else: - array_converted = array * frequency / 299792458.0 - return array_converted - - -def get_uv_wavelengths(ms): - if os.path.isdir(ms): - uvw = getcol_wrapper(ms=ms, table="", colname="UVW") - else: - raise IOError("{} does not exist".format(ms)) - - chan_freq = getcol_wrapper(ms=ms, table="SPECTRAL_WINDOW", colname="CHAN_FREQ") - - chan_freq_shape = np.shape(chan_freq) - if np.shape(chan_freq): - u_wavelengths, v_wavelengths = np.zeros(shape=(2, chan_freq_shape[0], uvw.shape[1])) - for i in range(chan_freq_shape[0]): - u_wavelengths[i, :] = convert_array_to_wavelengths(array=uvw[0, :], frequency=chan_freq[i]) - v_wavelengths[i, :] = convert_array_to_wavelengths(array=uvw[1, :], frequency=chan_freq[i]) - else: - u_wavelengths = convert_array_to_wavelengths(array=uvw[0, :], frequency=chan_freq) - v_wavelengths = convert_array_to_wavelengths(array=uvw[1, :], frequency=chan_freq) - uv_wavelengths = np.stack(arrays=(u_wavelengths, v_wavelengths), axis=-1) - return uv_wavelengths - - -def export_uv_wavelengths(ms, filename): - if os.path.isfile(filename): - print("{} already exists".format(filename)) - else: - uv_wavelengths = get_uv_wavelengths(ms=ms) - print("shape (uv_wavelengths):", uv_wavelengths.shape) - if astropy_is_imported: - fits.writeto(filename=filename + ".fits", data=uv_wavelengths, overwrite=True) - else: - with open(filename + ".numpy", 'wb') as file: - np.save(file, uv_wavelengths) - - -def get_frequencies(uid, field, spw): - ms = "{}_field_{}_spw_{}.ms.split.cal".format(uid, field, spw) - if os.path.isdir(ms): - chan_freq = getcol_wrapper(ms=ms, table="SPECTRAL_WINDOW", colname="CHAN_FREQ") - else: - raise IOError("The directory {} does not exist".format(ms)) - return chan_freq - - -def export_frequencies(uid, field, spw): - chan_freq = get_frequencies(uid=uid, field=field, spw=spw) - filename = "./{}_spw_{}_frequencies".format(uid, spw) - if astropy_is_imported: - fits.writeto(filename="{}.fits".format(filename), data=chan_freq) - else: - with open("{}.numpy".format(filename), 'wb') as file: - np.save(file, chan_freq) - - -def get_antennas(ms): - antenna1 = getcol_wrapper(ms=ms, table="", colname="ANTENNA1") - antenna2 = getcol_wrapper(ms=ms, table="", colname="ANTENNA2") - return np.array([antenna1, antenna2]) - - -def export_antennas(ms, filename): - if not os.path.isdir(ms): - raise IOError("The ms does not exist.") - antennas = get_antennas(ms=ms) - print("shape (antennas):", antennas.shape) - if astropy_is_imported: - filename += ".fits" - else: - filename += ".numpy" - if filename.endswith(".fits"): - fits.writeto(filename=filename, data=antennas, overwrite=True) - else: - with open(filename, 'wb') as file: - np.save(file, antennas) - - -def get_time(ms): - time = getcol_wrapper(ms=ms, table="", colname="TIME") - return np.asarray(time) - - -def export_time(ms, filename): - time = get_time(ms=ms) - if astropy_is_imported: - filename += ".fits" - else: - filename += ".numpy" - if filename.endswith(".fits"): - fits.writeto(filename=filename, data=time, overwrite=True) - else: - with open(filename, 'wb') as file: - np.save(file, time) - - -def get_scans(ms): - scans = getcol_wrapper(ms=ms, table="", colname="SCAN_NUMBER") - return np.asarray(scans) - - -def export_scans(ms, filename): - scans = get_scans(ms=ms) - if astropy_is_imported: - filename += ".fits" - else: - filename += ".numpy" - if filename.endswith(".fits"): - fits.writeto(filename=filename, data=scans, overwrite=True) - else: - with open(filename, 'wb') as file: - np.save(file, scans) - - -def main_func(uid, field, spws, width, directory=".", clean=False): - if not os.path.isdir("{}/uid___{}.ms.split.cal".format(directory, uid)): - raise IOError("The ms does not exist.") - - # split out the target field if not already done - if not os.path.isdir("{}/uid___{}_{}.ms.split.cal".format(directory, uid, field)): - split( - vis="{}/uid___{}.ms.split.cal".format(directory, uid), - outputvis="{}/uid___{}_{}.ms.split.cal".format(directory, uid, field), - keepmms=True, - field=field, - spw="", - datacolumn="data", - keepflags=False, - ) - - for spw in spws: - # split per spw, averaging channels by `width` - if not os.path.isdir("{}/uid___{}_{}_spw_{}_width_{}.ms.split.cal".format(directory, uid, field, spw, width)): - split( - vis="{}/uid___{}_{}.ms.split.cal".format(directory, uid, field), - outputvis="{}/uid___{}_{}_spw_{}_width_{}.ms.split.cal".format(directory, uid, field, spw, width), - keepmms=True, - field=field, - spw=spw, - datacolumn="data", - width=width, - keepflags=False, - ) - - ms_split = "{}/uid___{}_{}_spw_{}_width_{}.ms.split.cal".format(directory, uid, field, spw, width) - - filename_uv_wavelengths = "{}/uv_wavelengths_{}_{}_spw_{}_width_{}".format(directory, uid, field, spw, width) - if not (os.path.isfile(filename_uv_wavelengths + ".fits") or os.path.isfile(filename_uv_wavelengths + ".numpy")): - export_uv_wavelengths(ms=ms_split, filename=filename_uv_wavelengths) - - filename_visibilities = "{}/visibilities_{}_{}_spw_{}_width_{}".format(directory, uid, field, spw, width) - if not (os.path.isfile(filename_visibilities + ".fits") or os.path.isfile(filename_visibilities + ".numpy")): - export_visibilities(ms=ms_split, filename=filename_visibilities) - - export_antennas(ms=ms_split, filename="{}/antennas_{}_{}_spw_{}_width_{}".format(directory, uid, field, spw, width)) - export_scans(ms=ms_split, filename="{}/scans_{}_{}_spw_{}_width_{}".format(directory, uid, field, spw, width)) - - if clean: - pass - - -uids = ["A002_Xb9b1b9_X3046", "A002_Xb99cbd_X2456"] -spws = ["1", "2"] -width = 240 -for uid in uids: - main_func(uid=uid, field="G09v1.40", spws=spws, width=width, directory=".", clean=False) -``` - -## Key facts distilled from the conversation - -- Modern workflow: download already-calibrated/reduced data from the ALMA archive (no need to run the ALMA reduction pipeline locally). Download-first should be the default in PyAutoReduce; running extraction on locally-provided measurement sets is the second path. -- ALMA data are delivered as measurement sets (`.ms.split.cal` directories); one per execution block (uid), each typically with 4 spectral windows (spw). -- Extraction pipeline per (uid, spw): CASA `split` to isolate field, then `split` again per spw with channel averaging (`width`; for continuum modelling collapse the whole spw), then read MS tables (`DATA`, `UVW`, `SPECTRAL_WINDOW/CHAN_FREQ`, `ANTENNA1/2`, `TIME`, `SCAN_NUMBER`) via the `tb` tool. -- Outputs per uid per spw: visibilities shape [2, Nvis, 2] (leading 2 = polarizations), uv_wavelengths (UVW meters -> wavelengths via chan_freq/c), antennas, scans, frequencies, times. -- PyAutoLens wants visibilities and uv_wavelengths in shape (Nvis, 2) — polarizations averaged/concatenated across uids and spws before feeding into autolens. -- The script must run inside CASA (`tb`, `split` are CASA globals). Aris runs it via `casa` then `execute("file")`; a python-driven headless CASA invocation should be possible (e.g. modular casatools/casatasks pip packages, or `casa --nogui -c script.py`) — Aris never got this working, worth solving in PyAutoReduce. -- Aris can send further scripts/data; example project 2016.1.00282.S, field G09v1.40. - - diff --git a/active/analysis_shared_state_cross_factor.md b/active/analysis_shared_state_cross_factor.md deleted file mode 100644 index 7af301c8..00000000 --- a/active/analysis_shared_state_cross_factor.md +++ /dev/null @@ -1,271 +0,0 @@ -# Cross-`Analysis` shared-state mechanism for `FactorGraphModel` - -A large new PyAutoFit feature: let the per-factor `Analysis` objects in a -`FactorGraphModel` share **per-evaluation, model-dependent precomputed state** -across each other, so that work which is identical for every factor at a given -point in parameter space is computed **once** and reused by all factors — -instead of every factor recomputing it independently. - -Primary repo: **@PyAutoFit** (the mechanism). Consumer/proof: **@PyAutoLens** + -**@autolens_workspace** (the ALMA datacube likelihood that motivates it). - -## Hard constraint (read first) - -**PyAutoFit must not depend on PyAutoArray / PyAutoGalaxy / PyAutoLens** (see -`PyAutoFit/CLAUDE.md` — "PyAutoFit does NOT depend on..."). Therefore: - -- The mechanism PyAutoFit ships must be **completely domain-agnostic**: it knows - nothing about lensing, inversions, mappers, or visibilities. It only knows - "factors may want to compute a shared object once per evaluation and have all - factors see it." -- All lensing-specific logic (what to share, how to build the mapper once, how - each channel consumes it) lives in **PyAutoLens** (the `AnalysisInterferometer` - side) and is wired up in **autolens_workspace** datacube scripts. - -So the deliverable is a *generic shared-state protocol* in PyAutoFit plus a -*lensing consumer* in PyAutoLens that proves it on the datacube. - -## Existing hooks to build on (do not reinvent) - -PyAutoFit already has two precedents for injecting state into an `Analysis` -around a fit — study both before designing: - -1. **`EPAnalysisFactor`** (`autofit/graphical/declarative/factor/analysis.py:257+`) - attaches a per-iteration `_cavity_mean_field` onto its wrapped `Analysis` - immediately before optimisation, so the user's `log_likelihood_function` can - read shared cross-factor messages. This is *exactly* the shape of mechanism we - want — state computed at the graph level and attached to each factor's - Analysis — except EP attaches it once per EP outer-iteration, whereas the - datacube needs it recomputed **once per likelihood evaluation** (the lens - parameters change every sample). - -2. **`Analysis.modify_before_fit`** (`autofit/non_linear/analysis/analysis.py:320`) - is the existing per-`Analysis` pre-fit hook. It is per-analysis and runs once - before sampling, so it cannot host per-evaluation shared state, but its - docstring ("alter the `Analysis` in ways that can speed up the fitting") is - the precedent for "precompute-then-reuse" and the new hook should read as its - per-evaluation, cross-factor sibling. - -## Design (to refine in the issue, present options) - -The core need: at each call to `FactorGraphModel.log_likelihood_function(instance)`, -**before** the per-factor loop, optionally compute a shared object from the -instance, then make it available to every factor's `log_likelihood_function`. - -Sketch of the target loop in `collection.py`: - -``` -def log_likelihood_function(self, instance): - shared = self.compute_shared(instance) # None unless a shared-state provider is set - log_likelihood = 0 - for model_factor, instance_ in zip(self.model_factors, instance): - log_likelihood += model_factor.log_likelihood_function(instance_, shared=shared) - return shared_aware_sum(...) -``` - -Design questions the issue must resolve (present 2-3 concrete options, pick one): - -1. **Who computes the shared object?** Options: - - A `shared_state_provider` callable/object set on the `FactorGraphModel` - (domain-agnostic: it takes the instance, returns an opaque object). - - A designated "lead" factor whose Analysis exposes a - `compute_shared(instance)` method; remaining factors receive its output. - - A new optional `Analysis.shared_state_from(instance)` protocol method - (default returns `None`) so any Analysis can opt in. - Favour whichever keeps PyAutoFit domain-blind and makes the lensing side a - thin consumer. - -2. **How does a factor receive it?** Options: - - New optional kwarg `log_likelihood_function(self, instance, shared=None)` - with a default so every existing Analysis keeps working unchanged - (back-compat is mandatory — hundreds of Analyses exist). - - Attribute injection like `EPAnalysisFactor` (`analysis._shared_state = ...`) - set/cleared around the loop. - The kwarg is cleaner and JIT-friendlier; the attribute path matches the EP - precedent. Decide explicitly and justify. - -3. **JAX / pytree correctness.** The datacube path is JIT-compiled - (`use_jax=True`, `register_model` pytrees). The shared object will contain - traced arrays (mapper triplets, mapping matrix, curvature). It must: - - be threadable through `jax.jit` as a normal pytree (no Python-side caching - that cache-busts — see `feedback_jax_closure_cache_busts`); - - be recomputed inside the jitted region each eval (it depends on the traced - lens parameters), not memoised across evals on the instance; - - not break the single-factor / non-cube path (shared is `None` → identical - behaviour and identical numbers). - -4. **Correctness + ordering.** The shared object is only valid when the relevant - parameters really are shared across factors. The mechanism must not silently - produce wrong likelihoods if a user wires up factors whose "shared" inputs - actually differ. Decide whether to (a) trust the provider, (b) assert - structural equality of the relevant sub-instance across factors, or (c) - document the contract and leave it to the consumer. Note the physical caveat - from `alma_datacube.md`: sharing is only valid when `uv_wavelengths` and - `noise_map` are ~channel-invariant (narrow-emission-line regime); outside it, - the consumer must fall back to per-factor compute. - -## Why this is needed (the motivating problem) - -The ALMA **datacube** likelihood (autolens_workspace#120 and its roadmap, all -shipped: see `complete.md` "datacube roadmap") fits an N-channel spectral cube -as **N independent `AnalysisInterferometer` objects sharing one lens model**, -wired together with `af.FactorGraphModel`. The FactorGraph routes the shared -lens parameters to every per-channel `AnalysisInterferometer.log_likelihood_function` -and sums the results: - -``` -# autofit/graphical/declarative/collection.py:89-107 -def log_likelihood_function(self, instance): - log_likelihood = 0 - for model_factor, instance_ in zip(self.model_factors, instance): - log_likelihood += model_factor.log_likelihood_function(instance_) - return log_likelihood -``` - -`AnalysisFactor` just forwards to the wrapped analysis: - -``` -# autofit/graphical/declarative/factor/analysis.py:253-254 -def log_likelihood_function(self, instance): - return self.analysis.log_likelihood_function(instance) -``` - -**The problem:** because the lens model is shared across all channels, a large -fraction of each channel's likelihood is *identical work*. Profiling -(`autolens_profiling/likelihood_breakdown/datacube/delaunay.py`, results in -`autolens_profiling/likelihood_runtime/OPTIMIZATION_NOTES.md`) shows that for a -34-channel cube the step-by-step CPU cost is ~170-205 s/eval, of which: - -- **~78%** is the per-channel "inversion setup" — ray-tracing the shared lens - model, then building the source-plane **mapper** (Delaunay triangulation, - neighbours, pixel weights) and the **mapping matrix L**; -- **~17-19%** is the curvature matrix `F = Lᵀ W̃ L`; -- only **~5%** (data vector `D`, NNLS reconstruction, log-evidence) is genuinely - per-channel (it depends on each channel's distinct visibilities). - -In the **sparse / w̃ inversion route that production actually uses** (this is the -important subtlety — see `PyAutoPrompt/issued/alma_datacube.md` and the -investigation note in `complete.md` about the transformer-free per-likelihood -path), the expensive NUFFT is precomputed once at dataset load, so the -shareable per-eval work is the **traced grids + Delaunay mapper + mapping matrix -L + curvature F** — all pure functions of the shared lens model + shared source -mesh, currently rebuilt N times. The data vector and reconstruction are the only -irreducibly per-channel parts. - -This is "Aris's deferred shared-`Lᵀ W̃ L` optimisation" (autolens_workspace#120). -A decomposition of the dominant inversion-setup step -(`autolens_profiling/likelihood_breakdown/datacube/inversion_setup_decompose.py`, -SMA / CPU, sparse route) confirms **`Lᵀ W̃ L` is exactly the right thing to -share** and sizes the win: - -| inversion-setup sub-step | per-call | shareable? | -|---------------------------------------|----------|------------| -| ray-trace | ~0.001 s | ✅ invariant | -| Delaunay mapper + mapping matrix L | ~0.19 s | ✅ invariant | -| **curvature F = `Lᵀ W̃ L`** | **~1.57 s** | ✅ invariant | -| data vector D = `Lᵀ·dirty_image` | ~0.06 s | ❌ per-channel | - -So **~97% of the per-channel inversion work is channel-invariant**, and the -curvature `F` alone is **~86% of it** — not the mapper as an earlier hypothesis -assumed. Sharing the invariant block collapses the per-channel inversion total -from `N × ~1.81 s` to `~1.81 s + (N-1) × ~0.06 s` — roughly a **17× reduction on -the inversion-setup block** for a 34-channel cube (≈60 s → ≈3.5 s). The -remaining per-channel cost is just the `Lᵀ·dirty_image` matmul + NNLS + log-ev. - -(Absolute seconds are SMA-scale on a contended laptop CPU and provisional — the -*ratios* are the robust deliverable; re-measure at ALMA scale on a quiet A100 to -pin the cube-level number. The old `shared_lwl_savings_estimate ≈ 17%` field in -the breakdown JSON under-counts because it credits `F` against the full ~170 s -cube rather than against the inversion-setup block `F` actually dominates.) - -**The blocker is purely architectural, and it lives in PyAutoFit, not in -PyAutoLens.** As the design note in `PyAutoPrompt/autoarray/datacube.md` states: - -> "The problem here is the analysis list API does not currently share -> information across likelihood functions or analysis objects. We therefore -> either need to make a DataCube data class, Inversion object and add bespoke -> source code, or we need to have AnalysisCombined objects be able to share -> information in their likelihood functions." - -This prompt is the **second, general** option: give `FactorGraphModel` a way for -its factors to share per-evaluation state. The bespoke-`DataCube`-class option is -explicitly *not* what we want — it would solve only lensing cubes and bake -domain logic into a one-off path. - - -## Plan - -### Phase 1 — PyAutoFit: the generic mechanism -- Add the shared-state protocol (chosen option from Design Q1/Q2) to - `FactorGraphModel` (`collection.py`) and `AnalysisFactor` - (`declarative/factor/analysis.py`), with a default that is a no-op so all - existing graphs are byte-for-byte unchanged. -- Add the opt-in surface to `Analysis` (`non_linear/analysis/analysis.py`) — - default `shared_state_from(instance) -> None` (or equivalent), mirroring the - `modify_before_fit` precedent. -- Thread `shared=` through `log_likelihood_function` signatures with a defaulted - kwarg; keep `EPAnalysisFactor` working. -- Unit tests in `test_autofit/graphical/`: a 3-factor mock graph where the - shared object is a counter proving `compute_shared` runs **once** per eval (not - N times), the sum is correct, and a graph with no provider is unchanged. - -### Phase 2 — PyAutoLens: the datacube consumer -- On the interferometer datacube path, implement the lensing-specific - `compute_shared`: ray-trace the shared lens model once, build the Delaunay - mapper + mapping matrix L (and, where `uv`/`noise` are channel-invariant, the - curvature `F`) once, and hand it to every channel's - `AnalysisInterferometer.log_likelihood_function` to consume in place of its own - rebuild. -- Per-channel work that remains: data vector `D` (channel visibilities), - NNLS reconstruction, log-evidence. -- Fall back to the current per-channel path when the shared-invariance precondition - doesn't hold. - -### Phase 3 — autolens_workspace + profiling -- Update the datacube modeling/likelihood scripts to opt into the shared path. -- Re-run `autolens_profiling/likelihood_breakdown/datacube/delaunay.py` (which now - carries the inversion-setup sub-decomposition as a permanent step) and record - the new cube cost. Per the decomposition above, ~97% of the per-channel - inversion work is shareable, so the inversion-setup block should drop ~17× for - a 34-channel cube (≈60 s → ≈3.5 s); the cube total drops from ~170 s toward the - per-channel residual (data-vector matmul + NNLS + log-ev, a few seconds) plus - one shared mapper+L+F build. Compare against the - `inversion_setup_decompose_*.json` artifact for the channel-invariant/variant - split that sets the ceiling. - -## Critical files - -PyAutoFit (modify): -- `autofit/graphical/declarative/collection.py` — `FactorGraphModel.log_likelihood_function`, the per-factor sum loop -- `autofit/graphical/declarative/factor/analysis.py` — `AnalysisFactor.log_likelihood_function`, and the `EPAnalysisFactor` precedent -- `autofit/non_linear/analysis/analysis.py` — `Analysis` base: new opt-in protocol method, `modify_before_fit` sibling -- `test_autofit/graphical/` — new tests - -PyAutoFit (reference, do not modify): -- `EPAnalysisFactor` (`declarative/factor/analysis.py:257+`) — the attach-state-to-analysis precedent -- `autofit/non_linear/analysis/model_analysis.py`, `visualize.py` — other Analysis wrappers that must keep working - -PyAutoLens (consumer, Phase 2): -- the `AnalysisInterferometer` likelihood path + interferometer `Inversion`/mapper construction -- `autolens_workspace/scripts/interferometer/features/datacube/{likelihood_function,modeling,delaunay}.py` - -Profiling (Phase 3): -- `autolens_profiling/likelihood_breakdown/datacube/delaunay.py` -- `autolens_profiling/likelihood_runtime/OPTIMIZATION_NOTES.md` - -## Out of scope -- A bespoke `DataCube` data class / cube-specific `Inversion` (the rejected option). -- The dense-route variant (production uses sparse; dense is not the target). -- Generalising shared state to arbitrary cross-factor *gradients* — likelihood - value only for now. - -## Cross-references -- autolens_workspace#120 — Aris's shared-`Lᵀ W̃ L` optimisation, the origin -- `PyAutoPrompt/autoarray/datacube.md` — the "analysis list API does not share - information" problem statement -- `PyAutoPrompt/issued/alma_datacube.md` — Aris's Slack design + the channel- - invariance caveat (lines 24, 30, 34, 53, 207) -- `complete.md` datacube roadmap entries (Phases 1-4, all shipped) -- the paired decomposition note in `autolens_profiling` splitting the 78% - "inversion setup" block into mapper vs mapping-matrix vs data-vector, which - quantifies the real ceiling of this optimisation diff --git a/active/api_baseline_refresh.md b/active/api_baseline_refresh.md deleted file mode 100644 index 10c861a4..00000000 --- a/active/api_baseline_refresh.md +++ /dev/null @@ -1,28 +0,0 @@ -# Refresh the assistant's pinned API baseline to the released stack - -Type: maintenance -Target: autolens_assistant -Difficulty: medium -Autonomy: supervised -Priority: normal -Status: filed - -The assistant's wiki-currency CI fails its "Version drift (--check-version)" -leg on every branch: the pinned API baseline is behind the released stack -(nightlies live since 2026.7.9.1; drift report says "public API surface -changed: autoarray, autofit, autogalaxy, autolens, autolens.plot"). This is -also Heart's "autolens_assistant: pinned BEHIND installed" YELLOW reason. -Pre-existing — confirmed on assistant-benchmarks (#57/#58) where all other -legs (symbol audit, idioms, provenance, citations) passed. - -Do the al_update_wiki / refresh_api_docs workflow: audit what actually -changed in the released public API, update any stale wiki/core + skills -content it invalidates, regenerate + re-pin the baseline -(`--write-baseline`), and verify `--check-version` exits 0 against the -released stack. Watch for cascades: an API surface change may mean real doc -edits, not just a re-pin. Consider whether the nightly-release cadence needs -this refresh automated post-release (PyAutoBuild already regenerates the -baseline before wiki-currency at release time — investigate why the pin is -still behind despite that ordering). - - diff --git a/active/array2d_native_jit_safety.md b/active/array2d_native_jit_safety.md deleted file mode 100644 index fc45f3a6..00000000 --- a/active/array2d_native_jit_safety.md +++ /dev/null @@ -1,190 +0,0 @@ -# Refactor `Array2D.native` / `array_2d_via_indexes_from` for JAX-jit safety - -`SimulatorImaging.use_jax=True` and `SimulatorInterferometer.use_jax=True` -work end-to-end on **JAX-eager** today (shipped in Phase 2 of -`z_features/jax_user_intro.md` — PRs PyAutoArray#335, #336 + -PyAutoLens#539, #540 + PyAutoGalaxy#442, #443) but **cannot currently -be wrapped in `@jax.jit`**. This task unblocks that. - -## The blocker - -When the user wraps a simulator in their own `@jax.jit`: - -```python -simulator = al.SimulatorImaging(..., use_jax=True) - -@jax.jit -def simulate(tracer): - return simulator.via_tracer_from(tracer=tracer, grid=grid) -``` - -The call fails with `TracerArrayConversionError: __array__() was called -on traced array`. The traceback (full chain captured during Phase 2 PR 2 -implementation): - -``` -File ".../autolens/imaging/simulator.py", line 70, in via_tracer_from -File ".../autoarray/dataset/imaging/simulator.py", line 229, in via_image_from -File ".../autoarray/structures/arrays/uniform_2d.py", line 295, in native -File ".../autoarray/structures/arrays/uniform_2d.py", line 243, in __init__ -File ".../autoarray/structures/arrays/array_2d_util.py", line 147, in convert_array_2d -File ".../autoarray/structures/arrays/array_2d_util.py", line 516, in array_2d_native_from -File ".../autoarray/structures/arrays/array_2d_util.py", line 562, in array_2d_via_indexes_from -``` - -The two failing sites are: - -1. **`array_2d_via_indexes_from`** (`array_2d_util.py:535-562`) — for the - JAX path it does `array.at[tuple(native_index_for_slim_index_2d.T)].set(array_2d_slim)`. - The `tuple(jax_array.T)` call iterates the outermost axis of the JAX - array, which forces `__array__()` and breaks the trace. - -2. **`Array2D.native`** (`uniform_2d.py:295`) — accessed by the simulator - to re-wrap the image against the all-false output mask - (`Array2D(values=image.native, mask=mask)`). Routes through (1). - -3. **`Imaging.trimmed_after_convolution_from`** — same `.native` access on - the post-convolution dataset. - -## Why the simulator workaround in PR 2 doesn't fully solve it - -Phase 2 PR 2 (`PyAutoArray#335`) added a partial workaround at the -simulator's image re-wrap: - -```python -# autoarray/dataset/imaging/simulator.py -if xp is np: - image = Array2D(values=image.native, mask=mask) -else: - image = Array2D(values=image.array, mask=mask) -``` - -That guard skips `.native` for the image re-wrap *inside the simulator*. -But the `.native` access still fires later inside -`Imaging.trimmed_after_convolution_from` (PyAutoLens simulator override -line 70) — and any other consumer of `.native` outside the simulator -also breaks under JIT. - -The full fix is in `array_2d_via_indexes_from` itself. - -## Scope - -**In scope:** -- Refactor `array_2d_via_indexes_from` to use JAX-traceable operations - on the JAX path. The numpy path can stay as indexed assignment; the - JAX path needs an equivalent that doesn't iterate a tuple of native - indices in Python. -- Likely fix: replace - `array.at[tuple(native_index_for_slim_index_2d.T)].set(array_2d_slim)` - with `jnp.zeros(shape).at[native_index_for_slim_index_2d[:, 0], native_index_for_slim_index_2d[:, 1]].set(array_2d_slim)` - — uses 2D advanced indexing rather than tuple-unpacked rows, no Python - iteration of a traced array. -- Verify the change end-to-end: a `SimulatorImaging(use_jax=True)` - call wrapped in `@jax.jit` should now succeed and return an `Imaging` - with `jax.Array` data. -- Remove the temporary `if xp is np: ... else: ...` guard in - `autoarray/dataset/imaging/simulator.py:229` once the underlying - `.native` issue is fixed (it's now redundant). -- Update the `SimulatorImaging.use_jax=True` and `SimulatorInterferometer.use_jax=True` - docstrings to drop the "@jax.jit currently blocked by Array2D.native" - caveat. - -**Out of scope:** -- Other autoarray jit-incompatibilities not in this `array_2d_via_indexes_from` - / `.native` path. If new ones surface during validation, file as - separate prompts. -- Workspace doc updates beyond the simulator docstring caveat removal. - The workspace `__JAX Variant__` blocks already show the user-facing - `@jax.jit` pattern; they'll just start working at runtime once this - ships, no doc changes needed. - -## Implementation steps - -1. **Identify all `.native` use sites that fire under JIT.** Grep - PyAutoArray for `array_2d_via_indexes_from`, `array_2d_native_from`, - `.native`. Map which fire during a typical - `SimulatorImaging.via_tracer_from(use_jax=True)` call. - -2. **Refactor `array_2d_via_indexes_from`** in - `PyAutoArray/autoarray/structures/arrays/array_2d_util.py`: - - ```python - def array_2d_via_indexes_from(array_2d_slim, shape, native_index_for_slim_index_2d, xp=np): - array = xp.zeros(shape, dtype=array_2d_slim.dtype) - if xp.__name__.startswith("jax"): - # 2D advanced indexing — no Python iteration of the index tuple. - return array.at[native_index_for_slim_index_2d[:, 0], - native_index_for_slim_index_2d[:, 1]].set(array_2d_slim) - array[tuple(native_index_for_slim_index_2d.T)] = array_2d_slim - return array - ``` - -3. **Audit other slim/native helpers** for the same pattern - (`native_index_for_slim_index_2d_from`, `array_2d_slim_from`, - `array_2d_via_mask_from`, etc.). Fix any that use Python iteration - of traced arrays. - -4. **Remove the simulator temporary guard** in - `autoarray/dataset/imaging/simulator.py:229`: - ```python - # Before (Phase 2 PR 2 workaround): - if xp is np: - image = Array2D(values=image.native, mask=mask) - else: - image = Array2D(values=image.array, mask=mask) - - # After (now redundant): - image = Array2D(values=image.native, mask=mask) - ``` - -5. **Update docstrings** on `SimulatorImaging.__init__` and - `SimulatorInterferometer.__init__` to drop the - "Note: @jax.jit wrapping is currently blocked by Array2D.native ..." - caveat. - -6. **Tests:** - - Library unit tests stay NumPy-only per [[feedback_no_jax_in_unit_tests]]. - - Add a workspace_test parity script that wraps `SimulatorImaging(use_jax=True).via_tracer_from` - in `@jax.jit` and asserts the JIT'd dataset matches the eager-JAX - dataset to atol=1e-8 (extend the existing - `autolens_workspace_test/scripts/imaging/simulator_use_jax_parity.py` - — its disabled `@jax.jit` test block is currently a `print` saying - "currently blocked"; restore the active test). - - Same for interferometer: extend `autolens_workspace_test/scripts/interferometer/simulator_use_jax_parity.py`. - -7. **Workspace doc cleanup:** the `__JAX Variant__` blocks in - `autolens_workspace/scripts/{imaging,interferometer,group}/simulator.py` - and the `autogalaxy_workspace/scripts/{imaging,interferometer}/simulator.py` - each carry a "@jax.jit wrap is currently blocked by Array2D.native" - note. Sweep them and drop those notes. - -## Validation - -After all changes: - -1. `SimulatorImaging(use_jax=True)` + `@jax.jit` works end-to-end on a - typical lens (verified via the workspace_test parity script). -2. `SimulatorInterferometer(use_jax=True)` + `@jax.jit` works - end-to-end (verified via the analogous parity script). -3. Full PyAutoArray + PyAutoLens + PyAutoGalaxy test suites pass. -4. The simulator `__JAX Variant__` blocks in autolens / autogalaxy - workspace `simulator.py` scripts now run cleanly under `@jax.jit`. - -## References - -- Phase 2 PR 2 — `PyAutoArray#335` — added the temporary simulator guard - this task removes. -- Phase 0 design doc — `admin_jammy/notes/jax_interface.md` — the - end-state of `Simulator.use_jax=True` was originally meant to support - `@jax.jit` from the start; this refactor delivers on that. -- Phase 3a / 3b / 4a / 4b workspace PRs (autolens#203, autogalaxy#101) - flagged the `.native` limitation in each `__JAX Variant__` block. - Those notes need cleanup as step 7. - -## Out-of-band notes - -- This is the **only** known remaining structural JAX-compatibility gap - in the simulator path. PointSolver already works under `@jax.jit` - (it doesn't go through `.native`). Once this ships, the entire - "JIT-it-yourself" image-simulation story is functional, not - aspirational. diff --git a/active/assertions_fix.md b/active/assertions_fix.md deleted file mode 100755 index 8d8952b5..00000000 --- a/active/assertions_fix.md +++ /dev/null @@ -1,6 +0,0 @@ -Assertions are broken, which can be demonstrated by running the code @autofit_workspace_test/scripts/feature/assertion.py - -Assertions are defined in the autofit source code @PyAutoFit/autofit/mapper/prior/arithmetic/assertion.py. - -Inspect and compare both these files and then work on a way to fix the bug. Can you give me a plan of how you will do -this? \ No newline at end of file diff --git a/active/assistant_benchmarks.md b/active/assistant_benchmarks.md deleted file mode 100644 index 38720eb6..00000000 --- a/active/assistant_benchmarks.md +++ /dev/null @@ -1,44 +0,0 @@ -# Built-in benchmark package for autolens_assistant (4 standard prompts + run/track harness) - -Type: feature -Target: autolens_assistant -Difficulty: too-large -Autonomy: supervised -Priority: normal -Status: formalised - -for autolens_assistant, I now want to create in built bench marks, which have the goal of being standard prompts which we run using different AI agents and models to test performance. I want 3 prompts for assistant mode, the first is "easy" difficulty and simply does tasks already available in the workspace should be based on this prompt in README.md: Model the JWST imaging in dataset/imaging/cosmos_web_ring: perform data preparation steps, set up a sensible lens light and mass model with a pixelized source reconstruction, run the fit, and show me the reconstructed source and the fit residuals, the next is medium difficulty, it requires doing things that are not explicitly in the workspace (e.g. model comparison, changing mass profile for DM subhalo) and again is based on a README.md prompt: - -Assistant mode. - -The strong lens SLACS0946+1006 famously has a dark matter subhalo detection that many argue is unusually concentrated. I'd like to analyse the HST imaging of this lens provided at dataset/imaging/slacs0946+1006/ and reproduce that detection. - -Specifically, I want this analysis to perform Bayesian model comparison to (a) confirm a subhalo is preferred over a smooth-mass baseline by fitting a free-position, free-mass SIS perturber across the image plane and comparing the Bayesian evidence to the no-subhalo fit, and (b) test the "super-concentrated" claim by comparing the SIS subhalo against a more shallow NFW mass profile at the recovered position. - -Set the pipeline up so the smooth lens light and mass model, the pixelized source reconstruction, and the subhalo results are all inspectable on my computer, and report the Bayesian evidence for each comparison. - -Assess whether the analysis will run fast on my laptop / PC GPU, and if not, set this up as a small project on the HPC I have access to. - -The third should be hard mode and is a new prompt not on the README.md, with the goal that it requires us to combine 3 different packages on the autolens_workspace: group, multi, imaging and interferometer, here is the prompt: - -Assistant mode. - -First, I want to simulate imaging and interferometer data of a group-scale strong lens, which is composed of two SIE lens galaxies and a quadruply imaged Cored Sersic background source. - -Then, I want to perform modeling of this dataset, simultaneously fitting the imaging and interferometer data. I want the foreground lens model to use multi Gaussian Expansions for the lens light, SIE's for each lens and a multi Gaussian expansion for the background source. - -After this fit has been judged successful, do a follow up lens model that uses a pixelized source reconstruction, but retains the MGE lens light and SIE source. - -Present me with results confirming the fit was a success. - -I also want you to make one benchmark based on the Teacher mode example prompt: - -Teacher mode. - -I'm new to PyAutoLens and want to learn the basic workflow end-to-end. Can you walk me through it on a simple simulated example: simulate Euclid-like imaging of a simple strong lens (an isothermal mass with a Sersic source), then fit that simulated data and recover the lens model. - -Explain what each step is doing and why as we go: composing the lens and source model, running the simulation, choosing the mask, the non-linear search, and how to read the result. So I come away understanding the workflow, not just the commands. - -Put this in a benchmark package and have a design whereby I can run these benchmarks, track the conversation and results and store them so they can be pushed to GitHub as well as be run and tracked for different models and run with the same model to compare performance on different days. Think about if there is anything else benchmarking would benefit from and put this all in the clone agent too, e.g. make sure it's aware of it. - - diff --git a/active/autobrain.md b/active/autobrain.md deleted file mode 100644 index f962de30..00000000 --- a/active/autobrain.md +++ /dev/null @@ -1,69 +0,0 @@ -Rename the repository from **PyAutoAgent** to **PyAutoBrain**. - -## Context - -The PyAuto ecosystem is evolving into a software organism. - -Current architecture: - -* PyAutoMind — intent and goals. -* PyAutoBrain — reasoning, planning and orchestration. -* PyAutoHands — execution. -* PyAutoHeart — health monitoring. -* PyAutoMemory — long-term knowledge. - -This repository is responsible for reasoning. - -It performs: - -* planning -* decomposition -* orchestration -* agent coordination -* decision making - -It does **not** directly perform software execution. - -Execution belongs to PyAutoHands. - -The Brain determines how work should be performed. - -The Hands perform the work. - -## Task - -Perform a repository-wide rename from PyAutoAgent to PyAutoBrain. - -Update: - -* documentation -* README -* scripts -* workflows -* references -* package names (if present) -* architecture documents - -Throughout the documentation, replace descriptions centred around "agents" with descriptions centred around reasoning and planning where appropriate. - -Clarify the architectural boundary: - -Mind -→ decides what should be done. - -Brain -→ figures out how. - -Hands -→ performs the work. - -Heart -→ determines whether the organism is healthy. - -Maintain backwards compatibility wherever practical. - -Run all available validation. - -Create a single PR titled: - -Rename PyAutoAgent to PyAutoBrain diff --git a/active/autobuild_bash.md b/active/autobuild_bash.md deleted file mode 100644 index b7c364fd..00000000 --- a/active/autobuild_bash.md +++ /dev/null @@ -1,8 +0,0 @@ -There are lots of tools building in autobuild which I think are basically just bash scripts, or which could be -made into bash scripts without too much effort. Some may be skills which could be turned into bash. -I currently run autobuild via claude, but having both options seems logical. - -Furthemore, it'd be good to have an autobuild-help alias which shows all the options of bash scripts with docs of -what they do. - -Do this! \ No newline at end of file diff --git a/active/autofit_assistant_birth.md b/active/autofit_assistant_birth.md deleted file mode 100644 index 4002c472..00000000 --- a/active/autofit_assistant_birth.md +++ /dev/null @@ -1,153 +0,0 @@ -# autofit_assistant — birth the generic inference assistant - -Type: feature -Target: autofit_assistant -Difficulty: too-large -Autonomy: supervised -Priority: normal -Status: formalised - -## Original request (verbatim) - -autolens_assistant is proving to be very good, and is now excelling at various science cases. - -It is time to make autofit_assistant, noting that this has the following differences: - -- Autolens is tied to a specific scientific domain (lensing), whereas autofit_assistant is a tool to help someone perform inference in their own specific scientific domain. That means, when someone begins using autofit_assistant, one of their first tasks is probably going to be also training or adapting it to their scientific domain. This probably includes paper ingestion, manually providing code with a likelihood function (ideally) and the model composition. - -- AutoFit assistant would benefit from a wiki on all the core statistics concepts it uses during inference albeit many are probably there from the general foundation model. Nevertheless, the EP wiki we made would be valuable here, probably some stuff specific to each source code sampler, maybe stuff on priors, have a think. - -- like the autolens_assistant pairs to the autolens_workspace via skills, we want to do the exact same with the autofit assistant. - -- All the core features of the autolens_assistant (making a science project, open data repository design, benchmarks, assistant and teacher mode, HPC link, etc) should be kept and designed suitable for autofit. - -## Design - -### What it is - -`autofit_assistant` is the PyAutoFit AI Assistant: a generic, public agent workspace -following the autolens_assistant pattern (AGENTS.md canonical + skills + wiki + -science-project machinery), paired to **PyAutoFit + autofit_workspace**. - -The defining inversion versus autolens_assistant: the lensing assistant ships with its -scientific domain built in; the autofit assistant's user **brings their own domain**. -Domain adaptation is therefore a first-class onboarding product, not an afterthought — -the assistant's first job with a new user is to *become* their domain assistant. - -### Relationship to existing machinery - -- **Clone v1** (Brain#78 / Build#135 / Heart#56) already proved a lightweight seed can - regenerate the skeleton in one command (134 files, clean `al_→af_` substitution, - 341-entry PENDING queue). Per the 2026-07-10 decision the maintainer authors the real - assistant from this prompt rather than accepting a clone birth — but the clone plan - JSON and its derived prefix mapping are the authoritative checklist of what is - mechanical vs. what needs genuine re-authoring. Decide at plan time whether Phase 0 - uses the seed as scaffold or hand-authors against its file list. -- **Repo creation is an interactive gate**: a dedicated question naming the repo - (`PyAutoLabs/autofit_assistant`) and visibility must precede `gh repo create`. - Private-first, public flip only after the Heart `newborn_validation.md` checklist. -- **Absorbs** `research/autofit_assistant/autofit_assistant_planning.md` (the planning - anchor holding the PyAutoMemory migration notes). Its sequencing blocker — the EP - framework-review write-ups — landed 2026-07-10, so this prompt is unblocked. Retire - the research anchor when this ships. - -### Pillar A — domain adaptation as the first-run experience (the differentiator) - -- A domain-onboarding flow (extend `start-new-project` or a dedicated - `af_adapt_to_domain` skill) that interviews the user and drives three adaptation - channels: - 1. **Paper ingestion** — adapt `al_ingest_paper`: papers from the user's field grow a - `wiki/literature/` sub-wiki in their clone (autolens ships this full; autofit ships - it near-empty by design, with the schema/AGENTS.md so it grows well). - 2. **Likelihood wrapping (the ideal path)** — `af_wrap_likelihood`: user supplies - existing code with a likelihood function; the skill produces an `Analysis` class - around it, with the standard traps documented (data/noise conventions, `log_likelihood_function` - contract, JAX-compatibility triage). - 3. **Model composition** — `af_compose_model`: turn the user's parametrisation into - `af.Model`/`af.Collection` with priors, linking to the priors wiki pages. -- Adaptation output lands in `wiki/project/` (profile.md + domain journal), mirroring - how the lens assistant calibrates depth per user. - -### Pillar B — core statistics/inference wiki - -`wiki/core/concepts/` for the statistics the assistant leans on during inference. -Foundation-model knowledge covers the generic textbook layer; these pages earn their -place by being **PyAutoFit-specific** (what the implementation actually does, its knobs, -its pitfalls): - -- Model composition & priors (prior types, prior design pitfalls, `sigma=0` point-mass - idiom, latent variables). -- Non-linear search overview + **one page per shipped sampler family** (dynesty, - nautilus, ultranest, emcee, zeus, PySwarms, optimizers, …) — match the installed - PyAutoFit roster at write time, not memory. -- Nested sampling, MCMC/HMC, initialization & search chaining, sampler benchmarks. -- **Graphical models & EP** — migrate the EP write-up per the migration notes. -- Evidence, model comparison, samples/posteriors, aggregator/result analysis. - -Seeding: generalised public rewrites of `PyAutoMemory/methods_wiki/` pages -(expectation-propagation, nested-sampling, sampler-benchmarks, hamiltonian-monte-carlo, -gpu-nested-sampling, initialization-chaining, bayesian-inference, samplers source -notes). PyAutoMemory is personal — never referenced from the public repo; originals -stay there as the private superset. `wiki/core/stack/` shrinks to autoconf + autofit. - -### Pillar C — workspace pairing via skills - -Exactly the autolens_assistant↔autolens_workspace mechanism, retargeted at -**autofit_workspace** (`scripts/{overview,cookbooks,features,searches,model,plot,simulators}`): - -- Port the `autoassistant/` package (audit_skill_apis + API gate hook, refresh_api_docs, - to_notebook, literature `--check-citations` incl. the fifth wiki-currency leg, - benchmark runner) with `af_` naming and `sources.yaml` pointing at - PyAutoFit/autoconf/autofit_workspace clones @ main. -- Candidate `af_*` skill set (grounded against workspace scripts at build time): - setup_environment, compose_model, configure_search, run_search, chain_searches, - load_results (aggregator), custom_analysis / wrap_likelihood, simulate_dataset, - plot_fit, debug_fit_failure, graphical_ep, hierarchical_inference, - sensitivity_mapping, ingest_paper, adapt_to_domain, to_notebook, update_wiki, - audit_skill_apis, refresh_api_docs; plus project-workflow skills - (start-new-project, contribute-upstream) and the `_style.md`/`_bootstrap_skill.md` - meta-skills. - -### Pillar D — ported core features, generalised for inference - -- **Modes**: assistant / teacher / maintainer. Teacher mode anchors to **HowToFit** - chapters (as the lens teacher anchors to HowToLens). -- **Benchmarks**: `benchmarks/` prompts+runs+RESULTS with the test-enforced card↔README - parity; inference-flavoured cards (compose a model, wrap a supplied likelihood, run - and interpret a search, EP on a graph, debug a broken fit). -- **Science project / open-data repository design**: keep the shareable-project - template, redesigned for user-supplied data — dataset conventions must be - domain-neutral (the user's data format is part of onboarding, not assumed). -- **HPC link**: `hpc/` batch templates + sync, CPU-first with the GPU path where the - user's likelihood supports JAX. -- **Worked demo projects** (analogue of cosmos_web_ring / slacs0946): open decision — - the canonical 1D Gaussian plus at least one *real* non-lensing case that exercises - likelihood-wrapping end-to-end. Choose at plan time. -- Infrastructure parity: llms.txt, config/, Makefile, activate.sh, live-visual flag - where applicable, smoke CI via the proven reusable family workflow, firewall/policy - + url_fixups riders, safety invariants adapted (the real-data-inspection gate - generalises to "plot/inspect the user's dataset before first fit"). - -### Phasing (split at start_dev; each phase its own issue/PR cycle) - -- **Phase 0 — birth + skeleton**: interactive repo gate; AGENTS/CLAUDE/modes/config/ - Makefile/CI; safety invariants; empty wiki scaffolding. (Clone seed as scaffold vs - hand-author: decide here.) -- **Phase 1 — workspace pairing**: `autoassistant` tooling ported + the first tranche - of `af_*` skills grounded in autofit_workspace, API audit green. -- **Phase 2 — core wiki**: statistics concepts + per-sampler pages + EP/graphical - migration from methods_wiki (public rewrites). -- **Phase 3 — domain adaptation**: adapt_to_domain / ingest_paper / wrap_likelihood / - compose_model flow + start-new-project + worked demo project(s). -- **Phase 4 — parity & publish**: benchmarks, teacher mode, HPC link, llms.txt, - citation/currency CI, Heart newborn checklist → public flip. - -### Open design decisions (resolve during planning, with the user) - -1. Worked demo project choice (what plays the role of cosmos_web_ring). -2. Sampler roster for dedicated wiki pages (audit installed PyAutoFit). -3. Clone-seed scaffold vs. full hand-authoring for Phase 0. -4. Whether the shareable science-project template lives in-repo or as a separate - PyAutoScientist-family template repo. - - diff --git a/active/autofit_assistant_planning.md b/active/autofit_assistant_planning.md deleted file mode 100644 index 126ec2d3..00000000 --- a/active/autofit_assistant_planning.md +++ /dev/null @@ -1,55 +0,0 @@ -# autofit_assistant planning — generic inference wiki + content migration - -Type: research -Target: autofit_assistant -Difficulty: medium -Autonomy: supervised -Priority: normal -Status: formalised - -## Intent - -An `autofit_assistant` repo will ultimately be created, following the -`autolens_assistant` pattern: a generic, public, stripped-of-personal-content -template with its own wiki. This prompt is the planning anchor for it, and -holds migration notes until the repo exists. - -## Migration notes (content that moves to the generic wiki when it is made) - -The following inference-methods content currently lives in the personal -`PyAutoMemory/methods_wiki/` and should be migrated (as public, generalised -rewrites — PyAutoMemory itself is personal and must never be referenced from -public repos): - -1. **Expectation propagation** — the - `methods_wiki/concepts/expectation-propagation.md` page being produced by - Phase 1 of `research/graphical_ep/ep_framework_review.md` (formal EP - equations as PyAutoFit implements them, moment matching, damping, - pitfalls/findings). -2. **Searches / samplers wiki stuff**: - - `methods_wiki/concepts/nested-sampling.md` - - `methods_wiki/concepts/sampler-benchmarks.md` - - `methods_wiki/concepts/hamiltonian-monte-carlo.md` - - `methods_wiki/concepts/gpu-nested-sampling.md` - - `methods_wiki/concepts/initialization-chaining.md` - - `methods_wiki/sources/samplers.md` (source notes; strip personal - reading-log framing) - - related general pages as judged at migration time - (`concepts/bayesian-inference.md`, `sources/bayesian-inference.md`, - `sources/probabilistic-programming.md`) - -Migration means: rewrite for a generic PyAutoFit user (no personal project -context, no PyAutoMemory links), keep the personal originals in PyAutoMemory -as the private superset. - -## Planning scope (when this prompt is picked up) - -- Decide repo layout by cloning the `autolens_assistant` template structure - (`AGENTS.md`, `skills/`, `wiki/core|literature|project`). -- Decide which PyAutoFit demos ship as the assistant's worked examples - (analogue of cosmos_web_ring / slacs0946 in autolens_assistant). -- Define the wiki's core page set: model composition, priors, searches, - graphical/EP, result analysis. -- Sequence: do not start until the EP framework review - (`research/graphical_ep/ep_framework_review.md`) has produced the Phase 1/2 - write-ups, since those seed the wiki's inference pages. diff --git a/active/autofit_assistant_wiki_currency_wiring.md b/active/autofit_assistant_wiki_currency_wiring.md deleted file mode 100644 index 1e531380..00000000 --- a/active/autofit_assistant_wiki_currency_wiring.md +++ /dev/null @@ -1,12 +0,0 @@ -# PyAutoBuild: wire autofit_assistant wiki-currency into release.yml - -Type: feature -Target: PyAutoBuild -Difficulty: easy -Autonomy: safe -Priority: normal -Status: formalised - -Wire the autofit_assistant wiki-currency check into PyAutoBuild's release workflow, exactly as autolens_assistant is wired today: at stack-release time release.yml invokes the assistant's reusable wiki-currency.yml via workflow_call (passing the new stack_version and assistant_ref: main), and a dependent if: failure() job downloads the wiki-drift-report artifact and opens a "wiki drift" issue against PyAutoLabs/autofit_assistant. PyAutoBuild only orchestrates and reports — it holds no copy of the rules (the workflow in autofit_assistant is the single home of the checks). The assistant's workflow already exists with the workflow_call trigger (autofit_assistant PR #3); this task is the PyAutoBuild side only. - - diff --git a/active/autofit_workspace_plot_update.md b/active/autofit_workspace_plot_update.md deleted file mode 100755 index c1c59ab4..00000000 --- a/active/autofit_workspace_plot_update.md +++ /dev/null @@ -1,5 +0,0 @@ -The following issue implements a plot interface udpate https://github.com/rhayes777/PyAutoFit/pull/1174 - -This never got implemented in @autofit_workspace, so we just need to apply the API updates there. - -Do a quick check locally that indeed the old API is still present. \ No newline at end of file diff --git a/active/autofit_workspace_test_database_scrape_paths.md b/active/autofit_workspace_test_database_scrape_paths.md deleted file mode 100644 index 402aa96c..00000000 --- a/active/autofit_workspace_test_database_scrape_paths.md +++ /dev/null @@ -1,24 +0,0 @@ -# Fix autofit workspace-test database scrape paths - -Original user request: - -> continue - -Release report context: - -The PyAutoBuild release run reports two failures in `autofit_workspace_test`: - -- `scripts/database/scrape/grid_search.py` -- `scripts/database/scrape/sensitivity.py` - -Both fail at `assert len(agg) > 0` after `Aggregator.from_database(...).add_directory(...)`. - -Reproduction on current `main` using the PyAutoBuild environment: - -```bash -(cd autofit_workspace_test && env PYAUTO_TEST_MODE=2 PYAUTO_SMALL_DATASETS=1 PYAUTO_DISABLE_JAX=1 PYAUTO_FAST_PLOTS=1 JAX_ENABLE_X64=True NUMBA_CACHE_DIR=/tmp/numba_cache MPLCONFIGDIR=/tmp/matplotlib python3 scripts/database/scrape/grid_search.py) -``` - -The search writes results under `output/test_mode/database/scrape/...`, but the scrape scripts add `output/database/scrape/...`, so the aggregator finds zero search outputs. - -Fix the workspace-test scripts so they scrape the actual output path used by the active PyAutoBuild/test-mode environment while preserving normal non-test-mode behavior. diff --git a/active/autogalaxy_extra_galaxies_audit.md b/active/autogalaxy_extra_galaxies_audit.md deleted file mode 100644 index 4015367d..00000000 --- a/active/autogalaxy_extra_galaxies_audit.md +++ /dev/null @@ -1,80 +0,0 @@ -# Audit autogalaxy_workspace extra_galaxies feature, port modern API where stale - -## Background - -`feature/scaling-relation-update` (issue -[autolens_workspace#141](https://github.com/PyAutoLabs/autolens_workspace/issues/141)) refreshed -the autolens scaling-relation examples to use the modern API: - -- MGE bulge via `al.model_util.mge_model_from(...)` instead of `Sersic` / `SersicSph` -- Centres loaded from `extra_galaxies_centres.json` via `al.from_json` (and an additional - `scaling_galaxies_centres.json` for the group three-tier example) -- Final model collection structured as - `af.Collection(galaxies=..., extra_galaxies=..., scaling_galaxies=...)` - -A quick directory audit (2026-05-10) shows: - -- `autogalaxy_workspace/scripts/imaging/features/extra_galaxies/` — exists, with `modeling.py` + - `simulator.py` + `README.md`. Uses `Sersic` / `Exponential` light profiles in the modeling - example header. Need to verify whether the API has been kept in lockstep with the autolens - version. -- `autogalaxy_workspace/scripts/imaging/features/scaling_relation/` — **does not exist**. - -## Goals - -1. **Audit** the existing `autogalaxy_workspace` `extra_galaxies` example against the freshly-updated - `autolens_workspace` version. In particular check: - - Whether it uses `ag.model_util.mge_model_from` or still uses `Sersic` profiles in the lens light - - Whether centres are loaded via `ag.from_json` / `Grid2DIrregular` from a `*_centres.json`, or - hardcoded in Python - - Whether the final model uses `extra_galaxies=...` as a top-level collection key - - If the autogalaxy version has drifted (older API), port the same modernisation that landed in - the autolens example. - -2. **Decide** whether a `scaling_relation` feature belongs in autogalaxy_workspace at all. - - Autogalaxy is **light-only** — there is no lensing, so the `einstein_radius = scaling_factor * - luminosity ** scaling_exponent` relation does not transfer directly. A scaling relation could - still apply to other quantities (e.g. tying the `effective_radius` or `intensity` of extra - galaxies to a measured stellar-mass proxy), but whether that is a useful tutorial is a judgement - call — confirm with the user before porting. - - If we decide a scaling_relation example **does** make sense in autogalaxy: - - Add `scripts/imaging/features/scaling_relation/{__init__.py, simulator.py, modeling.py}` mirroring - the autolens structure - - The relation ties some light-profile quantity (e.g. `intensity = scaling_factor * - luminosity ** scaling_exponent`, or stellar-mass proxy) — settle on what it ties before - writing - - Use `ag.lp_linear` / MGE for the light models - - If a light-only scaling-relation tutorial is **not** useful: - - Document why in the autogalaxy `extra_galaxies/README.md` (one paragraph: "scaling - relations apply to mass, not light, so the autolens version of this feature is not mirrored - here") - - Cross-link to the autolens example for users who arrive looking for it. - -## Files likely to change - -- `autogalaxy_workspace/scripts/imaging/features/extra_galaxies/modeling.py` (modernise if drifted) -- `autogalaxy_workspace/scripts/imaging/features/extra_galaxies/simulator.py` (only if API drift - forces simulator changes — usually the simulator is fine) -- `autogalaxy_workspace/scripts/imaging/features/extra_galaxies/README.md` (cross-link or "not - applicable" note) -- (optional) `autogalaxy_workspace/scripts/imaging/features/scaling_relation/{__init__,simulator, - modeling}.py` if the answer to goal 2 is "yes, port it" - -## Reference reads - -- `autolens_workspace/scripts/imaging/features/scaling_relation/modeling.py` (post issue #141) — the - modernised pattern to port -- `autolens_workspace/scripts/imaging/features/extra_galaxies/modeling.py` — autolens version of - the same feature, baseline for the autogalaxy comparison -- `autogalaxy_workspace/scripts/imaging/features/extra_galaxies/modeling.py` — current state in the - autogalaxy workspace - -## Out of scope - -- HowToGalaxy tutorial updates (separate repo, separate task) -- Interferometer / multi-wavelength variants — start with imaging only -- CSV-driven loading — covered by the parallel `scaling_relation_csv_loader.md` follow-up diff --git a/active/autogalaxy_workspace_test.md b/active/autogalaxy_workspace_test.md deleted file mode 100644 index eb25ab42..00000000 --- a/active/autogalaxy_workspace_test.md +++ /dev/null @@ -1,13 +0,0 @@ -The autogalaxy_workspace_test is very light compared to autolens_workspacd_test in terms of its script -and it does not have a .github actions continuous integration. Therefore, plan the following work as a series -of prompts in this folder, run them one after another as seaprate agent tasks: - -- Set up contiuous integration server on autogalaxy_workspace_test -- Set up folder `scripts/model_composition` and set up autogalaxy version of existing scripts. -- Set up folder `scripts/jax_likelihood_functions/imaging` and set up autogalaxy versions of all examples (dont do dspl). This will likely require some spawn-off tasks which do pytree registration so make sure this prompt looks at our recent pytree work). -- Do same for `scripts/jax_likelihood_functions/interferometer`. -- Do same for `scripts/jax_likelihood_functions/multi`. -- Set up folder `scripts/jax_grad/imaging` and set up autogalaxy versions of all examples (dont do dspl). This will likely require some spawn-off tasks which do pytree registration so make sure this prompt looks at our recent pytree work). -- Do same for `scripts/jax_grad/interferometer`. -- Do same for `scripts/jax_grad/multi`. -- Set up a `scripts/imaging` folder like autolens_workspace_test's, but only include model_fit.py, modeling_visualization_jit.py, visualization.py, visualization_jax.py \ No newline at end of file diff --git a/active/autogalaxy_workspace_test_ci.md b/active/autogalaxy_workspace_test_ci.md deleted file mode 100644 index 15727ccc..00000000 --- a/active/autogalaxy_workspace_test_ci.md +++ /dev/null @@ -1,47 +0,0 @@ -Set up a GitHub Actions smoke-test workflow on @autogalaxy_workspace_test, mirroring the existing -setup in @autolens_workspace_test. This is the **blocker** for all sibling tasks in this folder — -once CI is green, the eight script-expansion tasks can land and be verified on every PR. - -__Reference layout__ - -@autolens_workspace_test/.github/workflows/smoke_tests.yml and -@autolens_workspace_test/.github/scripts/run_smoke.py are the templates to copy. Supporting files -already present in that repo: - -- `smoke_tests.txt` — list of scripts to run -- `config/build/env_vars.yaml` — per-script env overrides (test mode, small-dataset flag, JAX on/off) - -@autogalaxy_workspace_test already has `smoke_tests.txt` at the repo root (currently empty of all -but aggregator entries). It does **not** yet have `.github/`, `.github/scripts/run_smoke.py`, or -`config/build/env_vars.yaml`. - -__Deliverables__ - -1. `autogalaxy_workspace_test/.github/workflows/smoke_tests.yml` — fork the autolens version but - **drop the PyAutoLens checkout + install step**. The matrix should check out - PyAutoConf, PyAutoFit, PyAutoArray, PyAutoGalaxy only. Keep the branch-matching logic, the - Python 3.12 / 3.13 split, the numba install on 3.13, the tensorflow-probability pin, and the - Slack-on-failure webhook (same channel). -2. `autogalaxy_workspace_test/.github/scripts/run_smoke.py` — copy verbatim from autolens - (it is workspace-agnostic; it reads `smoke_tests.txt` and `config/build/env_vars.yaml` relative - to itself). -3. `autogalaxy_workspace_test/config/build/env_vars.yaml` — start with the autolens defaults - (`PYAUTOFIT_TEST_MODE=2`, `PYAUTO_WORKSPACE_SMALL_DATASETS=1`, `PYAUTO_DISABLE_JAX=1`, - `PYAUTO_FAST_PLOTS=1`, `JAX_ENABLE_X64=True`, `NUMBA_CACHE_DIR`, `MPLCONFIGDIR`) and the - `jax_likelihood_functions/` override that unsets the small-dataset / disable-JAX flags. - Drop any autolens-specific overrides (e.g. `database/scrape/`) unless they already apply. -4. Seed `smoke_tests.txt` with only scripts that exist **today** in autogalaxy_workspace_test - (aggregator entries). The eight sibling tasks will append their new scripts as they ship. -5. PR includes a CI run showing the aggregator suite passing on both Python 3.12 and 3.13. - -__Notes__ - -- Use LF line endings. All files in this repo are Unix LF — see repo CLAUDE.md. -- The smoke-test env var `PYAUTO_DISABLE_JAX=1` is honoured by `Analysis.__init__` in PyAutoFit - (see the `smoke-test-optimization` work in active.md for context). JAX-likelihood scripts opt - back in via the `jax_likelihood_functions/` override. - -__Umbrella issue__ - -This is task 1/9 in the `expand autogalaxy_workspace_test coverage` epic — track progress under -the umbrella issue on `PyAutoLabs/autogalaxy_workspace_test`. diff --git a/active/autogalaxy_workspace_test_imaging.md b/active/autogalaxy_workspace_test_imaging.md deleted file mode 100644 index 58ad66d6..00000000 --- a/active/autogalaxy_workspace_test_imaging.md +++ /dev/null @@ -1,53 +0,0 @@ -Create `scripts/imaging/` in @autogalaxy_workspace_test with autogalaxy versions of a **subset** -of the autolens_workspace_test imaging scripts. Per the user's directive, only port four scripts: - -- `model_fit.py` -- `modeling_visualization_jit.py` -- `visualization.py` -- `visualization_jax.py` - -Do **not** port `convolution.py`, `modeling_visualization_jit_delaunay.py`, -`modeling_visualization_jit_rectangular.py`, or the full `simulator/`, `config/`, `config_source/`, -`images/` trees unless individual scripts fail without them. - -__Reference__ - -@autolens_workspace_test/scripts/imaging/ - -Strip lens/source split — use `ag.Galaxy` + `ag.Galaxies` + `ag.ImagingAnalysis`. The -`visualization*.py` scripts exercise the plotting API and are mostly mechanical renames -(`al.*` → `ag.*`, remove tracer-specific plots). - -__Scripts__ - -1. `imaging/model_fit.py` — end-to-end model fit on a small imaging dataset. Use the same - `PYAUTOFIT_TEST_MODE=2` flow as the autolens version. -2. `imaging/modeling_visualization_jit.py` — exercises `analysis.fit_for_visualization` under - `jax.jit`. Depends on PyAutoGalaxy pytree registration (task 3). If - `linear_light_profile_intensity_dict_pytree` is needed (autogalaxy side), spawn the library - fix first. -3. `imaging/visualization.py` — exercises the autogalaxy plotter API end-to-end. NumPy only. -4. `imaging/visualization_jax.py` — same, under JAX. - -__Dataset / config__ - -Reuse an existing autogalaxy imaging dataset (check `autogalaxy_workspace/dataset/imaging/`). Add -a small `config/` directory at `scripts/imaging/config/` only if the default autogalaxy config -doesn't suffice. - -__Deliverables__ - -1. `autogalaxy_workspace_test/scripts/imaging/__init__.py` -2. The four scripts above. -3. Appended to `smoke_tests.txt`. -4. Any PyAutoGalaxy library PRs for missing pytree registration (likely already covered by - task 3; spawn off if surfacing here). - -__Depends on__ - -Task 3 (PyAutoGalaxy imaging pytree registration). `model_fit.py` and `visualization.py` can run -without it (NumPy path), but `modeling_visualization_jit.py` and `visualization_jax.py` cannot. - -__Umbrella issue__ - -Task 9/9. Track under the epic issue on `PyAutoLabs/autogalaxy_workspace_test`. diff --git a/active/autogalaxy_workspace_test_jax_grad_imaging.md b/active/autogalaxy_workspace_test_jax_grad_imaging.md deleted file mode 100644 index c00f8461..00000000 --- a/active/autogalaxy_workspace_test_jax_grad_imaging.md +++ /dev/null @@ -1,56 +0,0 @@ -Create `scripts/jax_grad/imaging/` in @autogalaxy_workspace_test exercising `jax.grad` on the -autogalaxy imaging likelihood path. - -__Important — layout divergence from autolens__ - -@autolens_workspace_test/scripts/jax_grad/ currently has files **at the top level** -(`imaging_lp.py`, `imaging_mge.py`) — there is no `jax_grad/imaging/` subfolder. The user's -prompt for this epic explicitly asks for `jax_grad/imaging/`, `jax_grad/interferometer/`, -`jax_grad/multi/` subfolders on autogalaxy. Options: - -1. Follow the user's directive — use subfolders on autogalaxy even though autolens is flat. - Flag this drift in the PR description and ask the user whether to retrofit autolens. -2. Ask the user before creating the subfolder. - -Recommended: go with (1) — the subfolder layout matches `jax_likelihood_functions/` and is more -extensible. Surface the autolens-retrofit question in the PR body. - -__Scripts to port__ - -From autolens top-level `jax_grad/`: - -- `imaging_lp.py` → `jax_grad/imaging/lp.py` -- `imaging_mge.py` → `jax_grad/imaging/mge.py` - -Add any further variants (rectangular, delaunay, …) only if the corresponding -`jax_likelihood_functions/imaging/` port from task 3 uncovered a usable `grad`-ready path. -Default: match autolens's coverage one-for-one and leave extras to follow-ups. - -**Skip**: anything `*_dspl.py`. - -__Pytree prerequisite__ - -Shares the same `_register_fit_imaging_pytrees` requirement as task 3. By this point the -registration should already be landed on PyAutoGalaxy — if not, block on that first. - -__`jax.grad` contract__ - -Each script prints the gradient vector of a scalar log-likelihood w.r.t. the parameter vector and -asserts it's finite and the shape matches the model's free parameter count. Follow the autolens -reference exactly for the assertions. - -__Deliverables__ - -1. `autogalaxy_workspace_test/scripts/jax_grad/__init__.py` -2. `autogalaxy_workspace_test/scripts/jax_grad/imaging/__init__.py` -3. Ported scripts. -4. Appended to `smoke_tests.txt`. -5. PR body includes the autolens-retrofit question in `## Notes`. - -__Depends on__ - -Task 3 (pytree registration on PyAutoGalaxy imaging analysis). - -__Umbrella issue__ - -Task 6/9. Track under the epic issue on `PyAutoLabs/autogalaxy_workspace_test`. diff --git a/active/autogalaxy_workspace_test_jax_grad_interferometer.md b/active/autogalaxy_workspace_test_jax_grad_interferometer.md deleted file mode 100644 index 09188af7..00000000 --- a/active/autogalaxy_workspace_test_jax_grad_interferometer.md +++ /dev/null @@ -1,45 +0,0 @@ -Create `scripts/jax_grad/interferometer/` in @autogalaxy_workspace_test exercising `jax.grad` on -the autogalaxy interferometer likelihood path. - -__Layout note__ - -Same subfolder-vs-flat divergence from autolens as task 6 — autogalaxy uses -`jax_grad/interferometer/`. Flag in PR body; don't retrofit autolens without user go-ahead. - -__Scripts__ - -autolens has **no** interferometer `jax_grad` scripts today. This task creates them from scratch -for autogalaxy, using the corresponding `jax_likelihood_functions/interferometer/` scripts as -templates for model + dataset setup, then wrapping the likelihood in `jax.grad`. - -Minimum coverage: - -- `jax_grad/interferometer/lp.py` -- `jax_grad/interferometer/mge.py` - -Additional variants only if the `jax_likelihood_functions/interferometer/` task surfaced a -ready-to-grad path. - -**Skip**: `*_dspl.py`. - -__Pytree prerequisite__ - -Task 4 must have landed (`AnalysisInterferometer` pytree registration on PyAutoGalaxy). - -__`jax.grad` contract__ - -Same as task 6 — finite gradient, correct free-parameter shape. - -__Deliverables__ - -1. `autogalaxy_workspace_test/scripts/jax_grad/interferometer/__init__.py` -2. Scripts above. -3. Appended to `smoke_tests.txt`. - -__Depends on__ - -Task 4 (pytree registration on PyAutoGalaxy interferometer analysis). - -__Umbrella issue__ - -Task 7/9. Track under the epic issue on `PyAutoLabs/autogalaxy_workspace_test`. diff --git a/active/autogalaxy_workspace_test_jax_grad_multi.md b/active/autogalaxy_workspace_test_jax_grad_multi.md deleted file mode 100644 index a4ec4a4e..00000000 --- a/active/autogalaxy_workspace_test_jax_grad_multi.md +++ /dev/null @@ -1,43 +0,0 @@ -Create `scripts/jax_grad/multi/` in @autogalaxy_workspace_test exercising `jax.grad` on the -autogalaxy multi-dataset likelihood path. - -__Layout note__ - -Same subfolder-vs-flat divergence as tasks 6 and 7. Flag in PR body. - -__Scripts__ - -autolens has no multi `jax_grad` scripts today. Create from scratch using the -`jax_likelihood_functions/multi/` templates (task 5). - -Minimum coverage: - -- `jax_grad/multi/lp.py` -- `jax_grad/multi/mge.py` - -Additional variants only if feasible. - -**Skip**: `*_dspl.py`. - -__Pytree prerequisite__ - -Task 5 scaffolding (multi-dataset factor-graph pytree registration) must be complete. - -__`jax.grad` contract__ - -Gradient is taken over the full parameter vector spanning all datasets. Assert finite and the -shape matches the combined free-parameter count. - -__Deliverables__ - -1. `autogalaxy_workspace_test/scripts/jax_grad/multi/__init__.py` -2. Scripts above. -3. Appended to `smoke_tests.txt`. - -__Depends on__ - -Task 5 (multi-dataset pytree registration). - -__Umbrella issue__ - -Task 8/9. Track under the epic issue on `PyAutoLabs/autogalaxy_workspace_test`. diff --git a/active/autogalaxy_workspace_test_jax_likelihood_imaging.md b/active/autogalaxy_workspace_test_jax_likelihood_imaging.md deleted file mode 100644 index 9144d425..00000000 --- a/active/autogalaxy_workspace_test_jax_likelihood_imaging.md +++ /dev/null @@ -1,65 +0,0 @@ -Create `scripts/jax_likelihood_functions/imaging/` in @autogalaxy_workspace_test with autogalaxy -ports of every autolens JAX-likelihood imaging script, **excluding** the `*_dspl.py` double-source- -plane variants (lens-specific, no autogalaxy analogue). - -__Scripts to port__ - -From @autolens_workspace_test/scripts/jax_likelihood_functions/imaging/: - -- `simulator.py` -- `lp.py` (parametric light profile) -- `mge.py` (MGE basis) -- `mge_group.py` -- `rectangular.py` (rectangular pixelization source) -- `rectangular_mge.py` -- `delaunay.py` -- `delaunay_mge.py` — currently disabled in autolens smoke suite (jax 0.7 regression, - see PyAutoPrompt/autobuild/smoke_workspace_fixes.md). Ship it but disable with the same - comment in `smoke_tests.txt`. - -**Skip**: `rectangular_dspl.py`, `simulator_dspl.py`. - -__Pytree prerequisite — likely blocker__ - -`autolens/imaging/model/analysis.py::AnalysisImaging._register_fit_imaging_pytrees` registers -`FitImaging`, `DatasetModel`, and `Tracer` with `autoarray.abstract_ndarray.register_instance_pytree`. -**@PyAutoGalaxy/autogalaxy/imaging/model/analysis.py has no such method today.** Before the -JAX likelihood scripts will JIT, you will need a library PR on PyAutoGalaxy that: - -1. Adds `_register_fit_imaging_pytrees` to `autogalaxy.imaging.model.analysis.AnalysisImaging`. -2. Registers the autogalaxy equivalents: `FitImaging` (autogalaxy's), `DatasetModel`, `Galaxies` - (with `no_flatten=("cosmology",)` if it holds cosmology the way `Tracer` does — check first). -3. Calls it from `__init__` under the same `use_jax` gate autolens uses. - -Treat this as a **spawn-off library task** if it surfaces: stop, open a PyAutoGalaxy issue via -`/start_dev`, ship the library PR first, then resume. Do not paper over missing registrations -with ad-hoc `register_pytree_node` calls inside the workspace script. - -Other known spawn-offs if they surface during porting: - -- **Linear light profile** models need `linear_light_profile_intensity_dict_pytree` fixed — see - @PyAutoPrompt/autolens/linear_light_profile_intensity_dict_pytree.md for the lens-side - counterpart. Only blocks scripts that use `ag.lp_linear.*` or MGE bases via - `fit_for_visualization`, not the scalar `fit_from` round-trip. -- Any autogalaxy profile that isn't pytree-registered (follow the per-profile pattern in - @PyAutoPrompt/autolens/fit_imaging_pytree_*.md). - -__Three-step JAX pattern__ - -Each script mirrors the autolens reference: NumPy baseline → `jax.jit`-wrapped `analysis.fit_from` -→ scalar `log_likelihood` match. The reference file `mge_pytree.py` in autolens is the gold -standard for this pattern (see @PyAutoPrompt/autolens/fit_imaging_pytree_lp.md for background). - -__Deliverables__ - -1. `autogalaxy_workspace_test/scripts/jax_likelihood_functions/__init__.py` -2. `autogalaxy_workspace_test/scripts/jax_likelihood_functions/imaging/__init__.py` -3. Each ported script prints `PASS: jit(fit_from) round-trip matches NumPy scalar.` -4. Scripts appended to `smoke_tests.txt` (delaunay_mge commented out with the jax-0.7 comment). -5. Any PyAutoGalaxy library PRs needed for pytree registration (shipped first, merged before - this workspace PR). -6. Verify locally with `JAX_ENABLE_X64=True python scripts/jax_likelihood_functions/imaging/.py`. - -__Umbrella issue__ - -Task 3/9. Track under the epic issue on `PyAutoLabs/autogalaxy_workspace_test`. diff --git a/active/autogalaxy_workspace_test_jax_likelihood_interferometer.md b/active/autogalaxy_workspace_test_jax_likelihood_interferometer.md deleted file mode 100644 index e1b41539..00000000 --- a/active/autogalaxy_workspace_test_jax_likelihood_interferometer.md +++ /dev/null @@ -1,50 +0,0 @@ -Create `scripts/jax_likelihood_functions/interferometer/` in @autogalaxy_workspace_test with -autogalaxy ports of every autolens JAX-likelihood interferometer script, **excluding** `*_dspl.py` -and `rectangular_sparse.py` unless it has an autogalaxy analogue (lens-specific; check first). - -__Scripts to port__ - -From @autolens_workspace_test/scripts/jax_likelihood_functions/interferometer/: - -- `simulator.py` -- `lp.py` -- `mge.py` -- `mge_group.py` -- `rectangular.py` -- `rectangular_mge.py` -- `delaunay.py` -- `delaunay_mge.py` - -**Skip**: `rectangular_dspl.py`, `simulator_dspl.py`, and `rectangular_sparse.py` (confirm with -user if unsure whether the sparse interferometer path has an autogalaxy equivalent). - -__Pytree prerequisite — likely blocker__ - -`autogalaxy/interferometer/model/analysis.py` has no pytree registration method. Compare the -autolens equivalent in @PyAutoLens/autolens/interferometer/model/analysis.py and mirror it on -autogalaxy — register `FitInterferometer`, `DatasetModel`, `Galaxies`. - -If the registration is missing, stop and ship a PyAutoGalaxy library PR first (treat as spawn-off -task via `/start_dev`). Same policy as task 3: do not paper over with in-script registrations. - -__Three-step JAX pattern__ - -Same contract as task 3 — NumPy baseline, JIT round-trip, scalar log-likelihood match. Print -`PASS: jit(fit_from) round-trip matches NumPy scalar.` - -__Deliverables__ - -1. `autogalaxy_workspace_test/scripts/jax_likelihood_functions/interferometer/__init__.py` -2. Ported scripts. -3. Appended to `smoke_tests.txt`. -4. Any required PyAutoGalaxy library PRs merged first. - -__Depends on__ - -Task 3 completing the PyAutoGalaxy `AnalysisImaging._register_fit_imaging_pytrees` scaffold — -some of its helpers (e.g. `DatasetModel` registration) will already be in place and should be -reused rather than duplicated. - -__Umbrella issue__ - -Task 4/9. Track under the epic issue on `PyAutoLabs/autogalaxy_workspace_test`. diff --git a/active/autogalaxy_workspace_test_jax_likelihood_multi.md b/active/autogalaxy_workspace_test_jax_likelihood_multi.md deleted file mode 100644 index 5791d6ac..00000000 --- a/active/autogalaxy_workspace_test_jax_likelihood_multi.md +++ /dev/null @@ -1,47 +0,0 @@ -Create `scripts/jax_likelihood_functions/multi/` in @autogalaxy_workspace_test with autogalaxy -ports of every autolens multi-dataset JAX-likelihood script, **excluding** `*_dspl.py`. - -__Scripts to port__ - -From @autolens_workspace_test/scripts/jax_likelihood_functions/multi/: - -- `simulator.py` -- `lp.py` -- `mge.py` -- `mge_group.py` -- `rectangular.py` -- `rectangular_mge.py` -- `delaunay.py` -- `delaunay_mge.py` - -**Skip**: any `*_dspl.py`. - -__Context__ - -The `multi/` scripts exercise `af.FactorGraphModel` / multi-dataset joint fits. The autolens -versions combine an imaging dataset and an interferometer dataset with tied lens-galaxy params. -Autogalaxy versions should tie **galaxy** params across datasets (no lens/source split). - -__Pytree prerequisite__ - -Both `AnalysisImaging` and `AnalysisInterferometer` on autogalaxy need pytree-registered -`fit_from`. If tasks 3 and 4 have landed first, this should follow naturally. If any multi-dataset -registration is missing (e.g. the factor-graph combining analysis), spawn a library task. - -__Three-step JAX pattern__ - -Same as tasks 3 and 4. - -__Deliverables__ - -1. `autogalaxy_workspace_test/scripts/jax_likelihood_functions/multi/__init__.py` -2. Ported scripts. -3. Appended to `smoke_tests.txt`. - -__Depends on__ - -Tasks 3 and 4 (imaging + interferometer pytree registration scaffolding in place). - -__Umbrella issue__ - -Task 5/9. Track under the epic issue on `PyAutoLabs/autogalaxy_workspace_test`. diff --git a/active/autogalaxy_workspace_test_model_composition.md b/active/autogalaxy_workspace_test_model_composition.md deleted file mode 100644 index 5e75072c..00000000 --- a/active/autogalaxy_workspace_test_model_composition.md +++ /dev/null @@ -1,29 +0,0 @@ -Create `scripts/model_composition/` in @autogalaxy_workspace_test with an autogalaxy version of -the autolens `multi_galaxy_mge.py` script. - -__Reference__ - -@autolens_workspace_test/scripts/model_composition/multi_galaxy_mge.py - -That script exercises PyAutoFit's model-composition machinery on an autolens lens-plus-source -model with MGE bases. Strip it to an autogalaxy equivalent — multiple `al.Galaxy` objects in a -single plane (no lens/source split, no ray-tracing), each composed from an MGE light basis. - -__Deliverables__ - -1. `autogalaxy_workspace_test/scripts/model_composition/__init__.py` -2. `autogalaxy_workspace_test/scripts/model_composition/multi_galaxy_mge.py` — ported script. Use - `ag.Galaxy`, `ag.Galaxies`, `ag.ImagingAnalysis` (not `al.` / Tracer). Pick a small dataset - from an existing autogalaxy workspace example and model two galaxies each with an MGE. -3. Append `model_composition/multi_galaxy_mge.py` to `smoke_tests.txt`. -4. Verify locally with `PYAUTOFIT_TEST_MODE=2 PYAUTO_WORKSPACE_SMALL_DATASETS=1 python - scripts/model_composition/multi_galaxy_mge.py`. - -__Depends on__ - -Task 1 (CI) must have merged so the new script runs in GitHub Actions. Review failure is cheaper -on CI than locally once CI is wired up. - -__Umbrella issue__ - -Task 2/9. Track under the epic issue on `PyAutoLabs/autogalaxy_workspace_test`. diff --git a/active/autolens_profiling.md b/active/autolens_profiling.md deleted file mode 100644 index 17aea9da..00000000 --- a/active/autolens_profiling.md +++ /dev/null @@ -1,27 +0,0 @@ -The likelihood package in autolens_profiling has scripts which profile the likelihood functions of many -dataset types and models. However, I think each script is trying to do too many things: - -1) It gives a step-by-step run trhough of the likelihood function. -2) It is used to compute run times on CPU, GPU and with different settings. - -The truth is, I often want to do these takss separately, so I either want to know - -"How fast is interferometer delaunay on an A100, and compare that to CPU, for ALMA and ALMA high res" - -or - -"Give me a step by step profiling of the imaging likelihood for JWST on GPU and suggest where we should optimize" - -The combination of all this information in the python scripts and likelihood package also means that the results, -profiling information and interrpetation is confused because different ways of getting timings are being mixed. - -Therefore, can we split this into two packages, `likelihood_break_down` (or suggest better term) and -`likelihood_total` (again feel free to suggest a better term). - -Then, do some deep research about how the profiling results for these two different use cases should be collected -and presents. Like I said, one is about estimating how long the analysis of a certain type of science -data might take with given hardware, the other is about understanding source code bottlenecks. - -This will also help a lot because the step-by-step profiling can take a long time, especially for things like -eager numpy. The overall run times and time to gather the information being requested will therefore also -reduce. \ No newline at end of file diff --git a/active/autolens_results_aggregator_valid_dataset.md b/active/autolens_results_aggregator_valid_dataset.md deleted file mode 100644 index 17269b69..00000000 --- a/active/autolens_results_aggregator_valid_dataset.md +++ /dev/null @@ -1,24 +0,0 @@ -# Fix autolens results aggregator dataset reload - -Original user request: - -> continue - -Release report context: - -The PyAutoBuild release run reports two failures in `autolens_workspace`: - -- `scripts/guides/results/aggregator/data_fitting.py` -- `scripts/guides/results/aggregator/models.py` - -Both fail when `al.agg.ImagingAgg(...).dataset_gen_from()` attempts to reconstruct an imaging dataset: - -```text -TypeError: 'NoneType' object is not subscriptable - PyAutoGalaxy/autogalaxy/aggregator/agg_util.py:101 - header = aa.Header(header_sci_obj=fit.value(name=name)[0].header) -``` - -Reproduction on current `autolens_workspace/main` using the PyAutoBuild environment confirms that the aggregator finds fits under `output/results_folder`, but `agg.values("dataset.mask")` returns `[None, None]` for stale or incompatible results. The output tree can contain a completed fit without `image/dataset.fits`, while the aggregator tutorials require that FITS artifact. - -Fix the workspace results aggregator flow so the affected scripts only scrape reusable helper results that contain `image/dataset.fits`, while preserving the normal tutorial behavior and PyAutoBuild test-mode path handling. diff --git a/active/automemory.md b/active/automemory.md deleted file mode 100644 index 072a12d7..00000000 --- a/active/automemory.md +++ /dev/null @@ -1,57 +0,0 @@ -Rename the repository from **PyAutoPaper** to **PyAutoMemory**. - -## Context - -This rename is part of a broader architectural evolution of the PyAuto ecosystem into a software organism. - -Current organism architecture: - -* PyAutoMind — ideas, intent, goals and future work. -* PyAutoBrain — planning, reasoning and orchestration. -* PyAutoHands — execution. -* PyAutoHeart — health, testing and readiness. -* PyAutoMemory — long-term knowledge. - -PyAutoPaper has evolved beyond a repository of papers. - -It now stores and organises: - -* literature summaries -* LLM-generated wikis -* reading queues -* accumulated scientific knowledge -* project knowledge -* architecture notes -* information the organism has learned - -The repository is therefore functioning as the organism's long-term memory, not simply as a collection of papers. - -## Task - -Perform a repository-wide rename from PyAutoPaper to PyAutoMemory. - -Update: - -* README -* documentation -* repository metadata -* package names (if applicable) -* references in scripts -* GitHub workflows -* badges -* URLs -* internal documentation - -Where documentation currently refers to "papers", consider whether "knowledge", "memory", "literature", or "learned information" better reflects the repository's new role. - -Do not change functionality. - -Maintain backwards compatibility wherever practical. - -Add a README section explaining that PyAutoMemory stores what the organism has learned, including literature summaries, scientific knowledge and project knowledge. - -Run all available validation. - -Create a single PR titled: - -Rename PyAutoPaper to PyAutoMemory diff --git a/active/automind.md b/active/automind.md deleted file mode 100644 index c71c019b..00000000 --- a/active/automind.md +++ /dev/null @@ -1,73 +0,0 @@ -Rename the repository from **PyAutoPrompt** to **PyAutoMind**. - -## Context - -The PyAuto ecosystem is evolving into a software organism. - -Current organism architecture: - -* PyAutoMind — ideas, intent, priorities and goals. -* PyAutoBrain — reasoning and planning. -* PyAutoHands — execution. -* PyAutoHeart — health and readiness. -* PyAutoMemory — accumulated knowledge. - -Although this repository began life as a prompt repository, it has evolved significantly. - -It now contains: - -* ideas -* prompts -* active tasks -* completed tasks -* planning -* priorities -* workflow state -* project direction - -The repository no longer simply stores prompts. - -Instead, it represents the organism's intentions and ambitions. - -The Mind decides what the organism wants to become. - -The Brain determines how to achieve those goals. - -## Task - -Perform a careful repository-wide rename from PyAutoPrompt to PyAutoMind. - -Update: - -* README -* documentation -* architecture documents -* scripts -* GitHub workflows -* references throughout the repository - -Update documentation so it consistently describes the repository as the source of: - -* ideas -* goals -* intent -* future work -* priorities - -rather than simply "prompts". - -Preserve all functionality. - -Maintain backwards compatibility wherever practical. - -Add a README section describing the relationship between: - -Mind → Brain → Hands → Heart - -and explain why this repository now represents the Mind of the PyAuto organism. - -Run all available validation. - -Create one PR: - -Rename PyAutoPrompt to PyAutoMind diff --git a/active/blackjax_nuts.md b/active/blackjax_nuts.md deleted file mode 100644 index 9f039a76..00000000 --- a/active/blackjax_nuts.md +++ /dev/null @@ -1,13 +0,0 @@ -The folloeing examples show simple searches using JAX gradients in autofit: - -autofit_workspace_developer/searches_minimal - -Wrtie a example nuts_jax.py, which runs blackjax's NUTS sampler on the examples in this folder and provide the -timing and whatnot information on how the run. goes. - -This is BlackJAX: - -https://github.com/blackjax-devs/blackjax - -Once that is running and working, then go to the use case in @z_projects/concr/scripts/cancer_sim/graphical.py and -write a file graphical_nuts.py which runs nuts on that. diff --git a/active/bootstrap.md b/active/bootstrap.md deleted file mode 100644 index 24f6d2ae..00000000 --- a/active/bootstrap.md +++ /dev/null @@ -1,58 +0,0 @@ -Phase 0 of the `autolens_profiling` z_feature -(see `z_features/autolens_profiling.md` for the full roadmap). - -Create a new GitHub repository `PyAutoLabs/autolens_profiling` and scaffold it -so the later mirror phases (likelihood JIT, simulators, searches) have a clean -place to drop content. - -What this task should produce on first commit: - -- Empty public repo on GitHub (`PyAutoLabs/autolens_profiling`), description - line along the lines of "Profiling and run-time tracking for PyAutoLens - likelihood functions, simulators, and samplers across CPU, laptop GPU, and - HPC GPU." Match the style of other PyAutoLabs repos. -- Local checkout at `/home/jammy/Code/PyAutoLabs/autolens_profiling/` so - later /start_library calls can pick it up. -- Top-level `README.md` with: - - One-paragraph vision / scope statement. - - Section index linking to the planned `likelihood/`, `simulators/`, - `searches/`, `results/` directories (some will still be empty stubs at - this phase — that's fine). - - A clear "JAX gradients are out of scope for now — see - `PyAutoLabs/autolens_workspace_developer/jax_profiling/gradient/`" callout. - - A "Related repos" section linking to `PyAutoLabs/PyAutoLens`, - `PyAutoLabs/autolens_workspace`, `PyAutoLabs/autolens_workspace_developer` - (source-of-truth during the migration), and the sibling - `Jammy2211/autolens_colab_profiling` (Colab-specific scope, not yet - migrated to PyAutoLabs). - - A short "How to read this repo" guide that points readers at the - versioned `results/*_v.{json,png}` pattern that the existing - `_developer` results folder already uses. -- `LICENSE` matching the other PyAutoLabs repos (almost certainly MIT — check - PyAutoLens to confirm and copy). -- `.gitignore` mirrored from `autolens_workspace_developer/.gitignore` with - any obvious additions (e.g. `results/**/*.tmp`, profiler trace files). -- Folder skeleton: `likelihood/`, `simulators/`, `searches/`, `results/`, - each containing only a placeholder `README.md` saying "populated by Phase - N of the z_feature". -- Decide: is this a Python *package* (with `pyproject.toml`) or just a - collection of standalone scripts? Lean toward "scripts only" — the repo's - job is to surface results, not to be importable. Note that decision in - the top-level README. -- No code from `_developer` is moved yet — that's Phases 1–3. - -Pre-existing context the implementer should look at first: - -- `autolens_workspace_developer/jax_profiling/results/` — already uses the - `*_summary_v2026.X.Y.Z.{json,png}` versioned-artifact pattern. Reuse it. -- `autolens_workspace_developer/CLAUDE.md` — describes the JIT profiling - conventions (`.array` extraction, `xp` parameter) that the new repo's - scripts will follow once Phase 1 starts. -- Other PyAutoLabs repo READMEs for tone / structure parity (e.g. - `PyAutoLabs/PyAutoLens`, `PyAutoLabs/autolens_workspace`). - -Out of scope for this task: any actual profiling scripts, any CI, any -result tables beyond placeholders. Those land in Phases 1–5. - -When the repo is live and pushed to GitHub, the follow-up `/start_dev` -invocations for Phases 1–3 can begin. diff --git a/active/border_relocator_none_mesh_grid.md b/active/border_relocator_none_mesh_grid.md deleted file mode 100644 index b04a06f8..00000000 --- a/active/border_relocator_none_mesh_grid.md +++ /dev/null @@ -1,70 +0,0 @@ -When running pixelization scripts under `PYAUTO_TEST_MODE=2`, an `AttributeError` is raised -inside `BorderRelocator.relocated_mesh_grid_from` because `source_plane_mesh_grid` arrives as -`None`. This was masked for a long time by the `positions_likelihood_from` zero-size-array -crash (now fixed in `@PyAutoLens` PR #479 / issue #477). With that crash gone, the next test- -mode pixelization run hits this one immediately. - -## Reproducer - -```bash -cd autolens_workspace -PYAUTO_TEST_MODE=2 python scripts/imaging/features/pixelization/delaunay.py -``` - -## Traceback - -``` -File "@PyAutoLens/autolens/imaging/model/analysis.py", line 79, in log_likelihood_function -File "@PyAutoArray/autoarray/fit/fit_dataset.py", line 361, in figure_of_merit -File "@PyAutoLens/autolens/imaging/fit_imaging.py", line 156, in inversion -File "@PyAutoLens/autolens/lens/to_inversion.py", line 479, in inversion -File "@PyAutoGalaxy/autogalaxy/galaxy/to_inversion.py", line 205, in linear_obj_list -File "@PyAutoGalaxy/autogalaxy/galaxy/to_inversion.py", line 188, in linear_obj_galaxy_dict -File "@PyAutoLens/autolens/lens/to_inversion.py", line 436, in mapper_galaxy_dict -File "@PyAutoGalaxy/autogalaxy/galaxy/to_inversion.py", line 489, in mapper_from -File "@PyAutoArray/autoarray/inversion/mesh/mesh/delaunay.py", line 158, in interpolator_from -File "@PyAutoArray/autoarray/inversion/mesh/mesh/abstract.py", line 91, in relocated_mesh_grid_from -File "@PyAutoArray/autoarray/inversion/mesh/border_relocator.py", line 450, in relocated_mesh_grid_from - grid=mesh_grid.array, origin=origin, a=a, b=b, phi=phi, xp=xp - ^^^^^^^^^^^^^^^ -AttributeError: 'NoneType' object has no attribute 'array' -``` - -## Where the None comes from - -`@PyAutoArray/autoarray/inversion/mesh/mesh/abstract.py:90-94` calls -`border_relocator.relocated_mesh_grid_from(grid=source_plane_data_grid, mesh_grid=source_plane_mesh_grid, xp=xp)` -without checking that `source_plane_mesh_grid` is non-None — under `PYAUTO_TEST_MODE=2` the -random/unphysical mass model traces all the data grid into NaN/inf and the upstream pipeline -hands the relocator a `None` mesh grid. - -`@PyAutoArray/autoarray/inversion/mesh/border_relocator.py:429-456` then dereferences -`mesh_grid.array` without a None guard. - -## What to investigate - -1. Walk back up the call chain to find where `source_plane_mesh_grid` becomes `None`. Is it - produced lazily inside the mesh class? Inside the `to_inversion` machinery on - `@PyAutoGalaxy`? Or somewhere in the source-plane ray-tracing? The right fix probably - lives at the point of construction, not at the relocator. -2. Decide between **(a)** a root-cause fix that prevents `None` from being produced (preferred - — keeps relocator semantics tight), or **(b)** a defensive guard in `BorderRelocator`/the - abstract mesh layer that returns the unrelocated grid (or a synthetic stand-in) when the - mesh grid is missing under `is_test_mode()`. -3. Check whether option (b) should follow the same pattern as the recently-shipped - `Result.positions_likelihood_from` fallback (`is_test_mode()` guard, synthetic stand-in, - single `logger.warning`) so test-mode behaviour is consistent across the codebase. - -## Out of scope - -- Don't reintroduce a workaround at the workspace level — the `os.environ.pop` dance was just - removed in `@autolens_workspace` PR #102. -- Don't change `@PyAutoLens` `Result.positions_likelihood_from` — that fallback already works. - This is a separate downstream bug in `@PyAutoArray`. - -## Acceptance - -`PYAUTO_TEST_MODE=2 python scripts/imaging/features/pixelization/delaunay.py` should no -longer crash inside the relocator. Other pixelization scripts (`rectangular.py`, -`voronoi.py`, etc.) should be sanity-checked under the same env vars to confirm they aren't -hiding the same `None` mesh-grid path. diff --git a/active/brain_agent_commands.md b/active/brain_agent_commands.md deleted file mode 100644 index fce07211..00000000 --- a/active/brain_agent_commands.md +++ /dev/null @@ -1,99 +0,0 @@ -# Concise PyAutoBrain agent commands (the command veneer + NL router) - -Give the PyAuto organism a thin, human-friendly **command surface** over the -already-existing PyAutoBrain router (`bin/pyauto-brain`). The Brain should be -implicit: users type short verbs (or plain natural language) and the Brain routes -to the right specialist agent — they never have to say "PyAutoBrain". - -> Design sentence: **Users speak in short commands; PyAutoBrain performs the routing.** - -## Original request (verbatim) - -Set up concise PyAutoBrain agent invocation commands. - -The Brain should be implicit. Like a human: you don't say "Brain, activate the -visual cortex" — you say "look at this", and your brain routes it. Normal usage -should never mention the Brain. Desired user-facing commands: - - /health /build /feature /bug /refactor /docs /research - -Internally every one routes through PyAutoBrain to the appropriate specialist -agent, e.g. `/health -> PyAutoBrain -> Health Agent -> PyAutoHeart`. Commands are -user-facing shortcuts only; they must **not** bypass PyAutoBrain. - -Also support **natural-language routing** via an optional router command -(`/route` or `/brain`) that infers the agent from the request: - - /route Fix failing tests -> bug / health - /route Implement issue #417 -> feature - /route Publish PyAutoLens -> build - -Keep a lower-level debugging door (`/brain `) for explicit invocation. - -Command responsibilities: - -- `/health` — health readiness, GREEN/YELLOW/RED, PyAutoHeart checks. -- `/build` — build, release, shipping, PR/deployment execution via PyAutoBuild. -- `/feature` — new capabilities, task selection/phasing, start_dev workflow. -- `/bug` — regressions, failing tests, incorrect behaviour, issue triage. -- `/refactor` — architecture cleanup, internal restructuring, no behaviour change. -- `/docs` — documentation, examples, notebooks, tutorials. -- `/research` — investigation, design notes, scientific background, pre-impl analysis. - -Requirements: preserve the architecture (never bypass Brain); minimise tokens -(each command file short — under 100 lines where possible, definitely under 200; -no duplicated architecture prose — factor shared rules into a common doc); keep -Brain implicit; support NL routing; install into the correct canonical location -with no stale duplicate copies (update install scripts/symlinks if needed); -update docs; validate command discovery + that no command bypasses Brain. One PR -titled "Add concise PyAutoBrain agent commands". - -## Approved design (from analysis of the current system) - -The Brain already routes: `PyAutoBrain/bin/pyauto-brain ` dispatches to -conductors (`feature`, `build`, `release`, `health`) and faculties (`vitals`), -and `PyAutoMind/ROUTING.md` already maps the work-type taxonomy -(feature/bug/refactor/docs/test/release/maintenance/research) to Brain agents. -So this task is a **thin honest veneer over an existing router**, not seven new -agents. Only `feature`, `build`, `health` have real conductors today; -`bug/refactor/docs/research` do **not** — they must not pretend to. - -Ship, in **one PR**: - -1. **`/route` (or `/brain `) — the star.** A natural-language entry that - self-classifies the request to a work-type and routes it. This is the - "look at this" path. Lean on the existing `start_dev` / Feature-Agent - classifier rather than re-inventing classification. -2. **Real verbs → real conductors.** `/feature`→`pyauto-brain feature`, - `/build`→`pyauto-brain build`, `/health`→`pyauto-brain health` (→ vitals → - Heart). `/health` becomes the human front door and *calls* the faster sweeps - (`health_check`, `pyauto-status`) as legs of its loop rather than competing - with them. -3. **Work-type verbs → `start_dev` pre-tagged.** `/bug /refactor /docs /research` - enter the existing Brain feature-flow with their PyAutoMind work-type fixed — - still through the Brain, so nothing is bypassed — documented as work-type - entries **until** dedicated conductors earn promotion (tracked follow-ups). -4. **`/brain ` debug passthrough** — execs `bin/pyauto-brain "$@"` raw. - -Mechanism: each verb is a thin `PyAutoBrain/skills//.md` command file -(the installer already turns `skills//.md` with no `SKILL.md` into a -flat `~/.claude/commands/.md`). Shared architecture prose lives in **one** -`COMMANDS.md` referenced (not copied) so files stay short and pass -`bin/check_skill_line_counts.sh`. Location = **PyAutoBrain** (the router). Do not -place stale copies in `admin_jammy` (vestigial) or hand-edit `~/.claude`. - -Divergences from the raw spec: do **not** ship 7 co-equal commands (4 have no -agent); make the NL router primary, not optional; resist creating 4 new -conductors — add bug/refactor/docs/research as classifications first, promote -later. - -## Follow-ups (not this PR) - -- Promote `bug` / `refactor` / `docs` / `research` from work-type entries to - dedicated PyAutoBrain conductors, each when its behaviour earns a front door. -- Reconcile `/health` with `/health_check` and `/pyauto-status` so there is one - obvious health front door. - -Target repo: **@PyAutoBrain** (command surface + router + docs). Touches -`PyAutoMind/ROUTING.md` docs only if the routing table needs a pointer to the -command surface. diff --git a/active/bug_agent.md b/active/bug_agent.md deleted file mode 100644 index 5dfe8287..00000000 --- a/active/bug_agent.md +++ /dev/null @@ -1,356 +0,0 @@ -Implement the initial PyAutoBrain Bug Agent. - -Context - -The PyAuto ecosystem now uses an organism architecture. - -Current architecture: - -PyAutoMind stores intent, tasks and workflow state. -PyAutoMemory stores long-term knowledge and prior decisions. -PyAutoBrain contains specialist reasoning agents. -PyAutoHeart performs health checks and readiness validation. -PyAutoBuild performs execution, build and release operations. - -Existing or planned PyAutoBrain agents include: - -Health Agent -Build Agent -Feature Agent - -The next specialist agent should be the Bug Agent. - -The Bug Agent handles regressions, failing tests, incorrect behaviour, broken workflows and health issues that require fixes. - -Goal - -Implement the initial Bug Agent. - -The Bug Agent should reason about bugs and produce a clear repair workflow. - -It should not directly duplicate PyAutoHeart checks or PyAutoBuild execution. - -Core responsibilities - -The Bug Agent should: - -accept a specific bug report, failing test, GitHub issue or PyAutoHeart finding, -classify the bug, -determine likely owning repository or repositories, -consult PyAutoMemory for relevant historical context, -consult PyAutoHeart for reproduction / health-check context, -decide whether the bug is small, medium, large or ambiguous, -decide whether it can be fixed directly or requires investigation first, -produce a repair plan compatible with the PyAuto workflow, -route execution through PyAutoBuild where appropriate, -require PyAutoHeart validation before shipping. - -Inputs - -The Bug Agent should support: - -/bug issue #123 -/bug failing tests in PyAutoArray -/bug health issue from PyAutoHeart -/bug regression after PR #456 -/bug choose an important bug to fix -/bug choose an easy bug suitable for limited tokens - -Modes - -1. Specific bug mode - -The user provides a known issue, failing test or error. - -The Bug Agent should: - -inspect the report, -identify reproduction steps, -classify severity, -find likely affected repos, -identify relevant tests, -produce a fix plan. - -2. Health issue mode - -The bug comes from PyAutoHeart. - -The Bug Agent should: - -read the PyAutoHeart finding, -understand which check failed, -determine whether this is a real bug, flaky test, config problem or expected failure, -decide whether the fix belongs in the affected repo, PyAutoHeart, PyAutoBuild or PyAutoBrain. - -3. Selection mode - -The user asks the agent to choose a bug. - -The Bug Agent should: - -inspect open bug issues, -inspect PyAutoMind bug prompts, -consider priority and severity, -consider model/token constraints, -choose a suitable bug, -explain why. - -4. Difficulty-constrained mode - -The user may ask for: - -easy bug, -high-impact bug, -low-risk bug, -bug suitable for weak model, -bug suitable for strong model, -bug suitable for overnight run. - -The Bug Agent should estimate complexity before selecting. - -Classification - -The Bug Agent should classify bugs by: - -severity: - critical | high | medium | low - -scope: - single-file | single-repo | multi-repo | ecosystem - -type: - test-failure | runtime-error | wrong-result | docs-error | workflow-error | config-error | release-error | flaky | unknown - -confidence: - high | medium | low - -Relationship to PyAutoHeart - -PyAutoHeart measures health. - -The Bug Agent reasons about failures. - -The Bug Agent should use PyAutoHeart to: - -reproduce failing checks, -identify affected validation workflows, -confirm whether a fix worked, -obtain GREEN/YELLOW/RED readiness after patching. - -The Bug Agent must not reimplement PyAutoHeart checks. - -Relationship to PyAutoMemory - -The Bug Agent should consult PyAutoMemory when useful. - -Examples: - -recurring failures, -previous fixes, -known flaky tests, -architectural decisions, -scientific assumptions, -previous debugging notes. - -If PyAutoMemory influenced the plan, summarise the relevant context. - -Relationship to PyAutoMind - -Bug work should be tracked in PyAutoMind. - -The Bug Agent should understand paths such as: - -bug/autoarray/... -bug/autofit/... -bug/autolens/... -bug/workspaces/... - -If a bug report is actually a feature, refactor, docs issue or research task, reclassify it. - -Relationship to PyAutoBuild - -PyAutoBuild executes. - -The Bug Agent should route execution through PyAutoBuild when it is time to: - -create branches, -make code changes, -open PRs, -ship fixes. - -The Bug Agent should not bypass the established workflow. - -Development workflow - -The Bug Agent should support a lifecycle like: - -bug report - -> classify - -> reproduce or identify validation check - -> consult Memory if needed - -> decide fix strategy - -> start_dev - -> patch - -> PyAutoHeart validation - -> ship_library / ship_workspace - -Output format - -The Bug Agent should produce structured output: - -Bug: - - -Mode: -specific | health-issue | selection | difficulty-constrained - -Classification: -severity: -scope: -type: -confidence: - -Likely owner: - - -Reproduction: - - -Relevant context: - - -Fix strategy: - - -Recommended workflow: - - -Health validation: - - -Risks: -
- -Next action: - - -Claude skill constraints - -If implemented as Claude skills or markdown agent definitions: - -keep every .md file below 200 lines, -follow Claude skill guidelines, -keep the main Bug Agent instruction file concise, -move long examples and architecture notes into supporting docs. - -Validation - -Validate that the Bug Agent can: - -classify a known bug, -consume a PyAutoHeart finding, -choose a bug when none is specified, -respect difficulty constraints, -produce an output compatible with start_dev, -require PyAutoHeart validation before shipping. - -PR - -Create one PR titled: - -Implement initial PyAutoBrain Bug Agent - ---- - -## Extra directives (from the user, verbatim) - -Also make sure it has a specific prompt or context for just scanning Heart for -issues here: https://github.com/PyAutoLabs/PyAutoHeart/issues . - -Also think about if the bug agent is just a conductor or also could use some -faculties. Do deep research. - -One important aspect of the bug agent is that when it's editing workspace -scripts, it should know that workspace scripts are **user-facing documentation** -and therefore bug fixes should avoid changing the scripts in ways that make their -contents less clear to a user when possible. Without this context, agents often -change the script in weird ways (e.g. adding test environment variables in the -scripts or manually overwriting paths). The bug agent should critically assess if -a script ever needs changing or if the fix is better done in the source code in a -way that fixes things more generally. - ---- - -## Design decisions agreed in the planning session (2026-07-07) - -These reconcile the spec above with the real PyAutoBrain organism (conductors vs. -faculties, the vitals faculty, the existing Feature Agent core). Sources: -`PyAutoBrain/AGENTS.md`, `agents/conductors/feature/`, `agents/faculties/vitals/`. - -**Organism metaphor — the immune system.** The Bug Agent is the organism's -**immune system** (organism-facing name: *Immune Agent*): it recognises a pathogen -(a bug, regression, failing test or PyAutoHeart finding), tells it from benign -self, types the threat, recalls whether it has met it before (PyAutoMemory as -immune memory), and mounts a *targeted* response — neutralising the defect at its -source without harming healthy tissue. This framing goes at the top of its -`AGENTS.md`. Mapping: recognise pathogen → accept report; self-vs-non-self → -real-bug vs expected/flaky/mis-filed; type threat → severity/scope/type/confidence; -immune memory → PyAutoMemory; targeted response / spare healthy tissue → fix-locus; -autoimmunity → the failure mode to avoid. - -**Tier: conductor** (mirrors the Feature Agent). It decides and drives a plan into -the dev-flow, so it lives at `agents/conductors/bug/`. It **consults the existing -read-only `vitals` faculty** (`--check-health`) and **never queries Heart -directly**. Ship as a conductor only for v1; document a clean seam for a future -read-only `diagnosis` faculty (pure classify + locate + fix-locus reasoning, -reusable by Feature re-homing and the Health conductor) rather than building it now -— matching how Release stayed a mode of Build with a seam to split later. - -**Boundary with the Health conductor.** The Health conductor drives the assess → -triage → dispatch loop toward GREEN; its current cut is explicitly "validation + -recommend, no edit-in fixes." The Bug Agent is that deferred edit-in-fix arm: Health -hands it a red that is a genuine *code* failure, and the Bug Agent turns it into a -repair plan. No duplicated triage. - -**Deterministic core reuse.** `_bug.py` imports the Feature Agent's shared helpers -(`scan_mind` / `score_difficulty` / `downrank_inflight` / `emit_json` from -`_feature.py`) — minimal refactor, no standalone copy of the difficulty heuristic — -and adds only `classify()`, `reproduction()`, `fix_locus()`, `health_mode()`. -Stdlib-only, never writes (same contract as `_feature.py`). Selection adds a -severity weighting on top of the difficulty score. - -**Two health inputs (health-issue mode).** (1) the live **vitals verdict** (what is -RED now, via the vitals faculty) and (2) **filed PyAutoHeart GitHub issues** -scanned with `gh issue list --repo PyAutoLabs/PyAutoHeart` — the durable, detailed -findings Heart authored (e.g. #27 release-fidelity, #19/#7 degraded-health, #10 -url-check). Both route to `PyAutoMind/bug/health_fixes/` (whose README already cites -Heart issue #27). Give the issue-scan its own context/sub-mode. - -**Fundamental principle — a precise response, no autoimmunity.** The most delicate -tissue is the **user-facing workspace scripts — they are documentation**. A fix that -injects test env-vars, hard-codes a path, mutates `os.environ`, or drops a silent -guard into a tutorial script is an **autoimmune reaction** — it damages what it -exists to protect. Before proposing any patch the Bug Agent asks *where the fix -belongs* and strongly prefers a **general fix in library source** that resolves the -whole class of failure. It edits a workspace script only when the defect truly lives -there, never in a way that reduces clarity; sanctioned knobs go through -`config/build/env_vars.yaml` / `no_run.yaml`, not inline edits. This becomes an -explicit `Fix locus:` field in the BugDecision output. - -**Output — a BugDecision**, adopting the spec's fields but JSON-consistent with the -Feature Agent's `FeatureDecision` shape, plus the `Fix locus:` field above. - -**Files.** New: `agents/conductors/bug/{AGENTS.md, BUG_TAXONOMY.md, bug.sh, -_bug.py}`. Edits: register `bug` in `bin/pyauto-brain` (AGENT_SCRIPT / AGENT_DESC / -CONDUCTOR_ORDER); promote `skills/bug/bug.md` from work-type entry to a real -conductor command; move `/bug` from the "work-type entries" tier into "real -conductors" in `skills/COMMANDS.md`, `README.md`, and `AGENTS.md`. Keep every `.md` -under 200 lines (guarded by `bin/check_skill_line_counts.sh`). - -**Validation.** `pyauto-brain bug bug/autoarray/rect_adapt.md` (classify + fix-locus -= library source); `pyauto-brain bug` (selection over `bug/**` + open GitHub bug -issues, down-ranks in-flight); `pyauto-brain bug select --difficulty easy` / -`--impact`; `pyauto-brain bug health` (vitals + scan PyAutoHeart issues → -`bug/health_fixes/`); `--json` emits BugDecision; line-count guard passes. - - diff --git a/active/build.md b/active/build.md deleted file mode 100644 index 47db2054..00000000 --- a/active/build.md +++ /dev/null @@ -1,342 +0,0 @@ -# I actually think the Build Agent should be the second - -Type: feature -Target: PyAutoBrain -Difficulty: too-large -Autonomy: supervised -Priority: high -Status: formalised - -I actually think the Build Agent should be the second canonical PyAutoBrain agent, because it demonstrates that Brain agents don't execute directly—they coordinate organs. - -The flow becomes: - -PyAutoMind - ↓ -Build Agent (PyAutoBrain) - ↓ -Health Agent (PyAutoBrain) - ↓ -PyAutoHeart - ↓ -GREEN / YELLOW / RED - ↓ -Build Agent - ↓ -PyAutoBuild (future PyAutoHands) - -The Build Agent is therefore the executive function. It owns the build workflow, but delegates health decisions to the Health Agent. - -I'd give Codex this prompt: - -Implement the second specialist PyAutoBrain agent: Build Agent. - -Context - -The PyAuto ecosystem is evolving into a software organism. - -Current architecture: - -PyAutoMind -Stores intent, goals and future work. -PyAutoBrain -Contains specialist reasoning agents. -PyAutoHeart -Performs health monitoring, testing and readiness checks. -PyAutoBuild -Performs software execution, release tasks and repository actions. -(This may later be renamed PyAutoHands.) - -The first PyAutoBrain specialist agent is the Health Agent. - -The Build Agent should become the second. - -Fundamental architectural principle - -The Build Agent does not build software itself. - -PyAutoBuild performs builds. - -The Build Agent decides: - -whether building should happen, -what should be built, -when it should be built, -which PyAutoBuild capabilities should be invoked, -whether execution should continue or stop. - -The Build Agent is therefore an orchestration layer. - -It reasons. - -PyAutoBuild executes. - -Responsibilities - -The Build Agent should: - -understand build requests from PyAutoMind -inspect repository state -determine the required build actions -request a health assessment from the Health Agent -interpret the GREEN/YELLOW/RED result -decide whether execution may proceed -invoke the appropriate PyAutoBuild capabilities -monitor execution -produce a structured execution summary - -It must never duplicate PyAutoBuild functionality. - -Existing capability audit - -Before implementation: - -Audit the existing PyAutoBuild repository. - -Discover every existing capability including: - -bash scripts -Python scripts -Claude skills -slash commands -GitHub workflows -deployment logic -release logic -packaging -version management -PR creation -repository management -automation scripts - -The Build Agent should understand these capabilities. - -It should call them. - -Not replace them. - -Boundary audit - -Confirm that PyAutoBuild contains only execution behaviour. - -If health-related logic is found: - -identify it, -determine whether it belongs in PyAutoHeart, -document the finding, -avoid duplicating it inside the Build Agent. - -Likewise, ensure reasoning remains inside PyAutoBrain. - -The architecture should remain: - -PyAutoMind - -↓ - -Build Agent - -↓ - -Health Agent - -↓ - -PyAutoHeart - -↓ - -GREEN / YELLOW / RED - -↓ - -Build Agent - -↓ - -PyAutoBuild - -Build lifecycle - -The Build Agent should reason through a workflow similar to: - - - - -Receive build request. - - - - -Determine required execution steps. - - - - -Request health assessment. - - - - -Interpret Health Agent decision. - -GREEN - -Proceed automatically. - -YELLOW - -Proceed cautiously. - -Include warnings. - -RED - -Abort execution. - -Explain blockers. - - - - -Invoke appropriate PyAutoBuild capabilities. - - - - -Monitor execution. - - - - -Summarise results. - -Interface - -Design a simple interface similar to: - -BuildDecision = BuildAgent().execute(...) - -returning structured information including: - -requested action -health status -execution plan -execution summary -warnings -blockers -follow-up recommendations -Claude skill guidance - -If implementing as Claude skills: - -keep every .md file below 200 lines, -split large skills into reusable components, -keep instructions concise, -place architecture documentation elsewhere. -Documentation - -Update documentation describing the organism. - -Explain: - -PyAutoMind - -Creates intent. - -PyAutoBrain - -Reasons. - -Build Agent - -Coordinates execution. - -Health Agent - -Assesses readiness. - -PyAutoHeart - -Measures health. - -PyAutoBuild - -Performs work. - -The Build Agent should become the canonical example of how PyAutoBrain coordinates multiple organs. - -Future Brain agents - -Document how this pattern generalises to future agents such as: - -Feature Agent -Bug Agent -Refactor Agent -Documentation Agent -Research Agent -Release Agent - -The Build Agent should be designed as a reusable template for future specialist Brain agents. - -Validation - -Run all available validation. - -Ensure every existing PyAutoBuild capability remains usable through the Build Agent. - -No existing functionality should regress. - -PR - -Create one PR titled: - -Implement initial PyAutoBrain Build Agent - -One design suggestion that emerged while writing this: I wouldn't make the Build Agent simply "call the Health Agent." Instead, I'd introduce the idea that Brain agents can consult one another. That means in the future: - -Feature Agent asks Health Agent if the codebase is in a good state for a refactor. -Release Agent asks Build Agent to package a release. -Research Agent asks Feature Agent to estimate implementation complexity. - -You start to get a genuine society of reasoning agents within PyAutoBrain, while the organs (Heart, Hands, Memory, Nerves) provide capabilities and state. That's a richer and more extensible architecture than every agent talking directly to every organ. - - - -But for the first implementation, let Build Agent include release workflows and document that release may later become its own agent. - -Reason: - -Build is broad: run packaging, create artifacts, invoke scripts, open PRs, deploy. -Release is higher-stakes: versioning, changelog, PyPI, tags, cross-repo compatibility, human approval. -Release should consult Health Agent more strictly than ordinary build work. - -So the mature architecture is: - -Release Agent - ↓ consults -Health Agent - ↓ -PyAutoHeart - -Release Agent - ↓ requests execution from -Build Agent - ↓ -PyAutoBuild - -Whereas day one can be: - -Build Agent - ├─ build mode - ├─ deploy mode - └─ release mode - -I’d update the prompt with this paragraph: - -Treat release as part of the initial Build Agent scope, because PyAutoBuild currently owns release/build/deployment execution. However, design the Build Agent so that release logic is clearly isolated as a “release mode” or “release workflow”. Do not mix release-specific reasoning into generic build execution. - -Document that a future PyAutoBrain Release Agent may be split out. In that future architecture, the Release Agent will make release-specific decisions about versioning, changelogs, PyPI/tags and human approval, consult the Health Agent for readiness, and then request execution from the Build Agent / PyAutoBuild. - -So: one agent now, clean seam for two later. - - diff --git a/active/cached_property_audit.md b/active/cached_property_audit.md deleted file mode 100644 index de10e6e7..00000000 --- a/active/cached_property_audit.md +++ /dev/null @@ -1,83 +0,0 @@ -# Audit uncached @property on Fit classes — cache immutable computed results - -## Problem - -`FitImaging.model_data` is a plain `@property` that recomputes the -entire inversion pipeline on every access. Any code that touches -`fit.model_data` more than once (residuals, chi-squared, visualization, -aggregator scripts) pays the full cost again. For a Delaunay source -with 1500 mesh pixels, each recomputation is ~5-20s. - -This was discovered during quick-update rendering profiling: accessing -`fit.model_data` once and `fit.subtracted_images_of_planes_list` once -(which internally accesses `model_data` again) cost 13.7s just from -redundant recomputation. - -`FitImaging` is constructed once and never mutated — the tracer, -dataset, and settings are fixed at construction time. All computed -properties should be safe to cache. - -## Scope - -### Primary: FitImaging / FitInterferometer / FitDataset - -Sweep these classes for `@property` methods that: -1. Compute from other properties (creating a cascade of recomputation) -2. Are accessed more than once in normal usage patterns -3. Have no side effects and depend only on immutable constructor args - -Known candidates from profiling: -- `FitImaging.model_data` — recomputes blurred image / inversion -- `FitImaging.subtracted_images_of_planes_list` — accesses model_data -- `FitImaging.model_images_of_planes_list` — may also recompute -- `FitDataset.residual_map` — accesses model_data -- `FitDataset.normalized_residual_map` — accesses residual_map -- `FitDataset.chi_squared_map` — accesses residual_map + noise_map -- `FitDataset.log_likelihood` — accesses chi_squared_map - -### Secondary: Other Fit classes - -- `FitInterferometer` — same pattern, different dataset type -- `FitEllipse` / `FitQuantity` — check if same issue exists -- `FitPointDataset` — likely simpler but worth checking - -### Tertiary: Non-fit classes with expensive @property - -While auditing, note any other classes in PyAutoArray / PyAutoGalaxy / -PyAutoLens where `@property` methods do expensive computation that -should be cached. Common patterns: -- Grid transformations computed from immutable mask geometry -- Convolver matrices derived from fixed PSF + mask -- Tracer plane images derived from fixed galaxy list - -## Implementation - -For each candidate: -1. Verify the class is immutable after construction (no setattr on - the attributes the property depends on) -2. Change `@property` to `@cached_property` (from `functools` or - `autoconf`) -3. If the class uses `__getstate__` / `__setstate__` (for pickling), - ensure cached values are included or excluded appropriately -4. If the class is pytree-registered (for JAX), check that cached - values appear in `__dict__` and are handled by the flatten/unflatten - functions — cached properties that produce non-JAX-compatible types - (e.g. Python lists) may need to be excluded from the pytree - -## Testing - -- `pytest test_autoarray/` — must pass -- `pytest test_autogalaxy/` — must pass -- `pytest test_autolens/` — must pass -- Profiling: re-run `autolens_profiling/quick_update/imaging.py` and - `imaging_delaunay.py` to confirm model_data is no longer recomputed - during rendering -- Smoke tests across workspaces - -## What this unblocks - -- Quick-update rendering becomes even faster (model_data computed once - per fit, not once per property access) -- Aggregator scripts that iterate over many fits and access multiple - properties get a proportional speedup -- Any user code that accesses fit properties in a loop benefits diff --git a/active/ci_actions.md b/active/ci_actions.md deleted file mode 100644 index 9e60e263..00000000 --- a/active/ci_actions.md +++ /dev/null @@ -1,76 +0,0 @@ -Phase 5 of the `autolens_profiling` z_feature -(see `z_features/autolens_profiling.md` for the full roadmap). - -Wire up GitHub Actions for the new `autolens_profiling` repo. There are -two distinct workflows here that should be designed and committed -together but kept in separate `.yml` files. - -## Workflow 1 — Lint / format / smoke on PR - -Standard "did the contributor break anything" check that runs on every -PR and on push to `main`. Should be cheap (CPU-only, no JAX needed) and -fast (<5 minutes). - -Things to check: - -- Python syntax + basic linting (`ruff check` is the obvious choice, - matching other PyAutoLabs repos — confirm by looking at how - `PyAutoLens/.github/workflows/` is set up and copy the same config). -- `black` / `ruff format --check` formatting parity. -- Markdown link-rot check on all `README.md` files (use `lychee` or - similar; cheap to run). -- A *smoke* import / dry-run of one script per section to catch obvious - breakage (the smallest pixelization profile script, the cheapest - simulator, the smallest Nautilus run with `n_live=10`). Should not - produce real result artifacts — set an `AUTOLENS_PROFILING_SMOKE=1` - env var the scripts can read to short-circuit to a tiny problem size. - -## Workflow 2 — Re-run profiles + refresh README dashboard - -`workflow_dispatch` (manual trigger) and `release`-triggered workflow -that re-runs the real profile scripts, regenerates the JSON / PNG -artifacts under `results/`, runs the `scripts/build_readme.py` from -Phase 4 to refresh the tables in every README, and commits the result -back to `main`. - -Open decisions the implementer needs to make: - -- **CPU only on GitHub-hosted runners**, *or* **self-hosted runners for - GPU runs?** GitHub-hosted = free + simple but CPU-only. Self-hosted - GPU = expensive operationally but produces the laptop-GPU / HPC-GPU - numbers the front-page table promises. Recommend: start with - GitHub-hosted CPU-only; structure the workflow so a future - self-hosted GPU job can append its numbers without touching the - workflow shape. -- **Cadence**: every release? weekly cron? manual only? Lean toward - "manual + on release tag" so we don't burn CI minutes on noise. -- **Bot identity**: which GitHub Actions identity commits the - refreshed README back to `main`? Probably `github-actions[bot]` with - a `[skip ci]` tag on the commit message to avoid loops. -- **Failure handling**: if one profile script crashes, does the whole - workflow fail or do we mark that cell `ERR` in the tables and - continue? Recommend the latter so a single regression doesn't block - the dashboard refresh. - -## Files to produce - -- `.github/workflows/lint.yml` (Workflow 1) -- `.github/workflows/profile.yml` (Workflow 2) -- `pyproject.toml` (or `setup.cfg`) holding the `ruff` config that the - lint workflow references — copy what PyAutoLens uses. -- A short `.github/workflows/README.md` documenting which workflow is - for what and how to trigger Workflow 2 manually. - -## Pre-flight - -This phase depends on Phase 0 (repo exists) and benefits from Phase 4 -(dashboard build script exists). If Phase 4 hasn't shipped yet, -Workflow 2's "refresh README" step can be a TODO stub that just -commits the new JSONs; flip it on once `build_readme.py` lands. - -## Out of scope - -- Cross-platform matrix (macOS, Windows) — lens-modeling profiling is - Linux-only in practice. -- PyPI / conda packaging — the repo isn't a Python package. -- Coverage reporting — there are no unit tests in this repo, by design. diff --git a/active/ci_linkage.md b/active/ci_linkage.md deleted file mode 100644 index 9ca45962..00000000 --- a/active/ci_linkage.md +++ /dev/null @@ -1,88 +0,0 @@ -# Heart ↔ CI linkage: read release-grade CI from the Actions server, gate the right repos - -Type: feature -Target: PyAutoHeart -Repos: -- PyAutoHeart -Status: planned -Difficulty: too-large -Autonomy: supervised -Priority: high -Milestone: M0 — foundational; the release-validation gate -(`feature/pyautoheart/release_validation.md`) builds on a trustworthy CI signal. - -## Why - -The final design review of the Brain → Health → Heart chain found the CI signal -Heart consumes is too coarse and too narrow to gate a release on, and its repo -registry is stale. These are linkage bugs: Heart can read "CI", but not reliably -the *right* CI. Fix the link before layering the deep release-validation gate on -top of it. - -## Findings to fix (all verified in current code) - -1. **CI granularity is wrong for a gate.** `heart/checks/ci_status.sh` runs - `gh run list --repo --limit 1` — the single most recent run, *any - workflow, any branch*. But each workspace has THREE gating workflows - (`smoke_tests.yml`, `navigator_check.yml`, `url_check.yml`) on **two Pythons - (3.12, 3.13)**, and libraries gate on `pytest`. So "latest run" can report a - green `url_check` while `smoke_tests` was red, or a run on a feature branch. - For release readiness Heart must read the **conclusion of each *required* - workflow on the `main` HEAD commit**, not "the newest run." - -2. **readiness coverage is library-only.** `heart/readiness.py`'s hard gate loops - `DEFAULT_LIBRARIES` (the 5 libs) for CI/branch/dirty/behind. Workspace CI - conclusions are *observed* (`ci_status` writes per-repo sidecars) but **never - folded into the verdict** — only the aggregate `test_run` + `version_skew` - represent workspaces. Decision to make and implement: for release readiness, - **gate the workspaces' `smoke_tests` + `navigator_check` conclusions on main** - (RED on failure), or document explicitly why they stay advisory. (Recommended: - gate them — a red workspace smoke on main is a real release blocker.) - -3. **Read via the CI server, not via local reports.** The canonical continuous - "did it pass" signal should be the **GitHub Actions run conclusion** queried - from the Actions API — which is reachable from mobile via Brain's MCP GitHub - tools — with `report.json` kept as *detail enrichment only*, never a hard - dependency. This directly fixes the mobile fragility where a missing local - `report.json` makes `test_run` report "unknown → YELLOW" even though the cloud - `workspace-validation` run is green and queryable. (`test_run.py` already has a - `_cloud_verdict()` that reads the `workspace-validation.yml` conclusion via - `gh`; generalise that pattern: prefer the server conclusion, enrich with the - report when present, and make the server query work through the agent's MCP - path when `gh` is absent.) - -4. **`config/repos.yaml` is stale — broken linkage.** It still lists - `PyAutoPrompt` (renamed → PyAutoMind) under `build_workflow`, excludes - `PyAutoPaper` (renamed → PyAutoMemory), and does not poll the organism repos - `PyAutoBrain` / `PyAutoHeart` / `PyAutoMemory`. Update the registry to the - current names and add the organism repos so Heart watches what actually - exists. (Heart watching itself is fine and useful.) - -## Scope - -- Rework `ci_status` to record per-required-workflow conclusions on the `main` - HEAD per repo (keep the cheap one-line summary; the readiness consumer reads - the structured per-workflow detail). -- Extend `readiness.py` to gate workspace CI per the decision in finding 2. -- Make the run-conclusion the primary `test_run` signal (server-first, report as - enrichment), with a path that works without `gh` (agent-supplied via MCP). -- Fix `config/repos.yaml` names + add organism repos. -- Keep the `<30s` tick budget: the tick still reads conclusions cheaply (one - `gh`/API call per repo); the heavier per-workflow detail is fine because it is - still just metadata reads, no execution. - -## Validation - -- `pytest tests/` green; add cases for: per-workflow gating (a red `smoke_tests` - with a green `url_check` → RED, not green), workspace-CI gating, and the - server-first `test_run` resolution (report absent but server green → not - "unknown"). -- Run `pyauto-heart tick` + `readiness` and confirm the verdict reflects the - per-workflow, per-repo reality. - -## PR - -"PyAutoHeart: release-grade CI linkage (per-workflow gating, workspace coverage, -server-first signal, registry refresh)". - - diff --git a/active/cli_noise_clean.md b/active/cli_noise_clean.md deleted file mode 100644 index 1bbfe7bc..00000000 --- a/active/cli_noise_clean.md +++ /dev/null @@ -1,5 +0,0 @@ -When we run unit tests, integration tests, scripts and other things we get noise on the command -line due to libraries versions, badly formatted docstrings and other issues. - -Can you do a full run through of different scripts over the projects, find this noise and gradually fix -the issues as they crop up. \ No newline at end of file diff --git a/active/clone_mitosis_agent.md b/active/clone_mitosis_agent.md deleted file mode 100644 index 8cc9cede..00000000 --- a/active/clone_mitosis_agent.md +++ /dev/null @@ -1,58 +0,0 @@ -# PyAutoBrain Clone Agent (Mitosis Agent) to generate new assistants - -Type: feature -Target: PyAutoBrain -Difficulty: large -Autonomy: supervised -Priority: normal -Status: formalised - -Design (not yet implement) a future PyAutoBrain agent that can generate new -domain assistants modelled on autolens_assistant. - -Naming: the engineering term is **Clone Agent** (CLI: `pyauto-brain clone`); -the organism-facing name is **Mitosis Agent** — the agent that lets the PyAuto -organism reproduce a mature assistant cell, copying its core machinery and -then differentiating it for a new organ/domain. `clone` is the better CLI -name; Mitosis Agent is the nicer architecture name for docs. - -## Inputs - -- a source library repo, e.g. PyAutoFit; -- a workspace repo, e.g. autofit_workspace; -- an optional HowTo repo, e.g. HowToFit; -- the reference assistant repo, initially autolens_assistant. - -It produces a new domain assistant modelled on autolens_assistant (e.g. -autofit_assistant, autogalaxy_assistant, autoarray_assistant). - -## Behaviour - -The Clone / Mitosis Agent should: - -- inspect the source library, workspace examples and optional HowTo tutorial - material; -- identify the domain concepts, APIs, workflows and user audiences; -- copy the assistant architecture without blindly copying PyAutoLens-specific - science; -- generate appropriate AGENTS.md, README, skills, wiki structure, source - registry and project workflow; -- distinguish generic assistant infrastructure from domain-specific content; -- produce a **CloneDecision** before writing anything; -- ask whether this is an exact clone, a differentiated sibling, or a - lightweight seed assistant; -- preserve the PyAuto organism boundary: - - PyAutoMind stores the intent to create the assistant; - - PyAutoBrain reasons and plans the clone; - - PyAutoBuild/Hands executes repository creation and file generation; - - PyAutoHeart validates the resulting assistant; - - PyAutoMemory supplies reusable architectural knowledge. - -## Prerequisite - -The autolens_assistant audit prompt (filed separately) should land first: it -makes the reference assistant the clean canonical pattern this agent clones, -and leaves notes on which parts are PyAutoLens-specific vs generic assistant -infrastructure. - - diff --git a/active/codex_brain_skill_wrappers.md b/active/codex_brain_skill_wrappers.md deleted file mode 100644 index 8be0e49d..00000000 --- a/active/codex_brain_skill_wrappers.md +++ /dev/null @@ -1,33 +0,0 @@ -# Add Codex discovery for every PyAutoBrain agent - -Type: maintenance -Target: PyAutoBrain -Repos: -- @PyAutoBrain -Difficulty: medium -Autonomy: safe -Priority: normal -Status: formalised - -## Original request - -> yes, make SKILL.md wrappers for PyAutoBrain and any other repos you see a Claude bias - -## Scope - -Add thin Codex `SKILL.md` wrappers for every public agent exposed by -`bin/pyauto-brain` and for existing Brain command/workflow surfaces. Keep the -existing command Markdown and deterministic agent entrypoints canonical. - -Update `bin/install.sh` so a directory containing both `SKILL.md` and -`.md` installs both surfaces for Claude while installing the skill into -Codex. Add isolated tests for dual-harness installation and remove -Claude-specific assumptions from shared Brain workflow prose. - -## Acceptance criteria - -- Every public `pyauto-brain` conductor and faculty has a discoverable skill. -- Existing Claude commands remain installed when a wrapper is present. -- Existing and new skills install into both Claude and Codex skill roots. -- Tests use temporary destinations and do not modify live agent configuration. -- Skill metadata, links, shell syntax, and line-count checks pass. diff --git a/active/codex_organ_skill_wrappers.md b/active/codex_organ_skill_wrappers.md deleted file mode 100644 index b6247a21..00000000 --- a/active/codex_organ_skill_wrappers.md +++ /dev/null @@ -1,32 +0,0 @@ -# Add Codex wrappers across the remaining PyAuto organs - -Type: maintenance -Target: PyAutoBrain -Repos: -- @PyAutoMind -- @PyAutoHeart -- @PyAutoBuild -Difficulty: medium -Autonomy: safe -Priority: normal -Status: formalised - -Depends on: `maintenance/pyautobrain/codex_brain_skill_wrappers.md` - -## Original request - -> yes, make SKILL.md wrappers for PyAutoBrain and any other repos you see a Claude bias - -## Scope - -Apply the phase-1 wrapper and installer contract to command-only canonical -skills in PyAutoMind, PyAutoHeart, and PyAutoBuild. Update ownership and usage -wording that assumes Claude is the only harness. Preserve Heart's intentional -reference-only `/health` legs as non-top-level skills. - -## Acceptance criteria - -- `spawn`, `review_release`, `verify_install`, and `pre_build` are Codex skills. -- Existing Claude command behavior remains available. -- Cross-organ ownership documentation describes both harnesses. -- All wrappers pass the same validation and isolated installer tests as phase 1. diff --git a/active/codex_skill_metadata.md b/active/codex_skill_metadata.md deleted file mode 100644 index c188d36f..00000000 --- a/active/codex_skill_metadata.md +++ /dev/null @@ -1,26 +0,0 @@ -# Normalize profiling skill metadata for Codex - -Type: maintenance -Target: autolens_profiling -Repos: -- @autolens_profiling -Difficulty: small -Autonomy: safe -Priority: normal -Status: formalised - -Depends on: `maintenance/pyautobrain/codex_brain_skill_wrappers.md` - -## Original request - -> yes, make SKILL.md wrappers for PyAutoBrain and any other repos you see a Claude bias - -## Scope - -Change the existing `profile_likelihood` skill's frontmatter name to the -Codex-compatible `profile-likelihood`, validate the skill, and verify that the -dual-harness installer keeps the existing Claude directory while creating the -hyphenated Codex directory. - -This task must remain queued until current `autolens_profiling` worktree claims -clear; do not override profiling work for a metadata-only change. diff --git a/active/colab_link_rot.md b/active/colab_link_rot.md deleted file mode 100644 index 7ca2ac53..00000000 --- a/active/colab_link_rot.md +++ /dev/null @@ -1,53 +0,0 @@ -# Colab link rot: fix stale/wrong URLs + purge allowlists (phase 2 of colab-maturity) - -Autonomy: safe -Difficulty: medium -Status: launched 2026-07-09 --auto (human-directed in-session after merging phase 1). -Phase 1 merged 2026-07-09 (PyAutoConf#120, PyAutoBuild#125, PyAutoHeart#44); the new -Heart forbidden-URL patterns turn the Monday sweep red until this phase lands. - -SCOPE SPLIT at launch: PyAutoLens + PyAutoGalaxy are claimed by -release-docs-polish-learn-paths (awaiting-input, uncommitted) — their docs/howto* -dead-link fixes + allowlist purges are DEFERRED to a follow-up leg blocked on that -task. This run covers the unclaimed repos: HowToFit, HowToGalaxy, HowToLens, -euclid_strong_lens_modeling_pipeline. - -## Context - -Census 2026-07-09 (see `issued/colab_maturity.md` for the full census). Phase 1 -made every generated notebook Colab-runnable by construction and hardened the -guards. This phase fixes the accumulated link rot the guards now flag. - -Notebook regeneration is NOT part of this phase — the next release regenerates -all notebooks through PyAutoBuild's new injection path automatically. - -## The work - -1. **HowToLens** — chapter READMEs (`scripts/*/README.md`, mirrored into - `notebooks/*/README.md` by generate.py): 71 unpinned `blob/main` Colab URLs → - pin to the current release tag (the bumper maintains them thereafter). Fix - dead filenames: `tutorial_11_adapt_regularization.py.ipynb` → actual - `tutorial_11_adaptive_regularization.ipynb`; `tutorial_3_pixelizations` → - actual name; `tutorial_6_modeling` → `tutorial_6_lens_modeling`; verify every - link target exists at the pinned tag. -2. **HowToGalaxy** — chapter READMEs: 28 Colab URLs point at the WRONG repo - (`autogalaxy_workspace/.../notebooks/chapter_*` — chapters live in - HowToGalaxy). Repoint to `PyAutoLabs/HowToGalaxy/blob//...`. -3. **HowToFit** — chapter READMEs are tag-pinned + right repo; verify link - targets (allowlist shows two moved/renamed tutorials frozen at 2026.5.14.2). -4. **PyAutoLens / PyAutoGalaxy docs** — fix or remove dead Colab links - (`chapter_optional` tutorials, renamed pixelization tutorials) in - `docs/howtolens/` / `docs/howtogalaxy/`. -5. **euclid_strong_lens_modeling_pipeline/README.md** — forbidden - `Jammy2211` Colab URL → PyAutoLabs (or drop). -6. **Allowlist purge** — remove the now-fixed Colab entries from every - `.url_check_allowlist.txt` (PyAutoLens, PyAutoGalaxy, HowToLens, HowToGalaxy, - HowToFit); several entries are themselves stale (frozen 2026.5.14.2 forms). -7. **Verify** — run `PyAutoHeart/heart/checks/url_check.sh` (offline) per repo - and the live sweep locally; Monday cron should go green. - -## Conflicts to check at issue time - -At census time, `release-docs-polish-learn-paths` claimed PyAutoLens, -PyAutoGalaxy, autolens_workspace; `ep-examples-tests` claimed autofit_workspace. -Re-run `worktree_check_conflict` before starting. diff --git a/active/colab_maturity.md b/active/colab_maturity.md deleted file mode 100644 index 0e8c87d7..00000000 --- a/active/colab_maturity.md +++ /dev/null @@ -1,93 +0,0 @@ -# Colab infrastructure: census follow-up — polish, mature, maintainable - -Status: prompt - -## Original request (verbatim) - -> We have most of the infrastructure in place to make workspace exampels and -> tutorials run in Google colab, such that users can mess around with -> everything without installation. This includes docs pointing to them, links -> on our README.md, etc. But, the infrastructure could be developed in a more -> mature and robust way, including better on going maintenance during release -> and whatnot. Given we only have Fable access until Sunday, can you do a -> census of the Google Colab setup for all projects and do the work to make it -> more polished, mature and maintainable? - -## Census findings (2026-07-09) - -The Colab stack today: - -1. `PyAutoConf/autoconf/setup_colab.py` — runtime bootstrap (`for_autolens`, - `for_autogalaxy` only). Installs the stack `--no-deps`, clones the - workspace `main` HEAD, pushes config paths. -2. Hand-written Colab setup cells in exactly **9 / 489** notebooks (6 - autolens + 3 autogalaxy `start_here` scripts). autofit_workspace, HowToFit, - HowToGalaxy, HowToLens: zero. -3. `PyAutoBuild/autobuild/bump_colab_urls.sh` — release-time tag bumper, - tested, wired into `release.yml` (`release_workspaces` + - `bump_library_colab_urls`), rehearsal-aware. Only bumps canonical - date-tagged `PyAutoLabs/` URLs. -4. PyAutoHeart central `url-check.yml` (Monday cron) — offline forbidden - patterns (`url_check.sh`) + live Colab→raw 404 audit - (`url_check_live.py`) with per-repo `.url_check_allowlist.txt`. -5. Entry links in the 3 library READMEs/docs, 3 workspace READMEs, and HowTo - chapter pages, pinned to the current tag. - -### Defects / gaps - -- **Coverage**: docs link 100+ notebooks to Colab (all HowTo chapters in - PyAutoLens/PyAutoGalaxy docs, PyAutoFit README badge → - `overview_1_the_basics.ipynb`) but only the 9 start_here notebooks have a - setup cell — everything else dies on `ModuleNotFoundError`. No - `for_autofit` or HowTo* helpers exist. `generate.py` does plain py→ipynb, - no injection. -- **Link rot, allowlisted instead of fixed**: - - HowToLens chapter READMEs (scripts/ + notebooks/): 71 unpinned - `blob/main` Colab URLs — the bumper never touches them. - - HowToGalaxy chapter READMEs: 28 URLs point at the **wrong repo** - (`autogalaxy_workspace/.../chapter_*` instead of `HowToGalaxy/...`) — - dead, yet re-bumped every release. - - Dead filenames: `tutorial_11_adapt_regularization.py.ipynb` (actual: - `tutorial_11_adaptive_regularization.ipynb`), `tutorial_3_pixelizations` - (actual: `tutorial_1_pixelizations`), `tutorial_6_modeling` (actual: - `tutorial_6_lens_modeling`), removed `chapter_optional` tutorials still - linked from PyAutoLens/PyAutoGalaxy docs. - - `euclid_strong_lens_modeling_pipeline/README.md` has a forbidden - `Jammy2211` Colab URL. - - Allowlist colab entries are themselves stale (frozen at `2026.5.14.2` - forms that no longer match the files). -- **`setup_colab.py` defects**: `no_gpu` unbound if JAX returns no devices, - and only the last device's status counts; module-level - `os.environ['XLA_FLAGS']` mutation on import; duplicated package lists; - clones workspace `main` HEAD (version skew vs pip-installed release and the - tagged notebook), no `--depth 1`; no tests in `test_autoconf/`. -- **Guard gaps**: `url_check.sh` doesn't forbid unpinned `blob/main` Colab - URLs; nothing asserts a Colab-linked notebook can bootstrap itself. - -## The work - -1. **PyAutoConf** — generalize `setup_colab` into a single parameterized - registry covering autofit / autogalaxy / autolens / HowToFit / HowToGalaxy - / HowToLens; fix the `no_gpu` bug; move the env mutation inside the setup - function; one shared package table; clone the tag matching the installed - release (`--branch --depth 1`, fallback `main`); unit tests. -2. **PyAutoBuild** — inject a standard Colab setup cell (markdown + code) into - every generated notebook at `generate.py` / `build_util.py_to_notebook` - time, parameterized per project; handle the 9 scripts with hand-written - sections (strip or skip); tests; document the end-to-end Colab - architecture in `docs/internals.md`. -3. **Workspaces + HowTo repos (6)** — remove/align hand-written setup - sections, regenerate notebooks, fix chapter README URLs (right repo, - tag-pinned, real filenames). -4. **Library docs (PyAutoFit / PyAutoGalaxy / PyAutoLens)** — fix dead Colab - links (renamed/removed tutorials), then purge the now-fixed colab entries - from every `.url_check_allowlist.txt`. -5. **PyAutoHeart** — extend `url_check.sh` forbidden patterns: unpinned - `blob/main` (and non-date-tag) Colab URLs to the 6 notebook repos, so the - bumper's blind spot can't recur. Keep the live sweep as the existence - check. -6. **euclid_strong_lens_modeling_pipeline** — fix the `Jammy2211` Colab URL. - -Release maintenance (bumper) already works; after this the only per-release -moving part remains the URL tag bump, and setup-cell coverage is guaranteed -by construction at generation time. diff --git a/active/colab_sim_verify_install.md b/active/colab_sim_verify_install.md deleted file mode 100644 index ecfdd59e..00000000 --- a/active/colab_sim_verify_install.md +++ /dev/null @@ -1,40 +0,0 @@ -# Colab-simulation leg in verify_install (closes the last Colab maturity gap) - -Autonomy: safe -Difficulty: medium -Status: launched 2026-07-09 (user-approved in-session: "ok do this" on the exact proposal) - -## Original request (verbatim) - -> ok do this: - Nothing actually executes the Colab bootstrap path. The setup cell is -> unit-tested and the no-op-outside-Colab path is exercised everywhere, but no CI -> simulates a real Colab session (fresh env → pip from PyPI → setup_colab.setup() → -> clone → run a cell). Heart's verify_install is the nearest thing; a "colab-simulation" -> leg there (or a periodic real-Colab manual check, which your PyAutoMind overview -> already lists as a manual step) would close it. - -## Design - -New **check F** in `PyAutoHeart/heart/checks/verify_install.sh` (fits the existing -A–E suite: throwaway venv, PASS/FAIL/SKIP row, JSON sidecar → readiness): - -1. venv + `pip install autolens jax` — emulates Colab's preinstalled base env - (honours --version / --testpypi like check A). -2. Install a fake `google.colab` stub into the venv's site-packages so - `import google.colab` succeeds — activating the real on-Colab code path. -3. Run the injected setup cell's code verbatim: bootstrap `pip install autoconf - --no-deps`, then `setup_colab.setup("autolens", raise_error_if_not_gpu=False, - workspace_dir=)`. If the installed autoconf predates the registry - (`setup` missing) → SKIP with "ships next release" (honest, self-activating). -4. Assert: workspace cloned (at the installed-release tag when it exists), cwd - moved into it, autoconf config path pushed. -5. "Run a cell": `import autolens as al` + `al.Imaging.from_fits(dataset/imaging/ - simple/...)` from the cloned workspace — proves a notebook body would run. - -**PyAutoConf**: add `workspace_dir: str | None = None` override to -`setup_colab.setup` (threads into `_colab_setup`/`_clone_workspace`) — needed -because the registry hardcodes Colab's `/content/...`, unwritable in CI. Update -unit tests. - -Deep on-demand check — never in the <30s tick; runs via `pyauto-heart -verify_install` with the existing sidecar consumption. diff --git a/active/contents_block_renders_as_paragraph.md b/active/contents_block_renders_as_paragraph.md deleted file mode 100644 index 28642b20..00000000 --- a/active/contents_block_renders_as_paragraph.md +++ /dev/null @@ -1,103 +0,0 @@ -# Contents block renders as one paragraph in generated notebooks - -## The problem - -Every workspace tutorial script that has a `__Contents__` block at the top of -its module docstring uses the same pattern: - -``` -__Contents__ - -**Model:** Compose the lens model fitted to the data. -**Plotters:** Overview of plotting tools used for visualization. -**Dataset Paths:** The `dataset_type` describes the type of data ... -**Grid:** Define the 2d grid of (y,x) coordinates ... -``` - -When PyAutoBuild's `generate.py` converts the script to a notebook, this -block becomes the contents of the first markdown cell. Markdown collapses -consecutive non-blank lines into a single paragraph, so on GitHub -(and in JupyterLab) every contents entry runs together as one continuous -sentence — exactly the opposite of what the index is for. - -Confirmed in `ic50_workspace`'s `scripts/simulator.py` and verified by -inspecting `autolens_workspace/notebooks/imaging/simulator.ipynb`'s top -cell — the same one-big-paragraph rendering appears. - -## The fix - -Convert each `**Section:** description.` line to a Markdown bullet: - -``` -__Contents__ - -- **Model:** Compose the lens model fitted to the data. -- **Plotters:** Overview of plotting tools used for visualization. -- **Dataset Paths:** The `dataset_type` describes the type of data ... -- **Grid:** Define the 2d grid of (y,x) coordinates ... -``` - -Wrapped lines need a 2-space indent so they continue the same list item: - -``` -- **Real Data Preview:** Show one example real GDSC2 curve to anchor what - the simulator is trying to reproduce. -``` - -The change is purely Markdown — no Python semantics change, no .py-script -behaviour change, no notebook-execution change. Diff is text-only inside -triple-quoted module docstrings. - -A worked example shipped with `ic50_workspace` (commit -`4cde480` on `main`): - - -## Scope - -Apply this fix to every script in every workspace that has a -`__Contents__` (or equivalent inline index) block in its module docstring. -Confirmed-affected workspaces: - -- `autolens_workspace` -- `autogalaxy_workspace` -- `autofit_workspace` -- `autolens_workspace_test` -- `autogalaxy_workspace_test` -- `autofit_workspace_test` -- `HowToLens` -- `HowToGalaxy` -- `HowToFit` -- `euclid_strong_lens_modeling_pipeline` -- `BSc_Galaxies_Project` - -For each repo: - -1. `grep -rln "__Contents__" scripts/` to find the affected files. -2. For each file, inside the top-level `"""..."""` module docstring, - rewrite the `__Contents__` block so each `**Section:**` entry becomes - a `- **Section:**` bullet, with continuation lines indented two - spaces. -3. Spot-check by regenerating the notebook for one or two scripts - (`PYTHONPATH=../PyAutoBuild/autobuild python3 ../PyAutoBuild/autobuild/generate.py `) - and visually confirming the cell now renders as a list, not a - paragraph. -4. Commit per repo with a single tidy commit (e.g. - `docs: render __Contents__ blocks as Markdown lists`) and push; - notebook regeneration is normally handled by the next `pre_build` - run, so don't dirty up unrelated notebooks unless the workspace's - own CI requires it. - -## Other Markdown paragraph-collapse risks worth a quick scan - -Same root cause may bite anywhere a docstring uses adjacent -non-blank lines that the author intended as separate items: - -- `__Model__` blocks in workspace simulators that list bullet-like items - with a single leading space (` - foo` vs `- foo`) — these usually do - render as lists because Markdown allows up to 3 leading spaces, but - worth eyeballing. -- Any `Steps`, `Notes`, `Outputs` block where each line starts with a - bold label. - -Don't go beyond `__Contents__` unless you find an additional concrete -broken example — this prompt's scope is the contents-block fix. diff --git a/active/convergence_func_xp_threading.md b/active/convergence_func_xp_threading.md deleted file mode 100644 index 9492dce7..00000000 --- a/active/convergence_func_xp_threading.md +++ /dev/null @@ -1,38 +0,0 @@ -Thread xp=np through convergence_func for profiles that currently lack it. - -## Problem - -Several profiles' `convergence_func` methods don't accept `xp=np`, causing -`MGEDecomposer.decompose_convergence_via_mge` to crash with TypeError when -the MGE potential calls it with `xp=xp`. Currently caught and SKIPped in the -test suite, meaning these profiles' MGE-based potential is silently unavailable. - -## Affected Profiles - -- `PowerLawBroken` — inherits abstract `MassProfile.convergence_func` which - doesn't accept `xp` -- `dPIEMass` — same -- `dPIEPotential` — same -- `SersicGradient` — overrides `convergence_func` without `xp` parameter - -## Fix - -1. Add `xp=np` to `MassProfile.convergence_func` in `abstract/abstract.py` -2. Add `xp=np` to `SersicGradient.convergence_func` in `stellar/sersic_gradient.py` -3. Thread `xp=xp` in any internal calls within these methods -4. For profiles that override `convergence_func` (check all subclasses), ensure - `xp=np` is accepted - -After this fix, the MGE potential should work for PowerLawBroken, dPIEMass, -dPIEPotential, and SersicGradient — removing 14 SKIPs from the test suite -(though they may become FAILs due to Issue 1 if elliptical). - -## Verification - -Run `scripts/mass/total.py` and `scripts/mass/stellar.py` in -@autolens_workspace_test. Profiles that currently SKIP should now run -(PASS for spherical, FAIL for elliptical pending Issue 1 fix). - -## Repos - -- @PyAutoGalaxy (primary) diff --git a/active/cse_jax_port.md b/active/cse_jax_port.md deleted file mode 100644 index d3361461..00000000 --- a/active/cse_jax_port.md +++ /dev/null @@ -1,48 +0,0 @@ -Port the CSE (Cored Steep Ellipsoid) module in PyAutoGalaxy to support JAX. - -## Goal - -Make `@PyAutoGalaxy/autogalaxy/profiles/mass/abstract/cse.py` JAX-compatible by threading the `xp=np` parameter through all methods, mirroring how the MGE module (`mge.py`) already supports both NumPy and JAX backends. - -## What to Change - -### cse.py Methods - -1. `convergence_cse_1d_from(grid_radii, core_radius)` — static method - - Currently pure NumPy. Add `xp=np` parameter, no numpy calls to replace (pure arithmetic), but signature must accept `xp` for consistency. - -2. `deflections_via_cse_from(term1, term2, term3, term4, axis_ratio_squared, core_radius)` — static method - - Replace `np.sqrt` → `xp.sqrt`, `np.vstack` → `xp.vstack` - - Add `xp=np` parameter - -3. `_deflections_2d_via_cse_from(self, grid, **kwargs)` — instance method - - Thread `xp` through to `deflections_via_cse_from` calls - - Replace any `np.*` with `xp.*` (grid operations use `.array` already) - -4. `_convergence_2d_via_cse_from(self, grid_radii, **kwargs)` — instance method - - Thread `xp` through to `convergence_cse_1d_from` calls - -5. `_decompose_convergence_via_cse_from(self, func, radii_min, radii_max, ...)` — the decomposition solver - - This uses `scipy.linalg.lstsq` which has no JAX equivalent that works inside JIT - - **Design:** The decomposition (fitting amplitudes + core radii) is a one-time setup step, not part of the JIT-traced forward pass. Keep `scipy.linalg.lstsq` for the NumPy path. For JAX, add a `xp is not np` branch using `jnp.linalg.lstsq`. The decomposition results (amplitude_list, core_radius_list) should be cached on the profile instance so they're computed once and reused. - - Replace `np.logspace`, `np.zeros`, `np.log10` with `xp.*` equivalents - -### Callers - -All profiles that inherit `MassProfileCSE` and call these methods must thread `xp=xp` through: -- `@PyAutoGalaxy/autogalaxy/profiles/mass/dark/nfw.py` (NFW uses CSE for deflections) -- Any other dark matter profiles that mix in `MassProfileCSE` - -### Tests - -- Add CSE-based profiles (NFW via CSE path) to `@autolens_workspace_test/scripts/profiles_jit.py` in the JAX three-step pattern (NumPy / JAX outer / JAX JIT). -- Verify the Phase 1 self-consistency test suite still passes after the port. - -## Key Constraint - -The CSE decomposition (`_decompose_convergence_via_cse_from`) must NOT be called inside a `jax.jit` trace. It is a setup computation. The forward methods (`_deflections_2d_via_cse_from`, `_convergence_2d_via_cse_from`) that consume the cached decomposition results ARE traced and must be pure `xp` code. - -## Repos - -- @PyAutoGalaxy (primary) -- @autolens_workspace_test (test additions) diff --git a/active/data_preparation.md b/active/data_preparation.md deleted file mode 100644 index bbf53d4d..00000000 --- a/active/data_preparation.md +++ /dev/null @@ -1,9 +0,0 @@ -The files in autogalaxy_workspace/scripts/imaging/data_preparation were lost, -but they are still backed up and safe in /mnt/c/Users/Jammy/Code/PyAutoOld/AIBACKUP/autogalaxy_workspace/scripts/imaging/data_preparation. - -Th esame is true for autolens_workspace/scripts/imaging/data_preparation and -/mnt/c/Users/Jammy/Code/PyAutoOld/AIBACKUP/autolens_workspace/scripts/imaging/data_preparation.py - -Can you restore the code and make sure it works and runs on the new API? - -Can you also work out why we lost so much code and make sure we dont agian. \ No newline at end of file diff --git a/active/data_typing.md b/active/data_typing.md deleted file mode 100644 index c3424c9f..00000000 --- a/active/data_typing.md +++ /dev/null @@ -1,107 +0,0 @@ -The different data structures in PyAutoArray are quite confusing, in some ways: - -- Array2D -- ArrayIrrregular -- Grid2D -- GridIrregular - -And so on. - -They serve an important purpose, unifying the API and abstractions in a way which ensures a user can understand -that data and grids can be paired to something uniform or not. Furthermore, the slam / native API is important -in making both accessible, and streamlining how masked data vectors are stored. - -First, we should assess if this code can be simplified at all or if its acceptable. I dont have a better idea. - -You would probably beenfit from reading @autolens_workspace/scripts/guides/data_structurs.py to see how a user -interfaces with these objects. - -However, things get more complex, as these objcts are used to define mappings at the profile level of -@PyAutoGalaxy/autoglaxy/profiles. For example for this function: - -class Isothermal(PowerLaw): - def __init__( - self, - centre: Tuple[float, float] = (0.0, 0.0), - ell_comps: Tuple[float, float] = (0.0, 0.0), - einstein_radius: float = 1.0, - ): - """ - Represents an elliptical isothermal density distribution, which is equivalent to the elliptical power-law - density distribution for the value slope = 2.0. - - Parameters - ---------- - centre - The (y,x) arc-second coordinates of the profile centre. - ell_comps - The first and second ellipticity components of the elliptical coordinate system. - einstein_radius - The arc-second Einstein radius. - """ - - super().__init__( - centre=centre, - ell_comps=ell_comps, - einstein_radius=einstein_radius, - slope=2.0, - ) - - def axis_ratio(self, xp=np): - axis_ratio = super().axis_ratio(xp=xp) - return xp.minimum(axis_ratio, 0.99999) - - @aa.grid_dec.to_vector_yx - @aa.grid_dec.transform - def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs): - """ - Calculate the deflection angles on a grid of (y,x) arc-second coordinates. - - Parameters - ---------- - grid - The grid of (y,x) arc-second coordinates the deflection angles are computed on. - """ - - factor = ( - 2.0 - * self.einstein_radius_rescaled(xp) - * self.axis_ratio(xp) - / xp.sqrt(1 - self.axis_ratio(xp) ** 2) - ) - - psi = psi_from( - grid=grid, axis_ratio=self.axis_ratio(xp), core_radius=0.0, xp=xp - ) - - deflection_y = xp.arctanh( - xp.divide( - xp.multiply(xp.sqrt(1 - self.axis_ratio(xp) ** 2), grid.array[:, 0]), - psi, - ) - ) - deflection_x = xp.arctan( - xp.divide( - xp.multiply(xp.sqrt(1 - self.axis_ratio(xp) ** 2), grid.array[:, 1]), - psi, - ) - ) - return self.rotated_grid_from_reference_frame_from( - grid=xp.multiply(factor, xp.vstack((deflection_y, deflection_x)).T), - xp=xp, - **kwargs, - ) - -The decorator @aa.grid_dec.to_vector_yx is used to understand that fr this function, the result -that comes out must be a VectorYX object. It handles more typing, for example if a Grid2DIrregular comes in -a VectorYXIrregular comes out, but a Grid2D produces a VectorYX object. - -There is also b ehaviour where if a numpy ndarray comes in, a numpy ndarray comes out without type casting, -with the same behaviour for a JAX array. - -The problem is really just how complex things got i this decorator, which is all handled at -@PyAutoArray/autoarray/structures/decorators. Its complicated, messy and hard to trace. - -So, can you give me your assessment of whether theres a quite large, sweeping restructure that an simplify trhis -code but retain the desired functionality and behaviour? Think hard, this could require a good chunk of planning -and no doubt extensie testing after! \ No newline at end of file diff --git a/active/datacube_delaunay_release_memory.md b/active/datacube_delaunay_release_memory.md deleted file mode 100644 index b926f72c..00000000 --- a/active/datacube_delaunay_release_memory.md +++ /dev/null @@ -1,28 +0,0 @@ -# Datacube Delaunay release memory failure - -## Original Request - -ok merge and on to the next fix - -## Context - -After merging the Autolens JAX simulator release fixes, continue the release -failure list with the heavy datacube JAX Delaunay failure. - -## Current Failure - -Primary repo: @autolens_workspace_test - -- `@autolens_workspace_test/scripts/jax_likelihood_functions/datacube/delaunay.py` - - The DFT vmap and `jax.jit(factor_graph.log_likelihood_function)` checks pass - on current `main`. - - The process then dies during the `TransformerNUFFT` Delaunay cube - cross-check before printing the NUFFT result, matching the release report's - SIGKILL-style failure. - -## Proposed Scope - -Reduce or restructure the release script so it still validates the Delaunay -datacube JAX path without exceeding the release runner memory budget. Treat this -as workspace-only unless investigation proves the NUFFT memory spike is caused -by a library regression that should be fixed in @PyAutoArray or @PyAutoLens. diff --git a/active/datacube_shared_state_consumer.md b/active/datacube_shared_state_consumer.md deleted file mode 100644 index 7625f519..00000000 --- a/active/datacube_shared_state_consumer.md +++ /dev/null @@ -1,84 +0,0 @@ -# Datacube consumer for the cross-`Analysis` shared-state mechanism - -**Sub-task B of the `analysis_shared_state` epic** (see -`PyAutoPrompt/z_features/analysis_shared_state.md`). Do **not** `/start_dev` this -until sub-task A (the PyAutoFit mechanism + 1D Gaussian toy + autofit workspace -tutorial/tests, PyAutoFit#1307) is close to shipping — the mechanism it consumes -is delivered there. - -Primary repo: **@PyAutoLens**. Consumers/proof: **@autolens_workspace**, -**@autolens_workspace_test**, **@autolens_profiling**. - -## Depends on (delivered by sub-task A) - -PyAutoFit ships the generic, domain-agnostic protocol: - -- `Analysis.shared_state_from(instance) -> None` (opt-in, default `None`), the - per-evaluation cross-factor sibling of `modify_before_fit`. -- `log_likelihood_function(self, instance, shared=None, ...)` — defaulted kwarg. -- `FactorGraphModel.log_likelihood_function` computes the shared object **once** - from the lead factor before the per-factor loop and forwards it to each factor - **only when non-`None`** (so non-cube graphs are byte-for-byte unchanged). - -The 1D Gaussian toy in `af.ex` + `autofit_workspace` + `autofit_workspace_test` -is the worked, tested reference for how a consumer implements `shared_state_from` -and a `shared`-aware `log_likelihood_function`. **Mirror it.** - -## Scope (Phases 4-5 of the original prompt) - -### Phase 4 — PyAutoLens: the datacube consumer -- On the interferometer datacube path, implement the lensing-specific - `shared_state_from`: ray-trace the shared lens model once, build the Delaunay - mapper + mapping matrix `L` (and, where `uv_wavelengths`/`noise_map` are - channel-invariant, the curvature `F = LᵀW̃L`) once, returning them as a normal - **JAX pytree** of traced arrays (recomputed inside the jitted region each eval, - never memoised on the instance — see `feedback_jax_closure_cache_busts`). -- Make `AnalysisInterferometer.log_likelihood_function(self, instance, shared=None)` - consume the shared object in place of its own rebuild; the `shared is None` - fallback rebuilds everything so the single-channel path is unchanged. -- Per-channel work that remains: data vector `D = Lᵀ·dirty_image` (channel - visibilities), NNLS reconstruction, log-evidence. -- **Fall back to the current per-channel path** when the channel-invariance - precondition does not hold (`uv_wavelengths`/`noise_map` not ~channel-invariant — - i.e. outside the narrow-emission-line regime). The consumer owns this guard; - PyAutoFit trusts the provider. - -### Phase 5 — autolens_workspace + autolens_workspace_test + profiling -- Update the datacube modeling/likelihood scripts - (`autolens_workspace/scripts/interferometer/features/datacube/{likelihood_function,modeling,delaunay}.py`) - to opt into the shared path. -- Add a **fast-assert datacube script** to `autolens_workspace_test` mirroring the - autofit_workspace_test pattern: prove `shared_state_from` runs once per eval - (counter), shared-vs-unshared likelihood equality, and a tiny end-to-end run. -- Re-run `autolens_profiling/likelihood_breakdown/datacube/delaunay.py` (carrying - the inversion-setup sub-decomposition step) and record the new cube cost. Per - the decomposition, ~97% of the per-channel inversion work is shareable, so the - inversion-setup block should drop ~17× for a 34-channel cube (≈60 s → ≈3.5 s); - the cube total drops from ~170 s toward the per-channel residual (data-vector - matmul + NNLS + log-ev) plus one shared mapper+L+F build. Compare against the - `inversion_setup_decompose_*.json` artifact for the channel-invariant/variant - split that sets the ceiling. Re-measure at ALMA scale on a quiet A100 to pin the - cube-level number (laptop SMA seconds are provisional; the ratios are robust). - -## Critical files - -PyAutoLens (Phase 4): -- the `AnalysisInterferometer` likelihood path + interferometer `Inversion`/mapper construction - -Workspace / test / profiling (Phase 5): -- `autolens_workspace/scripts/interferometer/features/datacube/{likelihood_function,modeling,delaunay}.py` -- `autolens_workspace_test/` — new fast-assert datacube script -- `autolens_profiling/likelihood_breakdown/datacube/delaunay.py` -- `autolens_profiling/likelihood_runtime/OPTIMIZATION_NOTES.md` - -## Out of scope -- A bespoke `DataCube` data class / cube-specific `Inversion` (the rejected option). -- The dense-route variant (production uses the sparse / w̃ route; dense is not the target). -- Cross-factor *gradients* — likelihood value only. - -## Cross-references -- `PyAutoPrompt/z_features/analysis_shared_state.md` — the epic tracker -- PyAutoFit#1307 — sub-task A (the mechanism + toy this consumes) -- autolens_workspace#120 — Aris's shared-`Lᵀ W̃ L` optimisation, the origin -- `PyAutoPrompt/issued/alma_datacube.md` — Aris's Slack design + channel-invariance caveat -- `PyAutoPrompt/autoarray/datacube.md` — the "analysis list API does not share information" problem statement diff --git a/active/deep_audit_skills_tooling.md b/active/deep_audit_skills_tooling.md deleted file mode 100644 index 598173c4..00000000 --- a/active/deep_audit_skills_tooling.md +++ /dev/null @@ -1,65 +0,0 @@ -# Deep audit of autolens_assistant skills prose and tooling robustness - -Type: feature -Target: autolens_assistant -Difficulty: too-large -Autonomy: supervised -Priority: normal -Status: formalised - -Phase 2 of the reference-assistant audit (phase 1: issue #33 / PR #34 — README/AGENTS -cleanup and the two-mode model). Phase 1 deliberately did not read the individual skill -bodies or the tooling; this prompt is that deep pass. Run it with a strong reviewing model -(Fable): the skills themselves are executed by whatever model the user has, so the review -standard is "would this recipe steer a *weaker* model correctly?", not "can a strong model -fill the gaps?". - -**Prerequisite:** PR #34 merged, so the two-mode (teacher/assistant) model is settled text. - -**Expect to split into phased PRs at start_dev time** — suggested cut: - -## PR A — skill prose review (highest value) - -- Read every **mature** skill body end-to-end against `skills/_style.md` and the live - installed API (source-of-truth order in AGENTS.md; the code gate protects symbols, not - reasoning). -- Tighten the Orient → Ask → Branch → Combine arcs: weak phrasing, buried decisions, - missing "when NOT to use this", stale workspace-example pointers. -- Judge each recipe as instructions for a weaker model: are the steps executable without - inference leaps? Are failure modes and checks explicit? Fix in place; keep skill names - and frontmatter contracts stable. -- `skills/_style.md` itself gets the same treatment (phase 1 only touched one sentence). -- Optionally promote 1–2 stubs from the pending queue to full recipes if the review pass - makes them cheap (`al_subhalo_detect` first — README example 3 exercises it); do not - mass-fill stubs. - -## PR B — tooling robustness - -- `autoassistant/audit_skill_apis.py`: error paths, exit-code contract, and the - version-check UX — a version skew currently prints the same multi-paragraph - WorkspaceVersionMismatchError three times (once per library import); it should report - once, short, with the two remedies. Found while validating phase 1. -- The PreToolUse code-gate hook: review wiring, bypass ergonomics, false-positive/negative - behaviour on the current stack. -- `activate.sh`, `Makefile` targets, `config/` defaults, sandbox/cache env-var handling - (`NUMBA_CACHE_DIR`, `MPLCONFIGDIR`, `PYAUTO_SKIP_WORKSPACE_VERSION_CHECK`). -- Refresh the pinned version baseline against the installed 2026.7 stack (the standing - Heart skew finding) or record why it must wait for the next release. - -## PR C — workflow skills + wiki spot-checks - -- `start-new-project.md` end-to-end review — the highest-stakes skill (it scaffolds user - repos); verify the copy/never-copy lists, lifecycle stages, and refer-back contract - against the actual template behaviour. -- `contribute-upstream.md`, `init-slam.md`, `_bootstrap_skill.md` same treatment. -- Spot-check `wiki/core/` operational pages (installation, hpc, sandbox, dataset) against - the installed stack; flag — don't rewrite — scientific content issues in - `wiki/literature/`. - -## Constraints (unchanged from phase 1) - -- AGENTS.md stays canonical; no parallel instruction files; safety invariants untouched. -- Small reviewable diffs per skill; no renames; no premature generalisation — respect the - template-boundary notes in `modes/maintainer.md`. -- Validate each PR: `audit_skill_apis.py` symbol audit, `make - validate-literature-citations`, link sweep; draft PRs, merge stays human. diff --git a/active/default_branch_release_to_main.md b/active/default_branch_release_to_main.md deleted file mode 100644 index 280c2b3a..00000000 --- a/active/default_branch_release_to_main.md +++ /dev/null @@ -1,50 +0,0 @@ -Three workspace repos currently have `release` set as their GitHub default branch. `release` is supposed to be a downstream branch updated only by PyAutoBuild when it merges `main → release` during a release cut. Having `release` as the default means `gh pr create` without `--base main`, and the GitHub UI "Compare & pull request" button, both silently target `release`. PRs that land on `release` are orphaned from `main`'s history and get overwritten by the next `main → release` sync. - -This already happened: autolens_workspace PRs #54, #55, #58, #59, #61 and autogalaxy_workspace PR #28 were merged to `release` over 2026-04-13 → 2026-04-14 without anyone noticing. All have since been replayed onto `main` via cherry-pick PRs (autolens_workspace #62, #63 and autogalaxy_workspace #29), but the underlying misconfiguration is still there and will keep causing drift until fixed. - -## Reason - -Currnetly, release is what links to the pypi release, such that if a user runs pip install autolens, the -release branch should be in sync. - -What we should do instead is have workspaces use main, but have it so the two following things happen: - -1) Workspaces have tagged or version github branches, so that when a PyAuto version is released users can pair it to a specific workspace. -2) All PyAutoFit / PyAutoGalaxy / PyAutoLens docs tell the user (e.g. during installation) to use a version number for the workspace clone or download, paired to their version. -3) If a user has an out of sync workspace and source code, they get an error they have to manually disable. - -This will mean a user can run the main branches of the source repos and workspace repos without issue. - -## Action - -On each of the three repos, change the default branch from `release` back to `main`: - -- https://github.com/PyAutoLabs/autolens_workspace/settings/branches — set default to `main` -- https://github.com/PyAutoLabs/autogalaxy_workspace/settings/branches — set default to `main` -- https://github.com/PyAutoLabs/autofit_workspace/settings/branches — set default to `main` - -(autolens_workspace_test is already correct — default is `main`.) - -Then add solution to points 1), 2) and 3) above. - -## Verification - -After each change, confirm with: - -```bash -gh repo view PyAutoLabs/ --json defaultBranchRef --jq '.defaultBranchRef.name' -``` - -Expect `main` for all four workspace repos. - -## Why not purely rely on the `/ship_workspace` skill fix? - -The skill has already been patched to pass `--base main` explicitly on every `gh pr create` call (admin_jammy commit `8152d05`). That covers Claude-driven shipping. It does NOT cover: - -- PRs opened manually in the GitHub UI (the "Compare & pull request" banner defaults to the repo's default branch). -- PRs from contributors who don't use the shipping skill. -- `git push -u origin `'s suggested PR URL (pre-fills the default base). - -So changing the repo default is the root-cause fix; the skill patch is defence-in-depth. - -So, also remove this fix once we are on main everywhere. \ No newline at end of file diff --git a/active/deferred_skill_tranche.md b/active/deferred_skill_tranche.md deleted file mode 100644 index 2cea3a11..00000000 --- a/active/deferred_skill_tranche.md +++ /dev/null @@ -1,12 +0,0 @@ -# autofit_assistant: the deferred skill tranche (chain/custom/simulate/plot/debug) - -Type: feature -Target: autofit_assistant -Difficulty: medium -Autonomy: supervised -Priority: normal -Status: formalised - -Author the deferred autofit_assistant skill tranche (deferral human-approved 2026-07-10 on autofit_assistant#1): af_chain_searches (prior passing / start points, grounded in autofit_workspace scripts/searches/start_point.py), af_custom_analysis (Analysis subclass patterns beyond plain wrapping: custom Result/visualization hooks, grounded in cookbooks/analysis.py), af_simulate_dataset (simulate 1D/user-model data for testing, grounded in scripts/simulators), af_plot_fit (autofit.plot + matplotlib-over-instances conventions, grounded in cookbooks and plot scripts), af_debug_fit_failure (triage flowchart: likelihood sanity, prior coverage, sampler diagnostics — expands af_run_search's triage section). House style per skills/_style.md; each skill's recipe must execute against the installed stack before shipping; register in skills/README.md + .claude/skills symlinks; run the four currency legs. - - diff --git a/active/deflections_integral_fix.md b/active/deflections_integral_fix.md deleted file mode 100644 index b7687264..00000000 --- a/active/deflections_integral_fix.md +++ /dev/null @@ -1,10 +0,0 @@ -The followng PR Was meant to move all deflections_via_integral methods out of the source code -and into the @autolens_workspace_test/scripts/mass_via_integral folder: - -https://github.com/PyAutoLabs/PyAutoGalaxy/pull/324 - -The expectation is it would move the functions themselves, including the full calculation, into this folder -so the tests there could use it. - -However, it did not move the functions and their calculations themselves from the source code, meaning these tests did not -pass. Can you dig up the PR, look at its history, get the alculations and move them to this test package? \ No newline at end of file diff --git a/active/delaunay_qhull_only_callback.md b/active/delaunay_qhull_only_callback.md deleted file mode 100644 index a69c4cd3..00000000 --- a/active/delaunay_qhull_only_callback.md +++ /dev/null @@ -1,12 +0,0 @@ -# Delaunay qhull-only callback: move point location into JAX (likelihood-invariant) - -Type: refactor -Target: PyAutoArray -Difficulty: too-large -Autonomy: supervised -Priority: normal -Status: formalised - -Speed up the JAX Delaunay likelihood by shrinking the scipy pure_callback to qhull-only. Currently InterpolatorDelaunay (PyAutoArray autoarray/inversion/mesh/interpolator/delaunay.py) runs scipy_delaunay through jax.pure_callback with vmap_method=sequential: triangulation + find_simplex on the ~18k oversampled data points + split-point find_simplex + numpy assembly, ~40-320ms of host-serial work per likelihood eval (scales with over-sampling), serialized per vmap lane — the measured reason the A100 Delaunay cell is 106.8ms/lane vs rectangular's 32.9ms. Proven PoC in autolens_profiling/scratch/delaunay_speedup/demo_1_callback_shrink.py: callback returns only simplices_padded + two fixed-shape adjacency tables (vertex->incident simplices, vertex->neighbor vertices, one vectorized numpy pass); point location moves into JAX as nearest-vertex seed + full 2-ring signed-barycentric containment (max-min-weight winner, misses fall back to nearest-vertex mapping = existing outside-hull semantics); dual areas via scatter-add, split points and their location also JAX-side (seed split points at their own nearest vertex, NOT the parent, to match KDTree fallback). PoC parity: 99.99% identical mappings (the 0.01-0.04% tail is the fallback), split grid 100%, matched rows 1e-15; host-serial 4.4x less at sub_size 1, 30x at sub_size 4. Implement fully in PyAutoArray: JAX branch of InterpolatorDelaunay.delaunay only (numpy/scipy path unchanged), including the jax_delaunay_matern variant, fixed global pad widths for the adjacency tables (assert in callback, fail loudly), chunked nearest-vertex distance search (VRAM under vmap). Hard requirement: the likelihood must be numerically unchanged — add parity testing in autolens_workspace_test: jax.jit round-trip + fitness._vmap batch eval comparing new vs old path log-evidence on the Delaunay imaging configs (and interferometer if covered), rtol at the pinned-value level (e.g. 1e-8 on log_evidence, plus exact mapping-matrix comparison modulo the documented fallback tail), exercised near-caustic so folded/skinny triangles are covered. Also keep the eager (xp=np) FitImaging reference assert. Library unit tests stay numpy-only per repo rules: unit-test the new point-location helpers with xp=np against scipy find_simplex on random + adaptive meshes. Do NOT change the fnnls/NNLS solver (separate follow-up task). - - diff --git a/active/dependencies.md b/active/dependencies.md deleted file mode 100644 index 5478e78e..00000000 --- a/active/dependencies.md +++ /dev/null @@ -1,8 +0,0 @@ -The dependencies of many core libraries, for example numpy, scipy, matplotlib and ultimately JAX, -are capped or tied based on the code API. Its probably been like this for a long time, and looking to update -these dependencies is a good idea, but must be balanced against source code tweaks and updates. - -Can you do an assessment of whether we can udpate the version of these libraries whilst maintaining a stable -build where all github actions run and autobuild works ok. Dont just focus on the core libraries listed, but -also do an assessment of other key libraries like astropy, scikit-image, scikit-learn. Priotize simplicity if -necessary, but ultimately I think a version sweep update is long overdue. \ No newline at end of file diff --git a/active/docs_mass_rst_sync.md b/active/docs_mass_rst_sync.md deleted file mode 100644 index c5d3a35f..00000000 --- a/active/docs_mass_rst_sync.md +++ /dev/null @@ -1,30 +0,0 @@ -Sync `PyAutoLens/docs/api/mass.rst` to cover every exported `al.mp.*` -class and add documentation sections for `al.lmp.*` and -`al.lmp_linear.*`. Surfaced as a follow-up while writing the -`scripts/guides/profiles/{mass.py,light_and_mass_profiles.py}` guides -(issues #178 / #180) — both guides reference the published API -reference URL, so the reference itself should list every class the -guides demonstrate. - -Gaps to fix in the existing sections: - -- Total: add `dPIEMass`, `dPIEMassSph`, `PIEMass`, `dPIEPotential`, - `dPIEPotentialSph`. -- Mass Sheets: add `ExternalPotential` (the recently merged - line-of-sight potential). -- Stellar: add `GaussianGradient`, `SersicCore`, `SersicCoreSph`. -- Dark: add `cNFW`, `cNFWSph`, `cNFWMCRLudlow`, `cNFWMCRLudlowSph`, - `cNFWMCRScatterLudlow`, `cNFWMCRScatterLudlowSph`, - `gNFWVirialMassConcSph`, `gNFWVirialMassgNFWConcSph`, - `NFWVirialMassConcSph`. - -New sections to add: - -- `Point Mass [ag.mp]` — `PointMass`, `SMBH`, `SMBHBinary`. -- `Stellar Light+Mass [ag.lmp]` — every class in - `autogalaxy.profiles.light_and_mass_profiles`. -- `Linear Light+Mass [ag.lmp_linear]` — every class in - `autogalaxy.profiles.light_linear_and_mass_profiles`. - -Pure rST docs change — no Python code touched. PyAutoGalaxy has no -`docs/api/mass.rst` so the sync is one-way (PyAutoLens only). diff --git a/active/docs_theming_and_hub.md b/active/docs_theming_and_hub.md deleted file mode 100644 index 61c1f614..00000000 --- a/active/docs_theming_and_hub.md +++ /dev/null @@ -1,72 +0,0 @@ -# Docs infrastructure phases B+C: shared PyAuto theming + GitHub Pages hub - -Type: docs -Target: libraries -Difficulty: medium -Autonomy: supervised -Priority: medium -Status: formalised - -Phase A (rtd_hygiene, PyAutoFit#1341) merged 2026-07-09 — this prompt graduates. -Phase A carried the census findings and the RTD/CI hygiene scope; this prompt holds the -decision record and phases B/C. - -## Original request (verbatim) - -> We've done lots of sweeps of high level functionality or design to ask Fable whether it -> could be improved. This focused on the AI workflow and other things. We haven't asked it -> about how our docs for each project (PyAutoFit, PyAutoGalaxy, PyAutoLens) are set up and -> the link with readthedocs. Can you do a review and census of that and offer feedback? -> Could this be something we move entirely to GitHub pages? Not saying we should but making -> them more PyAuto-bespoke would be cool. - -Decision after the review: **middle path** — keep ReadTheDocs hosting, fix the hygiene -findings, make the docs bespoke via shared Furo theming, and add a bespoke hub on GitHub -Pages. Then: - -> Ok I don't want to use money for this currently, so I guess set up the github stuff but -> in a way that doesn't cost so I can easily retrofit it with money later. - -**Zero-cost constraint:** everything ships on free infrastructure now -(`*.readthedocs.io` + `pyautolabs.github.io`). Custom domains -(`docs.pyautolens.org` etc. — all of pyautolens/pyautofit/pyautogalaxy/pyautolabs/pyauto -`.org` were verified unregistered on 2026-07-09) are a later config-flip retrofit: register -domain → RTD dashboard custom-domain setting per project → Pages CNAME + DNS. Nothing in -this task may hard-bake the `pyautolabs.github.io` hostname in a way that makes that -retrofit require restructuring (use relative links inside the hub; the RTD sites keep -their canonical `*.readthedocs.io` URLs regardless). - -## Scope (phases B+C; phase A in rtd_hygiene.md) -### Phase B — shared bespoke theming - -- One shared PyAuto brand layer for Furo across PyAutoFit/PyAutoGalaxy/PyAutoLens: palette - (start from Lens's `#7C4DFF`, give each project an accent), logo/wordmark in the - sidebar, consistent `html_theme_options`, a single `pyauto.css` replicated (or - vendored via `_templates`) into each repo's `docs/_static/`. -- Light + dark variables both styled (Furo supports both natively). -- Keep the AI-assistant note framing per the existing user-docs convention. - -### Phase C — hub on GitHub Pages (zero-cost) - -- New repo `PyAutoLabs/pyautolabs.github.io`: a bespoke static landing page — the PyAuto - front door — linking to the three RTD doc sites, the workspaces, HowToFit/Galaxy/Lens, - and autolens_assistant. Plain static HTML/CSS (no build framework needed), same brand - layer as Phase B. -- Deploy via GitHub Pages from the repo (Actions or branch deploy — simplest that works). -- Include a short `RETROFIT.md` recording the paid-domain upgrade path (registrar → DNS → - RTD custom domain per project → Pages custom domain + CNAME). - -## Out of scope - -- Migrating doc hosting off ReadTheDocs. -- Versioned/`stable` docs activation. -- The `plot.rst` content rewrite (PyAutoLens#592) and any API-page staleness fixes beyond - what the CI warning baseline forces. -- PyAutoArray/PyAutoConf public docs (they have none; unchanged). - -## Cross-references - -- PyAutoLens#592 — stale plot API docs (separate content task). -- `PyAutoHeart/heart/checks/url_check_live.py` — existing RTD URL liveness/rewrite checks; - a Heart check for "RTD last-build recency" would have caught finding 1 (nice-to-have, - may be split out to a Heart task). diff --git a/active/docstring.md b/active/docstring.md deleted file mode 100644 index 21d0d935..00000000 --- a/active/docstring.md +++ /dev/null @@ -1,82 +0,0 @@ -I just used autolens_asssitant to produce the file @autolens_assistant/work/hst_lens_model.py - -It produced good code but the comment style was as follows: - -# --------------------------------------------------------------------------- -# 1. Load imaging + mask + adaptive over-sampling -# --------------------------------------------------------------------------- -# source: PyAutoArray:autoarray/dataset/imaging/dataset.py (Imaging.from_fits / apply_mask / apply_over_sampling) -# source: PyAutoArray:autoarray/mask/mask_2d.py (Mask2D.circular) - -dataset = al.Imaging.from_fits( - data_path=DATASET_PATH / "data.fits", - noise_map_path=DATASET_PATH / "noise_map.fits", - psf_path=DATASET_PATH / "psf.fits", - pixel_scales=PIXEL_SCALES, -) - - -If you look through the autolens_workspace/scripts, youll see we have a distinct style as follows: - -""" -__Dataset__ - -We begin by loading the dataset. Three ingredients are needed for lens modeling: - -1. The image itself (CCD counts). -2. A noise-map (per-pixel RMS noise). -3. The PSF (Point Spread Function). - -Here we use James Webb Space Telescope imaging of a strong lens called the COSMOS-Web ring. Replace these FITS paths -with your own to immediately try modeling your data. - -The `pixel_scales` value converts pixel units into arcseconds. It is critical you set this -correctly for your data. -""" -dataset = al.Imaging.from_fits( - data_path=DATASET_PATH / "data.fits", - noise_map_path=DATASET_PATH / "noise_map.fits", - psf_path=DATASET_PATH / "psf.fits", - pixel_scales=PIXEL_SCALES, -) - -This style is the PyAutoLens style, and it also allows us to do clever script to Notebook conversions which I think -we will want to make part of autolens_assistant. - -Furthermore, autolens style has pairs these headers to a Contents section at the top: - -""" -Start Here: Imaging -=================== - -Strong gravitational lenses are often observed with CCD imaging, for example using HST, JWST, -or ground-based telescopes. - -This script shows you how to model such a lens system using **PyAutoLens** with as little setup -as possible. In about 15 minutes you’ll be able to point the code at your own FITS files and -fit your first lens. - -We focus on a *galaxy-scale* lens (a single lens galaxy). If you have multiple lens galaxies, -see the `start_here_group.ipynb` and `start_here_cluster.ipynb` examples. - -__Contents__ - -- **JAX:** JAX acceleration for fast GPU/CPU model-fitting. -- **Google Colab Setup:** The introduction `start_here` examples are available on Google Colab, which allows you to run them. -- **Imports:** Import the required Python libraries. -- **Dataset:** Load and plot the strong lens dataset. -- **Extra Galaxy Removal:** There may be regions of an image that have signal near the lens and source that is from other. -- **Masking:** Lens modeling does not need to fit the entire image, only the region containing lens and source. -- **Model:** Compose the lens model fitted to the data. -- **Model Fit:** Perform the model-fit using the search and analysis. -- **Iterations Per Update:** Every `iterations_per_quick_update`, the non-linear search outputs the maximum likelihood model and. -- **Live Visual Update:** Opt-in live matplotlib window (scripts) or Jupyter cell refresh (notebooks) during the fit. -- **Result:** Overview of the results of the model-fit. -- **Extra Galaxy Removal GUI:** The model-fit above removed a region of the image to the south-east of the lens, which contains. -- **Model Your Own Lens:** If you have your own strong lens imaging data, you are now ready to model it yourself by adapting. -- **Simulator:** Let’s now switch gears and simulate our own strong lens imaging. -- **Sample:** Often we want to simulate *many* strong lenses — for example, to train a neural network or to. -- **Wrap Up:** Summary of the script and next steps. -""" - -Can you update autolens_assistant to use this style for all code it writes? \ No newline at end of file diff --git a/active/eager_numpy_regression_assertions.md b/active/eager_numpy_regression_assertions.md deleted file mode 100644 index 9ae9cbc4..00000000 --- a/active/eager_numpy_regression_assertions.md +++ /dev/null @@ -1,158 +0,0 @@ -# Eager-numpy regression assertions for imaging / interferometer profiling scripts - -## Context - -`jax_profiling/point_source/source_plane.py` anchors its **eager-numpy** -baseline log-likelihood against a hardcoded constant -`EXPECTED_LOG_LIKELIHOOD_SOURCE_PLANE = -4496.798984131583` (see -`source_plane.py:506`). That pattern catches silent forward-pass regressions -in the numpy stack — changes in a profile/blurring/chi-squared step that would -drift the likelihood without tripping any existing np↔jnp cross-check. - -The imaging and interferometer scripts have the pieces but not the assertion: - -- they compute `log_likelihood_ref = fit.log_likelihood` (and often - `log_evidence_ref = fit.figure_of_merit`) from the eager path at the top, and -- they already assert `float(full_result)` (JIT) and `np.array(result_vmap)` - (vmap) against `EXPECTED_LOG_LIKELIHOOD_*` / `EXPECTED_LOG_EVIDENCE_*`, -- **but** the eager `_ref` baseline is never itself asserted against the - hardcoded constant. - -That's a gap: a regression in the eager numpy stack would change -`log_likelihood_ref` without the JIT/vmap assertions necessarily catching it -(the JIT and eager paths can drift in lockstep if the shared upstream code -changes). - -## Task - -Add an **eager-numpy regression assertion** to every -`jax_profiling/imaging/*.py` and `jax_profiling/interferometer/*.py` script -that currently uses the `EXPECTED_LOG_*` hardcoded-constant pattern. Model -the assertion on the point-source template: - -```python -# From point_source/source_plane.py ~line 508 -np.testing.assert_allclose( - log_likelihood_ref, - EXPECTED_LOG_LIKELIHOOD_SOURCE_PLANE, - rtol=1e-4, - err_msg=( - f"point_source/source_plane: regression — eager log_likelihood drifted " - f"(got {log_likelihood_ref}, expected {EXPECTED_LOG_LIKELIHOOD_SOURCE_PLANE})" - ), -) -print( - f" Eager regression assertion PASSED: log_likelihood matches " - f"{EXPECTED_LOG_LIKELIHOOD_SOURCE_PLANE:.6f}" -) -``` - -The assertion must run on the eager baseline **before** the existing JIT / -vmap assertions, so a failure points straight at the numpy stack rather than -at compilation. - -Use whichever `_ref` variable each script already has (`log_likelihood_ref` -or `log_evidence_ref`) and match it to the existing `EXPECTED_LOG_LIKELIHOOD_*` -or `EXPECTED_LOG_EVIDENCE_*` constant. If a script happens to assert against -`log_evidence` (pixelization / delaunay) rather than `log_likelihood`, mirror -that here too. - -## Affected scripts - -### Imaging — scalar forward log-likelihood (full pattern) - -- `jax_profiling/imaging/mge.py` - (ref: `log_likelihood_ref`, constant: `EXPECTED_LOG_LIKELIHOOD_HST`) -- `jax_profiling/imaging/pixelization.py` - (ref: `log_evidence_ref`, constant: `EXPECTED_LOG_EVIDENCE_HST`) -- `jax_profiling/imaging/delaunay.py` - (ref: `log_evidence_ref`, constant: `EXPECTED_LOG_EVIDENCE_HST`) - -### Interferometer — scalar forward log-likelihood (full pattern) - -- `jax_profiling/interferometer/mge.py` - (ref: `log_likelihood_ref`, constant: `EXPECTED_LOG_LIKELIHOOD_SMA`) -- `jax_profiling/interferometer/pixelization.py` - (ref: `log_likelihood_ref`, constant follows the file's existing - `EXPECTED_LOG_*` if present; otherwise introduce one and pin it to the - observed eager value on a clean run) - -### Gradient scripts — scope limited to scalar forward log-likelihood - -Gradient scripts (`*_gradients.py`) use `jax.value_and_grad`, and numpy has -no autograd equivalent, so the gradient **vector** cannot be cross-checked -against a numpy reference. **Do not** add any assertion on the gradient -itself. - -However, each gradient script still performs an eager forward-pass -log-likelihood computation before running autodiff (e.g. -`imaging/mge_gradients.py:247` prints `fit.log_likelihood` from the eager -`FitImaging`). That scalar forward pass is worth anchoring for exactly the -same reason it is worth anchoring in the non-gradient scripts: it catches -forward-stack regressions that would silently perturb every gradient -downstream. - -Apply the assertion pattern to the **scalar** eager log-likelihood (or -log-evidence) only — and if the current script only prints the value without -capturing it, lift it to a `log_likelihood_ref = fit.log_likelihood` variable -first. Scripts to cover: - -- `jax_profiling/imaging/mge_gradients.py` -- `jax_profiling/imaging/pixelization_gradients.py` -- `jax_profiling/interferometer/mge_gradients.py` - -Skip any gradient script that does **not** compute an eager forward scalar -log-likelihood (unlikely — they all currently do), since in that case there -is nothing scalar to anchor. Do NOT introduce a new eager forward call just -for the assertion; only wire the assertion into values the script already -computes. - -### Out of scope - -- The gradient vector from `jax.value_and_grad` — no numpy reference exists. -- `imaging/mapper_grad_isolate.py`, `imaging/mapper_grad_probe.py`, - `imaging/nnls_precondition_bench.py` — these are intermediate-step - diagnostic scripts that do not compute a full-pipeline log-likelihood or - use an `EXPECTED_LOG_*` constant. Leave them alone. -- `jax_profiling/point_source/source_plane.py` — already has the pattern. - `point_source/image_plane.py` — add the pattern if it already captures a - `log_likelihood_ref` and defines an `EXPECTED_LOG_*` constant; otherwise - out of scope. - -## Verification - -After wiring each assertion: - -```bash -source ~/Code/PyAutoLabs-wt//activate.sh -cd autolens_workspace_developer -python jax_profiling/imaging/mge.py -python jax_profiling/imaging/pixelization.py -python jax_profiling/imaging/delaunay.py -python jax_profiling/imaging/mge_gradients.py -python jax_profiling/imaging/pixelization_gradients.py -python jax_profiling/interferometer/mge.py -python jax_profiling/interferometer/pixelization.py -python jax_profiling/interferometer/mge_gradients.py -``` - -Every script should print an "Eager regression assertion PASSED" line -immediately after the eager fit and before the JIT / vmap profiling runs. - -## Affected repos - -- `autolens_workspace_developer` (only — workspace-only task, no library - changes required) - -## Suggested branch - -`feature/eager-numpy-regression-assertions` - -## Notes - -- Use `rtol=1e-4` for consistency with the existing JIT / vmap assertions. -- If any eager `_ref` value drifts vs. the existing `EXPECTED_*` constant, - that is the signal this prompt is designed to surface — stop and - investigate rather than bumping the constant. -- The assertion must use `np.testing.assert_allclose`, not `assert abs(...)` — - it produces better diagnostic output on failure. diff --git a/active/einstein_radius_zero_contour_migration.md b/active/einstein_radius_zero_contour_migration.md deleted file mode 100644 index 48c0a2c4..00000000 --- a/active/einstein_radius_zero_contour_migration.md +++ /dev/null @@ -1,207 +0,0 @@ -# Fast visualization Phase B — Euclid effective_einstein_radius latent via zero_contour - -Re-enable the `effective_einstein_radius` latent variable in the Euclid -pipeline workspace by routing it through the JAX-traceable -`einstein_radius_via_zero_contour_from()` rather than the legacy -marching-squares `einstein_radius_from(grid=...)` that forced the latent -off-JAX. - -This is Phase B of `z_features/fast_visualization.md`. Phase A′ (PR -PyAutoGalaxy#434, PR PyAutoLens#527, PR autolens_workspace_test#111) -landed the perf cache + safety net that makes this migration viable -without paying the ~10s ZeroSolver compile cost on every Nautilus -sample. - -## Background - -The `effective_einstein_radius` latent was commented out in -`euclid_strong_lens_modeling_pipeline/util.py` because the only -available method, `tracer.einstein_radius_from(grid=...)`, routes -through `skimage.measure.find_contours` — not JAX-traceable. With -`use_jax=True` (the default for Euclid pipeline fits) the latent -computation either had to flip `self._use_jax = False` per sample -(slow workaround used in `z_projects/euclid`) or be disabled outright -(approach taken in `euclid_strong_lens_modeling_pipeline`). - -After PR `Jammy2211/PyAutoGalaxy#434`, the -`einstein_radius_via_zero_contour_from()` path is fast on repeat calls -(~68 ms warm on CPU on an SIE) because `LensCalc` now caches its -`(f, ZeroSolver)` pair. That makes it viable as the standard latent -computation: the first sample pays the one-time ~10 s ZeroSolver JIT -compile; every subsequent sample reuses the cached compile. - -`z_projects/euclid/scripts/util.py` is live science work for the DR1 -catalogue paper and is **not** touched by this migration; the science -branch picks up the change on its own cadence. - -## What to change - -### 1. `LATENT_KEYS` list - -`@euclid_strong_lens_modeling_pipeline/util.py` around line 305-315 - -Uncomment the commented-out `latent_effective_einstein_radius` entry -and **rename to `latent.effective_einstein_radius`** so the dotted -naming matches the surrounding entries (`latent.total_lens_flux`, -`latent.magnification`, etc.). The underscore form is a typo from the -original commented-out version. - -```python -LATENT_KEYS = [ - "latent.total_lens_flux", - "latent.total_lens_flux_1_fwhm", - "latent.total_lens_flux_2_fwhm", - "latent.total_lens_flux_3_fwhm", - "latent.total_lens_flux_4_fwhm", - "latent.total_lensed_source_flux", - "latent.total_source_flux", - "latent.magnification", - "latent.effective_einstein_radius", -] -``` - -### 2. `compute_latent_variables` body — Einstein radius computation - -`@euclid_strong_lens_modeling_pipeline/util.py` around line 488-495 - -Replace the commented-out block with a dispatch that respects the -Analysis's `use_jax` setting: - -```python -# EFFECTIVE EINSTEIN RADIUS - -try: - if self._use_jax: - effective_einstein_radius = tracer.einstein_radius_via_zero_contour_from() - else: - effective_einstein_radius = tracer.einstein_radius_from( - grid=self.dataset.grids.lp, - ) -except ValueError: - # No tangential critical curve found (degenerate model — e.g. very - # weak lens with no eigenvalue zero-crossing). The latent is - # undefined for this sample; record as NaN. - effective_einstein_radius = xp.nan -``` - -Notes: - -- **Branch on `self._use_jax`.** `_via_zero_contour_from()` always - imports `jax.numpy` and calls `ZeroSolver` internally — it does not - consult the caller's `xp`. Routing a `use_jax=False` user through it - would silently pull JAX into their critical path and raise - `ModuleNotFoundError` if they don't have `jax_zero_contour` - installed. The dispatch above keeps the legacy marching-squares - numpy path for users who explicitly opted out of JAX, and routes - JAX users through the fast traceable path that PR #434 makes viable. -- **Use `except ValueError:` specifically**, not bare - `except Exception:`. Both `_init_guess_from_coarse_grid` (zero_contour - path) and `find_contours`-based fallbacks raise `ValueError` for - "no zero crossings"; that's the expected recoverable failure. - Other exceptions (e.g. `ModuleNotFoundError` if `jax_zero_contour` - is uninstalled despite `use_jax=True`, or a `TypeError` from a JAX - trace mismatch) must propagate loudly per - `feedback_no_silent_guards`. -- **No `grid=` argument on the zero_contour branch.** That method - traces the curve directly without a dense grid. The legacy - `tracer.einstein_radius_from(grid=...)` call still needs - `self.dataset.grids.lp`. -- **Do NOT flip `self._use_jax = False`** to coerce JAX users back to - the numpy path. The point of the migration is that JAX users get - the JAX path end-to-end; the workaround that `z_projects/euclid` - carries is no longer needed. - -### 3. Returned tuple at line 497-507 - -Uncomment the `effective_einstein_radius` entry so the returned tuple -matches the `LATENT_KEYS` length: - -```python -return ( - total_lens_flux_muJy, - total_lens_flux_muJy_aperture_list[0], - total_lens_flux_muJy_aperture_list[1], - total_lens_flux_muJy_aperture_list[2], - total_lens_flux_muJy_aperture_list[3], - total_lensed_source_flux_muJy, - total_source_flux_muJy, - magnification, - effective_einstein_radius, -) -``` - -Verify the tuple length matches `len(LATENT_KEYS)` (now 9). - -## Out of scope - -- **No `z_projects/euclid` edits.** Live science work for the DR1 - catalogue paper. The science branch picks up the change on its own - cadence after this lands. -- **No library changes.** This is purely a workspace edit; the library - API needed (`einstein_radius_via_zero_contour_from()`) already exists - and shipped fast in PyAutoGalaxy #434. -- **No tracker / `LATENT_KEYS` extension** beyond Einstein radius. If - you want additional latents (e.g. critical-curve area, magnification - at a specific position), file a separate prompt. - -## Verification - -1. **Pipeline smoke run:** - `/smoke_test euclid_strong_lens_modeling_pipeline` — all 6 scripts - pass with the updated latent code path. Set - `PYAUTO_SKIP_WORKSPACE_VERSION_CHECK=1` in the env prefix per the - pattern used during Phase A′ smoke (the workspace's - `config/general.yaml` doesn't pin `workspace_version`, which causes - the version check to mismatch the installed library version — this - is a routine workspace-vs-library drift, not a regression of this - PR). - -2. **Latent output inspection (use_jax=True path).** Run one of the - pipeline scripts with the latent computation enabled - (`PYAUTO_SKIP_FIT_OUTPUT=0`, `PYAUTO_SKIP_VISUALIZATION=0`, - `PYAUTO_TEST_MODE=1` to get a small real fit). Inspect the resulting - `output/.../latent/latent_summary.json` and confirm - `latent.effective_einstein_radius` is present and finite. The value - should be in the expected range for the lens (typically 0.5–2.0 - arcsec for a typical Euclid strong lens). - -3. **Both `use_jax` paths exercised.** The dispatch branches on - `self._use_jax`, so both branches need a smoke. The smoke list runs - under `PYAUTO_DISABLE_JAX=1` (forces `use_jax=False`) per the - workspace's `config/build/env_vars.yaml` — that already covers the - numpy branch. For the JAX branch, run **one** pipeline script - manually without `PYAUTO_DISABLE_JAX`: - - ```bash - PYAUTO_SKIP_WORKSPACE_VERSION_CHECK=1 PYAUTO_TEST_MODE=1 \ - python start_here.py \ - --dataset=102018665_NEG570040238507752998 \ - --sample=q1_walsmley - ``` - - Confirm `latent.effective_einstein_radius` is finite in the output - `latent_summary.json` and that no `ModuleNotFoundError` / - `falling back to numpy` warning fires during latent computation. - -4. **No silent `_use_jax = False` flip.** Skim the script logs for - any "JAX backend disabled" / "falling back to numpy" warning during - the latent step. If something flips backends silently mid-search, - the migration hasn't actually landed the JAX path. - -## References - -- `z_features/fast_visualization.md` — parent tracker, Phase B section. -- PyAutoGalaxy PR #434 (merged 2026-05-21) — `(f, ZeroSolver)` cache - fix that makes warm `_via_zero_contour_from()` calls ~68 ms instead - of ~10 s. The migration depends on this PR shipping in an installed - library version. -- PyAutoLens PR #527 (merged 2026-05-21) — broad `except` tighten in - the visualizer; relevant because the migration's `except ValueError` - uses the same "tight catch, loud on unexpected" pattern. -- `feedback_no_silent_guards` (memory) — codebase rule against bare - `except Exception: return nan` patterns. -- `feedback_euclid_pipeline_not_z_projects` (memory) — `z_projects/euclid` - is off-limits; this prompt targets the pipeline workspace only. -- `feedback_jax_closure_cache_busts` (memory) — the JIT cache identity - bug that PR #434 fixed; relevant if the warm-call latency on Euclid - ever regresses past ~100 ms. diff --git a/active/ellipse_fitting_jax.md b/active/ellipse_fitting_jax.md deleted file mode 100755 index e7e17f22..00000000 --- a/active/ellipse_fitting_jax.md +++ /dev/null @@ -1,54 +0,0 @@ -- Ellipse fitting is defined in @PyAutoGalaxy/autogalaxy/ellipse, with scientific walk throughts in -@autogalaxy_workspace/scripts/ellipse . - -It currently does not support JAX, the final goal is to add JAX support through the likelihood function, such that -@PyAutoGalaxy/autogalaxy/ellipse/model/analysis.py, Analysis.log_likelihood_function is JAX compatible. - -You can refer to @PyAutoGalaxy/autogalaxy/imaging/model/analysis.py to see how JAX is supported in an -analysis class. - -Before starting JAX work, can you make integration tests in @autogalaxy_workspace_test/scripts, in particular: - -- visualization.py to test the ellipse/visualization of the ellipse fitting. -- jax_likelihood_functions/ellipse to test the JAX likelihood function of the ellipse fitting. -- Note how this will give a step by step guide of the code in numpy, which are then the steps we need to convert to support JAX. - - -There are a few nasty and poorly written loops which will ned careful conversion to JAX, these are unit tests -but lets make double sure we keep our numerics in the test workspace and so that when these are changed -we dont lose functionality: - - if self.interp.mask_interp is not None: - - i_total = 300 - - total_points_required = points.shape[0] - - for i in range(1, i_total + 1): - - total_points = points.shape[0] - total_points_masked = np.sum(self.interp.mask_interp(points) > 0) - - if total_points_required == total_points - total_points_masked: - continue - - if total_points_required < total_points - total_points_masked: - - number_of_extra_points = ( - total_points - total_points_masked - total_points_required - ) - - unmasked_indices = np.where(self.interp.mask_interp(points) == 0)[0] - unmasked_indices = unmasked_indices[number_of_extra_points:] - - points = points[unmasked_indices] - - continue - -Adding specific unit tests on this before the conversion to something which supports JAX Is adviced. - -Note that we will need the Drawer searchin autofit to support JAX jit. - -Put this all together as a sequence of prompts which we run as a feature in z_features (analogous to [weak_shear.md](../z_features/weak_shear.md)) - -Do deep research and thikning on what else is required for this feature before makin the final plan. \ No newline at end of file diff --git a/active/ellipse_modeling_visualization_jit.md b/active/ellipse_modeling_visualization_jit.md deleted file mode 100644 index 15c32007..00000000 --- a/active/ellipse_modeling_visualization_jit.md +++ /dev/null @@ -1,130 +0,0 @@ -# Phase D.2.b.ii — `ellipse/visualization_jax.py` + `ellipse/modeling_visualization_jit.py` for autogalaxy_workspace_test - -Authors the missing JAX-backed visualization integration scripts for -the `ellipse` dataset type. `autogalaxy_workspace_test/scripts/ellipse/` -currently has only the non-JAX `visualization.py`; both -`visualization_jax.py` (single-shot) and `modeling_visualization_jit.py` -(JIT-cached + live Nautilus) are absent. - -Phase D.2.b.ii of `z_features/fast_visualization.md`. With the quantity -package archived (D.2.b.i scope was dropped), this and D.2.b.iii (weak -lensing) are the remaining gaps. Per session decision, only **ellipse** -is in scope here; weak lensing is parked indefinitely (needs library -work first to author `AnalysisWeak`). - -## Inherited infrastructure - -- `AnalysisEllipse.__init__` already accepts `use_jax=True`. It passes - `**kwargs` to `super().__init__(...)` (the parent `af.Analysis` - init), so `use_jax_for_visualization=True` flows through to the - parent — no library change required *unless* `fit_for_visualization` - fails on a `FitEllipseSummed` return type (the JIT path requires the - return type to be pytree-registered). -- `VisualizerEllipse` exists at `autogalaxy/ellipse/model/visualizer.py` - with `visualize_before_fit` and `visualize` methods following the - imaging variant's signature. -- Existing `ellipse/visualization.py` provides the dataset - (`dataset/imaging/jax_test` — pre-built from - `jax_likelihood_functions/imaging/simulator.py`), mask construction - (`mask_generous`, `mask_tight`), and the model shapes - (`Ellipse` + `EllipseMultipole` + `EllipseMultipoleScaled`). - -## What to author - -### 1. `autogalaxy_workspace_test/scripts/ellipse/visualization_jax.py` - -Single-shot JAX-backed visualization pilot. Mirror the structure of -`autogalaxy_workspace_test/scripts/imaging/visualization_jax.py` -(PR #54 / #55 lineage): - -- Same dataset path and mask as `ellipse/visualization.py` — single - `mask_generous` scenario. -- Simpler model: a single `af.Model(ag.Ellipse)` (no multipoles for - the pilot — multipoles are exercised by `visualization.py`). -- `analysis = ag.AnalysisEllipse(dataset=dataset, use_jax=True, use_jax_for_visualization=True, title_prefix="JAX_PILOT")`. -- `VisualizerEllipse.visualize(analysis=..., paths=..., instance=..., during_analysis=False)`. -- Assert the expected ellipse PNG lands on disk (verify the actual - artifact name at impl time — `fit_ellipse.png` is plausible per the - existing `visualization.py`). -- Append the autogalaxy non-lensing Sanity block from Phase D.2.a - (PR #55) — `fit.figure_of_merit` finite. **Skip `fit.model_data` - assertion** — `FitEllipseSummed` may not expose a `model_data` - attribute; verify at impl time and either use it or document why - it's omitted. - -### 2. `autogalaxy_workspace_test/scripts/ellipse/modeling_visualization_jit.py` - -JIT-cached + live Nautilus pattern. Mirror the structure of -`autogalaxy_workspace_test/scripts/imaging/modeling_visualization_jit.py` -(PR #54): - -- Part 1 — caching probe. Build `model_mge` (here: a single - `ag.Ellipse` model — name kept for parity with the imaging variant's - `_mge` naming). Call `analysis_mge.fit_for_visualization(instance_mge)` - twice; assert `cached_time < compile_time * 0.5`. **Note:** the - `cached < 0.5 * compile` assertion was fragile on the autogalaxy - imaging pixelization variants but stable on autogalaxy imaging - parametric. Ellipse is parametric — should be stable. If local - timings show the assertion fails on CPU with these small datasets, - loosen to `< compile_time` (any speedup at all) and document. -- Sanity block — same shape as the autogalaxy non-lensing template - used in `visualization_jax.py` above, on the cached `fit_2`. -- Part 2 — live Nautilus quick-update. Build a similar single-ellipse - model and run `af.Nautilus(..., n_live=50, n_like_max=1500, iterations_per_quick_update=500)`. - Assert `fit_ellipse.png` (or whichever artifact is produced) - lands under `output/scripts/ellipse/images/modeling_visualization_jit/...`. - Mirror the autogalaxy imaging variant's `rglob` + `len > 0` shape. - -## Possible library snags - -If `analysis.fit_for_visualization(instance)` raises under -`use_jax_for_visualization=True`: - -1. **`FitEllipseSummed` not pytree-registered** — most likely cause. - `jax.jit` needs every return type to be a registered pytree. - `register_instance_pytree(FitEllipseSummed, no_flatten=[...])` in - PyAutoGalaxy (similar to the existing `FitImaging` registration) - would fix it. Fork a follow-up library prompt if so; ship the - workspace scripts with `use_jax=True, use_jax_for_visualization=False` - in that case and document the gap inline. -2. **`fit_from` not JAX-traceable for ellipse** — the docstring at - `ellipse/model/analysis.py:115` claims it is. If it isn't, same - follow-up. - -These are out of scope for this prompt — surface as follow-up rather -than fixing inline. - -## Verification - -1. **Local end-to-end.** Run both scripts directly with - `python scripts/ellipse/visualization_jax.py` and - `python scripts/ellipse/modeling_visualization_jit.py` from the - worktree. Confirm assertions pass and the printed Sanity values - are sensible. -2. **Workspace smoke.** `/smoke_test autogalaxy_workspace_test` — - `visualization_jax*.py` and `modeling_visualization_jit*.py` - scripts are not in the smoke list (build-server only via - `run_all_scripts.sh`), so smoke covers other scripts for regression. -3. **Pattern conformance.** Spot-check the new scripts diff-cleanly - against the autogalaxy imaging variants modulo dataset/model swap. - -## Out of scope - -- **Weak lensing.** `autolens/weak/` has no `model/analysis.py` / - `AnalysisWeak` — entire modeling layer absent. Tracked separately - as a parked roadmap item; not in this PR. -- **Library changes** beyond surfacing pytree-registration follow-ups - if `fit_for_visualization` fails. - -## References - -- `autogalaxy_workspace_test/scripts/imaging/visualization_jax.py` and - `imaging/modeling_visualization_jit.py` — primary templates (PR #54, - PR #55). -- `autogalaxy_workspace_test/scripts/ellipse/visualization.py` — - dataset + mask + model template for the ellipse-specific bits. -- `complete.md::viz-sanity-rollout-jit-scripts` (PR #54) and - `complete.md::viz-sanity-rollout-jax-scripts` (PR #55) — establish - the Sanity-block patterns used here. -- `z_features/fast_visualization.md` — parent tracker; declare Phase D - shipped after this lands. diff --git a/active/ellipse_no_run.md b/active/ellipse_no_run.md deleted file mode 100644 index 86df427f..00000000 --- a/active/ellipse_no_run.md +++ /dev/null @@ -1,25 +0,0 @@ -- All ellipse example scripts under `autogalaxy_workspace/scripts/ellipse/` are currently in - `autogalaxy_workspace/config/build/no_run.yaml` with a `NEEDS_FIX 2026-04-24` marker. - - The five entries are: - - - `ellipse/simulator` - - `ellipse/fit` - - `ellipse/modeling` - - `ellipse/multipoles` - - `ellipse/database` - - They were parked because the ellipse model needs a refactor and JAX support (tracked separately in - `PyAutoPrompt/autogalaxy/ellipse_fitting_jax.md`). In particular, `ellipse/modeling` and - `ellipse/multipoles` time out under `PYAUTO_TEST_MODE=1` in the mega-run, and - `ellipse/modeling` additionally raises a `KeyError` on `ellipses.0.centre_0` kwargs after API drift. - - When the JAX refactor lands: - - 1. Try running each ellipse script with `PYAUTO_TEST_MODE=2` first — some may just need the - stronger sampler bypass. - 2. Remove the five `ellipse/*` lines from `autogalaxy_workspace/config/build/no_run.yaml`. - 3. If the refactor also unlocks aggregator-style usage, the `ellipse/database` entry in - `PyAutoBuild/autobuild/config/no_run.yaml` (the fallback list) can be removed too. - 4. Re-run the mega-run (`run_all_script_fix_failures` skill in `autogalaxy_workspace`) to confirm - every ellipse script passes. diff --git a/active/env_variable_check.md b/active/env_variable_check.md deleted file mode 100644 index 06743804..00000000 --- a/active/env_variable_check.md +++ /dev/null @@ -1,3 +0,0 @@ -The workspace_test repos have PYAUTOFIT_TEST_MODE in teir cbuild configs, which was updated to PYAUTO_TEST_MODE. - -Can you fix this and dop a general scan for out of data env variables. \ No newline at end of file diff --git a/active/ep_graphical.md b/active/ep_graphical.md deleted file mode 100644 index 8545107c..00000000 --- a/active/ep_graphical.md +++ /dev/null @@ -1,10 +0,0 @@ -The project @z_projects/ic50_workspace is our IC50 use case which we are now aiming to scale up the EP framework -to the IC50 use case. - -We have EP fits, which fit each Hill Curve one-by-one, and then do the AnalysisGlobal model set up, but we dont -have a graphical.py example which fits everything at once in one huge parameter space. - -An example of the graphical modeling API, which should be adapted for this use, is given in -HowToFit/scritps/chapter_3_graphical_models, read all 5 tutorials to work out how individual models, graphical -models and EP fits are related. Then come up with a sensible way to make a graphicl variant of the existing EP -fit. \ No newline at end of file diff --git a/active/ep_hpc_run.md b/active/ep_hpc_run.md deleted file mode 100644 index 1d65756e..00000000 --- a/active/ep_hpc_run.md +++ /dev/null @@ -1,11 +0,0 @@ -The project @z_projects/ic50_workspace is our IC50 use case which we are now aiming to scale up the EP framework -to the IC50 use case. - -Can we have this set up so I can do runs on the HPC, noting that all hpc interface information that forms -the link is given in @autolens_assistant/hpc. Thus, this folder should be read carefully and it then -worked out how it links to the HPC on whichw e do all runs. - -Note that the HPC folder there describes a PyAutoLens lensing project whereas the use case we will test here -is Ic50 datasets, but the HPC link itself is the same. - -We will ultimately want to do runs on CPU and GPU so make sure both are supported. \ No newline at end of file diff --git a/active/ep_normal_message_negative_sigma_crash.md b/active/ep_normal_message_negative_sigma_crash.md deleted file mode 100644 index 17f595b6..00000000 --- a/active/ep_normal_message_negative_sigma_crash.md +++ /dev/null @@ -1,63 +0,0 @@ -# EP guide crashes: strict NormalMessage built from a negative-variance EP message - -Type: bug -Target: autofit -Repos: -- PyAutoFit -Difficulty: medium -Autonomy: safe -Priority: normal -Status: formalised - -The nightly PyAutoHeart `workspace-validation.yml` run is RED on a single cell, -`run_scripts (3.12, autolens, guides)`, because -`autolens_workspace/scripts/guides/modeling/advanced/expectation_propagation.py` -crashes during expectation propagation with: - -``` -autofit.exc.MessageException: NormalMessage sigma cannot be negative, got sigma=-0.016... -``` - -It is **stochastic / unseeded**: `sigma=-0.016` on 2026-07-13, `sigma=-0.17` on -2026-07-11 (the 2026-07-10 red was a different, earlier step). Different negative -values across runs ⇒ the negative width is produced inside the EP message-passing -algebra, not from a fixed model mistake. - -Root cause (grounded, not the stale hint in the error string): - -- The guard `assert_sigma_non_negative` (`autofit/messages/normal.py:45`) rejects - `sigma < 0` at strict `NormalMessage.__init__` (`normal.py:110`). It was added - deliberately for the *prior-passing* misuse case and is load-bearing there — - do not simply delete it. -- Its error hint blames `RelativeWidthModifier`, but that hint is **stale for this - path**: `RelativeWidthModifier.__call__` already returns `value * abs(mean)` - (`autofit/mapper/prior/width_modifier.py:110`, #1331 D5), so it cannot emit a - negative sigma. The EP crash does not come from prior passing. -- EP legitimately produces intermediate messages with negative / infinite - variance (cavity division of Gaussians). The codebase already has the correct - vehicle for this: `NaturalNormal` (`normal.py:508`), which permits - `eta2 ∈ (-inf, 0)` and bypasses the guard by calling `AbstractMessage.__init__` - directly. The bug is that somewhere on the EP / declarative path a **strict - `NormalMessage`** (guard-enforced) is being constructed from a negative-variance - state where a `NaturalNormal` should be used (or the projection/inversion that - yields the passed `sigma` has a sign/validity bug), so a legitimate transient - EP message is hard-crashing instead of being handled/damped. - -Fix locus is the **PyAutoFit library** (`autofit/graphical/expectation_propagation` -and/or `autofit/messages/normal.py`), NOT the workspace guide. Per no-autoimmunity: -do **not** seed the guide script, constrain its priors, or otherwise mask the -symptom — the guide is documentation and must keep exercising the real EP path. - -Scope of a fix should include: identify the exact construction site turning a -negative-variance EP message into a strict `NormalMessage`; route it through -`NaturalNormal` / the invalid-message handling (`AbstractMessage.update_invalid`, -`normal.py:346`) so transient negative-variance cavities are tolerated or damped -rather than raised; keep the strict guard intact for the genuine prior-passing -case; correct the now-misleading `RelativeWidthModifier` hint in the exception; -add a regression test in `test_autofit/graphical/` that drives an EP update -through a negative-variance cavity without raising; confirm the EP guide script -runs green in workspace-validation. - -Evidence: PyAutoHeart workspace-validation runs 29227574734 (2026-07-13) and -29153442364 (2026-07-11). Related history: #1331 / #1348 (sigma<=0 semantics, -point-mass idiom), EP inherent-randomness work. diff --git a/active/ep_profiling_breakdown.md b/active/ep_profiling_breakdown.md deleted file mode 100644 index 4808e7ef..00000000 --- a/active/ep_profiling_breakdown.md +++ /dev/null @@ -1,16 +0,0 @@ -The project @z_projects/ic50_workspace is our IC50 use case which we are now aiming to scale up the EP framework -to the IC50 use case. - -Can you perform a run of ep_sim.py, and perform a timing break down of all the different steps that go into -the overall EP run time, which would include things like: - -1) Time spent doing each IC50 Hill curve fit in a FactorAnalysis using Dynesty, total time and time per EP iteration. -2) Time spent fitting the global model. -3) Time spent doing all non fitting boiler plate (e.g. PyAutoFit over heads seting up graph, iterations around the EP loop, and so forth). - -Can you attempt to break 3) down into sub categories. - -Given the time taken for 5 datasets in this example, present a proejction for how long 100, 1000, 10000 would take. - -This will then form the basis of us optimizing and improving all EP functioanlity so it runs fast enough to scale up -to lsrger samples. \ No newline at end of file diff --git a/active/ep_statistics_fix_batch.md b/active/ep_statistics_fix_batch.md deleted file mode 100644 index 47f657dd..00000000 --- a/active/ep_statistics_fix_batch.md +++ /dev/null @@ -1,52 +0,0 @@ -# `@PyAutoFit` EP statistics fix batch — F1/F2/F4/F8 from the #1332 audit - -Type: bug -Target: autofit -Difficulty: medium -Autonomy: supervised -Priority: high -Status: formalised - -The EP statistics audit (PyAutoFit#1332, `ep-statistics-audit`) confirmed -9 findings; its recommended fix batch (F1+F2+F3+F4+F8) was gated on the -priors/messages decision hub #1331. That guidance is now delivered and -shipped (#1345, #1348), and F3 + the F10 guard landed in #1349, F5 in -#1334, F7(c) in #1345. This prompt executes the remainder. - -## Scope (all in `autofit/graphical` + `autofit/messages` + `laplace/`) - -- **F1** — `MeanField.__truediv__` / `__pow__` pass `log_norm` into the - `plates` ctor slot: evidence silently dropped, a float lands in - `_plates`. Fix the ctor calls (and `__pow__`'s meaningless - `log_norm * other.log_norm` branch). This is F7(a). -- **F2 (+extended)** — `GammaMessage.kl` and `BetaMessage.kl` compute the - reverse direction vs Normal/TruncatedNormal. Contract: - `self.kl(other) = KL(self‖other)`, stated once, enforced family-wide - with a property test (EPHistory sums per-variable KLs — mixed graphs - currently mix directions). -- **F4** — `AbstractMessage.update_invalid` scalar branch is self-flagged - broken (`# TODO: Fairly certain this would not work`); it is the - BAD_PROJECTION recovery path. Fix + unit test both branches. -- **F8** — delete dead/suspect quasi-Newton variants in - `laplace/newton.py` (`diag_sr1_bfgs_update` returns None, - `bfgs1_update` sign-disputed, `diag_sr1_update_` unused); only the - exported `full_*` variants stay. - -## Explicitly out of scope - -- **F6** (truncated-normal KL uses untruncated formula) — needs - truncated-moment math; own prompt later. -- **F7(b)** (where sampler per-factor evidence is recorded — - `MeanField.from_priors` defaults `log_norm=0`) — design decision. -- **F9** (private scipy `_linesearch` import) — robustness chore. - -## Validation - -Full `test_autofit/` suite; new unit tests per fix (log_norm/plates -round-trip through `__truediv__`/`__pow__`, KL direction property test -across Normal/TruncatedNormal/Gamma/Beta, update_invalid scalar+array); -re-run `autofit_workspace_test/scripts/graphical/ep_*.py` (parity, -deterministic, exact) as integration smoke. - - diff --git a/active/ep_walkthrough_mean_field_summary.md b/active/ep_walkthrough_mean_field_summary.md deleted file mode 100644 index 69f50825..00000000 --- a/active/ep_walkthrough_mean_field_summary.md +++ /dev/null @@ -1,19 +0,0 @@ -# `@autofit_workspace` Wire mean_field_summary into the EP walkthrough - -Type: docs -Target: autofit_workspace -Difficulty: easy -Autonomy: safe -Priority: low -Status: formalised - -Follow-up from PyAutoFit#1335 (merged #1349): the Phase-4 diagnostics -module exports `mean_field_summary()` / `EPDiagnostics` / -`check_sigma_collapse` from `autofit.graphical`. Wire an end-of-example -`mean_field_summary()` call (and a pointer at the emitted -`ep_history.csv` / `mean_field_evolution.png` artifacts) into -`scripts/features/expectation_propagation.py` (merged via -autofit_workspace#82), so the walkthrough demonstrates the built-in -diagnostics instead of stopping at the raw mean field. - - diff --git a/active/euclid_assistant_duplicate_bibtex_keys.md b/active/euclid_assistant_duplicate_bibtex_keys.md deleted file mode 100644 index a6adf024..00000000 --- a/active/euclid_assistant_duplicate_bibtex_keys.md +++ /dev/null @@ -1,124 +0,0 @@ -# Detect duplicate BibTeX entry keys - -Add a deterministic, provenance-backed warning to `euclid_assistant` for literal -BibTeX keys that are defined more than once in referenced `.bib` files or inline -`thebibliography` entries. Include focused tests and update generated and -human-readable rule documentation. - -## Original request - -You are working in the `euclid_assistant` repository (a deterministic style - auditor for Euclid/A&A LaTeX papers). Read `AGENTS.md` first and follow it: - rules must carry Style-Guide/PDD provenance, tests must pass, never modify the - original source files under `knowledge/sources/`, and keep dependencies light - (PyYAML only). - - # Task - Add a deterministic check for **duplicate BibTeX entry keys** — the same entry - key defined two or more times across the bibliography. This is Style Guide - Sect. 2.6 item 8 (ii) ("Multiple occurrences of the same paper ... the .bib file - contains the same paper multiple times"), p. 20. BibTeX silently keeps only one - such entry, so duplicates are real bugs. - - # How the linter is structured (read these before coding) - - `src/euclid_assistant/lint/rule_engine.py` — defines `Finding` (dataclass: - rule_id, severity, title, scope, file, line, column, message, snippet, source) - and `RuleEngine`. Function-type rules dispatch to `checks.REGISTRY[name]` with - signature `fn(doc, rule, engine) -> List[Finding]`. - - `src/euclid_assistant/lint/checks.py` — the check functions + `REGISTRY` dict. - Look at `bibliography_not_euclid` for the pattern that locates `\bibliography`/ - `\addbibresource` arguments via `_command_arg_spans(code, cmd)`. - - `src/euclid_assistant/lint/latex_scanner.py` — `FlatDoc` (flattened .tex); - `doc.code` is the comment-stripped flattened source, `doc.main_path` is the - main .tex path; `_command_arg_spans` extracts command arguments. - - `src/euclid_assistant/ingest/extract_latex_assets.py` — has `_BIB_ENTRY = - re.compile(r"@(\w+)\s*\{\s*([^,\s]+)", re.MULTILINE)` which extracts BibTeX - (type, key). Reuse this regex shape for parsing .bib files. - - `rules/euclid_rules.yaml` — rule definitions. Existing bib rules: - `EUCLID-BIB-FILE`, `EUCLID-REF-IN-PREP`, `EUCLID-REF-ARXIV-EPRINTS`. - - `rules/bibliography_rules.yaml` — already documents the approved journal - abbreviations and the manual checks (context only; no code reads it yet). - - # Implement - - 1. **New rule** in `rules/euclid_rules.yaml` (mirror the existing schema exactly): - ```yaml - - id: EUCLID-BIB-DUPLICATE-KEY - title: 'Duplicate BibTeX entry key' - severity: warning - scope: bibliography - source: {file: style_guide, section: "2.6 References (item 8)", page: 20} - rationale: > - 'The same BibTeX entry key is defined more than once; BibTeX keeps only one - and silently drops the rest, so citations may resolve to the wrong paper.' - detection: - type: function - name: bibliography_duplicate_keys - autofix: {safe: false} - examples: - bad: "@ARTICLE{Smith2020,...}\n@ARTICLE{Smith2020,...}" - good: "@ARTICLE{Smith2020,...}\n@ARTICLE{Smith2020b,...}" - ``` - - 2. **New check** `bibliography_duplicate_keys(doc, rule, engine)` in `checks.py`, - registered in `REGISTRY`: - - Collect candidate `.bib` files: for `cmd in ("bibliography", - "addbibresource")`, read each arg via `_command_arg_spans(doc.code, cmd)`, - split on commas, append `.bib` if there is no extension, and resolve - relative to `doc.main_path.parent`. Skip files that do not exist. - - Also collect inline keys from `\bibitem{key}` / `\bibitem[..]{key}` in - `doc.code` (these live in the flattened .tex, so you can map them with - `doc.offset_to_location`). - - Parse each existing `.bib` with the `@type{key,` regex, recording every - key together with its **source file (relative path) and 1-based line - number** (compute the line by counting `\n` up to the match start). - - Build `key -> list of (file, line)`. For any key occurring more than once, - emit findings. Because .bib findings are NOT in the flattened .tex line map, - construct `Finding` objects directly (import `Finding` from - `.rule_engine`) rather than using `engine.make_finding`; for `\bibitem` - duplicates you may use `engine.make_finding`. Use: - severity=rule["severity"], title=rule["title"], - scope=rule.get("scope","bibliography"), source=rule.get("source",{}), - file=, line=, column=1, - message=f"Duplicate BibTeX key '{key}' (also defined at )." - Emit one finding per duplicate occurrence after the first (so a key defined - 3x yields 2 findings), and make the message list the other locations. - - Be robust: a missing/unreadable .bib must not crash — skip it. Comments in - .bib start with `%`; ignoring them is optional (the `@type{` regex already - avoids most false hits). - - 3. **Tests** in a new `tests/test_bibliography.py` (mirror `tests/test_lint_checks.py`): - - Use `tmp_path`. Write a `main.tex` with `\bibliography{refs}` and a `refs.bib` - containing two `@ARTICLE{Dup2020,...}` entries plus a unique one. Flatten, - run `RuleEngine(load_rules()).run(doc)`, assert a `EUCLID-BIB-DUPLICATE-KEY` - finding exists, that its message names `Dup2020`, and that its `file` ends in - `refs.bib`. - - A clean `refs.bib` (all unique keys) yields no such finding. - - A `\bibitem{X}` appearing twice in a `thebibliography` block is flagged. - - Optional: assert no duplicate-key findings on the real corpus .bib files - under `knowledge/sources/paper_sources/*/` (good false-positive check). - - 4. **Docs**: add the rule to `wiki/rules/references.md` (one bullet citing - Sect. 2.6 item 8, p. 20, with the `EUCLID-BIB-DUPLICATE-KEY` id), regenerate the - coverage matrix with `python -m euclid_assistant.cli wiki`, and tick the - bibliography item in `wiki/project/open-issues.md` (note duplicate-key detection - is done; journal-name normalisation and the JCAP volume fix remain). - - # Run and validate (cache env vars matter on this machine) - ``` - NUMBA_CACHE_DIR=/tmp/numba_cache MPLCONFIGDIR=/tmp/matplotlib PYTHONPATH=src python -m pytest -q - NUMBA_CACHE_DIR=/tmp/numba_cache MPLCONFIGDIR=/tmp/matplotlib PYTHONPATH=src python -m euclid_assistant.cli rules | tail -1 - ``` - All existing tests must still pass, and `audit` on the four papers under - `knowledge/sources/paper_sources/` must still report 0 errors (the new rule is a - warning). Do NOT add Claude/AI co-author trailers to any commit. Leave the commit - to me unless I ask you to commit; if you do commit, use a plain descriptive - message and do not push. - - A couple of notes for you: - - - The trickiest part is that duplicate keys live in .bib files, which aren't part of the flattened .tex the linter normally walks — so the check has to locate, read, and line-number the .bib itself and - build Finding objects directly. The prompt spells that out, which is where Codex would otherwise stumble. - - I scoped it to literal duplicate keys (a real, deterministic BibTeX error). The Style Guide's "same paper under different labels" is a separate, fuzzier problem — I'd leave that out of this task. - - Once that's in, the natural follow-ups (same prompt style) are journal-name normalisation against the approved-abbreviation list and the JCAP volume/issue fix, both already documented in - rules/bibliography_rules.yaml. diff --git a/active/euclid_assistant_style_guide_wiki_and_paper_sweep.md b/active/euclid_assistant_style_guide_wiki_and_paper_sweep.md deleted file mode 100644 index 99fb798e..00000000 --- a/active/euclid_assistant_style_guide_wiki_and_paper_sweep.md +++ /dev/null @@ -1,15 +0,0 @@ -# Euclid assistant style guide wiki and paper sweep - -## Repository - -@euclid_assistant - -## Request - -Review the Euclid assistant wiki/rules against the PDF style-guide sources so that key style information is not missed, with specific attention to telescope/mission names such as the James Webb Space Telescope and its required italicisation where applicable. - -Then perform another text-only formatting and typesetting sweep of the LaTeX paper tree at `/mnt/c/Users/Jammy/Science/euclid` in line with the Euclid Style Guide. Do not change images. - -## Original user request - -> You have already done one type setting review on this paper, but I noted certain things were missing, firstly that Jaesm Webb Space Telecope was not correctly italicised. Can you have another sweep at making sure the wiki is fully in line with the .pdf docs and doesnt miss key information, andthen try and do another sweep of improving the formatting and type setting of text (not images) of /mnt/c/Users/Jammy/Science/euclid in line with the style guide diff --git a/active/euclid_pipeline.md b/active/euclid_pipeline.md deleted file mode 100644 index 830faf33..00000000 --- a/active/euclid_pipeline.md +++ /dev/null @@ -1,12 +0,0 @@ -I currently have source code repos (PyAutoConf, PyAutoFit, PyAutoArray, PyAutoGalaxy, PyAutoLens) and -workspaces (autofit_workspace, autogalaxy_workspace, autolens_workspace). - -These are buuilt into the claude dev cycle, including ship_library and ship_workspace. - -I want to include @euclid_strong_lens_modeling_pipeline in the ship_workspace cycle, and in the PyAutoBuild deployment. -This thing should be maintained and up to date as the core workspasce repos. - -Thus, can you first add a .github actions to it, which runs after ships like the workspace github actions do. - -Can you also give it github actions following similar logic to things like autolens_workspace which now -uses github actions. \ No newline at end of file diff --git a/active/factor_graph_visualize_combined_dispatch.md b/active/factor_graph_visualize_combined_dispatch.md deleted file mode 100644 index 38b2fb68..00000000 --- a/active/factor_graph_visualize_combined_dispatch.md +++ /dev/null @@ -1,16 +0,0 @@ -# FactorGraphModel per-type visualize_combined dispatch - -Type: refactor -Target: PyAutoFit -Difficulty: small -Autonomy: safe -Priority: high -Status: formalised - -FactorGraphModel.visualize_combined (autofit/graphical/declarative/collection.py) routes EVERY factor into -the lead factor's Visualizer.visualize_combined — mixed-dataset graphs (AnalysisImaging + AnalysisWeak, -first built by the weak series step 8) crashed until PyAutoLens's visualizers grew type filters -(PyAutoLens#587). Fix the producer: group model_factors by their analysis Visualizer class and call each -group's visualize_combined once with that group's factors and the matching sub-instances. Homogeneous -graphs must produce byte-identical behaviour (single group == today's call). The PyAutoLens type filters -stay as defence in depth. Unit test with two stub Visualizer classes recording their calls. diff --git a/active/fast_nnls_solver_optimization.md b/active/fast_nnls_solver_optimization.md deleted file mode 100644 index c75b4d00..00000000 --- a/active/fast_nnls_solver_optimization.md +++ /dev/null @@ -1,15 +0,0 @@ -# Optimize the fast non-negative least squares solver for the Delaunay and rectangular mesh pipelines - -Type: feature -Target: autolens_profiling -Repos: -- PyAutoArray -- PyAutoLens -Difficulty: medium -Autonomy: supervised -Priority: normal -Status: formalised - -You are working in the PyAutoLens codebase. First, review the recent Delaunay mesh speed-up work and understand what was changed and why. Then assess whether optimizing the fast non-negative least squares solver is the right next target, considering both Delaunay and rectangular mesh pipelines. If it is, propose and implement a careful optimization with benchmarks and numerical stability checks. Keep the design clean and document it so that it can be ingested into PyAutoBrain later. Before coding, summarize your understanding, confirm the optimization target, and outline a step-by-step plan. - - diff --git a/active/fast_viz_zero_contour_perf_fix.md b/active/fast_viz_zero_contour_perf_fix.md deleted file mode 100644 index dfaa8cd6..00000000 --- a/active/fast_viz_zero_contour_perf_fix.md +++ /dev/null @@ -1,222 +0,0 @@ -# Fast visualization Phase A′ — zero_contour perf fix + safety net - -This task is the **prerequisite for any future Phase A flip / Phase B -latent migration** of `z_features/fast_visualization.md`. Two coupled -bugs prevent `zero_contour`-based critical curves and Einstein radii -from being viable defaults today; this task fixes both and lands the -first regression-net assertion to catch any future regression. - -## Background - -Two earlier attempts to put `zero_contour` on the default visualization -path were reverted because the silent-failure mode looked successful -from outside: - -- **2026-04-18 → 2026-04-19** — PyAutoGalaxy commit `aea3bc95` flipped - the YAML default in `autogalaxy/config/visualize/general.yaml` from - `marching_squares` to `zero_contour`, reverted in `abd7b717` because - `ZeroSolver` raised inside model-fits and the exception was swallowed - by the broad `except Exception: return None, None, None, None` at - `PyAutoLens/autolens/imaging/plot/fit_imaging_plots.py:52`. Critical - curves silently vanished on supercomputer runs. -- **2026-05-16 → 2026-05-17** — same failure shape on the Euclid DR1 - pipeline (PR #1280 reverted `use_jax_for_visualization=True` default). - Source-plane FITS files wrote all-zero, Einstein-radius posteriors - collapsed to the full prior across every tile, none of which raised. - -The 2026-05-21 perf benchmark on an SIE + circular source revealed a -third, independent issue: `_critical_curve_list_via_zero_contour` at -`PyAutoGalaxy/autogalaxy/operate/lens_calc.py:1167-1170` builds a fresh -`f = self._make_eigen_fn(...)` and `solver = ZeroSolver(...)` on every -invocation. Because JAX's compiled function cache is keyed on callable -identity, every call rebuilds the JIT cache and pays the full -~10-second compile cost. Measured on CPU: - -| Method | First call | Warm call | -|---|---|---| -| `marching_squares` | 32 ms | 32 ms | -| `zero_contour` (current code) | 10300 ms | 10300 ms | -| `zero_contour` (reused `f` / solver) | 10679 ms | **66 ms** | - -With the closure cached, `zero_contour` is fast enough on warm calls to -be a sensible default for any JIT'd likelihood function. The compile -cost still applies on the first call in a process — that's the reason -`marching_squares` stays the plotter's YAML default for one-shot plotting, -while JIT'd callers explicitly route via `_via_zero_contour_from()`. - -## What to change - -### 1. PyAutoGalaxy — cache `(f, solver)` in `LensCalc` - -`@PyAutoGalaxy/autogalaxy/operate/lens_calc.py` - -In `_critical_curve_list_via_zero_contour` (around line 1121), cache the -`(f, solver)` tuple on the `LensCalc` instance keyed on -`(kind, pixel_scales, tol, max_newton)`. Suggested shape: - -```python -def _critical_curve_list_via_zero_contour(self, kind, ...): - cache_key = (kind, pixel_scales, tol, max_newton) - cached = getattr(self, "_zero_contour_cache", {}).get(cache_key) - if cached is None: - f = self._make_eigen_fn(kind=kind, pixel_scales=pixel_scales) - solver = ZeroSolver(tol=tol, max_newton=max_newton) - self._zero_contour_cache = getattr(self, "_zero_contour_cache", {}) - self._zero_contour_cache[cache_key] = (f, solver) - else: - f, solver = cached - ... -``` - -Pick whichever idiomatic-Python shape is cleanest (`functools.cached_property` -won't work because the key is parameterised; a plain dict on the instance -or a `functools.lru_cache`-decorated helper that returns `(f, solver)` are -both fine). - -**Verification:** the regression-net script in step 3 below asserts that -the second call from a fresh `LensCalc` is under 100 ms on CPU. - -### 2. PyAutoLens — tighten the broad `except` - -`@PyAutoLens/autolens/imaging/plot/fit_imaging_plots.py:52` - -Replace: - -```python -try: - tan_cc, rad_cc = _critical_curves_from(tracer, grid) - tan_ca, rad_ca = _caustics_from(tracer, grid) - ... - return image_plane_lines, ..., source_plane_line_colors -except Exception: - return None, None, None, None -``` - -with specific catches for the *known recoverable* failure modes plus a -loud warning for anything else: - -```python -import logging -logger = logging.getLogger(__name__) - -try: - tan_cc, rad_cc = _critical_curves_from(tracer, grid) - tan_ca, rad_ca = _caustics_from(tracer, grid) - ... - return image_plane_lines, ..., source_plane_line_colors -except ModuleNotFoundError: - # jax_zero_contour missing in this environment — already handled - # upstream in plot_utils._critical_curves_method() with a warning. - return None, None, None, None -except ValueError: - # No zero crossings in the eigenvalue grid (e.g. slope >= 2 - # isothermal where lambda_r > 0 everywhere). Curves don't exist - # for this model. - return None, None, None, None -except Exception: - logger.warning( - "Critical-curve computation failed unexpectedly; rendering " - "without overlays. Investigate — this used to be silent.", - exc_info=True, - ) - return None, None, None, None -``` - -The unit test for this change should re-broaden the bare `except` to -`except Exception:` in a test fixture, raise a synthetic exception -inside `_critical_curves_from`, and assert that: - -1. The original broad-except returns `(None, None, None, None)` silently. -2. The new tightened-except logs at `WARNING` level with traceback info. - -### 3. autolens_workspace_test — first `__Visualization Sanity__` block - -`@autolens_workspace_test/scripts/imaging/modeling_visualization_jit.py` - -Append a `__Visualization Sanity__` block following the same prose-block -style as the existing `__Likelihood Sanity__` block in the same file -(see line ~170). The block sits inline before the Nautilus search, -builds the prior-median instance, and asserts: - -```python -import time -import numpy as np -from autogalaxy.operate.lens_calc import LensCalc - -# Build the LensCalc from the prior-median tracer. -instance = model.instance_from_prior_medians() -tracer = analysis.tracer_via_instance_from(instance=instance) -od = LensCalc.from_tracer(tracer) - -# Correctness: zero_contour produces a non-empty tangential critical curve -# and a finite, positive Einstein radius. -tc = od.tangential_critical_curve_list_via_zero_contour_from() -assert len(tc) > 0, "no tangential critical curves (zero_contour regression)" -er = od.einstein_radius_via_zero_contour_from() -assert np.isfinite(er) and er > 0, "Einstein radius unconstrained (zero_contour regression)" - -# Perf regression net: with the (f, solver) cache fix, the second call -# from the SAME LensCalc must be under 100 ms on CPU. The first call -# pays the ~10 s ZeroSolver compile. -od.tangential_critical_curve_list_via_zero_contour_from() # warm the cache -t0 = time.perf_counter() -od.tangential_critical_curve_list_via_zero_contour_from() -dt = time.perf_counter() - t0 -assert dt < 0.1, ( - f"zero_contour warm call took {dt*1000:.1f} ms — closure-cache-busting bug " - "may have regressed (see fast_viz_zero_contour_perf_fix)" -) -``` - -Also append the **silent-zero source-plane** assertion from the tracker's -imaging template (line 163), so the block catches both failure modes -(perf regression *and* algorithmic collapse) in one place. - -Do NOT propagate this pattern to other dataset types in this task — that's -Phase D rollout, gated on this pilot landing. - -## Out of scope - -- **No config flip.** `autogalaxy/config/visualize/general.yaml:8` stays - `marching_squares`. The config-flip / context-aware-dispatch design - is Phase A's follow-up sub-prompt. -- **No `z_projects/euclid` edits.** That tree is live science work. The - Euclid latent migration (Phase B) targets - `euclid_strong_lens_modeling_pipeline/util.py:491` and is a separate - sub-prompt to author after this lands. -- **No new `__Visualization Sanity__` blocks** beyond - `modeling_visualization_jit.py`'s. Rollout across other scripts is - Phase D. -- **No IPython display wiring.** `BackgroundQuickUpdate` already exists; - the `update_display(fig, display_id=...)` work is Phase C. - -## Verification - -1. **Library unit tests:** - - `pytest test_autogalaxy/operate/test_lens_calc.py` (cache behaviour - + correctness of the cached path). - - `pytest test_autolens/imaging/plot/` (broad-except tightening, log - assertion). -2. **Workspace smoke:** - - `python autolens_workspace_test/scripts/imaging/modeling_visualization_jit.py` - runs cleanly under `use_jax=True`, the `__Visualization Sanity__` - block passes (curve count, Einstein radius, < 100 ms warm). -3. **Pipeline smoke:** - - `/smoke_test` against `euclid_strong_lens_modeling_pipeline` after - library changes — confirms the perf fix didn't regress the pipeline - scripts. Catches any latent / lens_calc API drift. -4. **Benchmark re-run:** - - `python /tmp/bench_critical_curves.py` on the fix branch — warm - `zero_contour` calls should drop from ~10300 ms to under 100 ms. - -## References - -- `z_features/fast_visualization.md` — parent tracker, Phase A′ section. -- `complete.md::jax-viz-default-broken` — 2026-05-17 revert of the same - failure shape. -- PyAutoGalaxy commit `abd7b717` — 2026-04-19 revert of the YAML default - flip; the message describes the broad-except silent-zero failure mode. -- `feedback_no_silent_guards` (memory) — codebase rule against silent - catch-and-degrade. -- `feedback_euclid_pipeline_not_z_projects` (memory) — `z_projects/euclid` - is off-limits; pipeline workspace is the target for related changes. diff --git a/active/feature.md b/active/feature.md deleted file mode 100644 index 746a6d59..00000000 --- a/active/feature.md +++ /dev/null @@ -1,304 +0,0 @@ -# So I’d document it as - -Type: feature -Target: PyAutoBrain -Difficulty: too-large -Autonomy: supervised -Priority: normal -Status: formalised - -So I’d document it as: - -Feature Agent, the growth function of PyAutoBrain. - -Here’s the prompt. - -Implement the PyAutoBrain Feature Agent. - -Context - -The PyAuto ecosystem is evolving into a software organism. - -Current architecture: - -PyAutoMind -Stores intent, goals, prompts, priorities and future work. -PyAutoBrain -Contains specialist reasoning agents. -PyAutoMemory -Stores accumulated scientific, software and project knowledge. -PyAutoHeart -Performs health checks and readiness validation. -PyAutoBuild -Executes build, development and release workflows. -This may later become PyAutoHands. - -The Feature Agent is the PyAutoBrain agent responsible for deciding how the organism should grow. - -It reasons over feature work. - -It does not directly implement code unless explicitly delegated through the existing development workflow. - -Goal - -Implement the initial Feature Agent. - -The Feature Agent should: - -select suitable feature tasks from PyAutoMind, -accept an explicitly requested feature task, -select tasks by difficulty or available model capability, -decide whether a task is too large and must be phased, -use PyAutoMemory for scientific and architectural context, -integrate closely with the existing development lifecycle: -start_dev -ship_library -ship_workspace -coordinate with Health Agent and Build Agent where appropriate. -Core responsibilities - -The Feature Agent should support three modes. - -1. Specific task mode - -The user provides a specific PyAutoMind prompt or issue. - -The Feature Agent should: - -read the task, -inspect relevant context, -consult PyAutoMemory, -classify target repositories, -decide whether it is ready for development, -produce a plan compatible with start_dev, -identify whether library work, workspace work, or both are required. -2. Task selection mode - -The user asks the agent to choose what to work on. - -The Feature Agent should: - -inspect PyAutoMind priorities, queues and available prompts, -consider project priorities, -consider repository health, -consider recent work, -consider dependencies between tasks, -choose the best next feature task, -explain why it selected that task. - -It should not simply pick the first prompt in a list. - -3. Difficulty-constrained mode - -The user specifies constraints such as: - -choose an easy task, -choose a high-impact task, -choose something suitable for a weak model, -choose something suitable for a strong model, -choose something that can be done with limited tokens, -choose something ambitious for an overnight run. - -The Feature Agent should estimate task difficulty and select accordingly. - -Difficulty should consider: - -number of repositories affected, -amount of code likely to change, -scientific complexity, -architectural risk, -test burden, -documentation burden, -whether PyAutoMemory context is required, -whether the task requires human judgement. -Task sizing and phasing - -The Feature Agent must explicitly decide whether a task is: - -small enough to implement directly, -medium and suitable for one PR, -large and should be split into phases, -too ambiguous and should become a research/design task first. - -If a task is too large, it should produce phased feature prompts. - -For example: - -feature/autofit/sbi_phase_1_design.md -feature/autofit/sbi_phase_2_core_api.md -feature/autofit/sbi_phase_3_workspace_examples.md -feature/autofit/sbi_phase_4_docs.md - -Each phase should be independently shippable. - -Prefer multiple small PRs over one large fragile PR. - -Relationship to PyAutoMind - -PyAutoMind stores intent. - -The Feature Agent reasons over that intent. - -It should understand the PyAutoMind taxonomy, including paths such as: - -feature/autolens/... -feature/autofit/... -feature/autoarray/... -research/autofit/... -experiment/autoarray/... - -If the task belongs in another category, the Feature Agent should say so. - -For example: - -unclear science → research task -proof-of-concept → experiment task -behaviour fix → bug task -internal cleanup → refactor task -release/versioning → release/build task - -The Feature Agent should help keep PyAutoMind organised. - -Relationship to PyAutoMemory - -Before planning substantial scientific or architectural feature work, the Feature Agent should consult PyAutoMemory. - -Use PyAutoMemory to gather: - -scientific background, -prior design decisions, -relevant papers or summaries, -known architectural constraints, -previous related work, -project-specific context. - -The Feature Agent should cite or summarise which memory sources influenced the plan. - -Do not invent scientific context if PyAutoMemory has relevant material. - -Relationship to development workflow - -The Feature Agent should pair closely with the current PyAuto development lifecycle. - -It should know when to use or recommend: - -start_dev -start_library -start_workspace -ship_library -ship_workspace -pyauto-status -handoff -active / planned / complete task tracking - -The Feature Agent should not bypass this workflow. - -It should produce outputs that can be consumed by existing commands and skills. - -For library-only work: - -start_dev -> start_library -> ship_library - -For workspace-only work: - -start_dev -> start_workspace -> ship_workspace - -For combined work: - -start_dev -> library branch/PR -> workspace branch/PR -> ship both in order - -The agent should explicitly identify which path applies. - -Relationship to Health Agent and Build Agent - -Before recommending that work proceed, the Feature Agent should consult or request a health assessment where appropriate. - -Use the Health Agent when: - -repository health is unknown, -a task is risky, -a task affects multiple repositories, -a task is intended for release, -previous checks have failed. - -Use the Build Agent / PyAutoBuild when: - -development execution should begin, -PR creation is needed, -packaging or release steps are needed, -the task is ready to move from planning into action. - -The Feature Agent reasons. - -PyAutoBuild executes. - -PyAutoHeart measures health. - -Output format - -The Feature Agent should produce a structured decision. - -Include: - -Selected task: - - -Mode: -specific | selection | difficulty-constrained - -Why this task: - - -Difficulty: -small | medium | large | too-large - -Recommended workflow: -library | workspace | combined | research | experiment | refactor | bug - -Relevant context: - - -Phase decision: -direct | split-into-phases | research-first | defer - -Execution plan: - - -Health considerations: - - -Risks: -
- -Next action: - -Claude skill constraints - -If implemented as a Claude skill or markdown agent definition: - -keep every .md file below 200 lines, -follow Claude skill guidelines, -keep the main instruction file concise, -move long examples and architecture notes into supporting docs, -avoid large essays inside the skill file. -Validation - -Run available tests or smoke checks. - -Validate that: - -the Feature Agent can identify feature prompts in PyAutoMind, -it can select a task when none is specified, -it can respect difficulty constraints, -it can recommend phasing for large tasks, -it references PyAutoMemory where appropriate, -it outputs plans compatible with the current development workflow. -PR - -Create one PR titled: - -Implement initial PyAutoBrain Feature Agent - -My only tweak would be: long term, I’d maybe have Growth Agent as the organism-facing name and Feature Agent as the engineering-facing name. But for Codex and repo clarity, Feature Agent is the safer first implementation. - - diff --git a/active/feature_agent_infra_target_resolution.md b/active/feature_agent_infra_target_resolution.md deleted file mode 100644 index a845fd6c..00000000 --- a/active/feature_agent_infra_target_resolution.md +++ /dev/null @@ -1,56 +0,0 @@ -# Feature Agent misclassifies PyAutoBrain-infra prompts as research-first - -Type: bug -Target: PyAutoBrain -Difficulty: too-large -Autonomy: supervised -Priority: normal -Status: formalised - -Feature Agent misclassifies PyAutoBrain-infra prompts as research-first. - -## Symptom - -`bin/pyauto-brain feature feature/pyautobrain/bug_agent.md` returned -`Recommended workflow: research [re-home as research/]` and `Phase decision: -research-first` for a well-scoped PyAutoBrain-infra task (implementing the Bug -Agent). The same misfire applies to the already-shipped -`feature/pyautobrain/{feature,build,health}.md` prompts. - -## Root cause - -`_feature.py` resolves affected repos from `@RepoName` mentions against -`LIBRARY_REPOS` / `WORKSPACE_REPOS`. PyAutoBrain-infra work names no library or -workspace repo, so `repos` is empty; `recommend_workflow` then falls through to -`research` and `phase_decision` to `research-first`. The organs themselves -(PyAutoBrain / PyAutoHeart / PyAutoBuild / PyAutoMind / PyAutoMemory) are neither -library nor workspace, so there is no target class for them. - -## Fix (small, in @PyAutoBrain) - -Teach the Feature Agent an **infrastructure** target class, mirroring the Bug -Agent's `INFRA_TARGETS` map in `agents/conductors/bug/_bug.py`: - -- add an `INFRA_TARGETS` (or `INFRA_REPOS`) set to `_feature.py`; -- when the prompt's `target` (second folder, e.g. `pyautobrain`) or an `@`-mention - resolves to an organ, classify the workflow as `infrastructure` and skip the - `research-first` fallback; -- `feature/pyautobrain/*` prompts should plan `direct` (or phased on genuine size), - not re-home to research. - -Consider factoring the shared `INFRA_TARGETS` map into one place both agents import -(the Bug Agent already imports `_feature`), so the two cannot drift. - -## Validation - -`bin/pyauto-brain feature feature/pyautobrain/bug_agent.md` (from `issued/`, or an -equivalent infra prompt) classifies as `infrastructure` / `direct`, not -`research` / `research-first`. The four `feature/pyautobrain/*` prompts all -classify sensibly. - -## Provenance - -Found while shipping the Bug Agent (PyAutoBrain#18); the misfire was overridden by -hand during that task. - - diff --git a/active/file_subplot.md b/active/file_subplot.md deleted file mode 100644 index 3049b2b8..00000000 --- a/active/file_subplot.md +++ /dev/null @@ -1,11 +0,0 @@ -Currently, subplot_fit is 4 x 3 panels, with two source reconstructions in the middle-right and bottom-right row. - -However, it is common for the Zoom panel to over zoom, making it hard to judge the fit. - -Equally, the Data (Source Scale) panel is verging on useless. - -Therefore, can we: - -1) Move Model Image to where Data (Source Scale) is and remove Data (Scouece Scale). -2) Move Source Plane (Zoomed) up one to the top right and rename its title (Max Zoom). -3) Make a new panel, Source Plane (Mid Zoom), which uses the same centre as Max Zoom but expands the y axia and x axis to be 2.5x larger than Max Zoom. diff --git a/active/fit_imaging_pytree.md b/active/fit_imaging_pytree.md deleted file mode 100644 index 4619c463..00000000 --- a/active/fit_imaging_pytree.md +++ /dev/null @@ -1,107 +0,0 @@ -Can we register @PyAutoLens/autolens/imaging/fit_imaging.py `FitImaging` (and the autoarray / -autogalaxy types it transitively contains) as JAX pytrees, so that a function that returns -`FitImaging` can be wrapped in `jax.jit`? - -__Why this matters__ - -In the JAX-visualization pilot (#1227 on PyAutoFit, shipped via `use_jax_for_visualization`) -we intentionally took the "Path C" route: `fit_from` runs on the eager JAX path -(`use_jax=True` makes `self._xp` = `jnp`, operations run eagerly under JAX) and the -matplotlib plotters materialize arrays to NumPy at the boundary. No `jax.jit` wrapping. - -The endgame — "Path A" — is full `jax.jit` wrapping of `analysis.fit_from`, so visualization -gets the same compile-time speedup the likelihood function does. This requires the -`FitImaging` return type (and every autoarray / autogalaxy / autolens type reachable from -it) to be a JAX pytree. PyAutoLens's CLAUDE.md currently documents the reverse: - -> Autoarray types (`Array2D`, `ArrayIrregular`, `VectorYX2DIrregular`, etc.) are **not -> registered as JAX pytrees**. They can be constructed inside a JIT trace, but **cannot -> be returned** as the output of a `jax.jit`-compiled function. - -This task is the feasibility study for lifting that restriction. - -__What's already in place__ - -Recent autofit work added `autofit.jax.pytrees` (see `@PyAutoFit/autofit/jax/pytrees.py`) -which registers `Model`, `Collection`, `ModelInstance`, and user classes found by walking -a `Model` tree. The pattern: - -- `children` = dynamic (traced) arrays / sub-pytrees -- `aux` = static Python objects (concrete constants, class references, etc.) -- `flatten` / `unflatten` lift/restore the instance across the JIT boundary - -The same pattern needs to extend down through the autoarray / autogalaxy layers. - -__Assess, don't implement (yet)__ - -The deliverable is a **feasibility assessment**, not a fully-registered FitImaging. Produce -the assessment as a markdown document (e.g. in this prompt file's `issued/` location, or -as a draft PR) answering: - -1. **Type inventory.** Walk `FitImaging` (`@PyAutoLens/autolens/imaging/fit_imaging.py`) and - list every distinct class reachable from a populated instance. For each class: - - Where it's defined (autoarray / autogalaxy / autolens) - - Whether it currently carries `tree_flatten` / `tree_unflatten` methods - - Whether its constructor is compatible with pytree unflatten (takes the children as - positional / keyword args) or whether a `_build_*_pytree_funcs` helper (like - `autofit.jax.pytrees._build_instance_pytree_funcs`) would be needed - -2. **Dynamic vs static classification.** For each class, which attributes are dynamic - (JAX arrays — get traced) vs static (numpy arrays that mustn't change, masks, shapes, - pixel scales, redshifts, config objects)? The CLAUDE.md rule for autoarray is: - `grid.array[:, 0]` is traced, `grid.mask` is static. That rule needs to be restated - per type here. - -3. **Structural blockers.** Are there types that *cannot* be registered cleanly? Examples - to look for: - - Classes whose `__init__` does non-trivial work that can't be replayed during - unflatten (e.g. reading config, triggering a numba compilation) - - Classes that hold live references to an `Inversion` whose solver state isn't - pickleable / is stateful under JAX - - `Tracer` / `Galaxies` — these are `List[List[Galaxy]]` nested containers; are they - already pytree-friendly via Python list recursion, or do they need explicit - registration? - - `Mapper` / `LinearEqn` / NNLS solver state inside `Inversion` — likely the hardest - -4. **Interaction with the existing "xp is np" guard pattern.** PyAutoLens has an established - convention (see `@PyAutoGalaxy/autogalaxy/operate/lens_calc.py`): functions that return - autoarray wrappers guard with `if xp is np: return Array2D(...)` else return raw - `jax.Array`. Registering `Array2D` as a pytree means these guards may become - unnecessary. Is removing them desirable, or should they stay for the non-JIT path? - -5. **Proof-of-concept.** Register **one** concrete type (suggest `Array2D` — it's the - simplest leaf) and demonstrate that a toy function `def f(x): return Array2D(values=x*2, - mask=static_mask)` runs under `jax.jit` and returns an `Array2D` backed by - `jax.Array`. This both validates the approach and surfaces any wrinkles that the static - analysis missed. - -6. **Estimate.** Given the inventory and the proof-of-concept, roughly how many types need - registration to make `FitImaging` a pytree? Which ones are cheap (static dataclass-ish), - which are hairy (inversion, mapper)? - -__Scope boundary__ - -- Do **not** change `use_jax_for_visualization` behaviour. That flag currently dispatches - to the eager-JAX path; once this task produces a green light + registration PR, a - follow-up task will flip the dispatch to `jax.jit`. -- Do **not** start mass pytree-registration in this task. One-type PoC only. -- Do **not** register `FitImaging` itself yet — that's the goal *after* every type it - reaches is registered. - -__Starting points__ - -- `@PyAutoLens/autolens/imaging/fit_imaging.py` — the `FitImaging` subclass -- `@PyAutoGalaxy/autogalaxy/imaging/fit_imaging.py` — base `FitImaging` -- `@PyAutoArray/autoarray/structures/arrays/uniform_2d.py` — `Array2D` (suggested PoC type) -- `@PyAutoFit/autofit/jax/pytrees.py` — the existing autofit pytree machinery; mirror its - structure when proposing new registrations -- `@PyAutoLens/CLAUDE.md` — the "xp is np" guard rule this task's outcome will revise - -__Deliverables__ - -1. Type inventory + classification table (markdown) -2. List of structural blockers with suggested workarounds -3. Working PoC registering `Array2D` with a minimal test in - `@PyAutoArray/test_autoarray/jax/` following the three-step pattern in - `autolens_workspace_test/scripts/hessian_jax.py` -4. Effort estimate + recommended ordering for the follow-up implementation task diff --git a/active/fit_pytree_registration_other_datasets.md b/active/fit_pytree_registration_other_datasets.md deleted file mode 100644 index 95360b87..00000000 --- a/active/fit_pytree_registration_other_datasets.md +++ /dev/null @@ -1,102 +0,0 @@ -PyAutoGalaxy's `FitImaging` was given JAX pytree registration in PR #364 -(2026-04-22). `FitInterferometer` followed in PR #376 (see -`autogalaxy/interferometer/model/analysis.py:165-184` — -`_register_fit_interferometer_pytrees` reuses -`autogalaxy/analysis/jax_pytrees.py::register_galaxies_pytree` as a shared helper). - -The remaining gap is `FitEllipse` and `FitQuantity` — neither has pytree -registration today, so `use_jax_for_visualization=True` on `AnalysisEllipse` -or `AnalysisQuantity` either no-ops or crashes when `fit_for_visualization` -tries to lift the fit across the JIT boundary. - -This task ports the imaging / interferometer registration pattern to those -two remaining dataset types. - -__Why this matters__ - -This is **Phase 0c** of `z_features/jax_visualization.md`. Without it, -Phase 1C (`autogalaxy_workspace_test/jax_viz_dataset_coverage.md`) cannot -add JAX viz coverage for ellipse / quantity, and Phase 2 (default -`use_jax_for_visualization` on whenever `use_jax=True`) would silently -break those dataset types. - -__What to register__ - -For each fit class, register the fit and every distinct autoarray / -autogalaxy type reachable from a populated instance. Mirror the pattern -from `_register_fit_interferometer_pytrees` — particularly the use of -`autogalaxy/analysis/jax_pytrees.py::register_galaxies_pytree()` as the -shared galaxies hook. - -1. `@PyAutoGalaxy/autogalaxy/ellipse/fit_ellipse.py` — `FitEllipse`. - Reachable types include `Ellipse`, `Multipole`, `Array2D`, and the - `MaskedDataset` analogue used inside the analysis. Register entry - point in `@PyAutoGalaxy/autogalaxy/ellipse/model/analysis.py` as - `_register_fit_ellipse_pytrees`. - -2. `@PyAutoGalaxy/autogalaxy/quantity/fit_quantity.py` — `FitQuantity`. - Reachable types include `DatasetQuantity` and the autogalaxy quantity - container (`convergence_2d`, `deflections_yx_2d`, `potential_2d`). - Register entry point in `@PyAutoGalaxy/autogalaxy/quantity/model/analysis.py` - as `_register_fit_quantity_pytrees`. - -For each type follow the autofit `register_instance_pytree` pattern (see -`@PyAutoFit/autofit/jax/pytrees.py` and the imaging / interferometer -analogues in the same repo): - -- `children` = JAX-traced arrays / sub-pytrees (e.g. `ellipse.major_axis`, - `quantity.convergence_2d._array`). -- `aux` = static Python objects that mustn't change under tracing (masks, - pixel scales, redshifts, transformer config, `Inversion` solver state). -- The boundary rules from PyAutoLens `CLAUDE.md` apply unchanged: - `array._array` (or `.array`) is dynamic, `array.mask` and shape metadata - are static. - -__What to test__ - -For each fit type, add a registration test in -`@PyAutoGalaxy/test_autogalaxy//jax/test__pytree.py` -following the three-step pattern from -`autolens_workspace_test/scripts/hessian_jax.py`: - -1. Build a minimal populated `Fit*` instance from synthetic inputs. -2. Round-trip through `jax.tree_util.tree_flatten` + `tree_unflatten` and - assert the reconstructed fit matches the original on the dynamic fields. -3. Wrap a toy function that returns the fit in `jax.jit` and confirm it - compiles, runs, and returns a fit whose dynamic leaves are `jax.Array`. - -__Verification__ - -- New unit tests pass: `pytest test_autogalaxy/ellipse/jax`, - `pytest test_autogalaxy/quantity/jax`. -- Existing PyAutoGalaxy unit tests still pass: `pytest test_autogalaxy`. -- Run `/smoke_test` on `autogalaxy_workspace`. The non-JAX paths must be - unchanged — pytree registration only affects JIT, never the eager NumPy - path. - -__Out of scope__ - -- **Interferometer pytree registration** — shipped in PR #376; see - `complete.md` for the entry. Originally this prompt was scoped to - include interferometer; that work was discovered to be already done - during the audit on 2026-05-08 and dropped from scope. -- **No workspace_test JAX visualization scripts in this task.** Those are - written in the follow-up Phase 1C prompt - (`autogalaxy_workspace_test/jax_viz_dataset_coverage.md`). That prompt - is blocked on this one (and Phase 0b) landing. -- **No PyAutoLens equivalent.** PyAutoLens equivalents land separately if - needed. -- **No production workspace adoption.** Tutorials don't get - `use_jax_for_visualization=True` from this task. - -__Reference__ - -- `@PyAutoFit/autofit/jax/pytrees.py` — autofit pytree machinery -- `@PyAutoGalaxy/autogalaxy/interferometer/model/analysis.py:165-184` — - recently shipped sibling pattern to mirror (PR #376) -- `@PyAutoGalaxy/autogalaxy/analysis/jax_pytrees.py` — shared - `register_galaxies_pytree()` helper -- `@PyAutoLens/autolens/imaging/model/analysis.py` — - `AnalysisImaging._register_fit_imaging_pytrees` reference implementation -- `PyAutoPrompt/issued/fit_imaging_pytree.md` — Path A feasibility study (in-flight) -- `PyAutoPrompt/z_features/jax_visualization.md` — sequenced roadmap (Phase 0c) diff --git a/active/fix_ellipse_multipole_scaled_jax.md b/active/fix_ellipse_multipole_scaled_jax.md deleted file mode 100644 index 3fa4310e..00000000 --- a/active/fix_ellipse_multipole_scaled_jax.md +++ /dev/null @@ -1,56 +0,0 @@ -User-reported bug: `EllipseMultipoleScaled` (`@PyAutoGalaxy/autogalaxy/ellipse/ellipse/ellipse_multipole.py:118-168`) is not JAX-traceable. HPC job using `ag.EllipseMultipoleScaled(m=3 or 4, scaled_multipole_comps=Prior, major_axis=...)` inside an `af.Model` fails when `AnalysisEllipse(use_jax=True)` (now default after #412) traces the model. - -Root cause: `EllipseMultipoleScaled.__init__` does derivation work at construction time, calling `convert.multipole_k_m_and_phi_m_from(scaled_multipole_comps, m)` and `convert.multipole_comps_from(k_adjusted, phi, m)` **without `xp=xp`**. The convert helpers accept `xp=np` (fixed in PR #412) but `__init__` has no `xp` argument to thread, so the calls default to numpy. When `instance_from_vector(jax_array)` constructs the instance with JAX tracers in `scaled_multipole_comps`, the numpy calls raise `TracerArrayConversionError`. - -Secondary issue: even if `xp` could be threaded into `__init__`, storing the derived `specific_multipole_comps` at construction time is wrong under `vmap`. The pytree machinery flattens `__init__`-stored attributes; under `vmap`, different batch elements need fresh derivations but get the cached one from the constructing tracer. - -`EllipseMultipole` (non-scaled) is unaffected — its `__init__` just stores `multipole_comps` directly, no derivation. - -The workspace_test JAX parity scripts in `@autogalaxy_workspace_test/scripts/jax_likelihood_functions/ellipse/multipoles.py` cover `EllipseMultipole` only, not `EllipseMultipoleScaled` — that's how this gap shipped silently. - -Please: - -1. Move the derivation out of `EllipseMultipoleScaled.__init__` and into `points_perturbed_from`. The `__init__` should just store `self.scaled_multipole_comps`, `self.major_axis`, `self.m` and skip the `multipole_k_m_and_phi_m_from` / `multipole_comps_from` calls. Don't call `super().__init__(m, specific_multipole_comps)` with a pre-derived value — derive on-the-fly inside `points_perturbed_from`. - -2. In `points_perturbed_from`, the current code does a round-trip: `__init__` builds `specific_multipole_comps` from (k, phi); then `points_perturbed_from` calls `multipole_k_m_and_phi_m_from(specific_multipole_comps, ...)` to extract (k_orig, phi_orig) back. Collapse this round-trip by computing (k_adjusted, phi) directly from `scaled_multipole_comps` once at the top of `points_perturbed_from`: - - ```python - def points_perturbed_from(self, pixel_scale, points, ellipse, n_i=0, xp=np): - k_scaled, phi = multipole_k_m_and_phi_m_from( - multipole_comps=self.scaled_multipole_comps, m=self.m, xp=xp - ) - k = k_scaled * self.major_axis - - symmetry = 360.0 / self.m - comps_adjusted = multipole_comps_from( - k, - symmetry - 2 * phi + (symmetry - (ellipse.angle(xp=xp) - phi)), - self.m, - xp=xp, - ) - - theta = xp.arctan2(points[:, 0], points[:, 1]) - delta_theta = self.m * (theta - ellipse.angle_radians(xp=xp)) - radial = comps_adjusted[1] * xp.cos(delta_theta) + comps_adjusted[0] * xp.sin(delta_theta) - - x = points[:, 1] + radial * xp.cos(theta) - y = points[:, 0] + radial * xp.sin(theta) - return xp.stack(arrays=(y, x), axis=-1) - ``` - -3. Grep the repo for `.specific_multipole_comps` and `.multipole_comps` accessed on an `EllipseMultipoleScaled` instance. If anyone reads those, they need to either be turned into `@property`s that recompute on-the-fly (numpy-only, since they go to plotting/aggregation) OR have their callers updated to call the new derivation path. Likely no external readers — `EllipseMultipoleScaled` is internal. - -4. Add a parity test in `@autogalaxy_workspace_test/scripts/jax_likelihood_functions/ellipse/`: - - Either add a new `multipoles_scaled.py` covering the `EllipseMultipoleScaled` path with the full `fitness._vmap` + `jax.jit(fit_from)` round-trip blocks - - Or extend the existing `multipoles.py` with a `__Scaled Multipoles__` section testing `EllipseMultipoleScaled` alongside `EllipseMultipole` - The point is closing the gap: every multipole variant gets the vmap-validation bar going forward. - -5. Test bar: - - `pytest test_autogalaxy/ellipse/ -v` — 32/32 still pass (numpy semantics unchanged) - - `pytest test_autogalaxy/ -x` — 870/870 still pass - - The new workspace_test script(s) complete the vmap + JIT round-trip with `rtol=1e-4` parity - - Reference numbers for the existing `multipoles.py` script byte-stable (no `EllipseMultipoleScaled` there, so unaffected) - -This is a tight follow-up to PR #411/#412 (the keystone of `ellipse_fitting_jax`). The bug was technically present before the feature — `EllipseMultipoleScaled.__init__` always had the issue — but the feature flipping `use_jax=True` to default exposed it. - -Out-of-scope: the `power_law_multipole.py` (mass-profile) call sites flagged in PR #411's session notes have the same `multipole_comps_from` without `xp` threading issue at their call sites; not in scope here, separate prompt when mass-multipole JAX support is needed. diff --git a/active/frame_registration_shifts.md b/active/frame_registration_shifts.md deleted file mode 100644 index 8d136079..00000000 --- a/active/frame_registration_shifts.md +++ /dev/null @@ -1,49 +0,0 @@ -# Inter-exposure pixel shifts: quantify accuracy, extract for modeling, decide their role in the lens model - -Type: research -Target: pyautoreduce -Repos: -- PyAutoReduce -Difficulty: medium -Autonomy: supervised -Priority: high -Status: formalised - -Follow-up to the shipped frame-products mode (PyAutoReduce #16 / PR #18): the -per-exposure products exist; multi-frame lens modeling now needs to know how -well the frames are registered to each other. - -## Original request (user, 2026-07-10, verbatim) - -> we need to work out how well know the pixel shifts are across exposures, -> ensure we extract this information which is a key input to the lens modeling -> (e.g. maybe we outptu it as a .json or something) and assess if the shifts -> need to be part of the lens model or if they are knownperfect, I guess thats -> a choice. Then do the PSF work - -## Tasks - -1. **Quantify** how well the inter-exposure registration is known, per frame: - the astrometric solution in the `_flc`/`_flt` headers (WCSNAME family — - a-priori GSC/Gaia vs FIT-REL/FIT-IMG Gaia fits — plus their RMS_RA/RMS_DEC/ - NMATCH quality keywords), the align stage's tweakreg trigger + residuals, - and an empirical cross-check on the slacs0008 validation frames (already on - disk under output/frame_products_validation/). Express everything in native - pixels (ACS 0.05"/px). -2. **Extract**: audit what the frames output already carries (per-frame SIP WCS - in each data.fits header + full-distortion `target_pixel` anchor in - frames/manifest.json) and enrich the manifest so registration is explicit - modeling input — e.g. a per-frame `registration` block (wcs solution name, - fit rms in mas and native px, n_matches, relative shift vs the first frame - at the target position) in the existing frames/manifest.json (it IS the - .json the request asks for — no new file unless needed). -3. **Decide/recommend**: should the shifts be (a) treated as perfectly known - (registration rms ≪ the scale lens modeling is sensitive to) or (b) free - nuisance parameters (per-frame dy,dx with tight Gaussian priors set from - the recorded rms)? Deliver a criterion, not just a verdict — the choice is - documented in the design doc (hst_acs_pipeline.md frames section) and feeds - the future PyAutoLens multi-exposure fitting design. This is a scientific - judgment checkpoint (supervised): recommendation goes to the human. - -Constraint: PyAutoReduce claim is contested (slacs1430-acs-parity plans its -phase-4 ship on it) — analysis on main (keck-ao pattern), branch only at ship. diff --git a/active/graphical_ep_scale_up.md b/active/graphical_ep_scale_up.md deleted file mode 100644 index c54d4534..00000000 --- a/active/graphical_ep_scale_up.md +++ /dev/null @@ -1,108 +0,0 @@ -We are now going to begin scaling up the graphical model snad EP frameworks. - -First, in autofit_workspace_developer, we need to make two packages with examples called `graphical` and `ep`. -Lets adapt the following examples to set these up: - -z_projects/concr/scripts/toy - -But lets do this in two separate packages in autofit_workspace_developer called `graphical` and `ep`.. - -These scripts are slow and do not scale to large samples for many reasons, for the graphical example it is because: - -1) It uses the DynestySampler, which takes many samples as its nested sampling and does not scale well with dimensionality. -2) There are many overheads for example outputting lots of results and visuals on a per-factor basis. -3) Even if this output were fast, there is a point where it does not scale for a human because the information is spread out over -many folders in output and thus there are no single-point-of-references to inspect results or look at visualization. - -There are likely other limitations of graphical, thus your first task is to write a prompt PyAutoPrompt/graphical_ep/graphical_scoping.md -which define a prompt which sets up autofit_workspace_developer/graphical, runs it for 3, 10, 30, datasets and does an assessment -of how we can improve it for scalability. The goal should be to target the bigger reasons for it being slow first (e.g. using a faster) -sampling, which likely exploits gradients and then goes on to follow up with other aspects like visualizaition. Its not just about -run-time and speed, its also about making sure a scientist can be confident that as they scale up results are accurate and -robust, so high level information on results is key. Hard-disk size, and time spent writing results to hard disk, -may also prove important both in keep size down and reducing time -- thus moving to a model where runs can be resumed -but do not need to output so much stuff on the hard disk may be benefitial. anyway, have the prompt do the full scoping -with its end goal to break the work down into a series of sub tasks and prompts we can tackle one by one. - -We will then do the same thing for EP. Note that I have already done an EP profiling using a IC50 cancer use case where I got -the information at the bottom of this prompt. However, rerun everything for the toy model, and confirm you agree with the assessment. -Again, remember for now we are just writing prompts in PyAutoPrompt/graphical_ep/ep_scoping.md to break this down into a series of tasks. - -For EP, one of the biggest overheads is probably just autofit internals running slowly and needing speeding up. - -Another key point with this scaling up is I want us to always be sure that the changes we make do not change the results. -Therefore, can you have the `simulator.py` output a file `ground_truth.json` for every datasets AND ALWAYS have our graphical -and ep scripts do sanity checks, at the end after profiling, that they are recovering the ground truth values and that their -log likelihood values (whichc an be output by a simulator.py) by doing a fit are also being maximized or recovered correctly. - -Do deep research and plan for a while on this one. - -● Here's the bottleneck ranking with optimisation angles for each, ordered by importance. Each tier is a natural prompt scope. - - Tier 1 — Dynesty wrapper overhead (~86% of optimise time) - - Impact: ~5.5 s per Dynesty fit × (N+1)×M fits per run. This is ~5 × N seconds per EP iteration that has nothing to do with the math. At N=10000 this bucket alone is ~30 hours. - - What's actually in it (from the PyAutoFit log lines we saw on every fit): - - corner_anesthetic plot attempt — even though it always emits "posterior estimate not yet sufficient" for EP factors and contributes no end-of-run artifact - - Creating latent samples by drawing 100 from the PDF — latent-space resampling step - - Removing search internal folder — per-fit cleanup - - Generating initial samples of model using JAX LH Function cores — initial sample finding before sampling starts - - Sampler construction, paths object setup, bound construction inside Dynesty itself - - Candidate prompts (rank order by likely payoff): - - 1. Suppress per-fit plotting and visualisation during EP iterations. EP factors don't need per-iteration corner plots or latent-sample draws — only the final one matters. Add a paths.suppress_plots / - paths.ep_mode = True flag (or whatever PyAutoFit calls it) that the EP loop sets on each factor's paths before each iteration's fit. Probably a single-digit-percent change in PyAutoFit but cuts the wrapper - bucket noticeably. - 2. Reuse the Dynesty sampler / pool across fits. Right now each search.fit(...) re-instantiates the sampler from scratch. EP's structure is "fit the same factor with mildly different priors each iteration" - — there's an obvious cache there. May need a small force_x1_cpu carve-out so the no-pool branch reuses state. - 3. Skip Removing search internal folder for in-memory EP runs. When output-to-disk is disabled (we already see Output to hard-disk disabled, input a search name to enable), the folder removal step is doing - nothing useful but still costs wall time. - 4. cProfile/py-spy attribution pass on the same workload to confirm which of the above dominates inside the wrapper bucket. Easiest first prompt — non-invasive, just gives data. Optional but quick. - - Tier 2 — Local Hill LL evaluation (~6%) - - Impact: Linear in N. At N=5 it's 4.4 s; at N=10000 it'd be ~2.4 hours. Becomes dominant only past N≈1000. - - Lever: - - 5. Parallelise the N independent local Dynesty fits across CPU cores per EP iteration. Each local fit is embarrassingly parallel — no message passing within an iteration. PyAutoFit's EP loop currently runs - them serially. Either a process pool or shared-memory thread pool would help. Worth doing once Tier 1 is cut. - - (Could also vmap the Hill likelihood across datasets within a single fused Dynesty fit, but that's a bigger model-architecture change. The parallel-across-factors approach is more straightforward.) - - Tier 3 — Global LL evaluation (~5%) - - Impact: Constant in N — set_model_approx freezes hill_coef, so the global free-param count stays at 18 regardless of dataset count. ~1.7 s/iter forever. - - Lever: - - 6. Replace Dynesty on the global factor with a Laplace approximation or a small-scale gradient-based optimiser. For 18 free params with a smooth Gaussian likelihood (_global_log_likelihood_jit), nested - sampling is overkill. Laplace gives a Gaussian posterior approximation in a handful of Newton steps. PyAutoFit already has LaplaceOptimiser (we instantiate one in run_ep_fit) — investigate whether it can be - wired up as the global factor's optimiser instead of search_global. If yes, this drops to ~0. - - Tier 4 — EP-loop orchestration (~4%) - - Impact: ~1.3 s/iter, currently roughly constant in N. Largest unknown — message-field updates iterate over all priors, which could be linear-in-N at large scale. - - Lever: - - 7. Profile inside factor_graph.optimise(...) proper. This is cProfile/py-spy territory. A targeted prompt would say "drop a cProfile on ep_sim excluding search.fit(...) calls; report which - autofit/graphical/ functions show up at >5% time." Cheap, informative, and may surface a quadratic loop we don't know about. - - Tier 5 — set_model_approx (~0%) - - Not worth optimising — 2 ms total at N=5. If you ever scale to N=10⁶ this might matter (the prior-walk is O(N)), but at that point everything else has long since blown up. Skip. - - Recommended prompt sequencing - - The fastest path to a usable 1000-dataset run: - - 1. First write the cProfile attribution prompt (item 4) — it costs ~30 minutes and tells you which Tier 1 sub-optimisation to chase first. Don't guess; measure. - 2. Then suppress per-fit plotting + folder cleanup (items 1, 3) — likely the biggest single bang-for-buck once cProfile confirms. - 3. Then Laplace for the global factor (item 6) — small constant savings but easy. - 4. Then measure at N=100 to validate the projection model. If the wrapper bucket has shrunk as expected, the projection coefficients will be much friendlier. - 5. Then sampler reuse across fits (item 2) and parallelise local fits (item 5) — both are bigger lifts but address the still-dominant buckets once plotting is gone. - - Tier 4 (orchestration cProfile) is a "do once we're past Tier 1+2+3" item. \ No newline at end of file diff --git a/active/grid_irregular_xp_propagation.md b/active/grid_irregular_xp_propagation.md deleted file mode 100644 index 8ba0c0ca..00000000 --- a/active/grid_irregular_xp_propagation.md +++ /dev/null @@ -1,107 +0,0 @@ -# Grid2DIrregular: propagate `xp` through derived constructors - -## Context - -This issue surfaced while writing the source-plane point-source JAX -profiling script (`autolens_workspace_developer/jax_profiling/point_source/source_plane.py`, -shipped in PyAutoLabs/autolens_workspace_developer#22). - -Image-plane fitting (`al.FitPositionsImagePairAll`) JITs end-to-end. The -source-plane variant (`al.FitPositionsSource`) blocks at compile time -because `Grid2DIrregular` derived constructors do not propagate the `xp` -backend, so model_data ends up with `_xp=np` while holding JAX tracers. - -## Reproduction - -```python -import jax.numpy as jnp -import autolens as al - -# ... build tracer + dataset, then: -analysis = al.AnalysisPoint( - dataset=dataset, - solver=solver, - fit_positions_cls=al.FitPositionsSource, - use_jax=True, -) -jax.jit(lambda inst: analysis.log_likelihood_function(instance=inst))(params_tree) -# -> jax.errors.TracerArrayConversionError inside -# Grid2DIrregular.squared_distances_to_coordinate_from -``` - -## Root cause - -`@PyAutoArray/autoarray/structures/grids/irregular_2d.py` — -`Grid2DIrregular.grid_2d_via_deflection_grid_from` constructs the new -grid without propagating `xp`: - -```python -def grid_2d_via_deflection_grid_from(self, deflection_grid): - return Grid2DIrregular(values=self - deflection_grid) -``` - -When the receiver (`self`) is a numpy-backed `Grid2DIrregular` (the -observed dataset positions) but the deflection_grid carries JAX tracers, -the subtraction returns JAX tracers but the new wrapper's `_xp` defaults -to `np`. The next call into `squared_distances_to_coordinate_from` then -runs `self._xp.square(self.array - coordinate)` → `np.square(tracer)` → -`TracerArrayConversionError`. - -## Proposed fix - -Two complementary one-liners: - -1. `grid_2d_via_deflection_grid_from` should pass `xp=self._xp`: - - ```python - def grid_2d_via_deflection_grid_from(self, deflection_grid): - return Grid2DIrregular(values=self - deflection_grid, xp=self._xp) - ``` - -2. `AbstractFitPositions.__init__` should rewrap `data` with `xp=xp` - so the observed positions match the analysis backend regardless of - how the dataset was originally constructed (the dataset comes off - disk via `al.from_json` which always builds a numpy-backed grid). - -Either fix individually unblocks the source-plane JIT path. Both -together provide defence-in-depth. - -## Audit - -While there, sweep `Grid2DIrregular` (and `Grid2D` for parity) for any -other derived constructor that calls `Grid2DIrregular(values=...)` -without `xp=self._xp`. Likely candidates: any method returning a new -grid from arithmetic on `self`. - -## Validation - -After the fix, the gated assertion in -`autolens_workspace_developer/jax_profiling/point_source/source_plane.py` -will start firing automatically: - -```python -EXPECTED_LOG_LIKELIHOOD_SOURCE_PLANE = -4496.798984131583 - -if full_pipeline_jits: - np.testing.assert_allclose( - float(full_result), - EXPECTED_LOG_LIKELIHOOD_SOURCE_PLANE, - rtol=1e-4, - ) -``` - -Re-run that script post-fix; it should print: - -``` -Full pipeline (JIT):