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
25 changes: 12 additions & 13 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,29 +214,28 @@ It is important to note that these notebooks are designed to be robust. These sh
## Learned User Preferences

- Prefer `match` / `case` (including guarded `case _ if ...`) over long `if` / `elif` ladders when resolving unions such as dispersive SLD specs or routing symbolic keys.
- Keep refloxide minimal: avoid unsolicited git commits and scope-expanding refactors, treat the core product as fast uniaxial reflectivity (Rust default, Python via `use_rust=False`) plus opt-in energy-consistent slab primitives, and resist a second refnx/pyref fitting stack—move fitting, pickle migration, and comparison notebooks to refl-analysis.
- Prefer the native dispersive compiler pipeline (`compile_structure`/`compile_model`/`ReflectivityObjective`) over the `patch_pyref` shim for new energy-dispersive fitting workflows; it supersedes the `DispersiveReflectModel`/`BatchedGlobalObjective` glue.
- When work is Python-only (examples, plotting, `pxr` helpers), rerun with `uv run python` as needed; rebuild the Maturin extension only after Rust or extension-layout changes or when imports prove the wheel is missing or stale.
- Keep refloxide minimal: avoid unsolicited git commits and scope-expanding refactors; core product is Rust TMM by default, pure-Python TMM via `refloxide.python.tmm` only, plus opt-in `data`/`model`/`objective`/`optics`; drop pyref and `refloxide.integrations` (pin an old tag if integrations are still needed). Modeling stacks use refloxide and refnx only.
- When replacing `pyref.fitting` call sites, keep the MaterialSLD/ReflectModel shape via `import refloxide.python.model as py`; do not rewrite examples to `materialize_at` plus raw `python.tmm` kernel calls.
- Prefer the native dispersive compiler pipeline (`compile_structure`/`compile_model`/`ReflectivityObjective`) and `refloxide.model` / `refloxide.objective` for new energy-dispersive fitting workflows.
- When work is Python-only (examples, plotting, helpers), rerun with `uv run python` as needed; rebuild the Maturin extension only after Rust or extension-layout changes or when imports prove the wheel is missing or stale.
- Avoid module-level ALL_CAPS frozensets for key routing when designing accessors; derive allowlists locally from `typing.get_args` on `Literal` aliases, small methods, or instance caching instead.
- Treat uniaxial lab-frame s-in/p-in as the validated scope; do not claim biaxial or general-incidence correctness unless explicitly tested.
- When calling the Rust reflectivity kernel from pyref or other MCMC/fitting loops, pass `parallel=False` and parallelize walkers or chains at the fitter level to avoid nested Rayon oversubscription.
- Call `patch_pyref` / `patch_pyref_if_needed` explicitly when swapping kernels; do not rely on refl-analysis `utils.models` import-time auto-patching—it breaks stock pyref kernel comparison.
- When calling the Rust reflectivity kernel from MCMC/fitting loops, pass `parallel=False` and parallelize walkers or chains at the fitter level to avoid nested Rayon oversubscription.
- Fail early when a workflow cannot be verified; do not claim notebooks run without executing them in the target environment (refl-analysis kernel with `utils`, correct notebook cwd).
- Manuscript and fitting notebooks belong in refl-analysis (`notebooks/manuscript/`, `notebooks/fitting/`), not `refloxide/examples/`; field-profile/E-field analysis notebooks and `src/utils/field_profile.py` also live in refl-analysis (`notebooks/exploration/`).
- Prefer canonical public names (`RefloxideScatterer`, `DispersiveStructure`, `OocUniTensorScatterer`) over `EnergyDependent*` / `PXR_*` prefixes when adding or renaming APIs.
- Parity and benchmark scripts should time both single-threaded (`parallel=False`) and parallel Rust paths alongside reference implementations such as refnx.

## Learned Workspace Facts

