reflectosim 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Isaac Yong
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: reflectosim
3
+ Version: 0.1.0
4
+ Summary: X-ray reflectivity forward physics and synthetic data generation
5
+ Keywords: x-ray reflectivity,XRR,simulation,synthetic data,machine learning
6
+ Author: Isaac Yong
7
+ Author-email: Isaac Yong <isaacyong@naver.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 2 - Pre-Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Topic :: Scientific/Engineering :: Physics
18
+ Classifier: Typing :: Typed
19
+ Requires-Dist: numpy>=2.0
20
+ Requires-Dist: numba>=0.61
21
+ Requires-Dist: scipy>=1.13
22
+ Requires-Dist: periodictable>=2.1.0
23
+ Requires-Dist: h5py>=3.10 ; extra == 'hdf5'
24
+ Requires-Dist: torch>=2.5 ; extra == 'torch'
25
+ Requires-Python: >=3.12
26
+ Project-URL: Homepage, https://github.com/NaNIsaac/ReflectoSim
27
+ Project-URL: Repository, https://github.com/NaNIsaac/ReflectoSim
28
+ Project-URL: Issues, https://github.com/NaNIsaac/ReflectoSim/issues
29
+ Project-URL: Changelog, https://github.com/NaNIsaac/ReflectoSim/blob/main/CHANGELOG.md
30
+ Provides-Extra: hdf5
31
+ Provides-Extra: torch
32
+ Description-Content-Type: text/markdown
33
+
34
+ # ReflectoSim
35
+
36
+ [![Tests](https://github.com/NaNIsaac/ReflectoSim/actions/workflows/ci.yml/badge.svg)](https://github.com/NaNIsaac/ReflectoSim/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
37
+
38
+ <img src="https://raw.githubusercontent.com/NaNIsaac/ReflectoSim/main/docs/media/curves.png" width="720" alt="Four curves drawn from examples/lab_films.toml: the reflectivity before noise and the Poisson counts">
39
+
40
+ Realistic synthetic X-ray reflectivity (XRR) data for machine learning: priors over layer stacks and
41
+ instruments, counting noise, and a dataset format that records how to regenerate it.
42
+
43
+ It is for anyone training or testing an inverse model for XRR who needs simulated curves that look
44
+ like measured ones: random multilayers with roughness tied to thickness and a minimum contrast
45
+ between neighbours, a native oxide placed where you say, Gaussian resolution, footprint, Kα₂, 2θ
46
+ offset, background and Poisson counts. The package holds all the forward physics (Parratt
47
+ recursion with Névot-Croce roughness, the resolution integral, a differentiable torch forward) and
48
+ the generation on top of it. [ReflectoFit](https://github.com/NaNIsaac/ReflectoFit), the fitting
49
+ engine, depends on it, so a network trained on these curves and a fit of the same measurement use
50
+ the same physics.
51
+
52
+ Status: early development.
53
+
54
+ ## Visual overview
55
+
56
+ ![ReflectoSim prior-to-dataset pipeline](docs/media/pipeline.svg)
57
+
58
+ The **example prior bounds** below are configurable input ranges, not empirical distributions. Coupled thickness/roughness and SLD-contrast constraints can alter the accepted distributions.
59
+
60
+ ![Parameter bounds from examples/lab_films.toml](docs/media/prior-ranges.svg)
61
+
62
+ The XRR curves at the top of this README are generated by the simulator; solid curves show the pre-noise signal and dots show normalized Poisson counts. For a detailed interpretation, the layer-order diagram, units, reproducibility and caveats, see the [visual guide](docs/visual-guide.md).
63
+
64
+ ## Install
65
+
66
+ Not on PyPI yet. From a clone, with [uv](https://docs.astral.sh/uv/):
67
+
68
+ ```sh
69
+ git clone https://github.com/NaNIsaac/ReflectoSim
70
+ cd ReflectoSim
71
+ uv sync
72
+ ```
73
+
74
+ ## First run
75
+
76
+ Simulate 1000 curves of one to three films on SiO₂ on Si from the example prior, then summarise the
77
+ file:
78
+
79
+ <!-- test -->
80
+ ```sh
81
+ uv run reflectosim sim --prior examples/lab_films.toml --n 1000 --seed 20261024,1 -o train.npz
82
+ uv run reflectosim describe train.npz
83
+ ```
84
+
85
+ `train.npz` holds `q`, `counts`, `reflectivity` (before noise), `counts_scale` and one array per
86
+ parameter (`layer0.thickness`, `oxide.sld`, `tth_offset`, ...; NaN where a curve has fewer layers).
87
+ Its metadata holds the priors, the seed and the versions; `reflectosim.priors.from_metadata` turns it
88
+ back into priors and the same seed gives the same file. Use a `.h5` name for HDF5 (`uv sync --extra
89
+ hdf5`).
90
+
91
+ ## Priors
92
+
93
+ A prior is a TOML file; `examples/lab_films.toml` shows every field. The native oxide position has no
94
+ default: `oxide = "under"` puts it on the substrate, `"over"` on top of the films, `"none"` leaves it
95
+ out. The two orders give curves that differ by tens of percent, so every prior states its choice.
96
+
97
+ ## Python
98
+
99
+ ```python
100
+ import numpy as np
101
+ from reflectosim import InstrumentPrior, StackPrior, simulate, tth_to_q
102
+
103
+ stack = StackPrior(
104
+ n_layers=(1, 3),
105
+ thickness=(20, 250),
106
+ sld=(5, 80),
107
+ roughness=(2, 10),
108
+ min_contrast=4.0,
109
+ oxide="none",
110
+ )
111
+ instrument = InstrumentPrior(
112
+ sigma_q=(5e-4, 3e-3), tth_offset=(-0.01, 0.01), log_background=(-8.5, -7.0), log_counts=(6, 8)
113
+ )
114
+ batch = simulate(
115
+ stack, instrument, tth_to_q(np.linspace(0.05, 8, 500)), n=10_000, seed=(20261024, 1)
116
+ )
117
+ batch.counts, batch.reflectivity, batch.params["layer0.thickness"]
118
+ ```
119
+
120
+ ## Development
121
+
122
+ ```sh
123
+ uv run pytest
124
+ ```
125
+
126
+ ## License
127
+
128
+ MIT
@@ -0,0 +1,95 @@
1
+ # ReflectoSim
2
+
3
+ [![Tests](https://github.com/NaNIsaac/ReflectoSim/actions/workflows/ci.yml/badge.svg)](https://github.com/NaNIsaac/ReflectoSim/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
4
+
5
+ <img src="https://raw.githubusercontent.com/NaNIsaac/ReflectoSim/main/docs/media/curves.png" width="720" alt="Four curves drawn from examples/lab_films.toml: the reflectivity before noise and the Poisson counts">
6
+
7
+ Realistic synthetic X-ray reflectivity (XRR) data for machine learning: priors over layer stacks and
8
+ instruments, counting noise, and a dataset format that records how to regenerate it.
9
+
10
+ It is for anyone training or testing an inverse model for XRR who needs simulated curves that look
11
+ like measured ones: random multilayers with roughness tied to thickness and a minimum contrast
12
+ between neighbours, a native oxide placed where you say, Gaussian resolution, footprint, Kα₂, 2θ
13
+ offset, background and Poisson counts. The package holds all the forward physics (Parratt
14
+ recursion with Névot-Croce roughness, the resolution integral, a differentiable torch forward) and
15
+ the generation on top of it. [ReflectoFit](https://github.com/NaNIsaac/ReflectoFit), the fitting
16
+ engine, depends on it, so a network trained on these curves and a fit of the same measurement use
17
+ the same physics.
18
+
19
+ Status: early development.
20
+
21
+ ## Visual overview
22
+
23
+ ![ReflectoSim prior-to-dataset pipeline](docs/media/pipeline.svg)
24
+
25
+ The **example prior bounds** below are configurable input ranges, not empirical distributions. Coupled thickness/roughness and SLD-contrast constraints can alter the accepted distributions.
26
+
27
+ ![Parameter bounds from examples/lab_films.toml](docs/media/prior-ranges.svg)
28
+
29
+ The XRR curves at the top of this README are generated by the simulator; solid curves show the pre-noise signal and dots show normalized Poisson counts. For a detailed interpretation, the layer-order diagram, units, reproducibility and caveats, see the [visual guide](docs/visual-guide.md).
30
+
31
+ ## Install
32
+
33
+ Not on PyPI yet. From a clone, with [uv](https://docs.astral.sh/uv/):
34
+
35
+ ```sh
36
+ git clone https://github.com/NaNIsaac/ReflectoSim
37
+ cd ReflectoSim
38
+ uv sync
39
+ ```
40
+
41
+ ## First run
42
+
43
+ Simulate 1000 curves of one to three films on SiO₂ on Si from the example prior, then summarise the
44
+ file:
45
+
46
+ <!-- test -->
47
+ ```sh
48
+ uv run reflectosim sim --prior examples/lab_films.toml --n 1000 --seed 20261024,1 -o train.npz
49
+ uv run reflectosim describe train.npz
50
+ ```
51
+
52
+ `train.npz` holds `q`, `counts`, `reflectivity` (before noise), `counts_scale` and one array per
53
+ parameter (`layer0.thickness`, `oxide.sld`, `tth_offset`, ...; NaN where a curve has fewer layers).
54
+ Its metadata holds the priors, the seed and the versions; `reflectosim.priors.from_metadata` turns it
55
+ back into priors and the same seed gives the same file. Use a `.h5` name for HDF5 (`uv sync --extra
56
+ hdf5`).
57
+
58
+ ## Priors
59
+
60
+ A prior is a TOML file; `examples/lab_films.toml` shows every field. The native oxide position has no
61
+ default: `oxide = "under"` puts it on the substrate, `"over"` on top of the films, `"none"` leaves it
62
+ out. The two orders give curves that differ by tens of percent, so every prior states its choice.
63
+
64
+ ## Python
65
+
66
+ ```python
67
+ import numpy as np
68
+ from reflectosim import InstrumentPrior, StackPrior, simulate, tth_to_q
69
+
70
+ stack = StackPrior(
71
+ n_layers=(1, 3),
72
+ thickness=(20, 250),
73
+ sld=(5, 80),
74
+ roughness=(2, 10),
75
+ min_contrast=4.0,
76
+ oxide="none",
77
+ )
78
+ instrument = InstrumentPrior(
79
+ sigma_q=(5e-4, 3e-3), tth_offset=(-0.01, 0.01), log_background=(-8.5, -7.0), log_counts=(6, 8)
80
+ )
81
+ batch = simulate(
82
+ stack, instrument, tth_to_q(np.linspace(0.05, 8, 500)), n=10_000, seed=(20261024, 1)
83
+ )
84
+ batch.counts, batch.reflectivity, batch.params["layer0.thickness"]
85
+ ```
86
+
87
+ ## Development
88
+
89
+ ```sh
90
+ uv run pytest
91
+ ```
92
+
93
+ ## License
94
+
95
+ MIT
@@ -0,0 +1,85 @@
1
+ [project]
2
+ name = "reflectosim"
3
+ version = "0.1.0"
4
+ description = "X-ray reflectivity forward physics and synthetic data generation"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ keywords = [
9
+ "x-ray reflectivity",
10
+ "XRR",
11
+ "simulation",
12
+ "synthetic data",
13
+ "machine learning",
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 2 - Pre-Alpha",
17
+ "Intended Audience :: Science/Research",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Programming Language :: Python :: 3.14",
22
+ "Operating System :: OS Independent",
23
+ "Topic :: Scientific/Engineering :: Physics",
24
+ "Typing :: Typed",
25
+ ]
26
+ requires-python = ">=3.12"
27
+ dependencies = [
28
+ "numpy>=2.0",
29
+ "numba>=0.61",
30
+ "scipy>=1.13",
31
+ "periodictable>=2.1.0",
32
+ ]
33
+
34
+ [[project.authors]]
35
+ name = "Isaac Yong"
36
+ email = "isaacyong@naver.com"
37
+
38
+ [project.optional-dependencies]
39
+ hdf5 = ["h5py>=3.10"]
40
+ torch = ["torch>=2.5"]
41
+
42
+ [project.scripts]
43
+ reflectosim = "reflectosim.cli:main"
44
+
45
+ [project.urls]
46
+ Homepage = "https://github.com/NaNIsaac/ReflectoSim"
47
+ Repository = "https://github.com/NaNIsaac/ReflectoSim"
48
+ Issues = "https://github.com/NaNIsaac/ReflectoSim/issues"
49
+ Changelog = "https://github.com/NaNIsaac/ReflectoSim/blob/main/CHANGELOG.md"
50
+
51
+ [dependency-groups]
52
+ dev = [
53
+ "pytest>=8.0",
54
+ "h5py>=3.10",
55
+ "refnx>=0.1.60",
56
+ "jax>=0.4.30",
57
+ "torch>=2.5",
58
+ ]
59
+
60
+ [[tool.uv.index]]
61
+ name = "pytorch-cpu"
62
+ url = "https://download.pytorch.org/whl/cpu"
63
+ explicit = true
64
+
65
+ [[tool.uv.sources.torch]]
66
+ index = "pytorch-cpu"
67
+
68
+ [tool.pytest.ini_options]
69
+ testpaths = ["tests"]
70
+ pythonpath = ["."]
71
+
72
+ [tool.ruff]
73
+ line-length = 100
74
+ extend-exclude = ["tests/reference/reflectolearn_abeles.py"]
75
+
76
+ [tool.ruff.lint]
77
+ extend-select = [
78
+ "B",
79
+ "I",
80
+ "UP",
81
+ ]
82
+
83
+ [build-system]
84
+ requires = ["uv_build>=0.12.7,<0.13"]
85
+ build-backend = "uv_build"
@@ -0,0 +1,74 @@
1
+ [project]
2
+ name = "reflectosim"
3
+ version = "0.1.0"
4
+ description = "X-ray reflectivity forward physics and synthetic data generation"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [{ name = "Isaac Yong", email = "isaacyong@naver.com" }]
9
+ keywords = ["x-ray reflectivity", "XRR", "simulation", "synthetic data", "machine learning"]
10
+ classifiers = [
11
+ "Development Status :: 2 - Pre-Alpha",
12
+ "Intended Audience :: Science/Research",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.12",
15
+ "Programming Language :: Python :: 3.13",
16
+ "Programming Language :: Python :: 3.14",
17
+ "Operating System :: OS Independent",
18
+ "Topic :: Scientific/Engineering :: Physics",
19
+ "Typing :: Typed",
20
+ ]
21
+ requires-python = ">=3.12"
22
+ dependencies = [
23
+ "numpy>=2.0",
24
+ "numba>=0.61",
25
+ "scipy>=1.13",
26
+ "periodictable>=2.1.0",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ hdf5 = ["h5py>=3.10"]
31
+ torch = ["torch>=2.5"]
32
+
33
+ [project.scripts]
34
+ reflectosim = "reflectosim.cli:main"
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/NaNIsaac/ReflectoSim"
38
+ Repository = "https://github.com/NaNIsaac/ReflectoSim"
39
+ Issues = "https://github.com/NaNIsaac/ReflectoSim/issues"
40
+ Changelog = "https://github.com/NaNIsaac/ReflectoSim/blob/main/CHANGELOG.md"
41
+
42
+ [dependency-groups]
43
+ dev = [
44
+ "pytest>=8.0",
45
+ "h5py>=3.10",
46
+ "refnx>=0.1.60",
47
+ "jax>=0.4.30",
48
+ "torch>=2.5",
49
+ ]
50
+
51
+ # CPU torch wheels for this project's own lock (tests, CI); installs from PyPI are not affected.
52
+ [[tool.uv.index]]
53
+ name = "pytorch-cpu"
54
+ url = "https://download.pytorch.org/whl/cpu"
55
+ explicit = true
56
+
57
+ [tool.uv.sources]
58
+ torch = [{ index = "pytorch-cpu" }]
59
+
60
+ [build-system]
61
+ requires = ["uv_build>=0.12.7,<0.13"]
62
+ build-backend = "uv_build"
63
+
64
+ [tool.pytest.ini_options]
65
+ testpaths = ["tests"]
66
+ pythonpath = ["."]
67
+
68
+ [tool.ruff]
69
+ line-length = 100
70
+ # verbatim copies of other engines, compared against in tests/test_reference_engines.py
71
+ extend-exclude = ["tests/reference/reflectolearn_abeles.py"]
72
+
73
+ [tool.ruff.lint]
74
+ extend-select = ["B", "I", "UP"]
@@ -0,0 +1,32 @@
1
+ """X-ray reflectivity forward physics and realistic synthetic data for machine learning."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ from reflectosim import materials
6
+ from reflectosim.forward import Instrument, forward
7
+ from reflectosim.priors import InstrumentPrior, OxidePrior, StackPrior, SubstratePrior
8
+ from reflectosim.simulate import Batch, simulate
9
+ from reflectosim.structure import Layer, Stack
10
+ from reflectosim.units import CU_KALPHA1, CU_KALPHA2, fwhm_to_sigma, q_to_tth, tth_to_q
11
+
12
+ __version__ = version("reflectosim")
13
+
14
+ __all__ = [
15
+ "CU_KALPHA1",
16
+ "CU_KALPHA2",
17
+ "Batch",
18
+ "Instrument",
19
+ "InstrumentPrior",
20
+ "Layer",
21
+ "OxidePrior",
22
+ "Stack",
23
+ "StackPrior",
24
+ "SubstratePrior",
25
+ "__version__",
26
+ "forward",
27
+ "fwhm_to_sigma",
28
+ "materials",
29
+ "q_to_tth",
30
+ "simulate",
31
+ "tth_to_q",
32
+ ]
@@ -0,0 +1,76 @@
1
+ """Command line: reflectosim sim | describe."""
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+
7
+ import numpy as np
8
+
9
+ from reflectosim import __version__, dataset, priors
10
+ from reflectosim.grid import q_grid
11
+ from reflectosim.simulate import simulate
12
+
13
+
14
+ def _seed(text: str):
15
+ parts = [int(p) for p in text.split(",")]
16
+ return parts[0] if len(parts) == 1 else tuple(parts)
17
+
18
+
19
+ def _sim(args) -> int:
20
+ stack, inst = priors.load(args.prior)
21
+ q = q_grid(args.grid, inst.wavelength)
22
+ batch = simulate(stack, inst, q, n=args.n, seed=_seed(args.seed))
23
+ out = dataset.write(args.out, batch, stack, inst)
24
+ print(f"wrote {out}: {args.n} curves x {q.size} points, oxide {stack.oxide}, "
25
+ f"resolution nodes {batch.nodes or 'per point'}") # fmt: skip
26
+ return 0
27
+
28
+
29
+ def _describe(args) -> int:
30
+ arrays, params, meta = dataset.read(args.dataset)
31
+ n, nq = arrays["counts"].shape
32
+ print(
33
+ f"{args.dataset}: {n} curves x {nq} points, q {arrays['q'][0]:.4g}-{arrays['q'][-1]:.4g} 1/A"
34
+ )
35
+ width = max(len(k) for k in params)
36
+ for k, v in params.items():
37
+ ok = np.isfinite(v)
38
+ if ok.any():
39
+ print(f" {k:<{width}} {v[ok].min():.4g} .. {v[ok].max():.4g} ({ok.sum()} rows)")
40
+ print(
41
+ json.dumps(
42
+ {
43
+ k: meta[k]
44
+ for k in ("reflectosim", "reflectofit", "seed", "resolution_nodes")
45
+ if k in meta
46
+ }
47
+ )
48
+ )
49
+ return 0
50
+
51
+
52
+ def main(argv: list[str] | None = None) -> int:
53
+ p = argparse.ArgumentParser(prog="reflectosim", description="synthetic X-ray reflectivity data")
54
+ p.add_argument("--version", action="version", version=f"reflectosim {__version__}")
55
+ sub = p.add_subparsers(dest="command", required=True)
56
+
57
+ s = sub.add_parser("sim", help="simulate a dataset from a prior TOML")
58
+ s.add_argument(
59
+ "--prior", required=True, help="prior TOML: [stack], [oxide], [substrate], [instrument]"
60
+ )
61
+ s.add_argument("--n", type=int, required=True, help="number of curves")
62
+ s.add_argument("--grid", default="tth:0.05:8:500", help="tth:START:STOP:N or q:START:STOP:N")
63
+ s.add_argument("--seed", default="0", help="an int or comma-separated ints, e.g. 20261024,1")
64
+ s.add_argument("-o", "--out", required=True, help="output .h5 (needs h5py) or .npz")
65
+ s.set_defaults(run=_sim)
66
+
67
+ d = sub.add_parser("describe", help="summarise a dataset file")
68
+ d.add_argument("dataset")
69
+ d.set_defaults(run=_describe)
70
+
71
+ args = p.parse_args(argv)
72
+ return args.run(args)
73
+
74
+
75
+ if __name__ == "__main__":
76
+ sys.exit(main())
@@ -0,0 +1,84 @@
1
+ """Dataset files: HDF5 (with the `hdf5` extra) or compressed npz, chosen by the file suffix.
2
+
3
+ Arrays: `q` (Q,), `counts` and `reflectivity` (n, Q), `counts_scale` (n,), and one (n,) array per
4
+ parameter under `params/<name>` (HDF5) or `params.<name>` (npz). The metadata is one JSON string with
5
+ the priors, the seed, the resolution node count, the package versions and the conventions, enough
6
+ to regenerate the file.
7
+ """
8
+
9
+ import json
10
+ from importlib.metadata import version
11
+ from pathlib import Path
12
+
13
+ import numpy as np
14
+
15
+ from reflectosim import priors
16
+ from reflectosim.simulate import Batch
17
+
18
+ CONVENTIONS = (
19
+ "ambient first, roughness of the interface above, Im SLD > 0 "
20
+ "absorbs, SLD 1e-6 A^-2, q A^-1, angles in degrees; reflectivity is I0 * smeared R + background "
21
+ "before noise; counts are Poisson(counts_scale * reflectivity)"
22
+ )
23
+
24
+
25
+ def metadata(batch: Batch, stack: priors.StackPrior, instrument: priors.InstrumentPrior) -> dict:
26
+ seed = list(batch.seed) if isinstance(batch.seed, tuple | list) else batch.seed
27
+ return {
28
+ "reflectosim": version("reflectosim"),
29
+ "stack_prior": priors.to_dict(stack),
30
+ "instrument_prior": priors.to_dict(instrument),
31
+ "seed": seed,
32
+ "n": int(batch.counts.shape[0]),
33
+ "resolution_nodes": batch.nodes,
34
+ "backend": "reflectosim numba float64",
35
+ "conventions": CONVENTIONS,
36
+ }
37
+
38
+
39
+ def write(
40
+ path: str | Path, batch: Batch, stack: priors.StackPrior, instrument: priors.InstrumentPrior
41
+ ) -> Path:
42
+ path = Path(path)
43
+ meta = json.dumps(metadata(batch, stack, instrument))
44
+ arrays = {
45
+ "q": batch.q,
46
+ "counts": batch.counts.astype(np.float32),
47
+ "reflectivity": batch.reflectivity.astype(np.float32),
48
+ "counts_scale": batch.counts_scale,
49
+ }
50
+ if path.suffix in (".h5", ".hdf5"):
51
+ import h5py
52
+
53
+ with h5py.File(path, "w") as f:
54
+ for k, v in arrays.items():
55
+ f.create_dataset(k, data=v, compression="gzip" if v.ndim == 2 else None)
56
+ g = f.create_group("params")
57
+ for k, v in batch.params.items():
58
+ g.create_dataset(k, data=v)
59
+ f.attrs["metadata"] = meta
60
+ elif path.suffix == ".npz":
61
+ np.savez_compressed(
62
+ path, **arrays, **{f"params.{k}": v for k, v in batch.params.items()}, metadata=meta
63
+ )
64
+ else:
65
+ raise ValueError(f"unknown dataset suffix {path.suffix!r}; use .h5 or .npz")
66
+ return path
67
+
68
+
69
+ def read(path: str | Path) -> tuple[dict[str, np.ndarray], dict[str, np.ndarray], dict]:
70
+ """(arrays, params, metadata) of a dataset file."""
71
+ path = Path(path)
72
+ if path.suffix in (".h5", ".hdf5"):
73
+ import h5py
74
+
75
+ with h5py.File(path, "r") as f:
76
+ arrays = {k: f[k][()] for k in ("q", "counts", "reflectivity", "counts_scale")}
77
+ params = {k: f["params"][k][()] for k in f["params"]}
78
+ meta = json.loads(f.attrs["metadata"])
79
+ return arrays, params, meta
80
+ with np.load(path) as z:
81
+ arrays = {k: z[k] for k in ("q", "counts", "reflectivity", "counts_scale")}
82
+ params = {k.removeprefix("params."): z[k] for k in z.files if k.startswith("params.")}
83
+ meta = json.loads(str(z["metadata"]))
84
+ return arrays, params, meta