BornSim 0.2.6__py3-none-any.whl

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.
bornsim/ensemble.py ADDED
@@ -0,0 +1,253 @@
1
+ """Numerical ensemble quadrature and realization sampling uncertainty."""
2
+
3
+ import numpy as np
4
+ from ._validation import _integer
5
+ from .media import Medium
6
+ from .grid import Grid
7
+ from .sampling import AngularSampling
8
+ from .ensemble_sampling import EnsembleSampling
9
+ from .series import BornSeries
10
+ from .units import Quantity, validate_units, ureg
11
+
12
+
13
+ def ensemble_scattering(
14
+ *,
15
+ medium: Medium,
16
+ wavelength: Quantity,
17
+ grid=None,
18
+ sampling=None,
19
+ shape=None,
20
+ spacing=None,
21
+ order=3,
22
+ ensemble_sampling=None,
23
+ realizations=None,
24
+ seed=None,
25
+ angles=None,
26
+ azimuth_samples=None,
27
+ polar_samples=None,
28
+ ):
29
+ """Retain directional intensities and integrate sampled-volume scattering.
30
+
31
+ Parameters
32
+ ----------
33
+ medium : Medium
34
+ Random medium or structure with a random background, sampled at
35
+ consecutive seeds. Deterministic volumes use Solver.solve.
36
+ grid : Grid, optional
37
+ Shared spatial configuration. Cannot be combined with shape or spacing.
38
+ sampling : AngularSampling, optional
39
+ Shared output and integration settings. Cannot be combined with
40
+ individual angular keywords.
41
+ wavelength : Quantity
42
+ Positive, finite vacuum wavelength; length values require explicit units.
43
+ shape : tuple of int, optional
44
+ Three grid dimensions, each from 2 to 32. Default is (12, 12, 12).
45
+ spacing : Quantity, optional
46
+ Positive, finite cubic voxel spacing; length values require explicit units.
47
+ Default is 50 nm.
48
+ order : int, optional
49
+ Highest cumulative Born order, from 1 to 12. Default is 3.
50
+ realizations : int, optional
51
+ Independent sample count, from 1 to 32. Default is 4.
52
+ seed : int, optional
53
+ First seed, from 0 to 2**32 - 1. Default is 0. Consecutive seeds are
54
+ used, and the last seed must also lie in this range.
55
+ angles : array_like or Quantity, optional
56
+ One-dimensional plot angles in [0, pi], with 1 to 181 observations.
57
+ Angular values require explicit units. Default is 121 evenly spaced angles.
58
+ azimuth_samples : int, optional
59
+ Uniform azimuth sample count, from 4 to 32. Default is 8.
60
+ polar_samples : int, optional
61
+ Gauss-Legendre node count, from 16 to 128. Default is 32. Integration
62
+ nodes are independent of the supplied plot angles.
63
+
64
+ Returns
65
+ -------
66
+ ensemble : dict
67
+ Numeric SI arrays and metadata with the following keys:
68
+
69
+ * ``angles`` : radians, shape (observation,).
70
+ * ``azimuths`` : radians, shape (azimuth,), uniform in [0, 2*pi).
71
+ * ``directional_differential`` : cumulative intensities in
72
+ m^-1 sr^-1, shape (order, observation, azimuth), averaged only
73
+ over realizations, preserving directional asymmetry.
74
+ * ``mean`` : azimuth-averaged cumulative scattering in m^-1 sr^-1, shape
75
+ (order, observation).
76
+ * ``stderr`` : standard error of mean curves, same shape and units;
77
+ NaN for one realization.
78
+ * ``terms`` : mean isolated-term curves, same shape and units.
79
+ * ``mu_s`` and ``mu_s_prime`` : finite-sample effective total and
80
+ reduced scattering coefficients in m^-1, shape (order,).
81
+ * ``g`` : dimensionless anisotropy, shape (order,); NaN for zero scattering.
82
+ * ``field_norms`` : relative norms, shape (realization, order).
83
+ * ``warnings`` : sorted tuple of numerical diagnostic messages.
84
+ * ``realizations`` : independent sample count.
85
+
86
+ Raises
87
+ ------
88
+ ValueError
89
+ If grid, wavelength, units, angles, order, sample counts, or seed range
90
+ are invalid, generated linearized permittivity is nonpositive, or the
91
+ synchronous work estimate exceeds 100 million voxel-direction-order
92
+ operations across all realizations.
93
+
94
+ See Also
95
+ --------
96
+ bornsim.media.Medium.to_volume : Generate each seeded or deterministic sample.
97
+ bornsim.series.BornSeries : Compute per-sample coherent amplitudes and intensities.
98
+ bornsim.solver.Solver.ensemble : Return unitful ensemble data with plotting.
99
+
100
+ Notes
101
+ -----
102
+ Interference is preserved within each realization. Intensities, not random
103
+ amplitudes, are then averaged over realizations and azimuth. Standard
104
+ errors use the sample standard deviation divided by the square root of
105
+ the realization count; they exclude discretization and finite-size bias.
106
+ Anisotropy is computed from averaged angular moments.
107
+ Directional data retain each azimuth for the 3D phase surface;
108
+ one-dimensional curves remain azimuth averages. With one random sample,
109
+ sampling errors are unknown (NaN). A deterministic structure has no
110
+ realization sampling and must use Solver.solve.
111
+
112
+ The coefficients are finite-sample cross sections divided by volume,
113
+ not automatically infinite-medium transport coefficients. Check grid,
114
+ angular integration, sample size, synthesis box, and realization count.
115
+ """
116
+
117
+ if not isinstance(medium, Medium):
118
+ raise TypeError("medium must be a Medium.")
119
+
120
+ if not medium.is_random:
121
+ raise ValueError("Ensembles require a random medium or background; use Solver.solve for a fixed volume.")
122
+
123
+ validate_units(
124
+ wavelength,
125
+ unit="meter",
126
+ name="wavelength",
127
+ scalar=True,
128
+ )
129
+
130
+ grid = Grid._resolve(
131
+ grid=grid,
132
+ shape=shape,
133
+ spacing=spacing,
134
+ )
135
+
136
+ sampling = AngularSampling._resolve(
137
+ sampling=sampling,
138
+ angles=angles,
139
+ polar_samples=polar_samples,
140
+ azimuth_samples=azimuth_samples,
141
+ )
142
+
143
+ shape, spacing = grid.shape, grid.spacing
144
+
145
+ ensemble_sampling = EnsembleSampling._resolve(
146
+ ensemble_sampling=ensemble_sampling,
147
+ realizations=realizations,
148
+ seed=seed,
149
+ )
150
+
151
+ realizations = ensemble_sampling.realizations
152
+
153
+ order = _integer(value=order, name="order", low=1, high=12)
154
+
155
+ sampling.check_work(grid=grid, order=order, realizations=realizations)
156
+
157
+ if medium.background_refractive_index is None:
158
+ raise ValueError("Set an explicit background refractive index before solving an ensemble.")
159
+
160
+ engine = BornSeries(
161
+ grid=grid,
162
+ background_refractive_index=medium.background_refractive_index,
163
+ wavelength=wavelength,
164
+ directions=sampling.directions,
165
+ order=order,
166
+ )
167
+
168
+ notices: set[str]
169
+
170
+ curve_samples, directional_samples, directional_terms, terms, integrals, norms, notices = (
171
+ [],
172
+ [],
173
+ [],
174
+ [],
175
+ [],
176
+ [],
177
+ set(),
178
+ )
179
+
180
+ for sample_seed in ensemble_sampling.seeds:
181
+ volume = medium.to_volume(
182
+ grid=grid,
183
+ seed=sample_seed,
184
+ )
185
+
186
+ result = engine.solve(volume=volume)
187
+
188
+ sampled = sampling.summarize(result=result)
189
+
190
+ directional_samples.append(sampled["directional_differential"])
191
+
192
+ curve_samples.append(sampled["mean"])
193
+
194
+ terms.append(sampled["terms"])
195
+
196
+ directional_terms.append(sampled["directional_terms"])
197
+
198
+ integrals.append(sampled["integrals"])
199
+
200
+ norms.append(result.field_norms)
201
+
202
+ notices.update(result.warnings)
203
+
204
+ curves = np.asarray(curve_samples)
205
+
206
+ integrated = np.mean(integrals, axis=0)
207
+
208
+ mu = integrated[:, 0]
209
+
210
+ anisotropy = np.divide(integrated[:, 1], mu, out=np.full_like(mu, np.nan), where=mu != 0)
211
+
212
+ metadata = medium.metadata
213
+
214
+ statistics = metadata.get("background", metadata)
215
+
216
+ correlation_length_m = statistics.get("correlation_length_m")
217
+
218
+ correlation_length = None if correlation_length_m is None else correlation_length_m * ureg.meter
219
+
220
+ if correlation_length is not None:
221
+ if spacing > correlation_length / 2:
222
+ notices.add("Correlation length is poorly resolved; refine voxel spacing.")
223
+
224
+ if min(shape) * spacing < 6 * correlation_length:
225
+ notices.add("Sample is smaller than six correlation lengths; check finite-size and synthesis-box effects.")
226
+
227
+ stderr = curves.std(axis=0, ddof=1) / np.sqrt(realizations) if realizations > 1 else np.full_like(curves[0], np.nan)
228
+
229
+ directional = np.asarray(directional_samples)
230
+
231
+ directional_stderr = (
232
+ directional.std(axis=0, ddof=1) / np.sqrt(realizations)
233
+ if realizations > 1
234
+ else np.full_like(directional[0], np.nan)
235
+ )
236
+
237
+ return {
238
+ "angles": sampling.angles.copy(),
239
+ "azimuths": sampling.azimuths,
240
+ "directional_differential": directional.mean(axis=0),
241
+ "directional_terms": np.mean(directional_terms, axis=0),
242
+ "directional_stderr": directional_stderr,
243
+ "mean": curves.mean(axis=0),
244
+ "stderr": stderr,
245
+ "terms": np.mean(terms, axis=0),
246
+ "mu_s": mu,
247
+ "g": anisotropy,
248
+ "mu_s_prime": integrated[:, 2],
249
+ "field_norms": np.asarray(norms),
250
+ "warnings": tuple(sorted(notices)),
251
+ "realizations": realizations,
252
+ "seeds": ensemble_sampling.seeds,
253
+ }
@@ -0,0 +1,75 @@
1
+ """Reproducible independent-realization sampling configurations."""
2
+
3
+ from dataclasses import dataclass
4
+ import warnings
5
+ from ._validation import _integer
6
+
7
+
8
+ @dataclass(frozen=True, kw_only=True)
9
+ class EnsembleSampling:
10
+ """Choose consecutive seeds or an explicit ordered set of distinct seeds.
11
+
12
+ realizations defaults to four and seed to zero when seeds is omitted.
13
+ Explicit seeds cannot be combined with realizations or seed. Seeds range
14
+ from zero to 2**32-1; there are 1 to 32 independent realizations. Duplicate
15
+ seeds are rejected because they do not provide independent uncertainty.
16
+ """
17
+
18
+ realizations: int | None = None
19
+ seed: int | None = None
20
+ seeds: tuple | None = None
21
+
22
+ def __post_init__(self):
23
+ if self.seeds is not None:
24
+ if self.realizations is not None or self.seed is not None:
25
+ raise ValueError("Supply seeds or realizations/seed, not both.")
26
+
27
+ values = tuple(self.seeds)
28
+
29
+ _integer(value=len(values), name="realizations", low=1, high=32)
30
+
31
+ seeds = tuple(_integer(value=value, name="seed", low=0, high=2**32 - 1) for value in values)
32
+
33
+ if len(set(seeds)) != len(seeds):
34
+ raise ValueError("seeds must be distinct for independent realizations.")
35
+ else:
36
+ count = _integer(
37
+ value=4 if self.realizations is None else self.realizations, name="realizations", low=1, high=32
38
+ )
39
+
40
+ first = _integer(value=0 if self.seed is None else self.seed, name="seed", low=0, high=2**32 - 1)
41
+
42
+ if first + count - 1 > 2**32 - 1:
43
+ raise ValueError("seed range exceeds the supported maximum.")
44
+
45
+ seeds = tuple(range(first, first + count))
46
+
47
+ object.__setattr__(self, "seeds", seeds)
48
+
49
+ object.__setattr__(self, "realizations", len(seeds))
50
+
51
+ object.__setattr__(self, "seed", seeds[0])
52
+
53
+ @property
54
+ def metadata(self):
55
+ """Return exact ordered seeds for reproducibility."""
56
+
57
+ return {"realizations": self.realizations, "seeds": list(getattr(self, "seeds"))}
58
+
59
+ @classmethod
60
+ def _resolve(cls, *, ensemble_sampling=None, realizations=None, seed=None):
61
+ if ensemble_sampling is not None:
62
+ if not isinstance(ensemble_sampling, cls):
63
+ raise TypeError("ensemble_sampling must be an EnsembleSampling.")
64
+
65
+ if realizations is not None or seed is not None:
66
+ raise ValueError("Supply ensemble_sampling or realizations/seed, not both.")
67
+
68
+ return ensemble_sampling
69
+
70
+ if realizations is not None or seed is not None:
71
+ warnings.warn(
72
+ "Use EnsembleSampling instead of realizations/seed keywords.", DeprecationWarning, stacklevel=3
73
+ )
74
+
75
+ return cls(realizations=realizations, seed=seed)