- Shipped refloxide surface is `uniaxial_reflectivity` (Rust via `use_rust=True`, Python via `pxr.tjf4x4`) and optional `pxr.energy` for energy-consistent slabs (shared thickness/roughness, tabulated OOC, CXRO/`MaterialSLD`, or free tensor per energy, including bookended profiles); a native dispersive compiler pipeline (`compile_structure`/`SlabEnergyPlan`, `compile_model`/`CompiledReflectivityModel`, `ReflectivityObjective`) now supersedes the `patch_pyref`/`DispersiveReflectModel`/`BatchedGlobalObjective` shim, with `pxr/plugin/` fitting (`batched_global`, `dispersive_model`, `dft_fit`) retained as helpers pending relocation to refl-analysis. Removed orphans: do not import deleted `pxr.stacks`, `pxr.plotting`, or `pxr.plugin.unittensor`.
- `refloxide.pxr.tjf4x4.uniaxial_reflectivity` expects each slab `tensor` diagonal to carry δ + iβ per principal axis under `epsilon = conj(I - 2 * tensor)`, not raw n or an n²-derived packing; lightweight stack builders should populate rows accordingly.
- Default package surface is Rust TMM (`from refloxide import uniaxial_reflectivity` / `refloxide.tmm`). Modeling is opt-in (`refloxide.model`, `refloxide.objective`, `refloxide.data`, `refloxide.optics`). Pure-Python modeling (pyref.fitting-shaped) is `import refloxide.python.model as py` then `py.MaterialSLD` / `py.ReflectModel` (kernel: `refloxide.python.tmm`). Pure-Python TMM alone is `from refloxide.python.tmm import uniaxial_reflectivity`. `refloxide.python` does not re-export. Pyref and `refloxide.integrations` are removed.
- `refloxide.python.tmm.uniaxial_reflectivity` expects each slab `tensor` diagonal to carry δ + iβ per principal axis under `epsilon = conj(I - 2 * tensor)`, not raw n or an n²-derived packing; lightweight stack builders should populate rows accordingly.
- `periodictable.xsf.index_of_refraction` takes photon energy in keV; convert eV by dividing by 1000 before calling when pairing with eV-scale experiment parameters.
- `plugin` reflectivity helpers assume a `(n_q, 2, 2)` polarization block from the uniaxial kernel where native layout is `[:, 0, 0] = R_ss` and `[:, 1, 1] = R_pp` (matches `tjf4x4` / Rust). Legacy `pxr.layout.reflectivity_for_pol` intentionally inverts labels for pyref-dataset compatibility (`pol='s'` reads `[:,1,1]`, `pol='p'` reads `[:,0,0]`); `refloxide.model.Reflectivity` uses the native non-inverted labeling.
- Optional pyref integration lives at `refloxide.integrations.pyref` (`patch_pyref`, `patch_pyref_if_needed`, `pyref_patch_report`); keep it explicit and off the default import path so stock pyref stays comparable.
- `refloxide.pxr.energy` provides deferred-energy stacks: `RefloxideScatterer` / `OocUniTensorScatterer` in `pxr/energy/scatterer.py` (OOC from `OocAnchor`, DataFrame, or CSV); `DispersiveStructure.materialize_at`; bookended profiles via `EnergyBookendedOrientationDensityProfile` (`num_slabs` fixed at construction—rebuild to change microslab count); Rust `src/sld/mod.rs` binds `molecular_index_at_ooc`, `uniaxial_lab_tensor`, `tensor_to_slab_row` with public energy args named `energy_ev`/`energies_ev`; after energy/offset changes on book-ended films call `clear_ooc_cache()` then `cache_ooc_at(eff_energy)`; `FreeTensorScatterer` provides per-energy free tensors (`from_sld` reads the name from the source `SLD` and seeds from `SLD` but not `UniTensorSLD`, tensor components default `vary=False`, `group_at()` accessor). Per-energy instrument channels live in `pxr/plugin/dispersive_instrument.py` (`EnergyInstrumentSlice`, `make_instrument_channel`, `resolve_instrument`).
- Kernel polarization layout is `[:, 0, 0] = R_ss` and `[:, 1, 1] = R_pp` (matches Rust / `python.tmm`). Legacy `pxr.layout.reflectivity_for_pol` intentionally inverts labels for older datasets (`pol='s'` reads `[:,1,1]`, `pol='p'` reads `[:,0,0]`); `refloxide.model.Reflectivity` uses the native non-inverted labeling.
- `refloxide.pxr` is deprecated (emits `DeprecationWarning`) and will relocate into top-level modules / `refloxide.python.*`. Legacy deferred-energy stacks remain under `pxr.energy` for now: `RefloxideScatterer` / `OocUniTensorScatterer`, `DispersiveStructure.materialize_at`, bookended profiles, compile pipeline (`compile_structure`/`compile_model`/`ReflectivityObjective`). Rust `src/sld/mod.rs` binds `molecular_index_at_ooc`, `uniaxial_lab_tensor`, `tensor_to_slab_row` with public energy args named `energy_ev`/`energies_ev`; after energy/offset changes on book-ended films call `clear_ooc_cache()` then `cache_ooc_at(eff_energy)`; `FreeTensorScatterer` provides per-energy free tensors. Per-energy instrument channels live in `pxr/plugin/dispersive_instrument.py`.
- Rust Berreman raises `dynamic matrix is singular at layer 0` when `theta_offset` during L-BFGS-B maps low-q data to grazing incidence (q≈0); mitigate with data-derived `theta_offset` lower bounds (`tighten_theta_offset_bounds_from_terms`).
- `Scatterer.__call__(thick, rough)` in `pxr/plugin/structure.py` sets `vary=True` and auto-bounds `(0, 2*thick)`/`(0, 2*rough)` from construction geometry; when transferring geometry across reference stacks copy BOTH value and bounds (`apply_shared_slab_geometry_from_reference`), or thicknesses collapse to zero under the Nevot-Croce prior (`thick >= sqrt(2*pi)*rough/2`).
- Passler `compute_field` is documented but not exposed in bindings; isotropic E-field maps should use scalar Abeles/Fresnel reconstruction with `kx = k0 * sqrt(1 - (q/(2*k0))^2)` to match `tjf4x4`; the validated field reconstruction lives in `refl-analysis/src/utils/field_profile.py` (`uniaxial_field_profile`, `uniaxial_field_components`, `uniaxial_field_map`, `substrate_interface_depth`), reproduces the kernel `R_ss`/`R_pp` exactly, and is best shown as lab-frame real-space `(x, z)` maps of E-field density and phase (template: `refloxide/examples/test_structures.ipynb`), stopping at the SiO2/substrate interface with Nevot-Croce broadening and depth in angstroms—not `Re(E)`-only panels; wrapped `(q, depth)` phase maps are poor for slab-vs-graded comparison, and Brewster p-pol differences are intrinsically tiny.
- Local extension build: `make develop` (`scripts/develop.sh`) runs `uv sync --group dev`, then `UV_NO_CONFIG=1 uv run maturin develop --release` (project `uv` config must not steer maturin's internal `uv pip install`; sibling `../pyref` installed editable with `hvplot`, smoke applies `patch_pyref` when `pyref.fitting` imports); pre-release validation via `make release-smoke` (`scripts/smoke_release.sh`) requires `manylinux_*` (not bare `linux_x86_64`) Linux wheels and version bumps aligned across `pyproject.toml`, `Cargo.toml`, and `src/refloxide/__init__.py`; CI `wheels` job builds and smoke-imports on Linux/macOS/Windows; Release workflow builds manylinux (`2_28`) x86_64+aarch64, macOS x86_64+arm64 (py3.12/3.13), Windows x64 (py3.12/3.13), plus sdist, then publishes only on `v*` tags (`workflow_dispatch` builds without publishing).
- Passler `compute_field` is documented but not exposed in bindings; isotropic E-field maps should use scalar Abeles/Fresnel reconstruction with `kx = k0 * sqrt(1 - (q/(2*k0))^2)` to match the uniaxial kernels; the validated field reconstruction lives in `refl-analysis/src/utils/field_profile.py` (`uniaxial_field_profile`, `uniaxial_field_components`, `uniaxial_field_map`, `substrate_interface_depth`), reproduces the kernel `R_ss`/`R_pp` exactly, and is best shown as lab-frame real-space `(x, z)` maps of E-field density and phase (template: `refloxide/examples/test_structures.ipynb`), stopping at the SiO2/substrate interface with Nevot-Croce broadening and depth in angstroms—not `Re(E)`-only panels; wrapped `(q, depth)` phase maps are poor for slab-vs-graded comparison, and Brewster p-pol differences are intrinsically tiny.
- Local extension build: `make develop` (`scripts/develop.sh`) runs `uv sync --group dev`, then `UV_NO_CONFIG=1 uv run maturin develop --release` (project `uv` config must not steer maturin's internal `uv pip install`), then smoke-imports `refloxide` / Rust TMM; pre-release validation via `make release-smoke` (`scripts/smoke_release.sh`) requires `manylinux_*` (not bare `linux_x86_64`) Linux wheels and version bumps aligned across `pyproject.toml`, `Cargo.toml`, and `src/refloxide/__init__.py`; CI `wheels` job builds and smoke-imports on Linux/macOS/Windows; Release workflow builds manylinux (`2_28`) x86_64+aarch64, macOS x86_64+arm64 (py3.12/3.13), Windows x64 (py3.12/3.13), plus sdist, then publishes only on `v*` tags (`workflow_dispatch` builds without publishing).
- Compiled dispersive fitting: cache `CompiledReflectivityModel.parameters` (build once, call `invalidate_parameters()` after structure edits), batch objective terms by `(pol, shared q)` into ~2 kernel calls, and avoid per-term `q.tobytes()` keys; consume refloxide in refl-analysis via `uv sync --reinstall-package refloxide` then restart the Jupyter kernel; `refnx` `CurveFitter(workers=-1)` fails in notebooks (use a single worker) and `Interval` bounds are not tuple-unpackable.
- Legacy `pxr.plugin` Model/Objective stack (`ReflectModel`, `AnisotropyObjective`, `DispersiveReflectModel`, `BatchedGlobalObjective`, `patch_pyref`) remains load-bearing for refl-analysis notebooks and is not deleted here. New energy-dispersive fits use `compile_structure` / `compile_model` / `ReflectivityObjective` or `refloxide.model` / `refloxide.objective`. Retirement happens only after refl-analysis migrates; until then treat the old stack as a compatibility shim, not a second product surface.
- Legacy `pxr.plugin` Model/Objective stack (`ReflectModel`, `AnisotropyObjective`, `DispersiveReflectModel`, `BatchedGlobalObjective`) remains importable under the deprecated `pxr` path for refl-analysis notebooks. New fits use `compile_structure` / `compile_model` / `ReflectivityObjective` or `refloxide.model` / `refloxide.objective`. Treat `pxr` as a relocation shim, not a second product surface.
6 changes: 3 additions & 3 deletions docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ Python API pages are generated from source with mkdocstrings. The PyO3 extension

## Python

- [refloxide](python/refloxide.md) — top-level package
- [refloxide.pxr](python/pxr/index.md) — polarization / 4x4 helpers
- [refloxide.pxr.tjf4x4](python/pxr/tjf4x4.md)
- [refloxide](python/refloxide.md) — top-level package (Rust TMM default)
- [refloxide.python.tmm](python/python/tmm.md) — opt-in pure-Python TMM
- [refloxide.pxr](python/pxr/index.md) — legacy path (will relocate; prefer top-level modules)

## Rust

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# refloxide.pxr.tjf4x4
# refloxide.python.tmm

::: refloxide.pxr.tjf4x4
::: refloxide.python.tmm
options:
show_root_heading: true
show_source: true
Expand Down
4 changes: 2 additions & 2 deletions examples/bookended_performance_repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

uv run python examples/bookended_performance_repl.py

Forward modeling only -- no fitting, no pyref comparison. This picks up
Forward modeling only -- no fitting. This picks up
directly from `bookended_repl.py`'s structure/fitting showcase and the
performance investigation that followed it: `BookendedOrientationProfile`
composed via `refloxide.model.BookendedComponent` (see
`tests/test_bookended_model_integration.py`) is Rust-backed and about 10x
faster per call than the same profile run through the legacy, pure-Python
faster per call than the same profile run through the legacy
`pxr.plugin.model.ReflectModel` path. This script demonstrates that
capability directly:

Expand Down
8 changes: 2 additions & 6 deletions examples/bookended_repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

uv run python examples/bookended_repl.py

No pyref comparison here -- this is purely about
`refloxide.pxr.energy.bookended.BookendedOrientationProfile`: a single
component that represents a whole graded organic film as one smoothly
varying orientation/density profile between two book-ends (the vacuum-side
Expand All @@ -16,11 +15,8 @@
The profile composes into `refloxide.model.Structure`/`ReflectModel` via
`refloxide.model.BookendedComponent`, a thin adapter (see
`tests/test_bookended_model_integration.py` for the parity check against the
legacy `pxr.plugin` stack the profile also still supports). That matters
here for more than API consistency: `refloxide.pxr.plugin.model.ReflectModel`
calls the old pure-Python 4x4 matrix kernel unless separately patched to
Rust, while `refloxide.model.ReflectModel` is Rust-backed by construction --
about 10x faster for this exact structure.
legacy `pxr.plugin` stack the profile also still supports).
`refloxide.model.ReflectModel` is Rust-backed by construction.

The profile has nine shape parameters (`total_thick`, `surface_roughness`,
`density_bulk`/`density_si`/`density_vac`, `tau_si`/`tau_vac`,
Expand Down
Loading
Loading