Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions .github/scripts/run_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""
Run the workspace smoke test suite.

Reads `smoke_tests.txt` from the workspace root and `config/build/env_vars.yaml`
for per-script env var overrides, then runs each listed script with the
appropriate environment. Continues through failures and exits non-zero
if any script failed.

The env resolution itself is NOT implemented here: it is PyAutoBuild's
`autobuild/env_config.py`, imported below. This file used to carry a copy, and
the copy had already drifted (its `load_env_config` hardcoded
`config/build/env_vars.yaml`, so the PR gate was structurally unable to read
the release profile — the seed incident's failure mode 4/7). One resolver
means the PR gate and the release runner cannot disagree about what a script's
environment is. See PyAutoBuild docs/env_profile_redesign.md §5 (#161 step 2).

Mirrors the logic of the `/smoke-test` skill so CI and local runs stay
in sync.
"""

from __future__ import annotations

import subprocess
import sys
import time
from pathlib import Path


WORKSPACE = Path(__file__).resolve().parents[2]
SMOKE_FILE = WORKSPACE / "smoke_tests.txt"
ENV_VARS_FILE = WORKSPACE / "config" / "build" / "env_vars.yaml"
SCRIPTS_DIR = WORKSPACE / "scripts"

# CI puts PyAutoBuild/autobuild on PYTHONPATH (PyAutoHeart's reusable
# smoke-tests.yml clones it alongside the dependency chain); for local runs,
# fall back to the sibling checkout.
try:
from env_config import build_env_for_script, load_env_config
except ImportError: # pragma: no cover - local-run fallback
sys.path.insert(0, str(WORKSPACE.parent / "PyAutoBuild" / "autobuild"))
from env_config import build_env_for_script, load_env_config


def load_smoke_scripts() -> list[str]:
scripts: list[str] = []
for line in SMOKE_FILE.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
scripts.append(line)
return scripts


def load_cfg() -> dict | None:
"""Parsed env profile, or None when the workspace has none.

None flows through build_env_for_script -> None -> subprocess inherits the
parent environment, which is what the old local copy's empty-config path
did by hand.
"""
if not ENV_VARS_FILE.exists():
return None
return load_env_config(ENV_VARS_FILE)


def run_one(script_rel: str, cfg: dict | None) -> tuple[str, int, float, str]:
env = build_env_for_script(Path(script_rel), cfg)
script_path = SCRIPTS_DIR / script_rel
t0 = time.time()
result = subprocess.run(
[sys.executable, str(script_path)],
cwd=str(WORKSPACE),
env=env,
capture_output=True,
text=True,
)
elapsed = time.time() - t0
output = result.stdout + result.stderr
return script_rel, result.returncode, elapsed, output


def main() -> int:
if not SMOKE_FILE.exists():
print(f"ERROR: no smoke_tests.txt at {SMOKE_FILE}", file=sys.stderr)
return 1
scripts = load_smoke_scripts()
if not scripts:
print("No smoke test scripts listed.")
return 0
cfg = load_cfg()

print(f"Running {len(scripts)} smoke test script(s) from {SMOKE_FILE.name}\n")
failures: list[tuple[str, int, str]] = []
for script_rel in scripts:
print(f"::group::{script_rel}")
name, rc, elapsed, output = run_one(script_rel, cfg)
print(output, end="")
status = "PASS" if rc == 0 else f"FAIL (exit {rc})"
print(f"\n[{status}] {name} — {elapsed:.1f}s")
print("::endgroup::")
if rc != 0:
failures.append((name, rc, output))

total = len(scripts)
passed = total - len(failures)
print(f"\n=== Smoke test summary: {passed}/{total} passed ===")
for name, rc, _ in failures:
print(f" FAIL {name} (exit {rc})")
return 0 if not failures else 1


if __name__ == "__main__":
sys.exit(main())
18 changes: 18 additions & 0 deletions .github/scripts/smoke_install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Workspace-owned install epilogue for the reusable Smoke Tests workflow
# (PyAutoHeart/.github/workflows/smoke-tests.yml). Runs with cwd at the
# checkout root (the dependency chain is cloned beside `workspace/`).
set -e

# arcticpy (the C++ arctic clocking code) is a hard import of autocti but not
# a pip dependency: its sdist is source-only (needs libgsl-dev + cython) and
# its own requirements downgrade numpy below 2.0.
sudo apt-get update && sudo apt-get install -y libgsl-dev
pip install numpy cython
pip install arcticpy==2.6 --no-build-isolation --no-deps

pip install ./PyAutoConf ./PyAutoFit ./PyAutoArray ./PyAutoCTI
pip install "./PyAutoArray[optional]"
# The re-resolution above can upgrade autoconf to the stale PyPI release;
# pin the local source last so recent autoconf APIs are importable.
pip install --force-reinstall --no-deps ./PyAutoConf
15 changes: 15 additions & 0 deletions .github/workflows/smoke_tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Smoke Tests

# Thin caller for PyAutoHeart's reusable smoke-test workflow. The ceremony
# (chain checkout, branch matching, runner, Slack) lives centrally; this
# workspace declares its dependency chain and owns its install epilogue
# (.github/scripts/smoke_install.sh) and runner (.github/scripts/run_smoke.py).

on: [push, pull_request]

jobs:
smoke:
uses: PyAutoLabs/PyAutoHeart/.github/workflows/smoke-tests.yml@main
with:
chain: "PyAutoConf PyAutoFit PyAutoArray PyAutoCTI"
secrets: inherit
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
output/
*.sqlite
__pycache__/
*.pyc
/*.png
51 changes: 51 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# PyAutoCTI Workspace Test — Agent Instructions

This is the integration-test suite for **PyAutoCTI**, run in CI to verify the core library works
end-to-end. It is **not** a user-facing workspace — see `../autocti_workspace` for examples and
tutorials. These are the canonical, agent-agnostic instructions for this repo.

Dependencies: `autocti`, `autofit`, `autoarray`, and **arcticpy** (source-only C++ sdist — install
with `pip install arcticpy==2.6 --no-build-isolation --no-deps` after numpy+cython, with
`libgsl-dev` present; see `PyAutoCTI/AGENTS.md`).

## Repository Structure

```
scripts/ Integration-test scripts run in CI
dataset_1d/model_fit.py 1D calibration: simulate -> factor-graph fit -> aggregator round-trip
imaging_ci/model_fit.py 2D charge injection: simulate -> factor-graph fit -> result inspection
plot/subplots.py Drives the autocti.plot function surface to files
legacy/ Euclid VIS heritage (2022-2023, pre-resurrection API — not runnable;
see legacy/README.md)
config/ Current PyAutoCTI config (mirrors autocti_workspace)
config/build/env_vars.yaml Per-script env for smoke runs (PYAUTO_TEST_MODE=2 defaults)
smoke_tests.txt The curated smoke list (small on purpose)
```

## Running

```bash
python .github/scripts/run_smoke.py # the smoke list, with env_vars.yaml applied
python scripts/dataset_1d/model_fit.py # one script, real search (no env applied)
```

CI runs the smoke list through PyAutoHeart's reusable smoke workflow (thin caller in
`.github/workflows/smoke_tests.yml`, chain `PyAutoConf PyAutoFit PyAutoArray PyAutoCTI`; the
arcticpy build lives in `.github/scripts/smoke_install.sh`).

## Conventions

- Keep `smoke_tests.txt` a **small curated subset** — do not mass-promote scripts.
- Integration scripts are self-contained (simulate their own data, small shapes) and single-trap
(identical-prior ordered-trap models tie at prior medians under the `PYAUTO_TEST_MODE=2`
bypass and raise their own assertion — a filed autofit issue; avoid the pattern here).
- The test-mode knob is `PYAUTO_TEST_MODE` (`2` bypasses sampling); `PYAUTOFIT_TEST_MODE`
does not exist.
- Never edit `legacy/` — it is preserved Euclid VIS history.

## Never rewrite history

Never rewrite pushed history on any repo with a remote — no `git init` over a
tracked repo, no force-push to `main`, no fresh-start "Initial commit", no
`filter-repo` / `filter-branch` / `rebase -i` on pushed branches. To get a
clean tree: `git fetch origin && git reset --hard origin/main && git clean -fd`.
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# autocti_workspace_test — agent instructions

The canonical, agent-agnostic instructions live in `AGENTS.md`. Claude Code loads them via the
import below; if your tool does not process `@`-imports, open `AGENTS.md` in this directory and
read it directly.

@AGENTS.md
10 changes: 10 additions & 0 deletions config/build/env_vars.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Per-script environment variable configuration for automated runs
# (smoke tests, pre-release checks, CI). Same schema as the other
# *_workspace_test repos.
defaults:
PYAUTO_TEST_MODE: "2"
PYAUTO_SKIP_WORKSPACE_VERSION_CHECK: "1"
MPLBACKEND: "Agg"
NUMBA_CACHE_DIR: "/tmp/numba_cache"
MPLCONFIGDIR: "/tmp/matplotlib"
overrides: {}
16 changes: 12 additions & 4 deletions config/general.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,35 @@ fits:
hpc:
hpc_mode: false # If True, use HPC mode, which disables GUI visualization, logging to screen and other settings which are not suited to running on a super computer.
iterations_per_update: 5000 # The number of iterations between every update (visualization, results output, etc) in HPC mode.
inversion:
check_reconstruction: true # If True, the inversion's reconstruction is checked to ensure the solution of a meshs's mapper is not an invalid solution where the values are all the same.
reconstruction_vmax_factor: 0.5 # Plots of an Inversion's reconstruction use the reconstructed data's bright value multiplied by this factor.
model:
ignore_prior_limits: false # If ``True`` the limits applied to priors will be ignored, where limits set upper / lower limits. This stops PriorLimitException's from being raised.
output:
force_pickle_overwrite: false # If True pickle files output by a search (e.g. samples.pickle) are recreated when a new model-fit is performed.
force_pickle_overwrite: false # force_pickle_overwrite: false # If True, pickle files output by a search (e.g. samples.pickle) are recreated when a new model-fit is performed.
force_visualize_overwrite: true # If True, visualization images output by a search (e.g. subplots of the fit) are recreated when a new model-fit is performed.
info_whitespace_length: 80 # Length of whitespace between the parameter names and values in the model.info / result.info
log_level: INFO # The level of information output by logging.
log_to_file: false # If True, outputs the non-linear search log to a file (and not printed to screen).
log_file: output.log # The name of the file the logged output is written to (in the non-linear search output folder)
model_results_decimal_places: 3 # Number of decimal places estimated parameter values / errors are output in model.results.
remove_files: false # If True, all output files of a non-linear search (e.g. samples, visualization, etc.) are deleted once the model-fit has completed, such that only the .zip file remains.
samples_to_csv: true # If True, non-linear search samples are written to a .csv file.
unconverged_sample_size : 100 # If outputting results of an unconverged search, the number of samples used to estimate the median PDF values and errors.
parallel:
warn_environment_variables: true # If True, a warning is displayed when the search's number of CPU > 1 and enviromment variables related to threading are also > 1.

profiling:
parallel_profile: false # If True, the parallelization of the fit is profiled outputting a cPython graph.
should_profile: false # If True, the ``profile_log_likelihood_function()`` function of an analysis class is called throughout a model-fit, profiling run times.
repeats: 1 # The number of repeat function calls used to measure run-times when profiling.
structures:
use_dataset_grids: true # If True, dataset objects (e.g. Imaging) have a grid of (y,x) coordinates computed for them.
native_binned_only: false # If True, data structures are only stored in their native and binned format. This is used to reduce memory usage in autocti.
test:
check_figure_of_merit_sanity: false
bypass_figure_of_merit_sanity: false
check_likelihood_function: true # if True, when a search is resumed the likelihood of a previous sample is recalculated to ensure it is consistent with the previous run.
check_preloads: false
exception_override: false
lh_timeout_seconds: # If a float is input, the log_likelihood_function call is timed out after this many seconds, to diagnose infinite loops. Default is None, meaning no timeout.
preloads_check_threshold: 1.0 # If the figure of merit of a fit with and without preloads is greater than this threshold, the check preload test fails and an exception raised for a model-fit.
disable_positions_lh_inversion_check: false
14 changes: 5 additions & 9 deletions config/logging.yaml
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
version: 1
disable_existing_loggers: false

handlers:
console:
class: logging.StreamHandler
level: INFO
stream: ext://sys.stdout
formatter: formatter
file:
class: logging.FileHandler
level: INFO
filename: root.log
formatter: formatter

root:
level: DEBUG
handlers:
- console
- file
level: INFO
handlers: [ console ]

formatters:
formatter:
format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
4 changes: 2 additions & 2 deletions config/non_linear/README.rst
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
The ``non_linear`` folder contains configuration files which customize the default behaviour of non-linear searches in
**PyAutoLens**.
**PyAutoCTI**.

Files
-----

- ``mcmc.yaml``: Settings default behaviour of MCMC non-linear searches (e.g. Emcee).
- ``nest.yaml``: Settings default behaviour of nested sampler non-linear searches (e.g. Dynesty).
- ``optimizer.yaml``: Settings default behaviour of optimizer non-linear searches (e.g. PySwarms).
- ``mle.yaml``: Settings default behaviour of maximum likelihood estimator (mle) searches (e.g. PySwarms).
9 changes: 1 addition & 8 deletions config/non_linear/mcmc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@ Emcee:
number_of_cores: 1 # The number of cores the search is parallelized over by default, using Python multiprocessing.
printing:
silence: false # If True, the default print output of the non-linear search is silcened and not printed by the Python interpreter.
prior_passer:
sigma: 3.0 # For non-linear search chaining and model prior passing, the sigma value of the inferred model parameter used as the sigma of the passed Gaussian prior.
use_errors: true # If True, the errors of the previous model's results are used when passing priors.
use_widths: true # If True the width of the model parameters defined in the priors config file are used.
updates:
iterations_per_update: 500 # The number of iterations of the non-linear search performed between every 'update', where an update performs tasks like outputting model.results.
remove_state_files_at_end: true # Whether to remove the savestate of the seach (e.g. the Emcee hdf5 file) at the end to save hard-disk space (results are still stored as PyAutoFit pickles and loadable).
Expand Down Expand Up @@ -61,10 +57,7 @@ Zeus:
number_of_cores: 1 # The number of cores the search is parallelized over by default, using Python multiprocessing.
printing:
silence: false # If True, the default print output of the non-linear search is silenced and not printed by the Python interpreter.
prior_passer:
sigma: 3.0 # For non-linear search chaining and model prior passing, the sigma value of the inferred model parameter used as the sigma of the passed Gaussian prior.
use_errors: true # If True, the errors of the previous model's results are used when passing priors.
use_widths: true # If True the width of the model parameters defined in the priors config file are used.

updates:
iterations_per_update: 500 # The number of iterations of the non-linear search performed between every 'update', where an update performs tasks like outputting model.results.
remove_state_files_at_end: true # Whether to remove the savestate of the seach (e.g. the Emcee hdf5 file) at the end to save hard-disk space (results are still stored as PyAutoFit pickles and loadable).
Loading
Loading