From c9b25f98c08ffc103675737ad57c621933578082 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Thu, 15 Oct 2026 14:17:17 +0200 Subject: [PATCH 1/3] added planning MD file useful for agents --- .planning/codebase/ARCHITECTURE.md | 299 +++++++++++++++++++++++++++++ .planning/codebase/CONCERNS.md | 291 ++++++++++++++++++++++++++++ .planning/codebase/INTEGRATIONS.md | 199 +++++++++++++++++++ .planning/codebase/STACK.md | 139 ++++++++++++++ .planning/codebase/STRUCTURE.md | 241 +++++++++++++++++++++++ 5 files changed, 1169 insertions(+) create mode 100644 .planning/codebase/ARCHITECTURE.md create mode 100644 .planning/codebase/CONCERNS.md create mode 100644 .planning/codebase/INTEGRATIONS.md create mode 100644 .planning/codebase/STACK.md create mode 100644 .planning/codebase/STRUCTURE.md diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md new file mode 100644 index 00000000..89769ed3 --- /dev/null +++ b/.planning/codebase/ARCHITECTURE.md @@ -0,0 +1,299 @@ +# Architecture + +**Analysis Date:** 2026-10-15 **Branch analysed:** `develop` @ `66adaf54` + +## Pattern Overview + +**Overall:** Layered library with two facades. `Project` is the +stateful, GUI-oriented facade (experiments, models, persistence, +constraints, contrasts); `MultiFitter` is the fitting facade that turns +datasets into a `PreparedFit` and delegates execution to EasyScience's +`MultiFitter` / `Sampler`, which drive LMFit, BUMPS or DFO. Reflectivity +is computed by pluggable engine wrappers (refnx, refl1d) behind +`CalculatorFactory`. + +**Key Characteristics:** + +- Prepare / execute / record split: `Project.prepare_fit` → + `PreparedFit` (immutable snapshot of settings, arrays, bound fit + functions, configured core fitter) → `execute()` or a GUI worker → + `Project.record_fit` → `FitRun` +- Resolution follows the dataset, not the calculator: each + `DataSet1D.resolution_function` is bound into its fit function; + `CalculatorBase` keeps one resolution per model id +- Parameters are EasyScience `Parameter`s; all coupling (conformal + thickness, contrasts, user constraints, sum constraints) is expressed + as parameter dependencies and persisted by structural path +- Inequality constraints are enforced by patching the BUMPS driver in + the core's namespace, because the core offers no hook +- Shared objects (one material in two layers, one assembly in two + models) serialize once through `sample/references.py` (`file_format` + 3) + +## Layers + +**Project Facade:** + +- Purpose: Hold the session state an application needs and expose + index-based accessors for plots and tables +- Location: `src/easyreflectometry/project.py` (`Project`, + `ModelRemoval`, spin-asymmetry helpers) +- Contains: experiments `Dict[int, DataSet1D | PolarizedDataSet]`, + `ModelCollection`, `MaterialCollection` palette, `FitSettings`, + inequality specs, link records, `last_fit`, load/save, ORSO import, + contrasts, polarized loading, `spin_asymmetry_for_experiment_at_index` +- Depends on: every other package in the library +- Used by: EasyReflectometry GUI (ERA app), tutorials, `Summary` + +**Fitting Layer:** + +- Purpose: Zero-variance handling, per-dataset resolution binding, + settings snapshot, constraint enforcement, goodness-of-fit metrics +- Location: `src/easyreflectometry/fitting.py`, `fit_settings.py`, + `_bumps_constraints.py`, `inequality_constraints.py` +- Contains: `MultiFitter` (`fit`, `fit_single_data_set_1d`, + `fit_polarized`, `for_experiments`, `prepare`, `mcmc_sample`, + `switch_minimizer`, chi-square properties), `PreparedFit`, `FitRun`, + `_ConstrainedEasyScienceMultiFitter`, `FitSettings`, + `MinimizerOption`, `InequalitySpec`, `build_constraints_factory` +- Depends on: `easyscience.fitting` (`MultiFitter`, `Sampler`, + `AvailableMinimizers`, `FitResults`), `asteval`, `scipp` + +**Bayesian Analysis Layer:** + +- Purpose: Post-hoc posterior analysis and chain persistence +- Location: `src/easyreflectometry/analysis/bayesian.py` +- Contains: `PosteriorResults` (summary, corner, trace, distribution, + credible_interval, gelman_rubin, save), `posterior_predictive_*`, + `save_posterior` / `load_posterior` with fingerprinted sidecar +- Depends on: plotly (required), arviz (optional), numpy + +**Model / Sample Layer:** + +- Purpose: Physical description of the sample and measurement +- Location: `src/easyreflectometry/model/`, `src/easyreflectometry/sample/` +- Contains: `Model` (scale, background, resolution, `total_thickness` + derived parameter), `Sample` and assemblies/layers/materials, + `LayerMagnetism`, `MaterialDensity.sld_coupled`, `volume_fraction`, + `paths`, `references` +- Depends on: `easyscience.base_classes`, `easyscience.variable`, + periodictable (via `special/`) + +**Constraints and Contrasts Layer:** + +- Purpose: User-level coupling between parameters and between models +- Location: `src/easyreflectometry/constraints.py`, `contrasts.py` +- Contains: `constrain*`, `derived_parameter`, `clamp_sum_partners`; + `derive_contrast`, `ReplaceMaterial`, `ReplaceFormula`, `plan_link` / + `LinkPlan` / `LinkRecord`, `tie_corresponding`, `snapshot` / `restore` +- Depends on: EasyScience dependency mechanism; `USER_CONSTRAINT_FLAG` + marks constraints the project should persist + +**Data Layer:** + +- Purpose: Load, merge and describe measured data +- Location: `src/easyreflectometry/data/`, `orso_utils.py` +- Contains: `DataSet1D`, `PolarizedDataSet`, `load`, `load_as_dataset`, + `merge_datasets`, `resolution_from_dataset`, channel detection, ORSO + read/write and model-language conversion + +**Calculator Layer:** + +- Purpose: Pluggable reflectivity engines +- Location: `src/easyreflectometry/calculators/` +- Contains: `CalculatorBase` (item lifecycle via `ItemContainer`s, + per-model resolution, magnetism switches), `WrapperBase` (abstract + engine API incl. `calculate_channel`, `calculate_polarized`, + `magnetic_sld_profile`), `CalculatorFactory` (`fit_func`, + `fit_func_for(channel, resolution_function)`), `PolarizationChannel` +- Implementations: `refnx/` (unpolarized), `refl1d/` (magnetism, + polarized probes, oversampled resolution) + +**EasyScience Core (external, pinned 2.5.1):** + +- `Fitter` / `MultiFitter` (reshaping of multi-dataset arrays, minimizer + dispatch), `Sampler` / `SamplingResults` (DREAM chain, `extend`, + `save`, `load_state`), minimizer wrappers, `InterfaceFactoryTemplate` + +## Data Flow + +**Classical Fit (project path):** + +1. GUI/user calls `Project.prepare_fit(experiments=None, objective=None, skip_invalid=False)` +2. `_fit_scope` selects experiments with `include_in_fit` and a model; + `_fitter_for` builds `MultiFitter.for_experiments(...)` with a deep + copy of `fit_settings` and a weak `constraints_factory_provider` +3. `MultiFitter.prepare` applies the objective (`hybrid` default: + Mighell for zero-variance points, WLS otherwise), binds each dataset's + fit function to its own resolution (`_resolution_for_fitted_points` + for `legacy_mask`), collects `added_roots` (free parameters of + left-out models that fitted parameters follow), builds the core + fitter via `_core_fitter_for` + `FitSettings.configure`, checks + preconditions (finite bounds for engines that need them, warns on + starts at bounds) +4. Caller runs `prepared.execute(**kwargs)` or `core_fitter.fit(...)` in + a thread; the `_ConstrainedEasyScienceMultiFitter.fit` property + wraps the call in `_bumps_constraints.applied(factory)` +5. `Project.record_fit(prepared, results, status)` computes classical + metrics from the measured arrays (`prepared.finalize`), stores a + `FitRun` on `last_fit` and pushes results to `project.fitter` + +**Classical Fit (library path):** + +1. `MultiFitter(model, ...)` and `fitter.fit(data: sc.DataGroup)` +2. `_datagroup_inputs` pairs curve `Qz_` with model/dataset `n` in + numeric order +3. `prepare` → `execute` → results written back into the `DataGroup` + (model curves, SLD profiles, pooled chi-square) + +**Polarized Fit:** + +- `fit_polarized(polarized_dataset)` or `for_experiments([...])`: one fit + function per spin channel, all against one model, each bound to its + channel via `CalculatorFactory.fit_func_for(channel, resolution)`. + Structural parameters shared, magnetic parameters constrained by all + channels. + +**Bayesian MCMC:** + +1. `Project.prepare_sample()` → `for_experiments` fitter with settings + snapshot; or `fitter.mcmc_sample(data)` directly +2. `mcmc_sample` requires a BUMPS minimizer (`minimizer.package == +'bumps'`), refuses all-zero variances unless `objective='mighell'`, + calls `prepare(action='sampling')` +3. Builds `Sampler(core_fitter, x, y, weights)` inside the constraints + context, patches `sampler.extend` to re-enter it, keeps the sampler on + `fitter.sampler`, runs `sampler.sample(samples, burn, thin, population, sampler_kwargs, progress_callback, abort_test)` +4. Returns `{'draws', 'param_names', 'state', 'logp'}`; user wraps in + `PosteriorResults` or calls `save_posterior` + +**Persistence:** + +- `Project.as_dict` writes models, materials in order, experiments by + index and name with per-dataset resolution, fit settings, inequality + specs, user-constraint records by path, contrast provenance, links. + `references.writing()` replaces repeated objects with `@ref`. + `_needs_new_format` bumps `file_format` to 3 when older readers would + misread the file. +- `from_dict` rebuilds shared objects (`references.reading`), restores + constraints by path, reports problems in `load_report`. + +**State Management:** + +- `Project._sync_parameter_states` disables physically meaningless + parameters (superphase thickness/roughness, subphase thickness) and + applies default limits to enabled parameters created from project + defaults +- `PreparedFit` owns copies of the measured arrays; model parameters and + constraints stay live because they are what the fit changes +- Parameter values saved/restored around posterior predictive evaluation + (`_save_parameter_state` / `_restore_parameter_state`) + +## Key Abstractions + +**`PreparedFit` (fitting.py):** Snapshot pattern. Everything a run needs, +fixed at prepare time; owns copies of measured arrays, the configured +core fitter and both full-resolution (`curve_funcs`) and fitted-point +(`fit_funcs`) evaluators. + +**`FitRun` (fitting.py):** Frozen record of a run (status, minimizer, +objective, inputs by experiment key, pooled and per-dataset statistics). +Outlives model changes; `remapped()` follows experiment re-keying. + +**`FitSettings` (fit_settings.py):** Serializable configuration +(minimizer, mode, objective, tolerance, budget, per-minimizer options +validated by `MinimizerOption` schemas). `configure(fitter)` and +`engine_kwargs()` are the two consumers. + +**`_ConstrainedEasyScienceMultiFitter` / `_bumps_constraints`:** Property +override plus monkeypatch. The subclass overrides the core's read-only +`fit` property so the raw `easy_science_multi_fitter.fit(...)` path +resolves the owner's constraints provider in the calling thread and +consumes an explicit `constraints_factory` keyword (the core would +swallow it via `**kwargs`). `install()`, run at import of +`_bumps_constraints`, wraps `minimizer_bumps.FitDriver` so each +`FitProblem` gets `constraints = factory(pars)` before the driver runs. +Non-BUMPS engines raise `NON_BUMPS_ERROR`. + +**`InequalitySpec` (inequality_constraints.py):** Declarative +`lhs op rhs` over parameter paths, evaluated with asteval in a +restricted symbol table; `build_constraints_factory` returns BUMPS +penalty callables. + +**Contrasts (`contrasts.py`):** A contrast is an ordinary `Model` sharing +structure with a reference by identity or by `constrain_equal`. +`derive_contrast` copies touched assemblies, shares the rest and ties +parameters; `plan_link` / `apply_link` / `unlink` manage equalities +between two models of one layout with full state snapshots. + +**`CalculatorBase` / `WrapperBase` / `CalculatorFactory`:** Template + +Adapter + Factory. Items are claimed per calculator (`_claim`), per-model +resolution registry, magnetism toggles, channel-bound fit functions. + +**`references` (sample/references.py):** Context-managed shared-object +serialization (`writing()`, `reading(report)`, `suspended()`), with +`prune`, `rebuild` and `register` for load-time reconciliation. + +**`paths.walk` (sample/paths.py):** Structural addressing of every +parameter; the stable identity used by constraints and persistence +instead of EasyScience unique names. + +## Entry Points + +- `Project.prepare_fit` / `prepare_sample` / `record_fit` - GUI fit and + sampling +- `Project.load_experiment_from_files`, `load_polarized_experiment*`, + `load_orso_file`, `save_as_json`, `load_from_json` +- `Project.add_contrast`, `plan_link`, `apply_link`, `detach` +- `MultiFitter.fit`, `fit_polarized`, `mcmc_sample` +- `Summary(project).save_html_summary` / `save_pdf_summary` +- `python -m easyreflectometry.main` prints available calculators (no + console script registered) + +## Error Handling + +**Strategy:** Fail loudly at preparation time; warn for recoverable data +issues; refuse silent degradation. + +**Patterns:** + +- `FitScopeError` (nothing to fit / experiment without model), + `FitPreconditionError` (unusable bounds for the engine), `ValueError` + for invalid settings or empty datasets after masking +- `RuntimeError` when sampling without a BUMPS minimizer +- `ValueError(NON_BUMPS_ERROR)` when inequality constraints meet a + non-BUMPS engine +- `UnitError` (subclass of `ValueError`) for inequality unit mismatches +- `FileExistsError` / `FileNotFoundError` / `OSError` / `ValueError` + from project persistence (no printing) +- `warnings.warn` for zero-variance handling, ORSO column problems, + negative volume fractions, unreadable dependencies on load; load + issues also collected in `Project.load_report` +- `try/finally` restores parameter state around posterior predictive; + `Project.save_as_json` writes atomically + +## Cross-Cutting Concerns + +**Logging:** `logging.getLogger(__name__)` in a handful of modules; no +handler configured. `warnings` is the main user-facing channel. + +**Validation:** `FitSettings.validate`, `MinimizerOption.validate`, +`InequalitySpec.validate_syntax` + `check_units`, ORSO column validation, +`_check_fit_preconditions`. + +**Threading:** The constraints factory lives in a `contextvars` +variable, resolved in the calling thread; `PreparedFit` owns array +copies so a worker thread is isolated from later edits. Resolution for +one-off curves is bound to the evaluator rather than swapped on the +model to avoid racing a running fit. + +**Serialization:** EasyScience serializer plus library extensions: +`references` for shared objects, structural paths for constraints, per +dataset resolution entries, `file_format` negotiation. Posterior chains +have their own sidecar format. + +**Authentication:** Not applicable. + +--- + +_Architecture analysis: 2026-10-15 (develop @ 66adaf54)_ diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md new file mode 100644 index 00000000..25833c8d --- /dev/null +++ b/.planning/codebase/CONCERNS.md @@ -0,0 +1,291 @@ +# Codebase Concerns + +**Analysis Date:** 2026-10-15 **Branch analysed:** `develop` @ `66adaf54` + +## Summary + +`develop` is 51 commits past `v1.7.0` and carries the unreleased 1.9 +work (contrasts, shared-object persistence, per-dataset resolution, +prepare/record fit split, volume fractions, inequality constraints on +BUMPS). The earlier concerns about the `bayesian_mp` branch +(multiprocessing, chains/seed, git-branch core pin) are gone: the core +dependency is a PyPI range, `n_workers`/`chains`/`seed` and +`CalculatorFactory.__reduce__` no longer exist in the library, and MCMC +goes through the core `Sampler`. The concerns below are what remains. + +--- + +## 1. Coupling to EasyScience internals + +### Monkeypatch of `minimizer_bumps.FitDriver` + +**Issue:** Inequality constraints work only because +`_bumps_constraints.install()` rebinds the module-level `FitDriver` name +inside the core's `minimizer_bumps` namespace and inspects +`problem.models[0].pars`. Any core refactor (importing `FitDriver` +differently, building the problem elsewhere, renaming `.pars`) silently +disables every inequality constraint; the shim passes untouched +problems through without complaint. + +**Files:** `src/easyreflectometry/_bumps_constraints.py`, +`src/easyreflectometry/fitting.py` (`_constraints`, +`_keep_constraints_on_extend`), `tests/unit/test_bumps_constraints_shim.py` + +**Impact:** High. Physics silently dropped on a core upgrade. + +**Fix approach:** Keep the shim test that asserts penalties reach the +problem on both `fit` and `mcmc_sample`; add a start-up assertion that +the patched attribute exists; pursue an upstream hook +(`constraints=`/callback) in corelib and gate `<3` → `<2.6` until it +lands. + +### Pin `easyscience>=2.5.1,<3` vs. local core on `release-3.0` + +**Issue:** The local `C:\projects\easy\ERA\core` checkout is on +`release-3.0` (60 commits past 2.5.1) while pixi resolves 2.5.1 from +PyPI. Changes developed against the local core (e.g. the `_update`/fset +dependency fix, `update_from_calculator`) are not what tests run +against, and 3.0 is excluded by the pin. + +**Files:** `pyproject.toml`, `pixi.lock` + +**Impact:** Medium. Divergence between what is tested and what the next +core release ships. + +**Fix approach:** Add a CI job (allowed to fail) installing core from +`release-3.0`; list the library's core API surface +(`Sampler`, `MultiFitter._precompute_reshaping`, `MINIMIZER_PARAMETER_PREFIX`, +`InterfaceFactoryTemplate`, `minimizer_bumps.FitDriver`) and check it +against the branch. + +### Private core imports + +**Issue:** `MINIMIZER_PARAMETER_PREFIX` from `minimizer_base`, +`resolve_all_parameter_dependencies` from +`parameter_dependency_resolver`, `NewBase` from `base_classes.new_base`, +`ItemContainer` from `interface_factory` are not public core API. + +**Files:** `analysis/bayesian.py`, `project.py`, +`sample/collections/base_collection.py`, `calculators/calculator_base.py` + +**Impact:** Medium. + +--- + +## 2. Size and complexity hotspots + +### `project.py` is a 3211-line god object + +**Issue:** `Project` mixes session state, persistence (two file formats, +reference resolution, constraint records), fit orchestration, contrasts +and links, ORSO import, polarized loading, spin asymmetry, derived data +for plots, and GUI index bookkeeping. Most behaviours are tested through +it, so refactors are expensive. + +**Files:** `src/easyreflectometry/project.py` + +**Impact:** Medium-High for maintainability; the radon dashboard +publishes the complexity. + +**Fix approach:** Extract cohesive modules that `Project` delegates to: +`project_io.py` (as_dict/from_dict helpers, `_needs_new_format`), +`project_experiments.py` (loading, polarized, merge), +`spin_asymmetry.py` (`_spin_asymmetry`, `_unwrapped_angle`, channel +helpers). Keep `Project` as the facade. + +### `fitting.py` (1428 lines) and `analysis/bayesian.py` (1412 lines) + +**Issue:** `bayesian.py` is mostly plotly styling constants and +hand-rolled contour/density code for corner plots; `fitting.py` +combines data preparation, settings, constraints wiring and five public +fit paths. + +**Impact:** Low-Medium. + +**Fix approach:** Split `bayesian.py` into `posterior.py` (results, +persistence, predictive) and `posterior_plots.py`; consider moving +`PreparedFit`/`FitRun` to `fit_run.py`. + +### `Project.__init__` and `from_dict` ordering + +**Issue:** Loading depends on a specific order (materials → models → +experiments → constraints → contrasts → links) and on `references` +contexts being active. Several `_from_dict_*` helpers append to a shared +`report` list. Easy to break when adding a persisted field. + +**Files:** `project.py` lines ~2845-3000 + +**Impact:** Medium. + +--- + +## 3. Persistence format risks + +### `file_format=3` is refused by older readers + +**Issue:** By design (`_needs_new_format`), but the triggers are subtle: +shared objects, an experiment left out of the fit, duplicate model +names. A user may save a file in 1.9 that 1.8 refuses without an obvious +reason. + +**Files:** `project.py` (`_needs_new_format`, `_as_dict_add_experiments`) + +**Impact:** Medium. Document in release notes; already in CHANGELOG. + +### Constraint records keyed by structural path + +**Issue:** `_user_constraints` refuses (raises `ValueError`) when a +constrained parameter is not reachable from the models (e.g. a +constraint on a palette material not used by any model). Saving a +project then fails late, at save time. + +**Files:** `project.py` (`_constraint_candidates`, `_constraint_record`) + +**Impact:** Medium. Surfaced as an exception, which is the intended +behaviour, but the GUI must handle it. + +### `derived_parameter` is session-only + +**Issue:** Documented, but a user who builds a derived parameter and +saves loses it silently. + +**Files:** `constraints.py` + +**Impact:** Low. + +--- + +## 4. Testing gaps + +### Thin integration/functional layers + +**Issue:** `tests/functional/test_dummy.py` asserts `2 + 2 == 4` and is +the whole functional suite; `tests/integration/fitting/test_dummy.py` +and `tests/integration/scipp-analysis/test_dummy.py` are `slow`-marked +placeholders. Real integration coverage is `integration/convergence/`, +`test_cross_engine_resolution.py`, `test_ort_file_fitting.py`. + +**Impact:** Medium. CI jobs named "functional tests" pass vacuously. + +**Fix approach:** Either delete the placeholders or move the end-to-end +project tests (`test_project.py` fit paths, `test_polarized_fitting.py` +slow cases) under `integration/`. + +### Sampler continuation and persistence + +**Issue:** `fitter.sampler.extend()` after constraints, and +`save_posterior`/`load_posterior` fingerprint mismatch handling, have +limited coverage (`TestMCMCSample*` in `test_fitting.py`, `test_bayesian.py`); +all real DREAM runs are `slow` and only run on PRs/develop. + +**Impact:** Medium. + +### Default `pytest` skips `slow` + +**Issue:** `addopts = -m "not slow"` means local runs and some CI pushes +never execute a real minimizer or sampler. Regressions in engine wiring +surface only on PR CI. + +**Impact:** Medium; deliberate trade-off (see `pyproject.toml` comment). +34 of 1410 tests are `slow`. + +### refnx-only assumptions + +**Issue:** Several fits and convergence references are refnx-based; +refl1d is exercised mainly through polarized/magnetism tests and +`test_cross_engine_resolution.py`. + +**Impact:** Low-Medium. + +--- + +## 5. Repository hygiene + +### Stale build files + +**Issue:** `Makefile` references `setup.py`, `flake8`, Sphinx and an +`EasyReflectometry/` directory; `.coveragerc` points at +`EasyReflectometry/`. Neither reflects hatchling/ruff/MkDocs/`src/`. + +**Files:** `Makefile`, `.coveragerc` + +**Impact:** Low. Confusing for new contributors. + +**Fix approach:** Delete both (pixi tasks cover everything) or rewrite +to call `pixi run`. + +### Duplicate CI matrices + +**Issue:** `python-ci.yml` and `python-package.yml` (pip-based, run on +every push/PR) duplicate `test.yml`; `python-publish.yml` duplicates +`pypi-publish.yml`. + +**Impact:** Low-Medium (CI minutes, confusing failures). + +### Untracked planning/notes directories + +**Issue:** `.planning/`, `.claude/`, `MD/`, `examples/`, `integration/` +are untracked and not in `.gitignore`, so `git status` is always noisy +and an accidental `git add .` would commit them. + +**Impact:** Low. + +**Fix approach:** Add `.planning/`, `.claude/`, `MD/` to `.gitignore` (or +a global excludes file); decide whether `examples/` and `integration/` +belong in the repo. + +### Docstring tooling disabled + +**Issue:** `pydoclint` and `format-docstring` are configured with +`exclude = '\.'` (everything), `interrogate --fail-under=0`. The +`docstring-lint-check` CI step therefore cannot fail. + +**Impact:** Low. + +### `main.py` has no console entry + +**Issue:** `main()` prints available calculators but `pyproject.toml` +declares no `[project.scripts]`. + +**Impact:** Low; remove or register. + +--- + +## 6. Numerical and physics conventions to keep in view + +- **SLD unit convention:** values are stored in units of 1e-6 Å^-2 while + the declared unit is `1/angstrom^2`; EasyScience strips numeric unit + factors so this cannot be fixed without a breaking migration (issue + #377 left open on purpose). +- **Resolution convention:** `SIGMA_TO_FWHM` conversions in + `resolution_functions.py`, `orso_utils.py` and the refnx wrapper must + stay consistent; `tests/calculators/test_resolution_conventions.py` + and `tests/integration/test_cross_engine_resolution.py` pin them. +- **Weights convention:** core weights are 1/σ (issue #370); keep in + mind when touching `_prepare_fit_arrays`. +- **refl1d magnetism angle:** `DEFAULT_THETA_M = 270.0` and + `GUIDE_FIELD_ANGLE = 270.0`; thetaM=90 is parallel to the guide field + in refl1d. Past reviews got this backwards. +- **Mighell objective:** `hybrid` default substitutes Mighell for zero + variance points; sampling with `mighell` is a bias correction, not a + likelihood (warned in `mcmc_sample`). +- **Spin asymmetry heuristics:** `SPIN_ASYMMETRY_SIGNIFICANCE = 3.0`, + `SPIN_ASYMMETRY_CANCELLATION_FRACTION = 1e-3`, + `MAGNETIC_MOMENT_FLOOR_FRACTION = 0.01` in `project.py` are tuned + constants without a documented derivation. + +--- + +## 7. Work in flight (not on develop) + +- Branch `425-magnetism` (3 commits ahead: magnetism fixes for issue + #425 point 3, review fixes, ruff) is the current working branch. +- Uncommitted app-side work for per-dataset resolution (A1-A5) and the + magnetism/fitting gaps lives in the GUI repository, not here. +- Local corelib changes (`update_from_calculator`, dependent-parameter + callback fix) are uncommitted in `C:\projects\easy\ERA\core` and not + part of the 2.5.1 the tests use. + +--- + +_Concerns audit: 2026-10-15 (develop @ 66adaf54)_ diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md new file mode 100644 index 00000000..0fdeb4b3 --- /dev/null +++ b/.planning/codebase/INTEGRATIONS.md @@ -0,0 +1,199 @@ +# External Integrations + +**Analysis Date:** 2026-10-15 **Branch analysed:** `develop` @ `66adaf54` + +## APIs & External Services + +**Fitting Engine (EasyScience corelib, PyPI `easyscience>=2.5.1,<3`):** + +- `easyscience.fitting.multi_fitter.MultiFitter` - Subclassed as + `_ConstrainedEasyScienceMultiFitter` in `fitting.py` so the raw + `fit()` path still applies inequality constraints +- `easyscience.fitting.Sampler` / `SamplingResults` - Owns the DREAM + chain; `MultiFitter.mcmc_sample()` builds one and keeps it on + `fitter.sampler` for `extend()`, `save()`, `load_state()` +- `easyscience.fitting.AvailableMinimizers`, `FitResults` +- `easyscience.fitting.minimizers.minimizer_bumps` - Its module-level + `FitDriver` name is wrapped by `_bumps_constraints.install()` to attach + `FitProblem.constraints` +- `easyscience.fitting.minimizers.minimizer_base.MINIMIZER_PARAMETER_PREFIX` - + Stripped from parameter names in `analysis/bayesian.py` +- `easyscience.variable.Parameter` / `DescriptorNumber` and + `parameter_dependency_resolver.resolve_all_parameter_dependencies` - + Equality constraints (`constraints.py`), contrasts, project load +- `easyscience.base_classes.ModelBase` / `EasyList`, + `easyscience.io.SerializerComponent` / `SerializerBase` - Base classes + for `BaseCore`, `BaseCollection`, `DataSet1D`, `CalculatorBase` +- `easyscience.global_object` - Unique-name registry and undo stack +- `easyscience.fitting.calculators.interface_factory.InterfaceFactoryTemplate` / + `ItemContainer` - `CalculatorFactory` base +- Auth: None (Python import) + +**Reflectivity Calculators (pluggable via `CalculatorFactory`):** + +- refnx (`refnx.reflect`) - `src/easyreflectometry/calculators/refnx/`; + unpolarized only (`supports_magnetism()` is False) +- refl1d (`refl1d.names`, `refl1d.profile.build_profile`, + `refl1d.sample.layers.Repeat`) - + `src/easyreflectometry/calculators/refl1d/`; magnetism via + `rho_m`/`theta_m` per layer, polarized cross-sections, magnetic SLD + profile. `DEFAULT_THETA_M = 270.0`, resolution oversampling factor 21. +- BornAgain was removed in 1.8.0. + +**BUMPS (direct dependency and via core):** + +- DREAM MCMC through the core `Sampler` +- `FitProblem.constraints` list receives penalty callables built by + `inequality_constraints.build_constraints_factory`; penalties are + attached by the shim as the driver is built, not passed as kwargs +- `Bumps_*` minimizers are the only engines that can enforce inequality + constraints; `fit_settings.requires_finite_bounds` guards engines that + need finite bounds + +**ORSO (orsopy):** + +- Read: `.ort` text and `.orb` nexus via `orso_utils._load_orso_any`; + columns validated and unit-scaled (`_Q_UNIT_TO_INV_ANGSTROM`, + `_SLD_UNIT_TO_INV_ANGSTROM_SQUARED`), sQz cleaned to a `Pointwise` + resolution, polarization read from the header + (`data/polarized.channel_from_orso_polarization`) +- Model import: `load_orso_model` converts the ORSO model language + (`resolve_stack`, substacks → `RepeatingMultilayer`) into a `Sample` +- Write: `save_orso_experiment` / `sample_to_orso_model` export + experiments with the model embedded in the header + +**Chemistry (periodictable):** + +- `special/calculations.py` scattering lengths, molecular weights, + density → SLD; `contrasts.ReplaceFormula` validates formulas + +**Plotting / Reports:** + +- plotly - Corner, trace, distribution plots (`analysis/bayesian.py`) and + interactive summary figures; plotly is a hard dependency +- matplotlib - `plot.py`, static summary figures, `plot_volume_fraction` +- xhtml2pdf (`pisa`) - `Summary.save_pdf_summary`; stderr silenced via + `_silence_pdf_converter` +- arviz - Optional (`dev` extra), required only by `plot_trace` and + `gelman_rubin` (`_require_arviz()` raises a helpful ImportError) + +## Data Storage + +**Databases:** + +- None + +**File Storage:** + +- Local filesystem only +- Project files: `project.json` written atomically by + `Project.save_as_json` (temp file + fsync + replace, permission + preserving). `file_format` 2 by default, 3 when the file uses shared + objects (`@ref`/`@ref_id` from `sample/references.py`), excludes an + experiment from the fit, or has ambiguous model names. +- Posterior chains: `save_posterior` / `load_posterior` write the BUMPS + state plus a JSON sidecar (`_SIDECAR_SCHEMA_VERSION = 1`) carrying a + data fingerprint and the easyreflectometry version +- Example data fetched with pooch in tutorials; test fixtures under + `tests/_static/` (provenance documented in `PROVENANCE.md`) + +**Caching:** + +- `Project._fitter` cached per current model index, invalidated when + models change +- `Refl1dWrapper._model_state_token` caches polarized reflectivities per + model state + +## Authentication & Identity + +- Not applicable (scientific library) + +## Monitoring & Observability + +**Error Tracking:** + +- `warnings.warn()` for data quality and recoverable issues: zero + variance handling (`fitting.py`), ORSO column problems + (`orso_utils.py`, 14 sites), unreadable dependencies on load, volume + fractions below zero (`VolumeFractionWarning`), duplicate collection + items +- Domain exceptions: `FitScopeError`, `FitPreconditionError` + (`fitting.py`), `UnitError` (`inequality_constraints.py`), + `UnsupportedSubstitution`, `LayoutMismatch` (`contrasts.py`), + `VolumeFractionWarning`; file errors from `Project` are raised, not + printed (since 1.8.0) +- `Project.load_report` collects every warning of a load + +**Logs:** + +- `logging.getLogger` in `project.py`, `orso_utils.py`, `summary.py`, + `sample/paths.py`, `calculators/refl1d/wrapper.py`; no handler is + configured by the library + +## CI/CD & Deployment + +**Hosting:** + +- PyPI (`pypi-publish.yml` on tag push, via `pixi run default-build`; + legacy `python-publish.yml` also present) +- GitHub Pages via mike (`docs.yml`): `dev` alias from develop, versioned + + `latest` on release; a quality dashboard is published by + `dashboard.yml` (badgery + radon + interrogate) + +**CI Pipeline (`.github/workflows/`, 24 workflows):** + +- `test.yml` - Main matrix (3 OS × py-311/py-313 pixi envs): unit, + functional, integration tests, wheel build, then a package-test job. + Pull requests, master/develop and manual dispatch run the full set + including `slow`; other pushes run `-m fast` / the default fast suite. +- `lint-format.yml` - pyproject validity, SPDX headers, ruff lint + + format, pydoclint, prettier, nbqa on notebooks +- `coverage.yml` - docstring coverage, unit + integration coverage to + Codecov (`unit-tests-all`, `integration-tests-all`) +- `tutorial-tests.yml` - nbmake over `docs/docs/tutorials/`; scheduled + via `tutorial-tests-trigger.yml`; `test-trigger.yml` schedules code + tests +- `pypi-test.yml` - Scheduled install-from-PyPI smoke test +- `docs.yml`, `dashboard.yml` - site and metrics publishing +- `security.yml` / `codeql-analysis.yml` / `ossar-analysis.yml` - CodeQL + and OSSAR scans +- `release-drafter.yml`, `release-notes.yml`, `release-pr.yml` + (develop → master), `backmerge.yml` (master → develop), + `release-drafter-verify-pr-labels.yml`, `pr-labels.yml`, + `issues-labels.yml`, `cleanup.yml` +- `python-ci.yml`, `python-package.yml` - Older pip-based matrices that + still run on every push/PR alongside `test.yml` + +## Environment Configuration + +**Required env vars:** + +- None for library use + +**Secrets location:** + +- GitHub Secrets / `easyscience[bot]` app token used by release, + backmerge, docs and dashboard workflows; PyPI trusted publishing + +## Webhooks & Callbacks + +**Incoming:** + +- None + +**Outgoing (in-process callbacks):** + +- `progress_callback` on `MultiFitter.fit`, `fit_polarized`, + `mcmc_sample` and `PreparedFit.execute(**kwargs)` → forwarded to the + core fitter / `Sampler.sample` +- `abort_test` on `mcmc_sample` → `Sampler.sample` +- `constraints_factory_provider` - zero-argument callable (normally a + weak reference to `Project.build_constraints_factory`) resolved at fit + time, in the calling thread +- `MultiFitter.for_experiments` / `Project.prepare_fit` are designed for + a GUI worker thread: the caller executes `prepared.core_fitter` and + reports back through `Project.record_fit` + +--- + +_Integration audit: 2026-10-15 (develop @ 66adaf54)_ diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md new file mode 100644 index 00000000..858e7f93 --- /dev/null +++ b/.planning/codebase/STACK.md @@ -0,0 +1,139 @@ +# Technology Stack + +**Analysis Date:** 2026-10-15 **Branch analysed:** `develop` @ `66adaf54` +(tag `v1.7.0-51`; last release `v1.8.0` on 2026-09-18, develop carries +the unreleased 1.9 work listed under "Unreleased" in `CHANGELOG.md`) + +## Languages + +**Primary:** + +- Python 3.11–3.13 - All source code under `src/easyreflectometry/` + (~18.5k lines) and `tests/` (~21k lines) + +**Secondary:** + +- Jupyter Notebook (.ipynb) - Tutorials in `docs/docs/tutorials/` + (simulation, fitting, advancedfitting) and standalone notebooks in + `notebooks/` +- Markdown - MkDocs site under `docs/docs/`, `CHANGELOG.md` + +## Runtime + +**Environment:** + +- CPython 3.11, 3.12, 3.13 (`requires-python = '>=3.11'`) +- The pixi `default` environment is Python 3.13 with the `dev` extra + installed editable; `py-311-env` / `py-313-env` exist for CI matrices. + +**Package Manager:** + +- Pixi (`pixi.toml` + `pixi.lock`; conda-forge channel only) is the + canonical developer environment and what CI uses (`pixi run ...`). +- pip for end users (`pip install easyreflectometry[dev]`). +- A bare `python` on this machine is a conda environment with an old + easyscience; always use `pixi run python` for tests. + +## Frameworks + +**Core:** + +- EasyScience `easyscience>=2.5.1,<3` (PyPI; pixi resolves 2.5.1) - + Parameter model (`Parameter`, `DescriptorNumber`), dependency + resolver, `EasyList`/`ModelBase` base classes, serializer, + `Fitter`/`MultiFitter`, `Sampler`/`SamplingResults`, minimizer + wrappers (LMFit, Bumps, DFO), `InterfaceFactoryTemplate` +- scipp 26.3 - `sc.DataGroup`/`sc.array` with variances as the fit I/O + container +- refnx 0.1.63 - Reflectivity engine (unpolarized only) +- refl1d >= 1.0.0 (1.0.1 installed) - Reflectivity engine, including + magnetism / polarized channels (`pp`, `pm`, `mp`, `mm`) +- bumps 1.0.4 - DREAM MCMC sampler and `Bumps_*` minimizers (via core); + `FitProblem.constraints` is also the carrier for inequality penalties +- lmfit 1.3.4 - Default minimizer `LMFit_leastsq` (via core) +- orsopy >= 1.2 (1.2.3) - ORSO `.ort` text and `.orb` nexus I/O, model + language (`resolve_stack`) for sample import/export +- asteval - Safe expression evaluation for inequality constraints +- periodictable - Scattering lengths / formulas (via `special/`, + `contrasts.py`) +- scipy 1.17 - `erf` smearing in volume-fraction profiles, Bayesian + helpers +- plotly - Interactive corner/trace/distribution plots and interactive + HTML summary figures +- matplotlib - Static SLD / fit plots in `plot.py` and the PDF summary +- xhtml2pdf (+ `svglib<1.6` on Linux/macOS) - PDF export of the HTML + summary +- h5py, pooch, pyyaml - Nexus reading, example-data download, YAML + `repr` + +**Testing:** + +- pytest with `--import-mode=importlib` and `-m "not slow"` in + `addopts`; markers `fast` and `slow` +- pytest-cov, pytest-xdist, nbmake (notebook tests) + +**Build/Dev:** + +- hatchling build backend; versioningit derives the version from `v*` + tags (`default-tag = 'v999.0.0'` when untagged) +- Ruff (`line-length = 128`, single quotes, rules E/W/F/I/S, isort + `force-single-line`) +- pre-commit, prettier (non-Python formatting), pydoclint / + format-docstring (configured but disabled via `exclude = '\.'`), + interrogate (`fail-under = 0`), radon, spdx-headers, validate-pyproject +- copier (`.copier-answers.yml`) - repo scaffold synced from + `gh:easyscience/templates` (`template_type=lib`) +- MkDocs Material + mkdocstrings + mkdocs-jupyter + mike (versioned docs + on `gh-pages`) + +## Key Dependencies + +**Critical (fitting pipeline):** + +- `easyscience` - `MultiFitter`, `Sampler`, `AvailableMinimizers`, + `FitResults`, `MINIMIZER_PARAMETER_PREFIX`; the library subclasses the + core `MultiFitter` and monkeypatches `minimizer_bumps.FitDriver` (see + `_bumps_constraints.py`) +- `bumps` - DREAM, `FitProblem`, `Parameter`; required for inequality + constraints and MCMC +- `numpy` - Array math everywhere + +**Infrastructure:** + +- `scipp` - `DataGroup` is the public fit I/O format +- `orsopy` - Only supported structured data/model exchange format + +## Configuration + +**Environment:** + +- No `.env`; `PYTHONIOENCODING=utf-8` set by pixi activation +- `JUPYTER_PLATFORM_DIRS=1` and `PYTHONWARNINGS` set by the `docs-*` + pixi tasks + +**Build:** + +- `pyproject.toml` - dependencies, pytest, ruff, coverage, versioningit +- `pixi.toml` - environments and the task vocabulary CI calls + (`unit-tests`, `unit-tests-all`, `slow-tests`, `integration-tests`, + `functional-tests`, `notebook-tests`, `py-lint-check`, `docs-build`, + `dist-build`, ...) +- `.pre-commit-config.yaml`, `prettierrc.toml`, `.badgery.yaml`, + `.codecov.yml` + +## Platform Requirements + +**Development:** + +- Windows (win-64), Linux (linux-64), macOS 14+ (osx-arm64) per + `pixi.toml` platforms; CI matrices run all three +- `svglib` is only installed on Linux/macOS (pin `<1.6`) + +**Production:** + +- Pure-Python wheel on PyPI; engines (refnx, refl1d) ship compiled + extensions via their own wheels + +--- + +_Stack analysis: 2026-10-15 (develop @ 66adaf54)_ diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md new file mode 100644 index 00000000..5d55c9d3 --- /dev/null +++ b/.planning/codebase/STRUCTURE.md @@ -0,0 +1,241 @@ +# Codebase Structure + +**Analysis Date:** 2026-10-15 **Branch analysed:** `develop` @ `66adaf54` + +## Directory Layout + +``` +reflectometry-lib/ +├── src/easyreflectometry/ # Main package (~18.5k lines) +│ ├── __init__.py # Exports Project, PosteriorResults, constraint helpers, InequalitySpec, UnitError +│ ├── project.py # Project (3211 lines): experiments, models, persistence, fit orchestration +│ ├── fitting.py # MultiFitter, PreparedFit, FitRun, FitScopeError, FitPreconditionError (1428 lines) +│ ├── fit_settings.py # FitSettings dataclass, MinimizerOption schema, objective/minimizer aliases +│ ├── _bumps_constraints.py # FitDriver shim attaching inequality penalties to BUMPS problems +│ ├── constraints.py # constrain / constrain_equal / unconstrain / constrain_to_sum / derived_parameter +│ ├── inequality_constraints.py # InequalitySpec, asteval evaluation, build_constraints_factory, UnitError +│ ├── contrasts.py # derive_contrast, ReplaceMaterial/ReplaceFormula, plan_link/LinkPlan/LinkRecord +│ ├── limits.py # apply_default_limits (SLD, scale, percentage bounds) +│ ├── orso_utils.py # ORSO .ort/.orb read, model-language import/export (891 lines) +│ ├── plot.py # matplotlib plot(DataGroup), plot_volume_fraction +│ ├── utils.py # get_as_parameter, yaml_dump, parameter counting helpers +│ ├── main.py # Prints available calculators (no console_scripts entry) +│ ├── analysis/ +│ │ ├── __init__.py +│ │ └── bayesian.py # PosteriorResults, plot_corner/trace/distribution (plotly), credible_intervals, +│ │ # posterior predictive, save_posterior/load_posterior (1412 lines) +│ ├── calculators/ +│ │ ├── __init__.py # CalculatorBase, CalculatorFactory, PolarizationChannel +│ │ ├── calculator_base.py # Abstract calculator: create/claim items, per-model resolution, magnetism toggles +│ │ ├── wrapper_base.py # Abstract engine wrapper incl. calculate_channel / calculate_polarized +│ │ ├── factory.py # CalculatorFactory: fit_func, fit_func_for(channel, resolution_function) +│ │ ├── polarization.py # PolarizationChannel enum (pp, pm, mp, mm) + index map +│ │ ├── refnx/{calculator,wrapper}.py +│ │ └── refl1d/{calculator,wrapper}.py # magnetism, polarized probes, oversampled resolution +│ ├── data/ +│ │ ├── __init__.py +│ │ ├── data_store.py # DataSet1D (x, y, ye, xe, model, resolution_function, include_in_fit) +│ │ ├── measurement.py # load / load_as_dataset / merge_datasets / resolution_from_dataset +│ │ └── polarized.py # PolarizedDataSet, channel detection from ORSO header or filename +│ ├── model/ +│ │ ├── model.py # Model (scale, background, resolution_function, total_thickness, has_magnetism) +│ │ ├── model_collection.py # ModelCollection with colour palette +│ │ └── resolution_functions.py # ResolutionFunction, PercentageFwhm, LinearSpline, Pointwise +│ ├── sample/ +│ │ ├── __init__.py +│ │ ├── base_core.py # BaseCore(ModelBase): interface binding, to_dict/from_dict +│ │ ├── paths.py # walk(): structural paths like models/0/sample/1/layers/0/thickness +│ │ ├── references.py # @ref / @ref_id shared-object serialization (writing/reading contexts) +│ │ ├── volume_fraction.py # VolumeFractionProfile, volume_fraction_profile (erf-smeared occupancy) +│ │ ├── assemblies/ # BaseAssembly, Multilayer, RepeatingMultilayer, SurfactantLayer, Bilayer, GradientLayer +│ │ ├── collections/ # BaseCollection(EasyList), LayerCollection, MaterialCollection, Sample +│ │ └── elements/ +│ │ ├── layers/ # Layer, LayerAreaPerMolecule, LayerMagnetism (rho_m, theta_m) +│ │ └── materials/ # Material, MaterialDensity (sld_coupled), MaterialMixture, MaterialSolvated +│ ├── special/ # parse_formula, scattering-length and SLD calculations +│ └── summary/ # Summary: HTML/PDF report with parameter tables, goodness of fit, plotly figures +├── tests/ # ~21k lines, 1410 tests (34 marked slow) +│ ├── _static/ # example.ort, example.orb, e361r.txt (+ PROVENANCE.md) +│ ├── calculators/ # refnx/refl1d wrappers, polarization, resolution conventions, recycled names +│ ├── data/ # DataSet1D, PolarizedDataSet +│ ├── model/, sample/, special/, summary/ +│ ├── unit/ # constraints, inequality constraints, bumps shim, fit settings, project core, derived params +│ ├── functional/ # placeholder (test_dummy.py) +│ ├── integration/ # convergence/ (reference costs), cross-engine resolution, .ort fitting +│ ├── test_fitting.py # MultiFitter fit + MCMC (TestMCMCSample*) +│ ├── test_project.py, test_shared_persistence.py, test_contrasts.py, test_prepared_fit.py, +│ ├── test_per_dataset_resolution.py, test_polarized_fitting.py, test_bayesian.py, test_orso_*.py, ... +│ └── package_test.py +├── docs/ +│ ├── mkdocs.yml # MkDocs Material + mkdocstrings + mkdocs-jupyter + mike +│ └── docs/ +│ ├── user-guide/resolution.md # which resolution a model/dataset/fit uses +│ ├── tutorials/{basic,simulation,fitting,advancedfitting}/ # notebooks incl. bayesian_bumps, constraints, contrasts, polarized_fitting +│ └── api-reference/ # one page per module (constraints, inequality_constraints, contrasts, volume_fraction, ...) +├── notebooks/ # ess_hercules_2026, polarized_fitting, zero_variance_fitting (not in docs build) +├── tools/ # license_headers.py, update_docs_assets.py, update_github_labels.py +├── .github/workflows/ # 24 workflows (see INTEGRATIONS.md) +├── pyproject.toml, pixi.toml, pixi.lock +├── CHANGELOG.md # "Unreleased" section is the develop delta since 1.8.0 +├── .copier-answers.yml # scaffold from gh:easyscience/templates +└── Makefile, .coveragerc # stale (reference setup.py / EasyReflectometry/ dir) +``` + +Untracked in the working copy (not on `develop`): `.planning/`, +`.claude/`, `MD/`, `examples/`, `integration/`. + +## Directory Purposes + +**`src/easyreflectometry/` (root modules):** + +- `project.py` is the application-facing facade used by the GUI: + experiments dict, models, materials palette, persistence, fit/sample + preparation, contrasts/links, inequality constraints, ORSO import, + spin asymmetry, derived data for plots. +- `fitting.py` holds the fitting pipeline; `fit_settings.py` the + serializable settings it runs from; `_bumps_constraints.py` the engine + shim. These three plus `inequality_constraints.py` form the fit layer. +- `constraints.py` and `contrasts.py` build on EasyScience parameter + dependencies: equality constraints persisted by structural path, and + contrast derivation / linking between models. + +**`src/easyreflectometry/sample/`:** + +- Object tree (`Sample` → assemblies → layers → materials). `paths.py` + gives stable structural paths; `references.py` lets shared objects + serialize once (`file_format=3`); `volume_fraction.py` derives + occupancy profiles from the tree alone. + +**`src/easyreflectometry/calculators/`:** + +- Engine abstraction. `CalculatorBase` owns item creation and the + per-model resolution registry; `WrapperBase` is the per-engine + translation layer; `CalculatorFactory` hands out bound fit functions + (`fit_func_for(channel, resolution_function)`). + +**`src/easyreflectometry/data/`:** + +- `DataSet1D` carries its measured resolution and `include_in_fit`; + `PolarizedDataSet` groups channel datasets sharing one model; + `measurement.py` loads text/ORSO files and merges datasets of one + contrast. + +**`tests/`:** + +- Flat `test_*.py` for cross-cutting features, mirrored sub-packages for + modules, `unit/` for newer focused suites, `integration/` for real + minimizer runs (convergence references in `reference.md`). `slow` + marks guard sampler and long fits; default `pytest` deselects them. + +## Key File Locations + +**Entry Points:** + +- `src/easyreflectometry/project.py`: `Project.prepare_fit`, + `prepare_sample`, `record_fit`, `fitter`, `add_contrast`, + `load_experiment_from_files`, `save_as_json` / `load_from_json` +- `src/easyreflectometry/fitting.py`: `MultiFitter.fit`, + `fit_single_data_set_1d`, `fit_polarized`, `for_experiments`, + `prepare`, `mcmc_sample` +- `src/easyreflectometry/analysis/bayesian.py`: `PosteriorResults`, + `save_posterior` / `load_posterior` + +**Configuration:** + +- `pyproject.toml` (deps `easyscience>=2.5.1,<3`, pytest `addopts`, + ruff), `pixi.toml` (tasks), `docs/mkdocs.yml` + +**Core Logic (fitting pipeline):** + +- `fitting.py`: `_prepare_fit_arrays` (zero-variance objectives), + `PreparedFit`, `FitRun`, `MultiFitter._constraints`, + `_check_fit_preconditions` +- `fit_settings.py`: `FitSettings.engine_kwargs`, `configure`, + `option_schema` +- `_bumps_constraints.py`: `install`, `applied`, `_attach_constraints` +- Installed core (pixi env): `easyscience/fitting/multi_fitter.py`, + `sampler.py`, `minimizers/minimizer_bumps.py` + +**Persistence:** + +- `project.py`: `as_dict` / `from_dict`, `_needs_new_format`, + `_user_constraints`, `_as_dict_add_contrasts`; `sample/references.py`; + `sample/collections/base_collection.py` `to_dict` / `from_dict` + +## Naming Conventions + +**Files:** snake_case modules; tests `test_.py`; private module +`_bumps_constraints.py`. + +**Classes:** PascalCase (`MultiFitter`, `PreparedFit`, `FitRun`, +`InequalitySpec`, `PolarizedDataSet`); core import aliased as +`EasyScienceMultiFitter`; private subclasses prefixed `_`. + +**Functions:** snake_case; private helpers `_`-prefixed; `Project` +exposes `*_at_index` accessors for GUI use +(`model_data_for_experiment_at_index`). + +**Constants:** UPPER_SNAKE (`DEFAULT_MINIMIZER`, `GUIDE_FIELD_ANGLE`, +`DEFAULT_THETA_M`, `SIGMA_TO_FWHM`, `USER_CONSTRAINT_FLAG`). + +**Parameter paths:** `models//sample//layers//` (stable +across save/load; used by inequality constraints and constraint +records). + +## Where to Add New Code + +**New fit option or minimizer setting:** + +- `fit_settings.py` (`MinimizerOption`, `option_schema`, + `engine_kwargs`) → consumed by `MultiFitter.prepare`; tests in + `tests/test_fit_settings.py`, `tests/test_prepared_fit.py`. + +**New constraint kind:** + +- Equality: `constraints.py` + persistence in `Project._user_constraints` + / `_restore_user_constraints`. Inequality: `inequality_constraints.py` + + `Project.add_inequality_constraint`. Tests under `tests/unit/`. + +**New calculator backend:** + +- `calculators//calculator.py` + `wrapper.py`; registration is + automatic via `CalculatorBase.__init_subclass__`; mirror + `tests/calculators//` and add to + `tests/calculators/test_resolution_conventions.py` / + `tests/integration/test_cross_engine_resolution.py`. + +**New sample element or assembly:** + +- `sample/elements/...` or `sample/assemblies/...`, export from + `sample/__init__.py`, handle in `orso_utils.py` conversion if it maps + to ORSO, add an `api-reference` page and `mkdocs.yml` nav entry. + +**New Bayesian analysis:** + +- `analysis/bayesian.py`, export in `analysis/__init__.py`, tests in + `tests/test_bayesian.py`. + +**New Project behaviour that must persist:** + +- `Project.as_dict` / `from_dict` plus `_needs_new_format` if older + readers would misread it; tests in `tests/test_project.py` / + `tests/test_shared_persistence.py`; document in `CHANGELOG.md` + "Unreleased". + +## Special Directories + +**`.planning/codebase/`:** GSD analysis documents; untracked, not +gitignored (no `.gitignore` entry). + +**`docs/`:** MkDocs source; tutorials are executed by `notebook-exec` in +CI before the site is built. API pages use mkdocstrings over `../src`. + +**`notebooks/`:** Standalone notebooks, committed, not part of the docs +build. + +**`tests/_static/`:** Third-party and example fixtures with +`PROVENANCE.md`. + +--- + +_Structure analysis: 2026-10-15 (develop @ 66adaf54)_ From 9c1620164dd7246fb4383a6ff70314ea069fc2a5 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Thu, 15 Oct 2026 14:25:31 +0200 Subject: [PATCH 2/3] ruff --- .planning/codebase/ARCHITECTURE.md | 82 ++++++++++++++++-------------- .planning/codebase/CONCERNS.md | 37 ++++++++------ .planning/codebase/INTEGRATIONS.md | 26 +++++----- .planning/codebase/STACK.md | 10 ++-- .planning/codebase/STRUCTURE.md | 19 +++---- 5 files changed, 95 insertions(+), 79 deletions(-) diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md index 89769ed3..9c3bb05e 100644 --- a/.planning/codebase/ARCHITECTURE.md +++ b/.planning/codebase/ARCHITECTURE.md @@ -1,6 +1,7 @@ # Architecture -**Analysis Date:** 2026-10-15 **Branch analysed:** `develop` @ `66adaf54` +**Analysis Date:** 2026-10-15 **Branch analysed:** `develop` @ +`66adaf54` ## Pattern Overview @@ -27,8 +28,8 @@ is computed by pluggable engine wrappers (refnx, refl1d) behind - Inequality constraints are enforced by patching the BUMPS driver in the core's namespace, because the core offers no hook - Shared objects (one material in two layers, one assembly in two - models) serialize once through `sample/references.py` (`file_format` - 3) + models) serialize once through `sample/references.py` + (`file_format` 3) ## Layers @@ -64,14 +65,16 @@ is computed by pluggable engine wrappers (refnx, refl1d) behind - Purpose: Post-hoc posterior analysis and chain persistence - Location: `src/easyreflectometry/analysis/bayesian.py` - Contains: `PosteriorResults` (summary, corner, trace, distribution, - credible_interval, gelman_rubin, save), `posterior_predictive_*`, - `save_posterior` / `load_posterior` with fingerprinted sidecar + credible*interval, gelman_rubin, save), + `posterior_predictive*\*`, `save_posterior`/`load_posterior` with + fingerprinted sidecar - Depends on: plotly (required), arviz (optional), numpy **Model / Sample Layer:** - Purpose: Physical description of the sample and measurement -- Location: `src/easyreflectometry/model/`, `src/easyreflectometry/sample/` +- Location: `src/easyreflectometry/model/`, + `src/easyreflectometry/sample/` - Contains: `Model` (scale, background, resolution, `total_thickness` derived parameter), `Sample` and assemblies/layers/materials, `LayerMagnetism`, `MaterialDensity.sld_coupled`, `volume_fraction`, @@ -119,21 +122,22 @@ is computed by pluggable engine wrappers (refnx, refl1d) behind **Classical Fit (project path):** -1. GUI/user calls `Project.prepare_fit(experiments=None, objective=None, skip_invalid=False)` +1. GUI/user calls + `Project.prepare_fit(experiments=None, objective=None, skip_invalid=False)` 2. `_fit_scope` selects experiments with `include_in_fit` and a model; `_fitter_for` builds `MultiFitter.for_experiments(...)` with a deep copy of `fit_settings` and a weak `constraints_factory_provider` 3. `MultiFitter.prepare` applies the objective (`hybrid` default: - Mighell for zero-variance points, WLS otherwise), binds each dataset's - fit function to its own resolution (`_resolution_for_fitted_points` - for `legacy_mask`), collects `added_roots` (free parameters of - left-out models that fitted parameters follow), builds the core - fitter via `_core_fitter_for` + `FitSettings.configure`, checks - preconditions (finite bounds for engines that need them, warns on - starts at bounds) + Mighell for zero-variance points, WLS otherwise), binds each + dataset's fit function to its own resolution + (`_resolution_for_fitted_points` for `legacy_mask`), collects + `added_roots` (free parameters of left-out models that fitted + parameters follow), builds the core fitter via `_core_fitter_for` + + `FitSettings.configure`, checks preconditions (finite bounds for + engines that need them, warns on starts at bounds) 4. Caller runs `prepared.execute(**kwargs)` or `core_fitter.fit(...)` in - a thread; the `_ConstrainedEasyScienceMultiFitter.fit` property - wraps the call in `_bumps_constraints.applied(factory)` + a thread; the `_ConstrainedEasyScienceMultiFitter.fit` property wraps + the call in `_bumps_constraints.applied(factory)` 5. `Project.record_fit(prepared, results, status)` computes classical metrics from the measured arrays (`prepared.finalize`), stores a `FitRun` on `last_fit` and pushes results to `project.fitter` @@ -148,9 +152,9 @@ is computed by pluggable engine wrappers (refnx, refl1d) behind **Polarized Fit:** -- `fit_polarized(polarized_dataset)` or `for_experiments([...])`: one fit - function per spin channel, all against one model, each bound to its - channel via `CalculatorFactory.fit_func_for(channel, resolution)`. +- `fit_polarized(polarized_dataset)` or `for_experiments([...])`: one + fit function per spin channel, all against one model, each bound to + its channel via `CalculatorFactory.fit_func_for(channel, resolution)`. Structural parameters shared, magnetic parameters constrained by all channels. @@ -158,12 +162,13 @@ is computed by pluggable engine wrappers (refnx, refl1d) behind 1. `Project.prepare_sample()` → `for_experiments` fitter with settings snapshot; or `fitter.mcmc_sample(data)` directly -2. `mcmc_sample` requires a BUMPS minimizer (`minimizer.package == -'bumps'`), refuses all-zero variances unless `objective='mighell'`, - calls `prepare(action='sampling')` +2. `mcmc_sample` requires a BUMPS minimizer + (`minimizer.package == 'bumps'`), refuses all-zero variances unless + `objective='mighell'`, calls `prepare(action='sampling')` 3. Builds `Sampler(core_fitter, x, y, weights)` inside the constraints - context, patches `sampler.extend` to re-enter it, keeps the sampler on - `fitter.sampler`, runs `sampler.sample(samples, burn, thin, population, sampler_kwargs, progress_callback, abort_test)` + context, patches `sampler.extend` to re-enter it, keeps the sampler + on `fitter.sampler`, runs + `sampler.sample(samples, burn, thin, population, sampler_kwargs, progress_callback, abort_test)` 4. Returns `{'draws', 'param_names', 'state', 'logp'}`; user wraps in `PosteriorResults` or calls `save_posterior` @@ -191,10 +196,10 @@ is computed by pluggable engine wrappers (refnx, refl1d) behind ## Key Abstractions -**`PreparedFit` (fitting.py):** Snapshot pattern. Everything a run needs, -fixed at prepare time; owns copies of measured arrays, the configured -core fitter and both full-resolution (`curve_funcs`) and fitted-point -(`fit_funcs`) evaluators. +**`PreparedFit` (fitting.py):** Snapshot pattern. Everything a run +needs, fixed at prepare time; owns copies of measured arrays, the +configured core fitter and both full-resolution (`curve_funcs`) and +fitted-point (`fit_funcs`) evaluators. **`FitRun` (fitting.py):** Frozen record of a run (status, minimizer, objective, inputs by experiment key, pooled and per-dataset statistics). @@ -205,10 +210,10 @@ Outlives model changes; `remapped()` follows experiment re-keying. validated by `MinimizerOption` schemas). `configure(fitter)` and `engine_kwargs()` are the two consumers. -**`_ConstrainedEasyScienceMultiFitter` / `_bumps_constraints`:** Property -override plus monkeypatch. The subclass overrides the core's read-only -`fit` property so the raw `easy_science_multi_fitter.fit(...)` path -resolves the owner's constraints provider in the calling thread and +**`_ConstrainedEasyScienceMultiFitter` / `_bumps_constraints`:** +Property override plus monkeypatch. The subclass overrides the core's +read-only `fit` property so the raw `easy_science_multi_fitter.fit(...)` +path resolves the owner's constraints provider in the calling thread and consumes an explicit `constraints_factory` keyword (the core would swallow it via `**kwargs`). `install()`, run at import of `_bumps_constraints`, wraps `minimizer_bumps.FitDriver` so each @@ -220,15 +225,16 @@ Non-BUMPS engines raise `NON_BUMPS_ERROR`. restricted symbol table; `build_constraints_factory` returns BUMPS penalty callables. -**Contrasts (`contrasts.py`):** A contrast is an ordinary `Model` sharing -structure with a reference by identity or by `constrain_equal`. +**Contrasts (`contrasts.py`):** A contrast is an ordinary `Model` +sharing structure with a reference by identity or by `constrain_equal`. `derive_contrast` copies touched assemblies, shares the rest and ties parameters; `plan_link` / `apply_link` / `unlink` manage equalities between two models of one layout with full state snapshots. **`CalculatorBase` / `WrapperBase` / `CalculatorFactory`:** Template + -Adapter + Factory. Items are claimed per calculator (`_claim`), per-model -resolution registry, magnetism toggles, channel-bound fit functions. +Adapter + Factory. Items are claimed per calculator (`_claim`), +per-model resolution registry, magnetism toggles, channel-bound fit +functions. **`references` (sample/references.py):** Context-managed shared-object serialization (`writing()`, `reading(report)`, `suspended()`), with @@ -278,8 +284,8 @@ issues; refuse silent degradation. handler configured. `warnings` is the main user-facing channel. **Validation:** `FitSettings.validate`, `MinimizerOption.validate`, -`InequalitySpec.validate_syntax` + `check_units`, ORSO column validation, -`_check_fit_preconditions`. +`InequalitySpec.validate_syntax` + `check_units`, ORSO column +validation, `_check_fit_preconditions`. **Threading:** The constraints factory lives in a `contextvars` variable, resolved in the calling thread; `PreparedFit` owns array diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md index 25833c8d..81f5d0c9 100644 --- a/.planning/codebase/CONCERNS.md +++ b/.planning/codebase/CONCERNS.md @@ -1,6 +1,7 @@ # Codebase Concerns -**Analysis Date:** 2026-10-15 **Branch analysed:** `develop` @ `66adaf54` +**Analysis Date:** 2026-10-15 **Branch analysed:** `develop` @ +`66adaf54` ## Summary @@ -24,12 +25,13 @@ goes through the core `Sampler`. The concerns below are what remains. inside the core's `minimizer_bumps` namespace and inspects `problem.models[0].pars`. Any core refactor (importing `FitDriver` differently, building the problem elsewhere, renaming `.pars`) silently -disables every inequality constraint; the shim passes untouched -problems through without complaint. +disables every inequality constraint; the shim passes untouched problems +through without complaint. **Files:** `src/easyreflectometry/_bumps_constraints.py`, `src/easyreflectometry/fitting.py` (`_constraints`, -`_keep_constraints_on_extend`), `tests/unit/test_bumps_constraints_shim.py` +`_keep_constraints_on_extend`), +`tests/unit/test_bumps_constraints_shim.py` **Impact:** High. Physics silently dropped on a core upgrade. @@ -53,8 +55,8 @@ against, and 3.0 is excluded by the pin. core release ships. **Fix approach:** Add a CI job (allowed to fail) installing core from -`release-3.0`; list the library's core API surface -(`Sampler`, `MultiFitter._precompute_reshaping`, `MINIMIZER_PARAMETER_PREFIX`, +`release-3.0`; list the library's core API surface (`Sampler`, +`MultiFitter._precompute_reshaping`, `MINIMIZER_PARAMETER_PREFIX`, `InterfaceFactoryTemplate`, `minimizer_bumps.FitDriver`) and check it against the branch. @@ -66,7 +68,8 @@ against the branch. `ItemContainer` from `interface_factory` are not public core API. **Files:** `analysis/bayesian.py`, `project.py`, -`sample/collections/base_collection.py`, `calculators/calculator_base.py` +`sample/collections/base_collection.py`, +`calculators/calculator_base.py` **Impact:** Medium. @@ -96,9 +99,9 @@ helpers). Keep `Project` as the facade. ### `fitting.py` (1428 lines) and `analysis/bayesian.py` (1412 lines) **Issue:** `bayesian.py` is mostly plotly styling constants and -hand-rolled contour/density code for corner plots; `fitting.py` -combines data preparation, settings, constraints wiring and five public -fit paths. +hand-rolled contour/density code for corner plots; `fitting.py` combines +data preparation, settings, constraints wiring and five public fit +paths. **Impact:** Low-Medium. @@ -128,7 +131,8 @@ shared objects, an experiment left out of the fit, duplicate model names. A user may save a file in 1.9 that 1.8 refuses without an obvious reason. -**Files:** `project.py` (`_needs_new_format`, `_as_dict_add_experiments`) +**Files:** `project.py` (`_needs_new_format`, +`_as_dict_add_experiments`) **Impact:** Medium. Document in release notes; already in CHANGELOG. @@ -175,8 +179,9 @@ slow cases) under `integration/`. **Issue:** `fitter.sampler.extend()` after constraints, and `save_posterior`/`load_posterior` fingerprint mismatch handling, have -limited coverage (`TestMCMCSample*` in `test_fitting.py`, `test_bayesian.py`); -all real DREAM runs are `slow` and only run on PRs/develop. +limited coverage (`TestMCMCSample*` in `test_fitting.py`, +`test_bayesian.py`); all real DREAM runs are `slow` and only run on +PRs/develop. **Impact:** Medium. @@ -230,9 +235,9 @@ and an accidental `git add .` would commit them. **Impact:** Low. -**Fix approach:** Add `.planning/`, `.claude/`, `MD/` to `.gitignore` (or -a global excludes file); decide whether `examples/` and `integration/` -belong in the repo. +**Fix approach:** Add `.planning/`, `.claude/`, `MD/` to `.gitignore` +(or a global excludes file); decide whether `examples/` and +`integration/` belong in the repo. ### Docstring tooling disabled diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md index 0fdeb4b3..836c8662 100644 --- a/.planning/codebase/INTEGRATIONS.md +++ b/.planning/codebase/INTEGRATIONS.md @@ -1,6 +1,7 @@ # External Integrations -**Analysis Date:** 2026-10-15 **Branch analysed:** `develop` @ `66adaf54` +**Analysis Date:** 2026-10-15 **Branch analysed:** `develop` @ +`66adaf54` ## APIs & External Services @@ -14,8 +15,8 @@ `fitter.sampler` for `extend()`, `save()`, `load_state()` - `easyscience.fitting.AvailableMinimizers`, `FitResults` - `easyscience.fitting.minimizers.minimizer_bumps` - Its module-level - `FitDriver` name is wrapped by `_bumps_constraints.install()` to attach - `FitProblem.constraints` + `FitDriver` name is wrapped by `_bumps_constraints.install()` to + attach `FitProblem.constraints` - `easyscience.fitting.minimizers.minimizer_base.MINIMIZER_PARAMETER_PREFIX` - Stripped from parameter names in `analysis/bayesian.py` - `easyscience.variable.Parameter` / `DescriptorNumber` and @@ -25,8 +26,8 @@ `easyscience.io.SerializerComponent` / `SerializerBase` - Base classes for `BaseCore`, `BaseCollection`, `DataSet1D`, `CalculatorBase` - `easyscience.global_object` - Unique-name registry and undo stack -- `easyscience.fitting.calculators.interface_factory.InterfaceFactoryTemplate` / - `ItemContainer` - `CalculatorFactory` base +- `easyscience.fitting.calculators.interface_factory.InterfaceFactoryTemplate` + / `ItemContainer` - `CalculatorFactory` base - Auth: None (Python import) **Reflectivity Calculators (pluggable via `CalculatorFactory`):** @@ -69,8 +70,8 @@ **Plotting / Reports:** -- plotly - Corner, trace, distribution plots (`analysis/bayesian.py`) and - interactive summary figures; plotly is a hard dependency +- plotly - Corner, trace, distribution plots (`analysis/bayesian.py`) + and interactive summary figures; plotly is a hard dependency - matplotlib - `plot.py`, static summary figures, `plot_volume_fraction` - xhtml2pdf (`pisa`) - `Summary.save_pdf_summary`; stderr silenced via `_silence_pdf_converter` @@ -136,9 +137,10 @@ - PyPI (`pypi-publish.yml` on tag push, via `pixi run default-build`; legacy `python-publish.yml` also present) -- GitHub Pages via mike (`docs.yml`): `dev` alias from develop, versioned - + `latest` on release; a quality dashboard is published by - `dashboard.yml` (badgery + radon + interrogate) +- GitHub Pages via mike (`docs.yml`): `dev` alias from develop, + versioned + - `latest` on release; a quality dashboard is published by + `dashboard.yml` (badgery + radon + interrogate) **CI Pipeline (`.github/workflows/`, 24 workflows):** @@ -157,8 +159,8 @@ - `docs.yml`, `dashboard.yml` - site and metrics publishing - `security.yml` / `codeql-analysis.yml` / `ossar-analysis.yml` - CodeQL and OSSAR scans -- `release-drafter.yml`, `release-notes.yml`, `release-pr.yml` - (develop → master), `backmerge.yml` (master → develop), +- `release-drafter.yml`, `release-notes.yml`, `release-pr.yml` (develop + → master), `backmerge.yml` (master → develop), `release-drafter-verify-pr-labels.yml`, `pr-labels.yml`, `issues-labels.yml`, `cleanup.yml` - `python-ci.yml`, `python-package.yml` - Older pip-based matrices that diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md index 858e7f93..88d274cf 100644 --- a/.planning/codebase/STACK.md +++ b/.planning/codebase/STACK.md @@ -1,8 +1,9 @@ # Technology Stack -**Analysis Date:** 2026-10-15 **Branch analysed:** `develop` @ `66adaf54` -(tag `v1.7.0-51`; last release `v1.8.0` on 2026-09-18, develop carries -the unreleased 1.9 work listed under "Unreleased" in `CHANGELOG.md`) +**Analysis Date:** 2026-10-15 **Branch analysed:** `develop` @ +`66adaf54` (tag `v1.7.0-51`; last release `v1.8.0` on 2026-09-18, +develop carries the unreleased 1.9 work listed under "Unreleased" in +`CHANGELOG.md`) ## Languages @@ -80,7 +81,8 @@ the unreleased 1.9 work listed under "Unreleased" in `CHANGELOG.md`) `force-single-line`) - pre-commit, prettier (non-Python formatting), pydoclint / format-docstring (configured but disabled via `exclude = '\.'`), - interrogate (`fail-under = 0`), radon, spdx-headers, validate-pyproject + interrogate (`fail-under = 0`), radon, spdx-headers, + validate-pyproject - copier (`.copier-answers.yml`) - repo scaffold synced from `gh:easyscience/templates` (`template_type=lib`) - MkDocs Material + mkdocstrings + mkdocs-jupyter + mike (versioned docs diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md index 5d55c9d3..33a7b987 100644 --- a/.planning/codebase/STRUCTURE.md +++ b/.planning/codebase/STRUCTURE.md @@ -1,6 +1,7 @@ # Codebase Structure -**Analysis Date:** 2026-10-15 **Branch analysed:** `develop` @ `66adaf54` +**Analysis Date:** 2026-10-15 **Branch analysed:** `develop` @ +`66adaf54` ## Directory Layout @@ -164,16 +165,15 @@ Untracked in the working copy (not on `develop`): `.planning/`, ## Naming Conventions -**Files:** snake_case modules; tests `test_.py`; private module -`_bumps_constraints.py`. +**Files:** snake*case modules; tests +`test*.py`; private module `\_bumps_constraints.py`. **Classes:** PascalCase (`MultiFitter`, `PreparedFit`, `FitRun`, `InequalitySpec`, `PolarizedDataSet`); core import aliased as `EasyScienceMultiFitter`; private subclasses prefixed `_`. -**Functions:** snake_case; private helpers `_`-prefixed; `Project` -exposes `*_at_index` accessors for GUI use -(`model_data_for_experiment_at_index`). +**Functions:** snake*case; private helpers +`*`-prefixed; `Project`exposes`\*\_at_index` accessors for GUI use (`model_data_for_experiment_at_index`). **Constants:** UPPER_SNAKE (`DEFAULT_MINIMIZER`, `GUIDE_FIELD_ANGLE`, `DEFAULT_THETA_M`, `SIGMA_TO_FWHM`, `USER_CONSTRAINT_FLAG`). @@ -192,9 +192,10 @@ records). **New constraint kind:** -- Equality: `constraints.py` + persistence in `Project._user_constraints` - / `_restore_user_constraints`. Inequality: `inequality_constraints.py` - + `Project.add_inequality_constraint`. Tests under `tests/unit/`. +- Equality: `constraints.py` + persistence in + `Project._user_constraints` / `_restore_user_constraints`. Inequality: + `inequality_constraints.py` + - `Project.add_inequality_constraint`. Tests under `tests/unit/`. **New calculator backend:** From 63a2d4bb95ed19e2c9e74cb2861690167a0255c3 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Tue, 20 Oct 2026 09:36:03 +0200 Subject: [PATCH 3/3] ruff --- .planning/codebase/ARCHITECTURE.md | 5 ++--- .planning/codebase/STRUCTURE.md | 9 +++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md index 9c3bb05e..a1d963b4 100644 --- a/.planning/codebase/ARCHITECTURE.md +++ b/.planning/codebase/ARCHITECTURE.md @@ -65,9 +65,8 @@ is computed by pluggable engine wrappers (refnx, refl1d) behind - Purpose: Post-hoc posterior analysis and chain persistence - Location: `src/easyreflectometry/analysis/bayesian.py` - Contains: `PosteriorResults` (summary, corner, trace, distribution, - credible*interval, gelman_rubin, save), - `posterior_predictive*\*`, `save_posterior`/`load_posterior` with - fingerprinted sidecar + credible*interval, gelman_rubin, save), `posterior_predictive*\*`, + `save_posterior`/`load_posterior` with fingerprinted sidecar - Depends on: plotly (required), arviz (optional), numpy **Model / Sample Layer:** diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md index 33a7b987..2592f0b9 100644 --- a/.planning/codebase/STRUCTURE.md +++ b/.planning/codebase/STRUCTURE.md @@ -165,15 +165,16 @@ Untracked in the working copy (not on `develop`): `.planning/`, ## Naming Conventions -**Files:** snake*case modules; tests -`test*.py`; private module `\_bumps_constraints.py`. +**Files:** snake*case modules; tests `test*.py`; private module +`\_bumps_constraints.py`. **Classes:** PascalCase (`MultiFitter`, `PreparedFit`, `FitRun`, `InequalitySpec`, `PolarizedDataSet`); core import aliased as `EasyScienceMultiFitter`; private subclasses prefixed `_`. -**Functions:** snake*case; private helpers -`*`-prefixed; `Project`exposes`\*\_at_index` accessors for GUI use (`model_data_for_experiment_at_index`). +**Functions:** snake*case; private helpers `*`-prefixed; +`Project`exposes`\*\_at_index` accessors for GUI use +(`model_data_for_experiment_at_index`). **Constants:** UPPER_SNAKE (`DEFAULT_MINIMIZER`, `GUIDE_FIELD_ANGLE`, `DEFAULT_THETA_M`, `SIGMA_TO_FWHM`, `USER_CONSTRAINT_FLAG`).