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/__init__.py +69 -0
- bornsim/_archives.py +172 -0
- bornsim/_result_plotting.py +454 -0
- bornsim/_result_validation.py +89 -0
- bornsim/_validation.py +10 -0
- bornsim/_version.py +1 -0
- bornsim/_volume_plotting.py +395 -0
- bornsim/angular_data.py +338 -0
- bornsim/api.py +26 -0
- bornsim/directions.py +99 -0
- bornsim/ensemble.py +253 -0
- bornsim/ensemble_sampling.py +75 -0
- bornsim/geometry.py +716 -0
- bornsim/green.py +161 -0
- bornsim/grid.py +98 -0
- bornsim/material.py +46 -0
- bornsim/media.py +359 -0
- bornsim/model.py +276 -0
- bornsim/results.py +717 -0
- bornsim/rotation.py +68 -0
- bornsim/sampling.py +231 -0
- bornsim/series.py +300 -0
- bornsim/solver.py +561 -0
- bornsim/source.py +57 -0
- bornsim/units.py +81 -0
- bornsim/volume.py +319 -0
- bornsim-0.2.6.dist-info/METADATA +529 -0
- bornsim-0.2.6.dist-info/RECORD +31 -0
- bornsim-0.2.6.dist-info/WHEEL +5 -0
- bornsim-0.2.6.dist-info/licenses/LICENSE +21 -0
- bornsim-0.2.6.dist-info/top_level.txt +1 -0
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)
|