diff --git a/AGENTS.md b/AGENTS.md index 4b63f22..61398ab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -214,13 +214,13 @@ 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. @@ -228,15 +228,14 @@ It is important to note that these notebooks are designed to be robust. These sh ## 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. diff --git a/docs/api/index.md b/docs/api/index.md index 16da8c0..3c6cf51 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -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 diff --git a/docs/api/python/pxr/tjf4x4.md b/docs/api/python/python/tmm.md similarity index 66% rename from docs/api/python/pxr/tjf4x4.md rename to docs/api/python/python/tmm.md index 89d5440..1353d26 100644 --- a/docs/api/python/pxr/tjf4x4.md +++ b/docs/api/python/python/tmm.md @@ -1,6 +1,6 @@ -# refloxide.pxr.tjf4x4 +# refloxide.python.tmm -::: refloxide.pxr.tjf4x4 +::: refloxide.python.tmm options: show_root_heading: true show_source: true diff --git a/examples/bookended_performance_repl.py b/examples/bookended_performance_repl.py index a0b105a..aa378ea 100644 --- a/examples/bookended_performance_repl.py +++ b/examples/bookended_performance_repl.py @@ -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: diff --git a/examples/bookended_repl.py b/examples/bookended_repl.py index e025d1d..c26adb7 100644 --- a/examples/bookended_repl.py +++ b/examples/bookended_repl.py @@ -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 @@ -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`, diff --git a/examples/mixed_uni_tensor_znpc_repl.py b/examples/mixed_uni_tensor_znpc_repl.py index f960858..85bbf9e 100644 --- a/examples/mixed_uni_tensor_znpc_repl.py +++ b/examples/mixed_uni_tensor_znpc_repl.py @@ -1,4 +1,4 @@ -"""Interactive comparison: mixed-material uniaxial slabs, refloxide vs. pyref. +"""Interactive comparison: mixed-material uniaxial slabs, refloxide vs py. Run cell-by-cell (each ``# %%`` marker is one cell) or top-to-bottom with:: @@ -40,28 +40,26 @@ vacuum / surface / bulk_1 / mixing / bulk_2 / interface / SiO2 / Si `refloxide.model.MixedUniTensorSLD` is compared against -`PyrefMixedUniTensorSLD`, a from-scratch pyref-side reference implementation -defined below (pyref itself ships no mixed-material uniaxial scatterer). It -follows the same density-scaled, rotation-mixed uniaxial formula as pyref's +`PyMixedUniTensorSLD`, a from-scratch py-side reference implementation +defined below (python.model itself ships no mixed-material uniaxial scatterer). It +follows the same density-scaled, rotation-mixed uniaxial formula as python.model's own single-material `UniTensorSLD` and sums the result across components by volume fraction -- radians throughout, matching -`pyref.fitting.structure.Scatterer.get_rotation`'s own documented convention. +`python.model Scatterer.get_rotation`'s own documented convention. Three things (the graded film above, 285.1 eV -- the carbon K-edge pi* resonance): -1. Correctness -- refloxide vs. the local pyref-style reference, same +1. Correctness -- refloxide vs. the local py-side mixed reference, same blended tables, same volume fractions/tilts/densities. -2. Speed -- same two, timed against stock, UNPATCHED pyref (its own - pure-Python kernel; pyref's optional Rust patch is being phased out and - isn't the focus here). +2. Speed -- same two, timed. 3. Fitting -- recover the mixing layer's ZnPc volume fraction from a - synthetic noisy dataset, refloxide.objective.Objective vs. stock - pyref.fitting.AnisotropyObjective built around the pyref-side reference + synthetic noisy dataset, refloxide.objective.Objective vs + refloxide.python.model.AnisotropyObjective built around the py-side reference model. Note on s/p labeling: identical convention/inversion as -`model_objective_repl.py` -- stock pyref's `pol='s'` reads the kernel's +`model_objective_repl.py` -- python.model's `pol='s'` reads the kernel's `[:, 1, 1]`, `pol='p'` reads `[:, 0, 0]`; `refloxide.model.Reflectivity` uses the native, non-inverted labeling (`.s = [:, 0, 0]`, `.p = [:, 1, 1]`). """ @@ -76,25 +74,15 @@ import matplotlib.pyplot as plt import numpy as np import polars as pl -import pyref.fitting as fit -from pyref.fitting.structure import Scatterer as PyrefScatterer +import refloxide.python.model as py +from refloxide.pxr.plugin.structure import Scatterer as PluginScatterer from refnx.analysis import CurveFitter, Parameters, possibly_create_parameter from scipy.interpolate import interp1d from refloxide.data import OpticalConstants, ReflectDataset -from refloxide.integrations.pyref import pyref_patched from refloxide.model import MaterialSLD, MixedUniTensorSLD, ReflectModel from refloxide.objective import Objective -if pyref_patched(): - msg = ( - "pyref.fitting.ReflectModel is already patched (patch_pyref() ran " - "earlier in this process/kernel). The 'unpatched' timing below would " - "silently measure the Rust kernel on both sides. Restart the " - "interpreter/kernel and rerun this script on its own." - ) - raise RuntimeError(msg) - # %% Locate the DFT-computed ZnPc optical constants in the sibling refl-analysis repo ZNPC_DFT_CSV = ( @@ -280,18 +268,17 @@ def gaussian(center: float, width: float, amplitude: float) -> np.ndarray: } -# %% pyref-side reference: pyref ships no mixed-material uniaxial scatterer +# %% py-side reference: py ships no mixed-material uniaxial scatterer -class PyrefMixedUniTensorSLD(PyrefScatterer): - """pyref-style volume-fraction-weighted mixture of uniaxial materials. +class PyMixedUniTensorSLD(PluginScatterer): + """python.model-side volume-fraction-weighted mixture of uniaxial materials. - pyref has no built-in mixed-material uniaxial scatterer; this is a - from-scratch reference implementation for comparison, following the same - density-scaled, rotation-mixed formula as pyref's own single-material - `pyref.fitting.UniTensorSLD.tensor`, applied per component and then - summed by volume fraction. Rotation is radians throughout, matching - `Scatterer.get_rotation`'s own documented convention. + ``refloxide.python.model`` has no built-in mixed-material uniaxial + scatterer; this is a from-scratch reference for comparison, following the + same density-scaled, rotation-mixed formula as ``UniTensorSLD.tensor``, + applied per component and then summed by volume fraction. Rotation is + radians throughout, matching ``Scatterer.get_rotation``. """ def __init__( @@ -309,7 +296,7 @@ def __init__( lengths = {len(oocs), len(vf), len(rotation), len(density)} if lengths != {n}: msg = ( - "PyrefMixedUniTensorSLD component sequences must all be the same length" + "PyMixedUniTensorSLD component sequences must all be the same length" ) raise ValueError(msg) @@ -334,7 +321,7 @@ def __init__( ] self.energy = energy # A single, shared energy_offset (not one per blended component) -- - # `pyref.fitting.Structure.energy_offset`'s setter assumes every + # `python.model Structure.energy_offset`'s setter assumes every # component's `sld.energy_offset` is one Parameter it can `.setp(...)` # directly (it links every scatterer to one model-level offset), so a # per-component list here would break `ReflectModel.__init__` itself. @@ -395,7 +382,7 @@ def __complex__(self) -> complex: return complex((2 * t[0, 0] + t[2, 2]) / 3) def __repr__(self) -> str: - return f"PyrefMixedUniTensorSLD(n={len(self.volfrac)}, name={self.name!r})" + return f"PyMixedUniTensorSLD(n={len(self.volfrac)}, name={self.name!r})" # %% Shared physical structure, built two ways @@ -421,10 +408,10 @@ def build_refloxide_structure(): return structure | oxide | si -def build_pyref_structure(): - vacuum = fit.MaterialSLD("", density=0.0, energy=ENERGY_EV, name="vacuum") +def build_py_structure(): + vacuum = py.MaterialSLD("", density=0.0, energy=ENERGY_EV, name="vacuum") mixed_layers = [ - PyrefMixedUniTensorSLD( + PyMixedUniTensorSLD( [znpc_pd, znpc_variant_pd], vf=cfg["vf"], rotation=cfg["rotation"], @@ -434,8 +421,8 @@ def build_pyref_structure(): )(cfg["thick"], cfg["rough"]) for name, cfg in MIXED_LAYERS.items() ] - oxide = fit.MaterialSLD("SiO2", density=2.2, energy=ENERGY_EV, name="oxide") - si = fit.MaterialSLD("Si", density=2.33, energy=ENERGY_EV, name="si") + oxide = py.MaterialSLD("SiO2", density=2.2, energy=ENERGY_EV, name="oxide") + si = py.MaterialSLD("Si", density=2.33, energy=ENERGY_EV, name="si") structure = vacuum(0, 0) for layer in mixed_layers: structure = structure | layer @@ -443,8 +430,8 @@ def build_pyref_structure(): refloxide_model = ReflectModel(build_refloxide_structure()) -pyref_model = fit.ReflectModel(build_pyref_structure(), energy=ENERGY_EV, pol="sp") -pyref_model.structure.plot() +py_model = py.ReflectModel(build_py_structure(), energy=ENERGY_EV, pol="sp") +py_model.structure.plot() # %% Sharing guarantee: five mixed layers, same two tables, one cache entry each @@ -459,37 +446,37 @@ def build_pyref_structure(): "its noisy variant), not ten separate copies.\n" ) -# %% 1. Correctness -- refloxide vs. the local pyref-style reference +# %% 1. Correctness -- refloxide vs. the local py-style reference -pyref_model.pol = "s" -pyref_s = pyref_model.model(Q) # native kernel [:, 1, 1] -pyref_model.pol = "p" -pyref_p = pyref_model.model(Q) # native kernel [:, 0, 0] +py_model.pol = "s" +py_s = py_model.model(Q) # native kernel [:, 1, 1] +py_model.pol = "p" +py_p = py_model.model(Q) # native kernel [:, 0, 0] refloxide_r = refloxide_model(Q, ENERGY_EV) -max_err_s = np.max(np.abs(refloxide_r.p - pyref_s)) # refloxide .p <-> pyref pol='s' -max_err_p = np.max(np.abs(refloxide_r.s - pyref_p)) # refloxide .s <-> pyref pol='p' -print(f"max |refloxide.p - pyref(pol='s')| = {max_err_s:.3e}") -print(f"max |refloxide.s - pyref(pol='p')| = {max_err_p:.3e}") +max_err_s = np.max(np.abs(refloxide_r.p - py_s)) # refloxide .p <-> py pol='s' +max_err_p = np.max(np.abs(refloxide_r.s - py_p)) # refloxide .s <-> py pol='p' +print(f"max |refloxide.p - py(pol='s')| = {max_err_s:.3e}") +print(f"max |refloxide.s - py(pol='p')| = {max_err_p:.3e}") assert max_err_s < 1e-8 assert max_err_p < 1e-8 print( - "Correctness OK: refloxide.MixedUniTensorSLD matches the pyref-style reference.\n" + "Correctness OK: refloxide.MixedUniTensorSLD matches the py-style reference.\n" ) # %% Plot overlay fig, ax = plt.subplots(figsize=(7, 5)) -ax.plot(Q, refloxide_r.p, label="refloxide .p (== pyref pol='s')", lw=2) -ax.plot(Q, pyref_s, "--", label="pyref pol='s'", lw=1.5, color="k") -ax.plot(Q, refloxide_r.s, label="refloxide .s (== pyref pol='p')", lw=2) -ax.plot(Q, pyref_p, "--", label="pyref pol='p'", lw=1.5, color="0.4") +ax.plot(Q, refloxide_r.p, label="refloxide .p (== py pol='s')", lw=2) +ax.plot(Q, py_s, "--", label="py pol='s'", lw=1.5, color="k") +ax.plot(Q, refloxide_r.s, label="refloxide .s (== py pol='p')", lw=2) +ax.plot(Q, py_p, "--", label="py pol='p'", lw=1.5, color="0.4") ax.set_yscale("log") ax.set_xlabel(r"$q$ ($\mathrm{\AA}^{-1}$)") ax.set_ylabel("Reflectivity") ax.legend() -ax.set_title(f"Mixed-material ZnPc slabs, refloxide vs pyref-style, {ENERGY_EV:.1f} eV") +ax.set_title(f"Mixed-material ZnPc slabs, refloxide vs py-style, {ENERGY_EV:.1f} eV") fig.tight_layout() plt.show() @@ -514,10 +501,10 @@ def build_pyref_structure(): plt.show() refloxide_structure.plot.param("density|orientation") plt.show() -refloxide_structure.plot.param("vf_") +refloxide_structure.plot.param("vf_", roughness=True) plt.show() -# %% 2. Speed comparison -- vs. stock, UNPATCHED pyref (its own pure-Python kernel) +# %% 2. Speed comparison -- vs. stock, python.model (its own pure-Python kernel) def time_it(fn, n: int = 200, warmup: int = 5) -> float: @@ -531,13 +518,13 @@ def time_it(fn, n: int = 200, warmup: int = 5) -> float: t_refloxide = time_it(lambda: refloxide_model(Q, ENERGY_EV)) -t_pyref = time_it(lambda: pyref_model.model(Q)) +t_py = time_it(lambda: py_model.model(Q)) print(f"refloxide.model.ReflectModel: {t_refloxide * 1e3:.4f} ms/call") -print(f"stock pyref.fitting.ReflectModel (unpatched): {t_pyref * 1e3:.4f} ms/call") -print(f"speedup: {t_pyref / t_refloxide:.1f}x\n") +print(f"refloxide.python.model.ReflectModel: {t_py * 1e3:.4f} ms/call") +print(f"speedup: {t_py / t_refloxide:.1f}x\n") -# %% Memory footprint -- peak Python-heap bytes per call, refloxide vs stock pyref +# %% Memory footprint -- peak Python-heap bytes per call, refloxide vs python.model def peak_memory_bytes(fn, warmup: int = 5) -> int: @@ -552,20 +539,20 @@ def peak_memory_bytes(fn, warmup: int = 5) -> int: mem_refloxide = peak_memory_bytes(lambda: refloxide_model(Q, ENERGY_EV)) -mem_pyref = peak_memory_bytes(lambda: pyref_model.model(Q)) +mem_py = peak_memory_bytes(lambda: py_model.model(Q)) print(f"refloxide.model.ReflectModel: {mem_refloxide:>7,} B/call") -print(f"stock pyref.fitting.ReflectModel (unpatched): {mem_pyref:>7,} B/call") -print(f"memory ratio (pyref/refloxide): {mem_pyref / mem_refloxide:.2f}x\n") +print(f"refloxide.python.model.ReflectModel: {mem_py:>7,} B/call") +print(f"memory ratio (py/refloxide): {mem_py / mem_refloxide:.2f}x\n") # %% 3. Fitting comparison -- recover mixed_1's ZnPc volume fraction rng_fit = np.random.default_rng(0) -pyref_model.pol = "s" -r_s_true = pyref_model.model(Q) -pyref_model.pol = "p" -r_p_true = pyref_model.model(Q) -pyref_model.pol = "sp" +py_model.pol = "s" +r_s_true = py_model.model(Q) +py_model.pol = "p" +r_p_true = py_model.model(Q) +py_model.pol = "sp" r_s = r_s_true * (1 + rng_fit.normal(0, 0.01, size=Q.shape)) r_p = r_p_true * (1 + rng_fit.normal(0, 0.01, size=Q.shape)) @@ -581,7 +568,7 @@ def peak_memory_bytes(fn, warmup: int = 5) -> int: pol=np.concatenate( [np.full(Q.shape, "s", dtype=object), np.full(Q.shape, "p", dtype=object)] ), - r=np.concatenate([r_p, r_s]), # label swap: refloxide "s" <- pyref p-channel data + r=np.concatenate([r_p, r_s]), # label swap: refloxide "s" <- py p-channel data r_err=np.concatenate([err_p, err_s]), ) new_objective = Objective(new_model, new_data, anisotropy_weight=0.4) @@ -594,50 +581,50 @@ def peak_memory_bytes(fn, warmup: int = 5) -> int: new_fitter = CurveFitter(new_objective) -# %% ... pyref side (AnisotropyObjective around the pyref-style reference model) +# %% ... py side (AnisotropyObjective around the py-style reference model) -pyref_dataset = fit.XrayReflectDataset( +py_dataset = py.XrayReflectDataset( ( np.concatenate([Q, Q]), np.concatenate([r_s, r_p]), np.concatenate([err_s, err_p]), ) ) -pyref_objective = fit.AnisotropyObjective( - pyref_model, pyref_dataset, logp_anisotropy_weight=0.4 +py_objective = py.AnisotropyObjective( + py_model, py_dataset, logp_anisotropy_weight=0.4 ) -for p in pyref_model.structure.parameters.flattened(): +for p in py_model.structure.parameters.flattened(): if p.constraint is None: p.vary = False -pyref_mixing = pyref_model.structure[3].sld -pyref_mixing.volfrac[0].setp(vary=True, bounds=(0.3, 0.7)) +py_mixing = py_model.structure[3].sld +py_mixing.volfrac[0].setp(vary=True, bounds=(0.3, 0.7)) -pyref_fitter = fit.CurveFitter(pyref_objective) +py_fitter = py.CurveFitter(py_objective) # %% Compare logl before fitting, then fit both and compare timing/result print("logl before fit:") print(" refloxide:", new_objective.logl()) -print(" pyref: ", pyref_objective.logl()) +print(" py: ", py_objective.logl()) t0 = time.perf_counter() new_fitter.fit(method="differential_evolution", maxiter=40, polish=False, seed=1) t_new_fit = time.perf_counter() - t0 t0 = time.perf_counter() -pyref_fitter.fit(method="differential_evolution", maxiter=40, polish=False, seed=1) -t_pyref_fit = time.perf_counter() - t0 +py_fitter.fit(method="differential_evolution", maxiter=40, polish=False, seed=1) +t_py_fit = time.perf_counter() - t0 print( f"\nrefloxide fit: {t_new_fit:.3f} s, " f"recovered mixing-layer ZnPc vf = {new_mixing.vf[0].value:.3f}" ) print( - f"pyref fit: {t_pyref_fit:.3f} s, " - f"recovered mixing-layer ZnPc vf = {pyref_mixing.volfrac[0].value:.3f}" + f"py fit: {t_py_fit:.3f} s, " + f"recovered mixing-layer ZnPc vf = {py_mixing.volfrac[0].value:.3f}" ) print( - f"fit speedup: {t_pyref_fit / t_new_fit:.1f}x " + f"fit speedup: {t_py_fit / t_new_fit:.1f}x " f"(true ZnPc vf was {MIXING['vf'][0]})" ) diff --git a/examples/model_objective_repl.py b/examples/model_objective_repl.py index e486b3d..b270f71 100644 --- a/examples/model_objective_repl.py +++ b/examples/model_objective_repl.py @@ -1,4 +1,4 @@ -"""Interactive comparison: refloxide.model/objective vs. stock pyref.fitting. +"""Interactive comparison: refloxide.model/objective vs. refloxide.python.model. Run cell-by-cell in an interactive Python window (each ``# %%`` marker is one cell — VS Code's "Python Interactive" / Jupytext percent format), or @@ -9,24 +9,20 @@ Three things, same physical structure (vacuum / SiO2 film / Si substrate, 700 eV): -1. Numeric parity — refloxide.model.ReflectModel vs. stock, UNPATCHED - pyref.fitting.ReflectModel (pyref's own pure-Python kernel, - pyref.fitting.uniaxial — no Rust involved unless patched). -2. Speed — same two, timed. This is the actual "core fault" refloxide - exists to fix: stock pyref never touches Rust unless patch_pyref() is - called. A third timing (pyref, patched) isolates how much of the - speedup is "just Rust" vs. the new API/batching on top of it. -3. Fitting — refloxide.objective.Objective vs. stock - pyref.fitting.AnisotropyObjective, built from the same synthetic noisy +1. Numeric parity — refloxide.model.ReflectModel vs pure-Python + refloxide.python.model.ReflectModel (``refloxide.python.tmm``). +2. Speed — same two, timed. +3. Fitting — refloxide.objective.Objective vs + refloxide.python.model.AnisotropyObjective, built from the same synthetic noisy s+p dataset, compared by log-likelihood and by a short differential_evolution fit. -Note on s/p labeling: stock pyref's ``pol='s'``/``pol='p'`` extraction is a +Note on s/p labeling: python.model's ``pol='s'``/``pol='p'`` extraction is a historical inversion kept for legacy dataset compatibility (``pol='s'`` reads the kernel's ``[:, 1, 1]``, ``pol='p'`` reads ``[:, 0, 0]``). ``refloxide.model.Reflectivity`` uses the native, non-inverted kernel labeling (``.s = [:, 0, 0]``, ``.p = [:, 1, 1]``, matching ``rust.pyi``'s own -docs). So "pyref ``pol='s'``" corresponds to "refloxide ``.p``", and vice +docs). So "py ``pol='s'``" corresponds to "refloxide ``.p``", and vice versa — accounted for explicitly below, not a bug. See ``tests/test_legacy_parity.py`` for the same convention pinned as a test. """ @@ -38,7 +34,7 @@ import matplotlib.pyplot as plt import numpy as np -import pyref.fitting as fit +import refloxide.python.model as py from refnx.analysis import CurveFitter from refloxide.data import ReflectDataset @@ -58,45 +54,45 @@ def build_refloxide_structure(): return vacuum | film | substrate -def build_pyref_structure(): - vacuum = fit.MaterialSLD("", density=0.0, energy=ENERGY_EV, name="vacuum") - film = fit.MaterialSLD("SiO2", density=2.2, energy=ENERGY_EV, name="film") - substrate = fit.MaterialSLD("Si", density=2.33, energy=ENERGY_EV, name="substrate") +def build_py_structure(): + vacuum = py.MaterialSLD("", density=0.0, energy=ENERGY_EV, name="vacuum") + film = py.MaterialSLD("SiO2", density=2.2, energy=ENERGY_EV, name="film") + substrate = py.MaterialSLD("Si", density=2.33, energy=ENERGY_EV, name="substrate") return vacuum(0, 0) | film(100, 3) | substrate(0, 3) refloxide_model = ReflectModel(build_refloxide_structure()) -pyref_model = fit.ReflectModel(build_pyref_structure(), energy=ENERGY_EV, pol="sp") +py_model = py.ReflectModel(build_py_structure(), energy=ENERGY_EV, pol="sp") # %% 1. Numeric parity -pyref_model.pol = "s" -pyref_s = pyref_model.model(Q) # native kernel [:, 1, 1] -pyref_model.pol = "p" -pyref_p = pyref_model.model(Q) # native kernel [:, 0, 0] +py_model.pol = "s" +py_s = py_model.model(Q) # native kernel [:, 1, 1] +py_model.pol = "p" +py_p = py_model.model(Q) # native kernel [:, 0, 0] refloxide_r = refloxide_model(Q, ENERGY_EV) -max_err_s = np.max(np.abs(refloxide_r.p - pyref_s)) # refloxide .p <-> pyref pol='s' -max_err_p = np.max(np.abs(refloxide_r.s - pyref_p)) # refloxide .s <-> pyref pol='p' -print(f"max |refloxide.p - pyref(pol='s')| = {max_err_s:.3e}") -print(f"max |refloxide.s - pyref(pol='p')| = {max_err_p:.3e}") +max_err_s = np.max(np.abs(refloxide_r.p - py_s)) # refloxide .p <-> py pol='s' +max_err_p = np.max(np.abs(refloxide_r.s - py_p)) # refloxide .s <-> py pol='p' +print(f"max |refloxide.p - py(pol='s')| = {max_err_s:.3e}") +print(f"max |refloxide.s - py(pol='p')| = {max_err_p:.3e}") assert max_err_s < 1e-8 assert max_err_p < 1e-8 -print("Numeric parity OK: refloxide.model matches stock pyref exactly.\n") +print("Numeric parity OK: refloxide.model matches python.model exactly.\n") # %% Plot overlay fig, ax = plt.subplots(figsize=(7, 5)) -ax.plot(Q, refloxide_r.p, label="refloxide .p (== pyref pol='s')", lw=2) -ax.plot(Q, pyref_s, "--", label="pyref pol='s'", lw=1.5, color="k") -ax.plot(Q, refloxide_r.s, label="refloxide .s (== pyref pol='p')", lw=2) -ax.plot(Q, pyref_p, "--", label="pyref pol='p'", lw=1.5, color="0.4") +ax.plot(Q, refloxide_r.p, label="refloxide .p (== py pol='s')", lw=2) +ax.plot(Q, py_s, "--", label="py pol='s'", lw=1.5, color="k") +ax.plot(Q, refloxide_r.s, label="refloxide .s (== py pol='p')", lw=2) +ax.plot(Q, py_p, "--", label="py pol='p'", lw=1.5, color="0.4") ax.set_yscale("log") ax.set_xlabel(r"$q$ ($\mathrm{\AA}^{-1}$)") ax.set_ylabel("Reflectivity") ax.legend() -ax.set_title(f"refloxide.model vs stock pyref, {ENERGY_EV:.0f} eV") +ax.set_title(f"refloxide.model vs python.model, {ENERGY_EV:.0f} eV") fig.tight_layout() plt.show() @@ -131,13 +127,13 @@ def time_it(fn, n: int = 200, warmup: int = 5) -> float: t_refloxide = time_it(lambda: refloxide_model(Q, ENERGY_EV)) -t_pyref = time_it(lambda: pyref_model.model(Q)) +t_py = time_it(lambda: py_model.model(Q)) print(f"refloxide.model.ReflectModel: {t_refloxide * 1e3:.4f} ms/call") -print(f"stock pyref.fitting.ReflectModel (unpatched): {t_pyref * 1e3:.4f} ms/call") -print(f"speedup: {t_pyref / t_refloxide:.1f}x\n") +print(f"refloxide.python.model.ReflectModel: {t_py * 1e3:.4f} ms/call") +print(f"speedup: {t_py / t_refloxide:.1f}x\n") -# %% Memory footprint -- peak Python-heap bytes per call, refloxide vs stock pyref +# %% Memory footprint -- peak Python-heap bytes per call, refloxide vs python.model def peak_memory_bytes(fn, warmup: int = 5) -> int: @@ -152,36 +148,20 @@ def peak_memory_bytes(fn, warmup: int = 5) -> int: mem_refloxide = peak_memory_bytes(lambda: refloxide_model(Q, ENERGY_EV)) -mem_pyref = peak_memory_bytes(lambda: pyref_model.model(Q)) +mem_py = peak_memory_bytes(lambda: py_model.model(Q)) print(f"refloxide.model.ReflectModel: {mem_refloxide:>7,} B/call") -print(f"stock pyref.fitting.ReflectModel (unpatched): {mem_pyref:>7,} B/call") -print(f"memory ratio (pyref/refloxide): {mem_pyref / mem_refloxide:.2f}x\n") - -# %% Isolate "is it just Rust, or the new API too" by patching pyref itself - -from refloxide.integrations.pyref import patch_pyref # noqa: E402 - -patch_pyref(use_rust=True, parallel=False) -t_pyref_patched = time_it(lambda: pyref_model.model(Q)) - -print( - "stock pyref.fitting.ReflectModel (patched, Rust kernel): " - f"{t_pyref_patched * 1e3:.4f} ms/call" -) -print( - "refloxide.model speedup over patched pyref (API/batching overhead only): " - f"{t_pyref_patched / t_refloxide:.2f}x\n" -) +print(f"refloxide.python.model.ReflectModel: {mem_py:>7,} B/call") +print(f"memory ratio (python.model/refloxide): {mem_py / mem_refloxide:.2f}x\n") # %% 3. Fitting comparison — synthetic noisy s+p dataset, fit both ways rng = np.random.default_rng(0) -pyref_model.pol = "s" -r_s_true = pyref_model.model(Q) -pyref_model.pol = "p" -r_p_true = pyref_model.model(Q) -pyref_model.pol = "sp" +py_model.pol = "s" +r_s_true = py_model.model(Q) +py_model.pol = "p" +r_p_true = py_model.model(Q) +py_model.pol = "sp" r_s = r_s_true * (1 + rng.normal(0, 0.01, size=Q.shape)) r_p = r_p_true * (1 + rng.normal(0, 0.01, size=Q.shape)) @@ -197,7 +177,7 @@ def peak_memory_bytes(fn, warmup: int = 5) -> int: pol=np.concatenate( [np.full(Q.shape, "s", dtype=object), np.full(Q.shape, "p", dtype=object)] ), - r=np.concatenate([r_p, r_s]), # label swap: refloxide "s" <- pyref p-channel data + r=np.concatenate([r_p, r_s]), # label swap: refloxide "s" <- py p-channel data r_err=np.concatenate([err_p, err_s]), ) new_objective = Objective(new_model, new_data, anisotropy_weight=0.4) @@ -210,51 +190,51 @@ def peak_memory_bytes(fn, warmup: int = 5) -> int: new_fitter = CurveFitter(new_objective) -# %% ... stock pyref side +# %% ... python.model side -pyref_dataset = fit.XrayReflectDataset( +py_dataset = py.XrayReflectDataset( ( np.concatenate([Q, Q]), np.concatenate([r_s, r_p]), np.concatenate([err_s, err_p]), ) ) -pyref_objective = fit.AnisotropyObjective( - pyref_model, pyref_dataset, logp_anisotropy_weight=0.4 +py_objective = py.AnisotropyObjective( + py_model, py_dataset, logp_anisotropy_weight=0.4 ) -for p in pyref_model.structure.parameters.flattened(): +for p in py_model.structure.parameters.flattened(): if p.constraint is None: p.vary = False -pyref_film = pyref_model.structure[1] -pyref_film.thick.setp(vary=True, bounds=(20, 100)) +py_film = py_model.structure[1] +py_film.thick.setp(vary=True, bounds=(20, 100)) -pyref_fitter = fit.CurveFitter(pyref_objective) +py_fitter = py.CurveFitter(py_objective) # %% Compare logl before fitting, then fit both and compare timing/result print("logl before fit:") print(" refloxide:", new_objective.logl()) -print(" pyref: ", pyref_objective.logl()) +print(" py: ", py_objective.logl()) t0 = time.perf_counter() new_fitter.fit(method="differential_evolution", maxiter=40, polish=False, seed=1) t_new_fit = time.perf_counter() - t0 t0 = time.perf_counter() -pyref_fitter.fit(method="differential_evolution", maxiter=40, polish=False, seed=1) -t_pyref_fit = time.perf_counter() - t0 +py_fitter.fit(method="differential_evolution", maxiter=40, polish=False, seed=1) +t_py_fit = time.perf_counter() - t0 print( f"\nrefloxide fit: {t_new_fit:.3f} s, " f"recovered thick = {new_film.thick.value:.2f}" ) print( - f"pyref fit: {t_pyref_fit:.3f} s, " - f"recovered thick = {pyref_film.thick.value:.2f}" + f"py fit: {t_py_fit:.3f} s, " + f"recovered thick = {py_film.thick.value:.2f}" ) -print(f"fit speedup: {t_pyref_fit / t_new_fit:.1f}x (true thickness was 50.0)") -# %% Plot pyref structure +print(f"fit speedup: {t_py_fit / t_new_fit:.1f}x (true thickness was 50.0)") +# %% Plot py structure refloxide_structure = new_fitter.objective.model.structure refloxide_structure.plot.oc(ENERGY_EV) plt.show() diff --git a/examples/uni_tensor_znpc_repl.py b/examples/uni_tensor_znpc_repl.py index 4219266..97aa5f3 100644 --- a/examples/uni_tensor_znpc_repl.py +++ b/examples/uni_tensor_znpc_repl.py @@ -1,4 +1,4 @@ -"""Interactive comparison: refloxide vs. stock pyref, a graded ZnPc film. +"""Interactive comparison: refloxide vs. python.model, a graded ZnPc film. Run cell-by-cell (each ``# %%`` marker is one cell) or top-to-bottom with:: @@ -17,9 +17,9 @@ model evaluation instead of once, making any per-scatterer overhead far more visible in the speed comparison below. -Both refloxide's `UniTensorSLD` and pyref's `UniTensorSLD` implement the same +Both refloxide's `UniTensorSLD` and py's `UniTensorSLD` implement the same density-scaled, rotation-mixed uniaxial tensor -(`refloxide.optics.uniaxial_lab_tensor` matches pyref's `n_o`/`n_e` formula +(`refloxide.optics.uniaxial_lab_tensor` matches python.model's `n_o`/`n_e` formula term for term), so this is a genuine independent-implementation parity check, not an isotropic-vs-anisotropic strawman. @@ -33,25 +33,23 @@ carbon K-edge pi* resonance, where the DFT table's xx/zz dichroism is largest): -1. Numeric parity -- refloxide.model.ReflectModel vs. stock, UNPATCHED - pyref.fitting.ReflectModel (pyref's own pure-Python kernel -- the - comparison that matters day to day; pyref's optional Rust patch is being - phased out, so it isn't the focus here). +1. Numeric parity -- refloxide.model.ReflectModel vs + refloxide.python.model.ReflectModel (``refloxide.python.tmm``). 2. Speed -- same two, timed. -3. Fitting -- refloxide.objective.Objective vs. stock - pyref.fitting.AnisotropyObjective, fit against a synthetic noisy s+p +3. Fitting -- refloxide.objective.Objective vs + refloxide.python.model.AnisotropyObjective, fit against a synthetic noisy s+p dataset. 4. NEXAFS-style energy scan at fixed q across the resonance -- refloxide vs - pyref, both should reproduce the same resonance peak from the DFT table. + py, both should reproduce the same resonance peak from the DFT table. 5. Rotation (molecular tilt) sweep of the bulk sublayer at fixed (q, energy) - -- refloxide vs pyref, both should shift the same way as orientation + -- refloxide vs py, both should shift the same way as orientation changes. Note on s/p labeling: identical convention/inversion as -`model_objective_repl.py` -- stock pyref's `pol='s'` reads the kernel's +`model_objective_repl.py` -- python.model's `pol='s'` reads the kernel's `[:, 1, 1]`, `pol='p'` reads `[:, 0, 0]`; `refloxide.model.Reflectivity` uses the native, non-inverted labeling (`.s = [:, 0, 0]`, `.p = [:, 1, 1]`). So -"pyref `pol='s'`" corresponds to "refloxide `.p`", and vice versa -- +"py `pol='s'`" corresponds to "refloxide `.p`", and vice versa -- accounted for explicitly below, not a bug. """ # %% 0 @@ -64,25 +62,13 @@ import matplotlib.pyplot as plt import numpy as np import pandas as pd -import pyref.fitting as fit +import refloxide.python.model as py from refnx.analysis import CurveFitter from refloxide.data import ReflectDataset -from refloxide.integrations.pyref import pyref_patched from refloxide.model import MaterialSLD, ReflectModel, UniTensorSLD from refloxide.objective import Objective -if pyref_patched(): - msg = ( - "pyref.fitting.ReflectModel is already patched (patch_pyref() ran " - "earlier in this process/kernel -- e.g. from model_objective_repl.py, " - "or from importing a refl-analysis module that auto-patches on " - "import). The 'unpatched' timing below would silently measure the " - "Rust kernel on both sides. Restart the interpreter/kernel and rerun " - "this script on its own." - ) - raise RuntimeError(msg) - # %% Locate the DFT-computed ZnPc optical constants in the sibling refl-analysis repo ZNPC_DFT_CSV = ( @@ -134,31 +120,31 @@ def build_refloxide_structure(bulk_rotation: float = BULK["rotation"]): return vacuum | surface | bulk | interface | substrate -def build_pyref_structure(bulk_rotation: float = BULK["rotation"]): +def build_py_structure(bulk_rotation: float = BULK["rotation"]): ooc_pd = pd.read_csv(ZNPC_DFT_CSV) - vacuum = fit.MaterialSLD("", density=0.0, energy=ENERGY_EV, name="vacuum") - surface = fit.UniTensorSLD( + vacuum = py.MaterialSLD("", density=0.0, energy=ENERGY_EV, name="vacuum") + surface = py.UniTensorSLD( ooc_pd, density=SURFACE["density"], rotation=SURFACE["rotation"], energy=ENERGY_EV, name="ZnPc_surface", ) - bulk = fit.UniTensorSLD( + bulk = py.UniTensorSLD( ooc_pd, density=BULK["density"], rotation=bulk_rotation, energy=ENERGY_EV, name="ZnPc_bulk", ) - interface = fit.UniTensorSLD( + interface = py.UniTensorSLD( ooc_pd, density=INTERFACE["density"], rotation=INTERFACE["rotation"], energy=ENERGY_EV, name="ZnPc_interface", ) - substrate = fit.MaterialSLD("SiO2", density=2.2, energy=ENERGY_EV, name="substrate") + substrate = py.MaterialSLD("SiO2", density=2.2, energy=ENERGY_EV, name="substrate") return ( vacuum(0, 0) | surface(SURFACE["thick"], SURFACE["rough"]) @@ -169,37 +155,37 @@ def build_pyref_structure(bulk_rotation: float = BULK["rotation"]): refloxide_model = ReflectModel(build_refloxide_structure()) -pyref_model = fit.ReflectModel(build_pyref_structure(), energy=ENERGY_EV, pol="sp") +py_model = py.ReflectModel(build_py_structure(), energy=ENERGY_EV, pol="sp") # %% 1. Numeric parity -pyref_model.pol = "s" -pyref_s = pyref_model.model(Q) # native kernel [:, 1, 1] -pyref_model.pol = "p" -pyref_p = pyref_model.model(Q) # native kernel [:, 0, 0] +py_model.pol = "s" +py_s = py_model.model(Q) # native kernel [:, 1, 1] +py_model.pol = "p" +py_p = py_model.model(Q) # native kernel [:, 0, 0] refloxide_r = refloxide_model(Q, ENERGY_EV) -max_err_s = np.max(np.abs(refloxide_r.p - pyref_s)) # refloxide .p <-> pyref pol='s' -max_err_p = np.max(np.abs(refloxide_r.s - pyref_p)) # refloxide .s <-> pyref pol='p' -print(f"max |refloxide.p - pyref(pol='s')| = {max_err_s:.3e}") -print(f"max |refloxide.s - pyref(pol='p')| = {max_err_p:.3e}") +max_err_s = np.max(np.abs(refloxide_r.p - py_s)) # refloxide .p <-> py pol='s' +max_err_p = np.max(np.abs(refloxide_r.s - py_p)) # refloxide .s <-> py pol='p' +print(f"max |refloxide.p - py(pol='s')| = {max_err_s:.3e}") +print(f"max |refloxide.s - py(pol='p')| = {max_err_p:.3e}") assert max_err_s < 1e-8 assert max_err_p < 1e-8 -print("Numeric parity OK: refloxide graded ZnPc film matches stock pyref exactly.\n") +print("Numeric parity OK: refloxide graded ZnPc film matches python.model exactly.\n") # %% Plot overlay fig, ax = plt.subplots(figsize=(7, 5)) -ax.plot(Q, refloxide_r.p, label="refloxide .p (== pyref pol='s')", lw=2) -ax.plot(Q, pyref_s, "--", label="pyref pol='s'", lw=1.5, color="k") -ax.plot(Q, refloxide_r.s, label="refloxide .s (== pyref pol='p')", lw=2) -ax.plot(Q, pyref_p, "--", label="pyref pol='p'", lw=1.5, color="0.4") +ax.plot(Q, refloxide_r.p, label="refloxide .p (== py pol='s')", lw=2) +ax.plot(Q, py_s, "--", label="py pol='s'", lw=1.5, color="k") +ax.plot(Q, refloxide_r.s, label="refloxide .s (== py pol='p')", lw=2) +ax.plot(Q, py_p, "--", label="py pol='p'", lw=1.5, color="0.4") ax.set_yscale("log") ax.set_xlabel(r"$q$ ($\mathrm{\AA}^{-1}$)") ax.set_ylabel("Reflectivity") ax.legend() -ax.set_title(f"refloxide vs stock pyref, graded ZnPc film, {ENERGY_EV:.1f} eV") +ax.set_title(f"refloxide vs python.model, graded ZnPc film, {ENERGY_EV:.1f} eV") fig.tight_layout() plt.show() @@ -208,7 +194,7 @@ def build_pyref_structure(bulk_rotation: float = BULK["rotation"]): # `Structure.plot.oc` broadens each interface by an error function with # sigma = that interface's own roughness (the standard NC-consistent # SLD-profile convention), plus the xx/zz dichroism on a right-hand twin -# axis (`difference=True`, `inset=True` -- same convention as pyref's own +# axis (`difference=True`, `inset=True` -- same convention as python.model's # `Structure.plot`). `Structure.plot.param` walks the graded UniTensorSLD # sublayers for "density"/"orientation" (surface reoriented flat, bulk # tilted, interface partially relaxed) -- NaN only for the isotropic @@ -220,7 +206,7 @@ def build_pyref_structure(bulk_rotation: float = BULK["rotation"]): refloxide_structure.plot.param("density|orientation") plt.show() -# %% 2. Speed comparison -- vs. stock, UNPATCHED pyref (its own pure-Python kernel) +# %% 2. Speed comparison -- vs. stock, python.model (its own pure-Python kernel) def time_it(fn, n: int = 200, warmup: int = 5) -> float: @@ -234,13 +220,13 @@ def time_it(fn, n: int = 200, warmup: int = 5) -> float: t_refloxide = time_it(lambda: refloxide_model(Q, ENERGY_EV)) -t_pyref = time_it(lambda: pyref_model.model(Q)) +t_py = time_it(lambda: py_model.model(Q)) print(f"refloxide.model.ReflectModel: {t_refloxide * 1e3:.4f} ms/call") -print(f"stock pyref.fitting.ReflectModel (unpatched): {t_pyref * 1e3:.4f} ms/call") -print(f"speedup: {t_pyref / t_refloxide:.1f}x\n") +print(f"refloxide.python.model.ReflectModel: {t_py * 1e3:.4f} ms/call") +print(f"speedup: {t_py / t_refloxide:.1f}x\n") -# %% Memory footprint -- peak Python-heap bytes per call, refloxide vs stock pyref +# %% Memory footprint -- peak Python-heap bytes per call, refloxide vs python.model def peak_memory_bytes(fn, warmup: int = 5) -> int: @@ -255,20 +241,20 @@ def peak_memory_bytes(fn, warmup: int = 5) -> int: mem_refloxide = peak_memory_bytes(lambda: refloxide_model(Q, ENERGY_EV)) -mem_pyref = peak_memory_bytes(lambda: pyref_model.model(Q)) +mem_py = peak_memory_bytes(lambda: py_model.model(Q)) print(f"refloxide.model.ReflectModel: {mem_refloxide:>7,} B/call") -print(f"stock pyref.fitting.ReflectModel (unpatched): {mem_pyref:>7,} B/call") -print(f"memory ratio (pyref/refloxide): {mem_pyref / mem_refloxide:.2f}x\n") +print(f"refloxide.python.model.ReflectModel: {mem_py:>7,} B/call") +print(f"memory ratio (py/refloxide): {mem_py / mem_refloxide:.2f}x\n") # %% 3. Fitting comparison -- synthetic noisy s+p dataset, fit both ways rng = np.random.default_rng(0) -pyref_model.pol = "s" -r_s_true = pyref_model.model(Q) -pyref_model.pol = "p" -r_p_true = pyref_model.model(Q) -pyref_model.pol = "sp" +py_model.pol = "s" +r_s_true = py_model.model(Q) +py_model.pol = "p" +r_p_true = py_model.model(Q) +py_model.pol = "sp" r_s = r_s_true * (1 + rng.normal(0, 0.01, size=Q.shape)) r_p = r_p_true * (1 + rng.normal(0, 0.01, size=Q.shape)) @@ -284,7 +270,7 @@ def peak_memory_bytes(fn, warmup: int = 5) -> int: pol=np.concatenate( [np.full(Q.shape, "s", dtype=object), np.full(Q.shape, "p", dtype=object)] ), - r=np.concatenate([r_p, r_s]), # label swap: refloxide "s" <- pyref p-channel data + r=np.concatenate([r_p, r_s]), # label swap: refloxide "s" <- py p-channel data r_err=np.concatenate([err_p, err_s]), ) new_objective = Objective(new_model, new_data, anisotropy_weight=0.4) @@ -297,58 +283,58 @@ def peak_memory_bytes(fn, warmup: int = 5) -> int: new_fitter = CurveFitter(new_objective) -# %% ... stock pyref side +# %% ... python.model side -pyref_dataset = fit.XrayReflectDataset( +py_dataset = py.XrayReflectDataset( ( np.concatenate([Q, Q]), np.concatenate([r_s, r_p]), np.concatenate([err_s, err_p]), ) ) -pyref_objective = fit.AnisotropyObjective( - pyref_model, pyref_dataset, logp_anisotropy_weight=0.4 +py_objective = py.AnisotropyObjective( + py_model, py_dataset, logp_anisotropy_weight=0.4 ) -for p in pyref_model.structure.parameters.flattened(): +for p in py_model.structure.parameters.flattened(): if p.constraint is None: p.vary = False -pyref_bulk = pyref_model.structure[2] -pyref_bulk.thick.setp(vary=True, bounds=(80, 200)) +py_bulk = py_model.structure[2] +py_bulk.thick.setp(vary=True, bounds=(80, 200)) -pyref_fitter = fit.CurveFitter(pyref_objective) +py_fitter = py.CurveFitter(py_objective) # %% Compare logl before fitting, then fit both and compare timing/result print("logl before fit:") print(" refloxide:", new_objective.logl()) -print(" pyref: ", pyref_objective.logl()) +print(" py: ", py_objective.logl()) t0 = time.perf_counter() new_fitter.fit(method="differential_evolution", maxiter=40, polish=False, seed=1) t_new_fit = time.perf_counter() - t0 t0 = time.perf_counter() -pyref_fitter.fit(method="differential_evolution", maxiter=40, polish=False, seed=1) -t_pyref_fit = time.perf_counter() - t0 +py_fitter.fit(method="differential_evolution", maxiter=40, polish=False, seed=1) +t_py_fit = time.perf_counter() - t0 print( f"\nrefloxide fit: {t_new_fit:.3f} s, " f"recovered bulk thick = {new_bulk.thick.value:.2f}" ) print( - f"pyref fit: {t_pyref_fit:.3f} s, " - f"recovered bulk thick = {pyref_bulk.thick.value:.2f}" + f"py fit: {t_py_fit:.3f} s, " + f"recovered bulk thick = {py_bulk.thick.value:.2f}" ) -print(f"fit speedup: {t_pyref_fit / t_new_fit:.1f}x (true bulk thickness was 130.0)\n") +print(f"fit speedup: {t_py_fit / t_new_fit:.1f}x (true bulk thickness was 130.0)\n") -# %% 4. NEXAFS-style energy scan at fixed q across the resonance -- refloxide vs pyref +# %% 4. NEXAFS-style energy scan at fixed q across the resonance -- refloxide vs py Q_FIXED = 0.05 ENERGY_SCAN = np.linspace(270.0, 300.0, 150) scan_refloxide_model = ReflectModel(build_refloxide_structure()) -scan_pyref_model = fit.ReflectModel(build_pyref_structure(), energy=ENERGY_EV, pol="sp") +scan_py_model = py.ReflectModel(build_py_structure(), energy=ENERGY_EV, pol="sp") s_scan_refloxide = np.array( [scan_refloxide_model(Q_FIXED, e).p for e in ENERGY_SCAN] @@ -357,31 +343,31 @@ def peak_memory_bytes(fn, warmup: int = 5) -> int: [scan_refloxide_model(Q_FIXED, e).s for e in ENERGY_SCAN] ).ravel() -s_scan_pyref = [] -p_scan_pyref = [] +s_scan_py = [] +p_scan_py = [] for e in ENERGY_SCAN: - scan_pyref_model.energy = e - scan_pyref_model.pol = "s" - s_scan_pyref.append(scan_pyref_model.model(np.array([Q_FIXED]))[0]) - scan_pyref_model.pol = "p" - p_scan_pyref.append(scan_pyref_model.model(np.array([Q_FIXED]))[0]) -s_scan_pyref = np.asarray(s_scan_pyref) -p_scan_pyref = np.asarray(p_scan_pyref) + scan_py_model.energy = e + scan_py_model.pol = "s" + s_scan_py.append(scan_py_model.model(np.array([Q_FIXED]))[0]) + scan_py_model.pol = "p" + p_scan_py.append(scan_py_model.model(np.array([Q_FIXED]))[0]) +s_scan_py = np.asarray(s_scan_py) +p_scan_py = np.asarray(p_scan_py) fig, ax = plt.subplots(figsize=(7, 5)) -ax.plot(ENERGY_SCAN, s_scan_refloxide, label="refloxide .p (== pyref pol='s')", lw=2) -ax.plot(ENERGY_SCAN, s_scan_pyref, "--", label="pyref pol='s'", lw=1.5, color="k") -ax.plot(ENERGY_SCAN, p_scan_refloxide, label="refloxide .s (== pyref pol='p')", lw=2) -ax.plot(ENERGY_SCAN, p_scan_pyref, "--", label="pyref pol='p'", lw=1.5, color="0.4") +ax.plot(ENERGY_SCAN, s_scan_refloxide, label="refloxide .p (== py pol='s')", lw=2) +ax.plot(ENERGY_SCAN, s_scan_py, "--", label="py pol='s'", lw=1.5, color="k") +ax.plot(ENERGY_SCAN, p_scan_refloxide, label="refloxide .s (== py pol='p')", lw=2) +ax.plot(ENERGY_SCAN, p_scan_py, "--", label="py pol='p'", lw=1.5, color="0.4") ax.axvline(ENERGY_EV, color="k", lw=0.8, ls=":", label="C 1s -> pi*") ax.set_xlabel("Energy (eV)") ax.set_ylabel(f"Reflectivity at q={Q_FIXED}") ax.legend(fontsize="small") -ax.set_title("NEXAFS-style energy scan: refloxide vs pyref, both see the resonance") +ax.set_title("NEXAFS-style energy scan: refloxide vs py, both see the resonance") fig.tight_layout() plt.show() -# %% 5. Bulk-sublayer rotation (molecular tilt) sweep -- refloxide vs pyref +# %% 5. Bulk-sublayer rotation (molecular tilt) sweep -- refloxide vs py rotations = np.linspace(0.0, np.pi / 2, 5) fig, ax = plt.subplots(figsize=(7, 5)) @@ -390,10 +376,10 @@ def peak_memory_bytes(fn, warmup: int = 5) -> int: r_rot_refloxide = ReflectModel(build_refloxide_structure(bulk_rotation=rotation))( Q, ENERGY_EV ) - rot_pyref_model = fit.ReflectModel( - build_pyref_structure(bulk_rotation=rotation), energy=ENERGY_EV, pol="p" + rot_py_model = py.ReflectModel( + build_py_structure(bulk_rotation=rotation), energy=ENERGY_EV, pol="p" ) - r_rot_pyref = rot_pyref_model.model(Q) # native [:, 0, 0] == refloxide .s + r_rot_py = rot_py_model.model(Q) # native [:, 0, 0] == refloxide .s ax.plot( Q, @@ -401,11 +387,11 @@ def peak_memory_bytes(fn, warmup: int = 5) -> int: color=color, label=f"refloxide bulk rotation={rotation:.2f}", ) - ax.plot(Q, r_rot_pyref, "--", color=color, lw=1.2) + ax.plot(Q, r_rot_py, "--", color=color, lw=1.2) ax.set_yscale("log") ax.set_xlabel(r"$q$ ($\mathrm{\AA}^{-1}$)") -ax.set_ylabel("s-pol reflectivity (refloxide .s == pyref pol='p')") +ax.set_ylabel("s-pol reflectivity (refloxide .s == py pol='p')") ax.legend(fontsize="small") -ax.set_title("Bulk molecular tilt sweep: refloxide (solid) vs pyref (dashed)") +ax.set_title("Bulk molecular tilt sweep: refloxide (solid) vs py (dashed)") fig.tight_layout() plt.show() diff --git a/examples/uniaxial_parity.py b/examples/uniaxial_parity.py index 06f2646..6aa2df8 100644 --- a/examples/uniaxial_parity.py +++ b/examples/uniaxial_parity.py @@ -2,8 +2,9 @@ Builds a vacuum / polystyrene / silicon stack with :class:`~refloxide.pxr.plugin.structure.MaterialSLD`, then times -``refloxide.rust.uniaxial_reflectivity`` (sequential and parallel q), the -pure-Python ``tjf4x4`` kernel, and the plugin ``Structure.reflectivity`` path. +``refloxide.tmm.uniaxial_reflectivity`` (sequential and parallel q), the +pure-Python ``refloxide.python.tmm`` kernel, and the plugin +``Structure.reflectivity`` path. Run with:: uv run maturin develop --release @@ -29,8 +30,8 @@ import numpy as np from refloxide.pxr.plugin.structure import MaterialSLD, Structure -from refloxide.pxr.tjf4x4 import uniaxial_reflectivity as python_uniaxial_reflectivity -from refloxide.rust import uniaxial_reflectivity as rust_uniaxial_reflectivity +from refloxide.python.tmm import uniaxial_reflectivity as python_uniaxial_reflectivity +from refloxide.tmm import uniaxial_reflectivity as rust_uniaxial_reflectivity def _stack_at(energy_ev: float) -> Structure: @@ -70,7 +71,7 @@ def python_reflectivity( q: np.ndarray, energy_ev: float, ) -> tuple[np.ndarray, np.ndarray]: - """Run the pure-Python tjf4x4 kernel on the same arrays.""" + """Run the pure-Python TMM kernel on the same arrays.""" slabs, tensor = _arrays_at(structure, energy_ev) refl, _tran, *_ = python_uniaxial_reflectivity( np.asarray(q, dtype=np.float64), slabs, tensor, float(energy_ev) diff --git a/mkdocs.yml b/mkdocs.yml index d773816..e67ec97 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -94,7 +94,7 @@ nav: - Overview: api/index.md - Python: - refloxide: api/python/refloxide.md + - python.tmm: api/python/python/tmm.md - pxr: api/python/pxr/index.md - - tjf4x4: api/python/pxr/tjf4x4.md - Rust (core): api/rust.md - Contributing: contributing.md diff --git a/pyproject.toml b/pyproject.toml index 8ca9900..c46c945 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,6 +106,7 @@ known-first-party = ["refloxide"] [tool.ruff.lint.per-file-ignores] "tests/**/*.py" = ["ARG001"] "src/refloxide/pxr/**/*.py" = ["ERA001"] +"src/refloxide/python/tmm.py" = ["ERA001"] # ============================================================================= # ty (Astral type checker) @@ -115,8 +116,7 @@ known-first-party = ["refloxide"] include = ["src"] exclude = [ "**/pxr/plugin/**", - "**/integrations/**", - "**/pxr/tjf4x4.py", + "**/python/tmm.py", ] # ============================================================================= diff --git a/scripts/develop.sh b/scripts/develop.sh index 2e9be61..a0daf58 100755 --- a/scripts/develop.sh +++ b/scripts/develop.sh @@ -7,42 +7,16 @@ cd "$ROOT" uv python install 3.13 uv sync --group dev -PYREF_ROOT="${PYREF_ROOT:-$ROOT/../pyref}" -if [[ -f "$PYREF_ROOT/pyproject.toml" ]]; then - echo "Installing editable pyref from $PYREF_ROOT" - UV_NO_CONFIG=1 uv pip install -e "$PYREF_ROOT" "hvplot>=0.12.2" -fi - UV_NO_CONFIG=1 uv run maturin develop --release -export PYREF_ROOT uv run python - <<'PY' -import os -import sys - import refloxide import refloxide.rust as rust print(f"refloxide {refloxide.__version__}") print(f"rust extension: {rust.uniaxial_reflectivity.__name__}") - -try: - import importlib - - importlib.import_module("pyref.fitting") -except ImportError: - print( - "pyref not installed; Rust extension OK. " - "Set PYREF_ROOT or place pyref beside refloxide, then re-run make develop.", - file=sys.stderr, - ) - sys.exit(0) - -from refloxide.integrations.pyref import patch_pyref, pyref_patched - -patch_pyref(use_rust=True, parallel=False) -assert pyref_patched(), "pyref patch did not apply" -print("pyref patch OK") +assert hasattr(refloxide, "uniaxial_reflectivity") +print("develop OK") PY echo "develop OK" diff --git a/src/refloxide/__init__.py b/src/refloxide/__init__.py index 2e3ef1d..dc4a1fb 100644 --- a/src/refloxide/__init__.py +++ b/src/refloxide/__init__.py @@ -1,25 +1,32 @@ """Public Python interface for refloxide. -The native extension lives in :mod:`refloxide.rust` (Rust / PyO3). Prefer the -flat package surface for new work: +Default surface is the Rust-backed transfer-matrix method. Import modeling +and other surfaces by submodule name; they are not re-exported here. -* :mod:`refloxide.tmm` — uniaxial and book-ended reflectivity kernels. -* :mod:`refloxide.optics` — OOC interpolation and laboratory-frame tensors. +* :mod:`refloxide.tmm` — uniaxial and book-ended reflectivity kernels (Rust). +* :mod:`refloxide.optics` — OOC interpolation and laboratory-frame tensors (Rust). * :mod:`refloxide.data` — cached optical constants and reflectivity datasets. * :mod:`refloxide.model` — energy-deferred ``Structure`` / ``ReflectModel``. * :mod:`refloxide.objective` — multi-energy ``Objective`` for refnx fitters. +* :mod:`refloxide.python.tmm` — opt-in pure-Python TMM (import by submodule). +* :mod:`refloxide.python.model` — opt-in pure-Python modeling (pyref.fitting-shaped). -Legacy deferred-energy stacks and fitting glue remain under :mod:`refloxide.pxr` -(``pxr.energy``, ``pxr.plugin``, ``pxr.objective``) for existing notebooks until -that surface is retired. +Legacy helpers remain under :mod:`refloxide.pxr` and emit a deprecation warning +until they relocate into the top-level modules above. """ from __future__ import annotations -from refloxide import pxr +from refloxide.tmm import ( + bookended_uniaxial_reflectivity, + uniaxial_reflectivity, + uniaxial_reflectivity_batch, +) __all__ = [ "__version__", - "pxr", + "bookended_uniaxial_reflectivity", + "uniaxial_reflectivity", + "uniaxial_reflectivity_batch", ] __version__ = "0.1.5" diff --git a/src/refloxide/integrations/__init__.py b/src/refloxide/integrations/__init__.py deleted file mode 100644 index 267d11d..0000000 --- a/src/refloxide/integrations/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Optional adapters that connect refloxide kernels to external fitting stacks. - -Each submodule targets one host ecosystem (for example ``pyref``). Import -adapters explicitly when wiring a workflow; they are not loaded from the -top-level :mod:`refloxide` package. -""" - -from refloxide.integrations import pyref -from refloxide.integrations.pyref import ( - PyrefPatchReport, - patch_pyref, - patch_pyref_if_needed, - pyref_patch_report, - pyref_patched, - reflectivity, - require_pyref_patched, - uniaxial_reflectivity, -) - -__all__ = [ - "PyrefPatchReport", - "patch_pyref", - "patch_pyref_if_needed", - "pyref", - "pyref_patch_report", - "pyref_patched", - "reflectivity", - "require_pyref_patched", - "uniaxial_reflectivity", -] diff --git a/src/refloxide/integrations/pyref.py b/src/refloxide/integrations/pyref.py deleted file mode 100644 index 998d9ab..0000000 --- a/src/refloxide/integrations/pyref.py +++ /dev/null @@ -1,903 +0,0 @@ -"""Drop-in uniaxial reflectivity hooks for ``pyref.fitting`` workflows. - -Routes reflectivity through refloxide's native Jones layout (``R_ss`` at -``[:,0,0]``, ``R_pp`` at ``[:,1,1]``) and patches -:class:`pyref.fitting.model.ReflectModel` so polarization extraction matches -stock pyref (``pol='s'`` from ``[:,1,1]``, ``pol='p'`` from ``[:,0,0]``), -preserving ``pol='sp'`` / ``pol='ps'`` segment ordering for combined datasets. - -Scope is **uniaxial only** (``phi = 0``, ``backend = "uni"``). Do not use -this adapter for biaxial or general-incidence claims. -""" - -from __future__ import annotations - -import contextvars -import numbers -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Literal - -import numpy as np -from scipy.interpolate import splev, splrep - -if TYPE_CHECKING: - from numpy.typing import NDArray - -from refloxide.pxr.layout import apply_laboratory_scales, reflectivity_for_pol -from refloxide.pxr.tjf4x4 import uniaxial_reflectivity as _python_uniaxial - -_FWHM = 2 * np.sqrt(2 * np.log(2.0)) -_DEFAULT_PROBE_ENERGY_EV = 250.0 -_eval_energy: contextvars.ContextVar[float | None] = contextvars.ContextVar( - "refloxide_pyref_eval_energy", - default=None, -) - -SldClassesMode = Literal["legacy", "energy", "both"] - - -@dataclass(frozen=True, slots=True) -class PyrefPatchReport: - """Snapshot of which refloxide hooks are active on ``pyref.fitting``.""" - - kernels: bool - reflectivity: bool - model_layout: bool - fused_path: bool - structure_ior: bool - structure_materialization: bool - sld_classes: SldClassesMode - sld_exports: tuple[str, ...] - - -def _swap_sp_block(matrix: NDArray[Any]) -> NDArray[Any]: - """Legacy alias; prefer :func:`refloxide.pxr.layout.reflectmodel_layout`.""" - from refloxide.pxr.layout import reflectmodel_layout - - return reflectmodel_layout(matrix) - - -def _solve_refloxide( - q: NDArray[np.float64], - layers: NDArray[np.float64], - tensor: NDArray[np.complex128], - energy: float, - *, - use_rust: bool, - parallel: bool, -) -> tuple[NDArray[np.float64], NDArray[np.complex128]]: - if use_rust: - from refloxide.rust import uniaxial_reflectivity as rust_uniaxial - - refl, tran = rust_uniaxial( - q, - layers, - tensor, - float(energy), - parallel=parallel, - ) - refl_arr = np.asarray(refl, dtype=np.float64) - tran_arr = np.asarray(tran, dtype=np.complex128) - else: - refl_arr, tran_arr, *_ = _python_uniaxial( - q, - layers, - tensor, - float(energy), - phi=0.0, - ) - refl_arr = np.asarray(refl_arr, dtype=np.float64) - tran_arr = np.asarray(tran_arr, dtype=np.complex128) - - return refl_arr, tran_arr - - -def uniaxial_reflectivity( - q: NDArray[np.float64], - layers: NDArray[np.float64], - tensor: NDArray[np.complex128], - energy: float, - *, - use_rust: bool = True, - parallel: bool = False, - return_components: bool = False, -) -> tuple[NDArray[np.float64], NDArray[np.complex128], *tuple[Any, ...]]: - """Evaluate uniaxial reflectivity using refloxide's laboratory Jones layout. - - Accepts the same positional arguments as - ``pyref.fitting.uniaxial.uniaxial_reflectivity`` (``q``, ``layers``, - ``tensor``, ``energy`` in eV). Returns ``refl`` with ``[:,0,0] = R_ss`` and - ``[:,1,1] = R_pp``. Pair with :func:`patch_pyref` so - :class:`pyref.fitting.model.ReflectModel` reads those channels correctly. - - Parameters - ---------- - q - Scattering wavevectors in inverse angstroms, shape ``(n_q,)``. - layers - Slab table ``(n_layers, 4)`` with rows ``[thickness, sld_real, - sld_imag, roughness]`` in refnx/pyref units (SLD scaled by - ``1e-6`` Angstrom**-2). - tensor - Per-layer dispersion tensor ``(n_layers, 3, 3)`` with diagonals - carrying :math:`\\delta + i\\beta` per principal axis before the - ``epsilon = conj(I - 2 * tensor)`` conversion inside the kernel. - energy - Photon energy in eV. - use_rust - When ``True`` (default), call :func:`refloxide.rust.uniaxial_reflectivity`. - When ``False``, use the pure-Python port in - :mod:`refloxide.pxr.tjf4x4`. - parallel - Passed to the Rust kernel as ``parallel=``. Default ``False`` so - refnx/emcee worker pools are not oversubscribed. Ignored when - ``use_rust=False``. - return_components - When ``True`` and ``use_rust=False``, append the diagnostic arrays - returned by the Python port (``kx``, ``ky``, ``kz``, polarization - bases, transfer matrices). When ``True`` and ``use_rust=True``, the - trailing tuple is empty because the extension returns only ``refl`` - and ``tran``. - - Returns - ------- - refl - Power reflectance ``(n_q, 2, 2)`` with ``[:,0,0] = R_ss``, - ``[:,1,1] = R_pp``. - tran - Complex transmission amplitudes in the same Jones layout as ``refl``. - components - Optional trailing diagnostics when ``return_components=True``. - - Raises - ------ - ValueError - Propagated from the Rust kernel on malformed inputs. - RuntimeError - Propagated from the Rust kernel when the dynamic matrix is singular. - """ - q_arr = np.asarray(q, dtype=np.float64) - layers_arr = np.asarray(layers, dtype=np.float64) - tensor_arr = np.asarray(tensor, dtype=np.complex128) - refl, tran = _solve_refloxide( - q_arr, - layers_arr, - tensor_arr, - float(energy), - use_rust=use_rust, - parallel=parallel, - ) - if return_components and not use_rust: - *_, components = _python_uniaxial( - q_arr, - layers_arr, - tensor_arr, - float(energy), - phi=0.0, - ) - return refl, tran, *components - return refl, tran - - -def reflectivity( - q: np.ndarray, - slabs: np.ndarray, - tensor: np.ndarray, - energy: float = 250.0, - phi: float = 0.0, - scale_s: float = 1.0, - scale_p: float = 1.0, - bkg: float = 0.0, - dq: float = 0.0, - backend: Literal["uni"] = "uni", - *, - use_rust: bool = True, - parallel: bool = False, -) -> tuple[np.ndarray, np.ndarray, list[Any]] | None: - """Mirror ``pyref.fitting.model.reflectivity`` for uniaxial refloxide kernels. - - Applies laboratory-index scaling, background, and optional constant ``dQ/Q`` - smearing using the same recipe as pyref, but evaluates the stratified stack - with :func:`uniaxial_reflectivity`. - - Parameters - ---------- - q - Scattering wavevectors in inverse angstroms. - slabs - Slab table with shape ``(2 + N, 4)``; see - ``pyref.fitting.model.reflectivity`` for column semantics. - tensor - Per-layer ``(2 + N, 3, 3)`` dispersion tensor. - energy - Photon energy in eV. - phi - Accepted for API compatibility with pyref; ignored (uniaxial path - fixes ``phi = 0``). - scale_s, scale_p - Independent scale factors applied to ``refl[:,0,0]`` (``R_ss``) and - ``refl[:,1,1]`` (``R_pp``) before adding ``bkg``. - bkg - Constant background added to every reflectivity element. - dq - Constant ``dQ/Q`` resolution smearing in percent. ``0`` disables - smearing. - backend - Must be ``"uni"``; other backends raise ``ValueError``. - use_rust - Forwarded to :func:`uniaxial_reflectivity`. - parallel - Forwarded to :func:`uniaxial_reflectivity`. - - Returns - ------- - refl - Smear-adjusted, scaled reflectivity matrix plus ``bkg``. - tran - Transmission amplitudes from the final kernel call inside the smear - branch (unchanged by scaling). - components - Empty list when smearing is disabled; otherwise the trailing items - from the last internal kernel call. - - Raises - ------ - ValueError - When ``backend`` is not ``"uni"``. - """ - del phi - if backend != "uni": - msg = ( - "refloxide.integrations.pyref.reflectivity supports backend='uni' " - f"only; got {backend!r}" - ) - raise ValueError(msg) - - if not isinstance(dq, numbers.Real): - return None - - if float(dq) == 0: - refl, tran, *components = uniaxial_reflectivity( - q, - slabs, - tensor, - energy, - use_rust=use_rust, - parallel=parallel, - ) - refl = np.asarray(refl, dtype=np.float64) - apply_laboratory_scales(refl, scale_s, scale_p) - return refl + bkg, tran, list(components) - - smear_refl, smear_tran, *components = _smeared_reflectivity( - q, - slabs, - tensor, - energy, - float(dq), - use_rust=use_rust, - parallel=parallel, - ) - apply_laboratory_scales(smear_refl, scale_s, scale_p) - return smear_refl + bkg, smear_tran, list(components) - - -def _smeared_reflectivity( - q: np.ndarray, - slabs: np.ndarray, - tensor: np.ndarray, - energy: float, - resolution: float, - *, - use_rust: bool, - parallel: bool, -) -> tuple[np.ndarray, np.ndarray, list[Any]]: - if resolution < 0.5: - refl, tran, *components = uniaxial_reflectivity( - q, - slabs, - tensor, - energy, - use_rust=use_rust, - parallel=parallel, - ) - return np.asarray(refl), tran, list(components) - - resolution /= 100 - gaussnum = 51 - gaussgpoint = (gaussnum - 1) / 2 - - def gauss(x, s): - return 1.0 / s / np.sqrt(2 * np.pi) * np.exp(-0.5 * x**2 / s / s) - - lowq = float(np.min(q)) - highq = float(np.max(q)) - if lowq <= 0: - lowq = 1e-6 - - start = np.log10(lowq) - 6 * resolution / _FWHM - finish = np.log10(highq * (1 + 6 * resolution / _FWHM)) - interpnum = int( - np.round( - np.abs(1 * (np.abs(start - finish))) - / (1.7 * resolution / _FWHM / gaussgpoint) - ) - ) - xtemp = np.linspace(start, finish, interpnum) - xlin = np.power(10.0, xtemp) - - gauss_x = np.linspace(-1.7 * resolution, 1.7 * resolution, gaussnum) - gauss_y = gauss(gauss_x, resolution / _FWHM) - refl, tran, *components = uniaxial_reflectivity( - xlin, - slabs, - tensor, - energy, - use_rust=use_rust, - parallel=parallel, - ) - step = gauss_x[1] - gauss_x[0] - smeared_ss = np.convolve(refl[:, 0, 0], gauss_y, mode="same") * step - smeared_pp = np.convolve(refl[:, 1, 1], gauss_y, mode="same") * step - smeared_sp = np.convolve(refl[:, 0, 1], gauss_y, mode="same") * step - smeared_ps = np.convolve(refl[:, 1, 0], gauss_y, mode="same") * step - - smeared_output_ss = splev(q, splrep(xlin, smeared_ss)) - smeared_output_sp = splev(q, splrep(xlin, smeared_sp)) - smeared_output_ps = splev(q, splrep(xlin, smeared_ps)) - smeared_output_pp = splev(q, splrep(xlin, smeared_pp)) - - smeared_output = np.rollaxis( - np.array( - [ - [smeared_output_ss, smeared_output_sp], - [smeared_output_ps, smeared_output_pp], - ] - ), - 2, - 0, - ) - return smeared_output, tran, list(components) - - -def _structure_energy_offset_ev(structure: Any | None) -> float: - """Read the structure-level energy offset (eV) when present.""" - if structure is None: - return 0.0 - for attr in ("structure_energy_offset", "energy_offset"): - candidate = getattr(structure, attr, None) - if candidate is not None and hasattr(candidate, "value"): - return float(candidate.value or 0.0) - return 0.0 - - -def _resolve_reflectmodel_energy( - model: Any, - *, - default: float = _DEFAULT_PROBE_ENERGY_EV, -) -> float: - """Resolve a finite photon energy (eV) for ``ReflectModel`` q/theta adjustments. - - Stock pyref allows ``ReflectModel.energy is None`` at construction; this helper - falls back to scatterer ``get_energy()``/``.energy`` on the attached structure, - then ``default``. - """ - energy = getattr(model, "energy", None) - if energy is not None: - return float(energy) - structure = getattr(model, "structure", None) - if structure is not None: - for component in getattr(structure, "components", []): - sld = getattr(component, "sld", None) - if sld is None: - continue - get_energy = getattr(sld, "get_energy", None) - if callable(get_energy): - return float(get_energy()) - scatterer_energy = getattr(sld, "energy", None) - if scatterer_energy is not None: - return float(scatterer_energy) - return float(default) - - -def _slab_energy_probe( - _slab: Any, - energy: float | None, - *, - structure: Any | None = None, -) -> Any: - """Build a :class:`~refloxide.pxr.energy.probe.Probe` for one slab.""" - from refloxide.pxr.energy.probe import Probe - - ctx_energy = _eval_energy.get() - if energy is not None: - base = float(energy) - elif ctx_energy is not None: - base = float(ctx_energy) - else: - base = _DEFAULT_PROBE_ENERGY_EV - return Probe( - base_energy_ev=base, - structure_offset_ev=_structure_energy_offset_ev(structure), - ) - - -def _tensor_to_slab_row( - thick: float, - rough: float, - tensor: NDArray[Any], -) -> NDArray[np.float64]: - """Convert a diagonal tensor to refnx ``[d, delta, beta, sigma]`` layout.""" - n_avg = (tensor[0, 0] + tensor[1, 1] + tensor[2, 2]) / 3.0 - delta = float((1.0 - n_avg).real) - beta = float((1.0 - n_avg).imag) - return np.array([thick, delta, beta, rough], dtype=np.float64) - - -def _refloxide_sld_exports() -> dict[str, Any]: - """Scatterer symbols registered on ``pyref.fitting.structure``.""" - from refloxide.pxr.energy import ( - BookendedOrientationProfile, - DeferredScatterer, - DispersiveMaterialSLD, - DispersiveStructure, - EnergyBookendedOrientationDensityProfile, - EnergyDependentMaterialSLD, - EnergyDependentScatterer, - EnergyDependentStructure, - EnergyDependentUniTensorSLD, - EnergyProbe, - OocUniTensorScatterer, - Probe, - RefloxideScatterer, - TabulatedUniTensorSLD, - upgrade_scatterer, - upgrade_structure, - ) - - return { - "BookendedOrientationProfile": BookendedOrientationProfile, - "DeferredScatterer": DeferredScatterer, - "DispersiveMaterialSLD": DispersiveMaterialSLD, - "DispersiveStructure": DispersiveStructure, - "EnergyBookendedOrientationDensityProfile": ( - EnergyBookendedOrientationDensityProfile - ), - "EnergyDependentMaterialSLD": EnergyDependentMaterialSLD, - "EnergyDependentScatterer": EnergyDependentScatterer, - "EnergyDependentStructure": EnergyDependentStructure, - "EnergyDependentUniTensorSLD": EnergyDependentUniTensorSLD, - "EnergyProbe": EnergyProbe, - "OocUniTensorScatterer": OocUniTensorScatterer, - "Probe": Probe, - "RefloxideScatterer": RefloxideScatterer, - "TabulatedUniTensorSLD": TabulatedUniTensorSLD, - "upgrade_scatterer": upgrade_scatterer, - "upgrade_structure": upgrade_structure, - } - - -def _reflectmodel_q_grid( - model: Any, - x: np.ndarray, -) -> tuple[np.ndarray, np.ndarray, np.ndarray]: - """Mirror pyref ``ReflectModel`` q/theta adjustments before the kernel.""" - wavelength = 12398.42 / _resolve_reflectmodel_energy(model) - if model.pol in ("sp", "ps"): - concat_loc = int(np.argmax(np.abs(np.diff(x)))) - qvals_1 = x[: concat_loc + 1] - qvals_2 = x[concat_loc + 1 :] - num_q = max(len(x), concat_loc + 50) - theta_s = np.arcsin(qvals_1 * wavelength / (4 * np.pi)) * 180 / np.pi - theta_p = np.arcsin(qvals_2 * wavelength / (4 * np.pi)) * 180 / np.pi - theta_s += float(model.theta_offset_s.value or 0.0) - theta_p += float(model.theta_offset_p.value or 0.0) - qvals_1 = (4 * np.pi / wavelength) * np.sin(theta_s * np.pi / 180) - qvals_2 = (4 * np.pi / wavelength) * np.sin(theta_p * np.pi / 180) - x_out = np.concatenate([qvals_1, qvals_2]) - qvals = np.linspace(float(np.min(x_out)), float(np.max(x_out)), num_q) - return qvals, qvals_1, qvals_2 - if model.pol == "s": - theta = np.arcsin(x * wavelength / (4 * np.pi)) * 180 / np.pi - theta += float(model.theta_offset_s.value or 0.0) - qvals = (4 * np.pi / wavelength) * np.sin(theta * np.pi / 180) - return qvals, qvals, qvals - if model.pol == "p": - theta = np.arcsin(x * wavelength / (4 * np.pi)) * 180 / np.pi - theta += float(model.theta_offset_p.value or 0.0) - qvals = (4 * np.pi / wavelength) * np.sin(theta * np.pi / 180) - return qvals, qvals, qvals - return x, x, x - - -def _patch_reflect_model_fused(model_mod: Any, *, parallel: bool) -> None: - """Route book-ended stacks through the fused Rust evaluator when possible.""" - reflect_model = model_mod.ReflectModel - if getattr(reflect_model, "__refloxide_fused_patched__", False): - return - if not hasattr(reflect_model, "_model"): - return - original_model = reflect_model._model - - def _model(self, x, p=None, x_err=None): - if getattr(self, "energy", None) is None and not hasattr(self, "structure"): - return original_model(self, x, p=p, x_err=x_err) - if p is not None and hasattr(self, "parameters"): - self.parameters.pvals = np.array(p) - if x_err is None: - x_err = float(getattr(self, "dq", 0.0)) - x_arr = np.asarray(x, dtype=np.float64) - qvals, qvals_1, qvals_2 = _reflectmodel_q_grid(self, x_arr) - backend = getattr(self, "backend", "uni") - model_energy = _resolve_reflectmodel_energy(self) - if backend == "uni" and float(x_err) == 0.0 and hasattr(self, "structure"): - from refloxide.pxr.energy.fused import evaluate_fused_bookended_reflectivity - - structure_offset = 0.0 - if hasattr(self, "energy_offset"): - structure_offset = float(self.energy_offset.value or 0.0) - q_offset = float( - getattr(self, "q_offset", type("Q", (), {"value": 0.0})()).value or 0.0 - ) - q_kernel = qvals + q_offset - fused = evaluate_fused_bookended_reflectivity( - q_kernel, - self.structure, - model_energy, - structure_energy_offset=structure_offset, - parallel=parallel, - ) - if fused is not None: - refl, tran = fused - scale_s = float( - getattr(self, "scale_s", type("S", (), {"value": 1.0})()).value - or 1.0 - ) - scale_p = float( - getattr(self, "scale_p", type("S", (), {"value": 1.0})()).value - or 1.0 - ) - bkg = float( - getattr(self, "bkg", type("S", (), {"value": 0.0})()).value or 0.0 - ) - apply_laboratory_scales(refl, scale_s, scale_p) - refl = refl + bkg - return qvals, qvals_1, qvals_2, refl, tran, [] - return original_model(self, x, p=p, x_err=x_err) - - reflect_model._model = _model - reflect_model.__refloxide_fused_patched__ = True - - -def _patch_reflect_model_model(model_mod: Any) -> None: - """Read laboratory Jones channels in :meth:`ReflectModel.model`.""" - reflect_model = model_mod.ReflectModel - if getattr(reflect_model, "__refloxide_model_patched__", False): - return - - def model(self, x, p=None, x_err=None): - qvals, qvals_1, qvals_2, refl, _tran, _components = self._model(x, p, x_err) - output = reflectivity_for_pol(self.pol, refl, qvals, qvals_1, qvals_2) - return output - - reflect_model.model = model - reflect_model.__refloxide_model_patched__ = True - - -def _is_bookended_profile(other: Any) -> bool: - from refloxide.pxr.energy.bookended import BookendedOrientationProfile - - return isinstance(other, BookendedOrientationProfile) - - -def _patch_pyref_structure_ior() -> None: - """Allow refloxide book-ended profiles in pyref ``Structure`` stacks.""" - import importlib - from collections import UserList - - try: - st_mod = importlib.import_module("pyref.fitting.structure") - except ModuleNotFoundError: - return - if getattr(st_mod.Structure, "__refloxide_ior_patched__", False): - return - original_ior = st_mod.Structure.__ior__ - original_append = st_mod.Structure.append - - def append(self, item): - if _is_bookended_profile(item): - UserList.append(self, item) - return - return original_append(self, item) - - def __ior__(self, other): - if _is_bookended_profile(other): - UserList.append(self, other) - return self - return original_ior(self, other) - - st_mod.Structure.append = append # ty: ignore[invalid-assignment] - st_mod.Structure.__ior__ = __ior__ # ty: ignore[invalid-assignment] - st_mod.Structure.__refloxide_ior_patched__ = True # ty: ignore[unresolved-attribute] - - -def _patch_pyref_slab_materialization(st_mod: Any) -> None: - """Materialize refloxide energy scatterers inside stock pyref ``Slab`` rows.""" - from refloxide.pxr.energy.scatterers import EnergyDependentScatterer - - if getattr(st_mod.Slab, "__refloxide_slab_patched__", False): - return - if not hasattr(st_mod, "Slab") or not hasattr(st_mod.Slab, "tensor"): - return - original_tensor = st_mod.Slab.tensor - original_slabs = st_mod.Slab.slabs - - def tensor(self, energy=None): - if isinstance(self.sld, EnergyDependentScatterer): - probe = _slab_energy_probe(self, energy, structure=None) - layer = self.sld.tensor_at(probe) - return np.asarray([layer], dtype=np.complex128) - return original_tensor(self, energy=energy) - - def slabs(self, structure=None): - if isinstance(self.sld, EnergyDependentScatterer): - probe = _slab_energy_probe(self, None, structure=structure) - layer = self.sld.tensor_at(probe) - thick = float(self.thick.value or 0.0) - rough = float(self.rough.value or 0.0) - row = _tensor_to_slab_row(thick, rough, layer) - return np.asarray([row], dtype=np.float64) - return original_slabs(self, structure=structure) - - st_mod.Slab.tensor = tensor # ty: ignore[invalid-assignment] - st_mod.Slab.slabs = slabs # ty: ignore[invalid-assignment] - st_mod.Slab.__refloxide_slab_patched__ = True # ty: ignore[unresolved-attribute] - - -def _patch_pyref_sld_exports( - st_mod: Any, - mode: SldClassesMode, -) -> tuple[str, ...]: - """Expose refloxide energy-deferred scatterers on ``pyref.fitting.structure``.""" - if mode == "legacy": - return () - if getattr(st_mod, "__refloxide_sld_exports__", False): - return tuple(getattr(st_mod, "__refloxide_sld_export_names__", ())) - exports = _refloxide_sld_exports() - for name, symbol in exports.items(): - setattr(st_mod, name, symbol) - st_mod.__refloxide_sld_exports__ = True # ty: ignore[unresolved-attribute] - names = tuple(exports) - st_mod.__refloxide_sld_export_names__ = names # ty: ignore[unresolved-attribute] - if mode == "energy": - st_mod.MaterialSLD = exports["EnergyDependentMaterialSLD"] # ty: ignore[invalid-assignment] - st_mod.UniTensorSLD = exports["OocUniTensorScatterer"] # ty: ignore[invalid-assignment] - return names - - -def _patch_reflect_model_eval_energy(model_mod: Any) -> None: - """Set evaluation energy context around every ``ReflectModel._model`` call.""" - reflect_model = model_mod.ReflectModel - if getattr(reflect_model, "__refloxide_eval_energy_patched__", False): - return - if not hasattr(reflect_model, "_model"): - return - inner_model = reflect_model._model - - def _model(self, x, p=None, x_err=None): - token = _eval_energy.set(_resolve_reflectmodel_energy(self)) - try: - return inner_model(self, x, p=p, x_err=x_err) - finally: - _eval_energy.reset(token) - - reflect_model._model = _model - reflect_model.__refloxide_eval_energy_patched__ = True - - -def patch_pyref( - *, - use_rust: bool = True, - parallel: bool = False, - patch_reflectivity: bool = True, - sld_classes: SldClassesMode = "both", -) -> PyrefPatchReport: - """Install refloxide uniaxial kernels into an imported ``pyref.fitting`` stack. - - Replaces ``pyref.fitting.uniaxial.uniaxial_reflectivity`` with a partial - application of :func:`uniaxial_reflectivity`. Optionally replaces - ``pyref.fitting.model.reflectivity`` so smearing and scaling stay on the - refloxide code path. Patches :meth:`pyref.fitting.model.ReflectModel.model` - to extract polarization channels with the same indexing as stock pyref. - - Parameters - ---------- - use_rust - When ``True`` (default), patched calls use - :func:`refloxide.rust.uniaxial_reflectivity`. - parallel - Forwarded as ``parallel=`` to the Rust kernel. Default ``False`` for - fitting loops that already parallelize across walkers or datasets. - patch_reflectivity - When ``True`` (default), also patch - ``pyref.fitting.model.reflectivity``. - sld_classes - Controls refloxide scatterer exposure on ``pyref.fitting.structure``: - - - ``"legacy"`` — stock ``MaterialSLD`` / ``SLD`` / ``UniTensorSLD`` only. - - ``"both"`` (default) — keep stock classes and add ``EnergyDependent*`` - symbols plus ``upgrade_scatterer`` / ``upgrade_structure``. - - ``"energy"`` — same exports as ``"both"``, and alias ``MaterialSLD`` / - ``UniTensorSLD`` to the energy-deferred implementations. - - Returns - ------- - PyrefPatchReport - Idempotent snapshot of which hooks were installed. - - Raises - ------ - ImportError - When ``pyref.fitting`` is not installed in the active environment. - - Notes - ----- - Call this once at process startup before constructing - :class:`pyref.fitting.model.ReflectModel` instances. The patch is - process-global and affects every subsequent pyref reflectivity evaluation. - """ - import importlib - from functools import partial - - try: - uni_mod = importlib.import_module("pyref.fitting.uniaxial") - model_mod = importlib.import_module("pyref.fitting.model") - st_mod = importlib.import_module("pyref.fitting.structure") - except ModuleNotFoundError as exc: - msg = ( - "pyref.fitting is not importable; install pyref or add it to " - "PYTHONPATH before calling patch_pyref" - ) - raise ImportError(msg) from exc - - patched_uni = partial( - uniaxial_reflectivity, - use_rust=use_rust, - parallel=parallel, - ) - uni_mod.uniaxial_reflectivity = patched_uni # ty: ignore[unresolved-attribute] - uni_mod.__refloxide_patched__ = True # ty: ignore[unresolved-attribute] - if patch_reflectivity: - patched_refl = partial( - reflectivity, - use_rust=use_rust, - parallel=parallel, - ) - model_mod.reflectivity = patched_refl # ty: ignore[invalid-assignment, unresolved-attribute] - _patch_reflect_model_model(model_mod) - _patch_reflect_model_fused(model_mod, parallel=parallel) - _patch_reflect_model_eval_energy(model_mod) - _patch_pyref_structure_ior() - _patch_pyref_slab_materialization(st_mod) - export_names: tuple[str, ...] = () - if sld_classes != "legacy": - export_names = _patch_pyref_sld_exports(st_mod, sld_classes) - model_mod.__refloxide_patched__ = True # ty: ignore[unresolved-attribute] - model_mod.__refloxide_sld_classes__ = sld_classes # ty: ignore[unresolved-attribute] - return PyrefPatchReport( - kernels=True, - reflectivity=patch_reflectivity, - model_layout=getattr( - model_mod.ReflectModel, "__refloxide_model_patched__", False - ), - fused_path=getattr( - model_mod.ReflectModel, "__refloxide_fused_patched__", False - ), - structure_ior=getattr(st_mod.Structure, "__refloxide_ior_patched__", False), - structure_materialization=getattr( - st_mod.Slab, "__refloxide_slab_patched__", False - ), - sld_classes=sld_classes, - sld_exports=export_names, - ) - - -def patch_pyref_if_needed( - *, - use_rust: bool = True, - parallel: bool = False, - patch_reflectivity: bool = True, - sld_classes: SldClassesMode = "both", - force: bool = False, -) -> PyrefPatchReport: - """Apply :func:`patch_pyref` once per process unless already configured. - - Parameters - ---------- - use_rust, parallel, patch_reflectivity, sld_classes - Forwarded to :func:`patch_pyref`. - force - When ``True``, re-run the patch even if :func:`pyref_patched` is ``True``. - - Returns - ------- - PyrefPatchReport - Status after this call (existing report when skipped, fresh report when - applied). - """ - if pyref_patched() and not force: - return pyref_patch_report() - return patch_pyref( - use_rust=use_rust, - parallel=parallel, - patch_reflectivity=patch_reflectivity, - sld_classes=sld_classes, - ) - - -def pyref_patch_report() -> PyrefPatchReport: - """Return the current refloxide hook status on ``pyref.fitting``.""" - import importlib - - try: - model_mod = importlib.import_module("pyref.fitting.model") - st_mod = importlib.import_module("pyref.fitting.structure") - uni_mod = importlib.import_module("pyref.fitting.uniaxial") - except ModuleNotFoundError: - return PyrefPatchReport( - kernels=False, - reflectivity=False, - model_layout=False, - fused_path=False, - structure_ior=False, - structure_materialization=False, - sld_classes="legacy", - sld_exports=(), - ) - sld_classes = getattr(model_mod, "__refloxide_sld_classes__", "legacy") - return PyrefPatchReport( - kernels=bool(getattr(uni_mod, "__refloxide_patched__", False)), - reflectivity=getattr(model_mod, "reflectivity", None) is not None - and getattr(uni_mod, "__refloxide_patched__", False), - model_layout=getattr( - model_mod.ReflectModel, "__refloxide_model_patched__", False - ), - fused_path=getattr( - model_mod.ReflectModel, "__refloxide_fused_patched__", False - ), - structure_ior=getattr(st_mod.Structure, "__refloxide_ior_patched__", False), - structure_materialization=getattr( - st_mod.Slab, "__refloxide_slab_patched__", False - ), - sld_classes=sld_classes, - sld_exports=tuple(getattr(st_mod, "__refloxide_sld_export_names__", ())), - ) - - -def pyref_patched() -> bool: - """Return whether :func:`patch_pyref` has installed refloxide kernels in pyref.""" - import importlib - - try: - model_mod = importlib.import_module("pyref.fitting.model") - except ModuleNotFoundError: - return False - return bool(getattr(model_mod, "__refloxide_patched__", False)) and bool( - getattr(model_mod.ReflectModel, "__refloxide_model_patched__", False) - ) - - -def require_pyref_patched() -> None: - """Raise when pyref is not configured for refloxide laboratory channels. - - Call :func:`patch_pyref` or ``import utils.models`` (refl-analysis) before - constructing or evaluating :class:`pyref.fitting.model.ReflectModel`. - """ - if pyref_patched(): - return - msg = ( - "pyref.fitting is not patched for refloxide. Call patch_pyref() or " - "import utils.models before fitting so ReflectModel reads laboratory " - "pyref-compatible s/p channel extraction on the native Jones matrix." - ) - raise RuntimeError(msg) diff --git a/src/refloxide/model.py b/src/refloxide/model.py index ed5ce28..c5c17d4 100644 --- a/src/refloxide/model.py +++ b/src/refloxide/model.py @@ -1615,8 +1615,8 @@ def _smeared_uniaxial_reflectivity( Same log-grid + Gaussian-convolution + spline-interpolation technique `refloxide.pxr.plugin.model._smeared_reflectivity` uses, but calling - `refloxide.tmm.uniaxial_reflectivity` (Rust) instead of that module's - pure-Python `tjf4x4.uniaxial_reflectivity` fallback — the actual + `refloxide.tmm.uniaxial_reflectivity` (Rust) instead of + `refloxide.python.tmm.uniaxial_reflectivity` — the actual "proper Rust consideration" fix, since the smearing math itself (`numpy.convolve` over ~51 points, `scipy` spline evaluation) was already cheap and vectorized; the old bottleneck was the kernel call. @@ -1801,10 +1801,9 @@ class ReflectModel: Ports the correction stages of the original, load-bearing `refloxide.pxr.plugin.model.ReflectModel` (scale, background, resolution smearing, q/theta offsets) onto the Rust-backed kernel — - that original never used the Rust kernel at all (only - `refloxide.pxr.tjf4x4`'s pure-Python port, unless patched via - `patch_pyref`), so this port is also a real speed fix, not just a - rename. + that original historically used only the pure-Python TMM + (`refloxide.python.tmm`), so this port is also a real speed fix, not + just a rename. Parameters ---------- diff --git a/src/refloxide/pxr/__init__.py b/src/refloxide/pxr/__init__.py index 52b55f0..ad3c019 100644 --- a/src/refloxide/pxr/__init__.py +++ b/src/refloxide/pxr/__init__.py @@ -1,8 +1,20 @@ -""" -Pure python implementation of the 4x4 transfer matrix method. +"""Legacy polarization / energy / plugin helpers under the ``pxr`` label. -This module is based on the code produced by Thomas Ferron and -published in the following papers: -https://doi.org/10.1021/jacsau.3c00168 -https://doi.org/10.1021/acsami.1c19948 +This package path is deprecated. Prefer the top-level modules +(:mod:`refloxide.tmm`, :mod:`refloxide.optics`, :mod:`refloxide.data`, +:mod:`refloxide.model`, :mod:`refloxide.objective`) and, for the pure-Python +TMM, :mod:`refloxide.python.tmm`. Contents under ``pxr`` will relocate out of +this namespace in a future release. """ + +from __future__ import annotations + +import warnings + +warnings.warn( + "refloxide.pxr is deprecated and will be relocated into top-level " + "modules (tmm, optics, data, model, objective) and " + "refloxide.python.*; import those surfaces directly.", + DeprecationWarning, + stacklevel=2, +) diff --git a/src/refloxide/pxr/plugin/batched_global.py b/src/refloxide/pxr/plugin/batched_global.py index 0404d52..b6ade57 100644 --- a/src/refloxide/pxr/plugin/batched_global.py +++ b/src/refloxide/pxr/plugin/batched_global.py @@ -1,4 +1,4 @@ -"""Batched multi-energy global objectives for refnx / pyref fitters. +"""Batched multi-energy global objectives for refnx fitters. Evaluates many reflectivity datasets from one energy-parameterized :class:`~refloxide.pxr.plugin.model.ReflectModel` in a single ``logl`` call: @@ -21,9 +21,9 @@ ) from refnx.dataset import Data1D -from refloxide.integrations.pyref import reflectivity as refloxide_reflectivity -from refloxide.pxr.layout import reflectivity_for_pol +from refloxide.pxr.layout import apply_laboratory_scales, reflectivity_for_pol from refloxide.pxr.plugin.dispersive_model import resolve_instrument +from refloxide.tmm import uniaxial_reflectivity as rust_uniaxial_reflectivity if TYPE_CHECKING: from refloxide.pxr.energy.structure import StackSnapshot @@ -45,7 +45,8 @@ class ReflectivityBatchTerm: y_err Uncertainties on ``y``; use ones when unweighted. pol - ``'s'`` or ``'p'`` laboratory channel (pyref ``ReflectModel`` indexing). + ``'s'`` or ``'p'`` laboratory channel (legacy plugin indexing via + :func:`~refloxide.pxr.layout.reflectivity_for_pol`). energy Photon energy in eV for dispersive structure evaluation. lambda_ @@ -130,7 +131,7 @@ def _q_grid_for_pol( theta_offset_s: float, theta_offset_p: float, ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: - """Apply pyref theta offsets and return ``(qvals, qvals_1, qvals_2)``.""" + """Apply laboratory theta offsets and return ``(qvals, qvals_1, qvals_2)``.""" wavelength = 12398.42 / energy x = np.asarray(x, dtype=np.float64) if pol == "s": @@ -195,7 +196,7 @@ def _evaluate_reflectivity_term( parallel_kernels: bool, snapshot: StackSnapshot | None = None, ) -> np.ndarray: - """Evaluate one batched term's model curve on ``term.x``.""" + """Evaluate one batched term's model curve on ``term.x`` via Rust TMM.""" energy = float(term.energy) instrument = resolve_instrument(model, energy) # type: ignore[arg-type] structure = model.structure # type: ignore[union-attr] @@ -216,23 +217,36 @@ def _evaluate_reflectivity_term( ) dq_raw = term.x_err if term.x_err is not None else instrument.dq dq = float(np.asarray(dq_raw).flat[0]) - result = refloxide_reflectivity( - qvals + instrument.q_offset, - slabs, - tensor, - energy, - scale_s=instrument.scale_s, - scale_p=instrument.scale_p, - bkg=instrument.bkg, - dq=dq, - backend="uni", - use_rust=True, - parallel=parallel_kernels, - ) - if result is None: - msg = "reflectivity returned None; check dq / backend" - raise RuntimeError(msg) - refl, _tran, _components = result + q_eval = np.asarray(qvals + instrument.q_offset, dtype=np.float64) + if float(dq) == 0.0: + refl, _tran = rust_uniaxial_reflectivity( + q_eval, + np.asarray(slabs, dtype=np.float64), + np.asarray(tensor, dtype=np.complex128), + energy, + parallel=parallel_kernels, + ) + refl = np.asarray(refl, dtype=np.float64) + apply_laboratory_scales(refl, instrument.scale_s, instrument.scale_p) + refl = refl + instrument.bkg + else: + from refloxide.pxr.plugin.model import reflectivity as plugin_reflectivity + + result = plugin_reflectivity( + q_eval, + slabs, + tensor, + energy, + scale_s=instrument.scale_s, + scale_p=instrument.scale_p, + bkg=instrument.bkg, + dq=dq, + backend="uni", + ) + if result is None: + msg = "reflectivity returned None; check dq / backend" + raise RuntimeError(msg) + refl, _tran, _components = result return reflectivity_for_pol( term.pol, refl, @@ -343,8 +357,8 @@ class BatchedGlobalObjective(Objective): Per-term multipliers broadcast against ``terms`` (and anisotropy terms use their own ``lambda_`` fields). parallel_kernels - Forwarded to :func:`refloxide.integrations.pyref.reflectivity` as - ``parallel=``. Default ``False`` for nested fitters. + Forwarded to :func:`refloxide.tmm.uniaxial_reflectivity` as + ``parallel=`` when ``dq == 0``. Default ``False`` for nested fitters. parallel_terms Evaluate independent terms on a thread pool when ``True`` and ``parallel_kernels`` is ``False``. @@ -436,7 +450,7 @@ def from_anisotropy_objectives( parallel_terms: bool = False, **kwargs: Any, ) -> BatchedGlobalObjective: - """Build a batched objective from pyref-style anisotropy objectives. + """Build a batched objective from anisotropy-style objectives. Each source objective contributes s- and p-pol reflectivity terms plus an optional anisotropy residual when ``data.anisotropy`` is populated. @@ -647,8 +661,8 @@ class BatchedFitter: """Curve fitter entry point for :class:`BatchedGlobalObjective`. Thin wrapper around :class:`~refloxide.pxr.plugin.fitters.Fitter` so batched - objectives use the same ``sample``, ``fit``, and ``to_arviz`` surface as - pyref ``CurveFitter``. + objectives use the same ``sample``, ``fit``, and ``to_arviz`` surface as the + plugin :class:`~refloxide.pxr.plugin.fitters.Fitter`. """ def __init__( diff --git a/src/refloxide/pxr/plugin/model.py b/src/refloxide/pxr/plugin/model.py index e6290b0..451bed4 100644 --- a/src/refloxide/pxr/plugin/model.py +++ b/src/refloxide/pxr/plugin/model.py @@ -10,7 +10,7 @@ from scipy.interpolate import splev, splrep from refloxide.pxr.layout import apply_laboratory_scales, reflectivity_for_pol -from refloxide.pxr.tjf4x4 import uniaxial_reflectivity +from refloxide.python.tmm import uniaxial_reflectivity as _python_uniaxial_reflectivity if TYPE_CHECKING: from refnx.analysis import Parameter @@ -22,6 +22,25 @@ _FWHM = 2 * np.sqrt(2 * np.log(2.0)) +def _uniaxial_reflectivity( + q: np.ndarray, + slabs: np.ndarray, + tensor: np.ndarray, + energy: float, + *, + parallel: bool = False, +) -> tuple[np.ndarray, np.ndarray, list[Any]]: + """Pure-Python TMM kernel for the plugin / ``refloxide.python.model`` path.""" + _ = parallel + refl, tran, *components = _python_uniaxial_reflectivity( + np.asarray(q, dtype=np.float64), + np.asarray(slabs, dtype=np.float64), + np.asarray(tensor, dtype=np.complex128), + float(energy), + ) + return np.asarray(refl, dtype=np.float64), tran, list(components) + + class ReflectModel: r""" Reflectometry model for anisotropic interfaces. @@ -598,6 +617,8 @@ def reflectivity( bkg: float = 0.0, dq: float = 0.0, backend: Literal["uni", "bi"] = "uni", + *, + parallel: bool = False, ) -> tuple[np.ndarray, np.ndarray, list[Any]] | None: r""" Full calculation for anisotropic reflectivity of a stratified medium. @@ -693,32 +714,39 @@ def reflectivity( # constant dq/q smearing if isinstance(dq, numbers.Real): if float(dq) == 0: - if backend == "uni": - refl, tran, *components = uniaxial_reflectivity( - q, slabs, tensor, energy - ) - else: - refl, tran, *components = uniaxial_reflectivity( - q, - slabs, - tensor, - energy, - phi, # type: ignore - ) - refl = np.asarray(refl, dtype=np.float64) + _ = (phi, backend) + refl, tran, components = _uniaxial_reflectivity( + q, slabs, tensor, energy, parallel=parallel + ) apply_laboratory_scales(refl, scale_s, scale_p) return (refl + bkg), tran, components - else: - smear_refl, smear_tran, *components = _smeared_reflectivity( - q, slabs, tensor, energy, phi, dq, backend=backend - ) - apply_laboratory_scales(smear_refl, scale_s, scale_p) - return (smear_refl + bkg), smear_tran, components + smear_refl, smear_tran, components = _smeared_reflectivity( + q, + slabs, + tensor, + energy, + phi, + dq, + backend=backend, + parallel=parallel, + ) + apply_laboratory_scales(smear_refl, scale_s, scale_p) + return (smear_refl + bkg), smear_tran, components return None -def _smeared_reflectivity(q, w, tensor, energy, phi, resolution, backend="uni"): +def _smeared_reflectivity( + q, + w, + tensor, + energy, + phi, + resolution, + backend="uni", + *, + parallel: bool = False, +): """ Fast resolution smearing for constant dQ/Q. @@ -736,13 +764,9 @@ def _smeared_reflectivity(q, w, tensor, energy, phi, resolution, backend="uni"): ------- reflectivity: np.ndarray, np.ndarray, np.ndarray, """ + _ = (phi, backend) if resolution < 0.5: - if backend == "uni": - refl, tran, *components = uniaxial_reflectivity(q, w, tensor, energy) - else: - refl, tran, *components = uniaxial_reflectivity(q, w, tensor, energy, phi) # type: ignore - refl = np.asarray(refl, dtype=np.float64) - return refl, tran, components + return _uniaxial_reflectivity(q, w, tensor, energy, parallel=parallel) resolution /= 100 gaussnum = 51 @@ -767,11 +791,9 @@ def gauss(x, s): # resolution smear over [-4 sigma, 4 sigma] gauss_x = np.linspace(-1.7 * resolution, 1.7 * resolution, gaussnum) gauss_y = gauss(gauss_x, resolution / _FWHM) - if backend == "uni": - refl, tran, *components = uniaxial_reflectivity(xlin, w, tensor, energy) - else: - refl, tran, *components = uniaxial_reflectivity(xlin, w, tensor, energy) - refl = np.asarray(refl, dtype=np.float64) + refl, tran, components = _uniaxial_reflectivity( + xlin, w, tensor, energy, parallel=parallel + ) step = gauss_x[1] - gauss_x[0] smeared_ss = np.convolve(refl[:, 0, 0], gauss_y, mode="same") * step # type: ignore smeared_pp = np.convolve(refl[:, 1, 1], gauss_y, mode="same") * step # type: ignore diff --git a/src/refloxide/pxr/plugin/structure.py b/src/refloxide/pxr/plugin/structure.py index be10279..8035b4c 100644 --- a/src/refloxide/pxr/plugin/structure.py +++ b/src/refloxide/pxr/plugin/structure.py @@ -57,19 +57,15 @@ class Structure(UserList): Example ------- ```python - import pyref.fitting as fit + from refloxide.pxr.plugin.structure import MaterialSLD, SLD en = 284.4 # [eV] - # Create the materials for a given energy - vac = fit.PXR_MaterialSLD("", density=1, energy=en, name="vacuum") - si = fit.PXR_MaterialSLD("Si", density=2.33, energy=en, name="Si") - sio2 = fit.PXR_MaterialSLD("SiO2", density=2.4, energy=en, name="SiO2") - # Specify the complex index of refraction for a molecule - n_xx = complex(-0.0035, 0.0004) # [unitless] #Ordinary Axis - n_zz = complex(-0.0045, 0.0009) # [unitless] #Extraordinary Axis - molecule = fit.SLD(np.array([n_xx, n_zz]), name="material") # molecule - # Make the structure - # See 'PXR_Slab' for details on building layers + vac = MaterialSLD("", density=1, energy=en, name="vacuum") + si = MaterialSLD("Si", density=2.33, energy=en, name="Si") + sio2 = MaterialSLD("SiO2", density=2.4, energy=en, name="SiO2") + n_xx = complex(-0.0035, 0.0004) # ordinary axis + n_zz = complex(-0.0045, 0.0009) # extraordinary axis + molecule = SLD(np.array([n_xx, n_zz]), name="material") structure = vac(0, 0) | molecule(100, 2) | sio2(15, 1.5) | si(1, 1.5) ``` """ @@ -1935,7 +1931,8 @@ def compound_density(compound: str, *, desperate_lookup: bool = True) -> float: import matplotlib.pyplot as plt import pandas as pd # ty: ignore[unresolved-import] import seaborn as sns # ty: ignore[unresolved-import] - from pyref.fitting.model import ReflectModel # ty: ignore[unresolved-import] + + from refloxide.pxr.plugin.model import ReflectModel sns.set_palette("blend:#00829c,#ff9d8d", n_colors=3) @@ -1967,7 +1964,7 @@ def compound_density(compound: str, *, desperate_lookup: bool = True) -> float: name="ZnPc", )(196.441, 7.216) - struct: Structure = vac | znpc_slab | si - model = ReflectModel(struct) # type: ignore + struct = vac | znpc_slab | si + model = ReflectModel(struct) model.energy_offset = 2 print(struct) diff --git a/src/refloxide/python/__init__.py b/src/refloxide/python/__init__.py new file mode 100644 index 0000000..5b37b38 --- /dev/null +++ b/src/refloxide/python/__init__.py @@ -0,0 +1,19 @@ +"""Opt-in pure-Python implementations. + +Import a concrete submodule by name. This package does not re-export any +symbols, so default ``refloxide`` / ``refloxide.tmm`` / ``refloxide.model`` +callers cannot accidentally pick up the Python path. + +Examples +-------- +Pure-Python transfer-matrix kernel:: + + from refloxide.python.tmm import uniaxial_reflectivity + +Pure-Python modeling (pyref.fitting-shaped):: + + import refloxide.python.model as py + + vac = py.MaterialSLD("", density=0.0, energy=250.0, name="vacuum") + model = py.ReflectModel(vac(0, 0) | ..., energy=250.0, pol="sp") +""" diff --git a/src/refloxide/python/model.py b/src/refloxide/python/model.py new file mode 100644 index 0000000..d9ebe3a --- /dev/null +++ b/src/refloxide/python/model.py @@ -0,0 +1,55 @@ +"""Pure-Python modeling surface (pyref.fitting-shaped API). + +Import this submodule by name; :mod:`refloxide.python` does not re-export +these symbols:: + + import refloxide.python.model as py + + vacuum = py.MaterialSLD("", density=0.0, energy=250.0, name="vacuum") + model = py.ReflectModel(vacuum(0, 0) | ..., energy=250.0, pol="sp") + +Scatterers and :class:`ReflectModel` here evaluate reflectivity through +:mod:`refloxide.python.tmm` (pure Python), not the Rust kernels. Prefer +:mod:`refloxide.model` for the energy-deferred Rust-backed path. +""" + +from __future__ import annotations + +from refnx.analysis import CurveFitter, GlobalObjective, Objective, Transform +from refnx.dataset import ReflectDataset + +from refloxide.pxr.plugin.fitters import ( + AnisotropyObjective, + Fitter, + LogpExtra, +) +from refloxide.pxr.plugin.io import XrayReflectDataset +from refloxide.pxr.plugin.model import ReflectModel, reflectivity +from refloxide.pxr.plugin.structure import ( + SLD, + MaterialSLD, + MixedMaterialSlab, + Slab, + Structure, + UniTensorSLD, +) + +__all__ = [ + "SLD", + "AnisotropyObjective", + "CurveFitter", + "Fitter", + "GlobalObjective", + "LogpExtra", + "MaterialSLD", + "MixedMaterialSlab", + "Objective", + "ReflectDataset", + "ReflectModel", + "Slab", + "Structure", + "Transform", + "UniTensorSLD", + "XrayReflectDataset", + "reflectivity", +] diff --git a/src/refloxide/pxr/tjf4x4.py b/src/refloxide/python/tmm.py similarity index 99% rename from src/refloxide/pxr/tjf4x4.py rename to src/refloxide/python/tmm.py index e607381..43a7f35 100644 --- a/src/refloxide/pxr/tjf4x4.py +++ b/src/refloxide/python/tmm.py @@ -1,4 +1,8 @@ -"""Pure python port of the uniaxial 4x4 matrix formalism. +"""Pure-Python uniaxial 4x4 transfer-matrix kernels. + +Opt-in counterpart to :mod:`refloxide.tmm` (Rust). Import this module by name:: + + from refloxide.python.tmm import uniaxial_reflectivity This algorithm is used for the following publications: - https://doi.org/10.1021/acsami.1c19948 diff --git a/src/refloxide/rust.pyi b/src/refloxide/rust.pyi index d5f328d..acdad59 100644 --- a/src/refloxide/rust.pyi +++ b/src/refloxide/rust.pyi @@ -2,7 +2,7 @@ The native implementation is produced from ``src/lib.rs`` via PyO3 and ``maturin``. Function shapes and conventions mirror -``refloxide.pxr.tjf4x4.uniaxial_reflectivity`` so the two implementations +``refloxide.python.tmm.uniaxial_reflectivity`` so the two implementations can be exchanged without changing call sites. """ @@ -45,7 +45,7 @@ def uniaxial_reflectivity( ------- refl Real power reflectance with shape ``(numpnts, 2, 2)``. Index - layout matches ``refloxide.pxr.tjf4x4.uniaxial_reflectivity``: + layout matches ``refloxide.python.tmm.uniaxial_reflectivity``: ``refl[:, 0, 0] = R_ss``, ``refl[:, 1, 1] = R_pp``, ``refl[:, 0, 1] = R_sp``, ``refl[:, 1, 0] = R_ps``. tran diff --git a/tests/test_bookended_fused.py b/tests/test_bookended_fused.py index 9bb637d..b083893 100644 --- a/tests/test_bookended_fused.py +++ b/tests/test_bookended_fused.py @@ -14,7 +14,6 @@ import pandas as pd import pytest -from refloxide.integrations.pyref import uniaxial_reflectivity from refloxide.pxr.energy.bookended import ( BookendedOrientationProfile, bookended_from_three_slabs, @@ -22,6 +21,7 @@ from refloxide.pxr.energy.fused import evaluate_fused_bookended_reflectivity from refloxide.pxr.energy.ooc import OocAnchor from refloxide.pxr.plugin.structure import MaterialSLD, Slab, Structure, UniTensorSLD +from refloxide.tmm import uniaxial_reflectivity def _ooc_frame() -> pd.DataFrame: