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/volume.py ADDED
@@ -0,0 +1,319 @@
1
+ """Finite refractive-index fields on centred cubic voxel grids."""
2
+
3
+ from dataclasses import dataclass
4
+ import numpy as np
5
+ from ._validation import _integer
6
+ from .grid import Grid
7
+ from .media import RandomMedium
8
+ from .units import _refractive_index_values, Quantity
9
+
10
+
11
+ @dataclass(frozen=True, kw_only=True)
12
+ class Volume:
13
+ """Represent a finite refractive-index fluctuation field on cubic voxels.
14
+
15
+ Parameters
16
+ ----------
17
+ delta_refractive_index : array_like
18
+ Finite three-dimensional refractive index fluctuations relative to the background,
19
+ with 2 to 32 cells per axis. Quantities are rejected, including dimensionless ones.
20
+ grid : Grid, optional
21
+ Shared spatial configuration. Its shape must match delta_refractive_index.
22
+ Supply either grid or spacing.
23
+ spacing : Quantity, optional
24
+ Positive, finite cubic voxel width. Explicit length units are required.
25
+ Legacy alternative to supplying grid; shape is inferred from the array.
26
+ background_refractive_index : float
27
+ Positive, finite background refractive index. Plain numbers are required; quantities are rejected. A background must be supplied.
28
+ medium : RandomMedium, optional
29
+ Generation statistics recorded by ``random_volume``. None for a
30
+ manually supplied field. Must be supplied together with ``seed``.
31
+ seed : int, optional
32
+ Random seed recorded by ``random_volume``. None for a manual field.
33
+
34
+ Attributes
35
+ ----------
36
+ delta_refractive_index : numpy.ndarray
37
+ Copied, read-only, dimensionless fluctuation array of shape (nx, ny, nz).
38
+ grid : Grid
39
+ Spatial configuration shared with generation and scattering.
40
+ spacing : Quantity
41
+ Cubic voxel width with its supplied units, also available as grid.spacing.
42
+ background_refractive_index : float
43
+ Dimensionless background refractive index.
44
+ medium : RandomMedium or None
45
+ Recorded generation statistics, when available.
46
+ seed : int or None
47
+ Recorded generation seed, when available.
48
+
49
+ Raises
50
+ ------
51
+ ValueError
52
+ If the field or grid is invalid, units are incompatible, or linearized
53
+ relative permittivity is nonpositive in any voxel.
54
+ TypeError
55
+ If recorded generation statistics are not a RandomMedium.
56
+
57
+ See Also
58
+ --------
59
+ bornsim.media.random_volume : Generate a seeded sample from medium statistics.
60
+ bornsim.series.BornSeries.solve : Compute coherent scattering from the sample.
61
+
62
+ Notes
63
+ -----
64
+ Relative permittivity is ``background_refractive_index**2 + 2*background_refractive_index*delta_refractive_index``.
65
+ The quadratic refractive-index fluctuation term is omitted at every Born order.
66
+ Voxel centres are measured relative to the sample centre.
67
+
68
+ Examples
69
+ --------
70
+ >>> import numpy as np
71
+ >>> from bornsim import Volume
72
+ >>> from bornsim.units import ureg
73
+ ...
74
+ ...
75
+ >>> volume = Volume(
76
+ ... delta_refractive_index=np.zeros((2, 2, 2)),
77
+ ... spacing=50 * ureg.nanometer,
78
+ ... background_refractive_index=1.33,
79
+ ... )
80
+ >>> volume.positions.shape
81
+ (2, 2, 2, 3)
82
+ """
83
+
84
+ delta_refractive_index: np.ndarray
85
+ background_refractive_index: float
86
+ spacing: Quantity = None
87
+ grid: Grid | None = None
88
+ medium: RandomMedium | None = None
89
+ seed: int | None = None
90
+
91
+ def __post_init__(self) -> None:
92
+ data = np.array(
93
+ _refractive_index_values(
94
+ value=self.delta_refractive_index,
95
+ name="delta_refractive_index",
96
+ ),
97
+ copy=True,
98
+ )
99
+
100
+ if data.ndim != 3 or any(n < 2 or n > 32 for n in data.shape) or not np.all(np.isfinite(data)):
101
+ raise ValueError("delta_refractive_index must be a finite 3D array with 2–32 cells per axis.")
102
+
103
+ if self.grid is None:
104
+ if self.spacing is None:
105
+ raise ValueError("Supply grid or spacing for a Volume.")
106
+
107
+ grid = Grid(
108
+ shape=data.shape,
109
+ spacing=self.spacing,
110
+ )
111
+ else:
112
+ grid = Grid._resolve(
113
+ grid=self.grid,
114
+ spacing=self.spacing,
115
+ )
116
+
117
+ if grid.shape != data.shape:
118
+ raise ValueError("grid shape must match delta_refractive_index.")
119
+
120
+ object.__setattr__(self, "grid", grid)
121
+
122
+ object.__setattr__(self, "spacing", grid.spacing)
123
+
124
+ object.__setattr__(
125
+ self,
126
+ "background_refractive_index",
127
+ _refractive_index_values(
128
+ value=self.background_refractive_index,
129
+ name="background_refractive_index",
130
+ scalar=True,
131
+ ),
132
+ )
133
+
134
+ if not np.isfinite(self.background_refractive_index) or self.background_refractive_index <= 0:
135
+ raise ValueError("background_refractive_index must be finite and positive.")
136
+
137
+ if np.any(self.background_refractive_index**2 + 2 * self.background_refractive_index * data <= 0):
138
+ raise ValueError("Linearized relative permittivity must stay positive.")
139
+
140
+ if (self.medium is None) != (self.seed is None):
141
+ raise ValueError("medium and seed must be supplied together.")
142
+
143
+ if self.medium is not None:
144
+ if not isinstance(self.medium, RandomMedium):
145
+ raise TypeError("medium must be a RandomMedium.")
146
+
147
+ if self.medium.background_refractive_index != self.background_refractive_index:
148
+ raise ValueError("medium background_refractive_index must match the volume.")
149
+
150
+ object.__setattr__(
151
+ self,
152
+ "seed",
153
+ _integer(
154
+ value=self.seed,
155
+ name="seed",
156
+ low=0,
157
+ high=2**32 - 1,
158
+ ),
159
+ )
160
+
161
+ data.setflags(write=False)
162
+
163
+ object.__setattr__(self, "delta_refractive_index", data)
164
+
165
+ @property
166
+ def positions(self) -> Quantity:
167
+ """Return voxel-centre coordinates relative to the sample centre.
168
+
169
+ Returns
170
+ -------
171
+ positions : Quantity
172
+ Unit-bearing coordinates, shape (nx, ny, nz, 3), with the final
173
+ axis ordered as x, y, z.
174
+ """
175
+
176
+ return getattr(self, "grid").positions
177
+
178
+ @property
179
+ def volume(self) -> Quantity:
180
+ """Return the total physical volume of all voxels.
181
+
182
+ Returns
183
+ -------
184
+ volume : Quantity
185
+ Unit-bearing sample volume, equal to ``delta_refractive_index.size*spacing**3``.
186
+ """
187
+
188
+ return getattr(self, "grid").volume
189
+
190
+ def plot_slice(self, *, normal="z", index=None, field="refractive_index", length_unit="nanometer", title=None):
191
+ """Build a Matplotlib figure of one voxel plane.
192
+
193
+ Parameters
194
+ ----------
195
+ normal : {'x', 'y', 'z'}, optional
196
+ Axis perpendicular to the plane; default 'z'.
197
+ index : int, optional
198
+ Voxel index along the normal. Defaults to n//2, the positive
199
+ central plane for an even grid. The title gives its actual position.
200
+ field : {'refractive_index', 'delta_refractive_index', 'permittivity'}, optional
201
+ Display n0 + delta_refractive_index, fluctuations, or linearized relative
202
+ permittivity n0**2 + 2*n0*delta_refractive_index. Default 'refractive_index'.
203
+ length_unit : str, optional
204
+ Spatial display unit; default 'nanometer'. Stored SI data is unchanged.
205
+ title : str, optional
206
+ Override the title describing the field and plane position.
207
+
208
+ Returns
209
+ -------
210
+ figure : matplotlib.figure.Figure
211
+ Equal-aspect image with voxel-edge extents and a labelled colorbar.
212
+ Call pyplot.show() to display or figure.savefig() to export.
213
+ Colors use the full volume's range to compare different planes;
214
+ fluctuation colors are symmetric about zero.
215
+
216
+ Raises
217
+ ------
218
+ ValueError
219
+ If the plane, index, field, or length unit is invalid.
220
+ """
221
+
222
+ from ._volume_plotting import _VolumePlotter
223
+
224
+ return _VolumePlotter(
225
+ volume=self,
226
+ ).plot_slice(
227
+ normal=normal,
228
+ index=index,
229
+ field=field,
230
+ length_unit=length_unit,
231
+ title=title,
232
+ )
233
+
234
+ def plot_3d(
235
+ self,
236
+ *,
237
+ backend="plotly",
238
+ mode=None,
239
+ field="delta_refractive_index",
240
+ length_unit="nanometer",
241
+ surface_count=8,
242
+ opacity=None,
243
+ opacity_scale="uniform",
244
+ slice_indices=None,
245
+ ):
246
+ """Build a three-dimensional figure of the finite voxel medium.
247
+
248
+ Parameters
249
+ ----------
250
+ backend : {'matplotlib', 'plotly'}, optional
251
+ Default is Plotly for browser interaction. Select 'matplotlib'
252
+ explicitly for a Matplotlib figure.
253
+ mode : str, optional
254
+ Matplotlib supports 'slices' (default) and 'voxels'. Voxels display
255
+ cells with nonzero refractive index contrast, or the full box for a zero field.
256
+ Plotly supports 'volume' (default), 'isosurface', and 'slices'.
257
+ Uniform Plotly fields fall back to slices.
258
+ field : {'delta_refractive_index', 'refractive_index', 'permittivity'}, optional
259
+ Scalar field to display, default refractive index fluctuation. 'refractive_index' displays
260
+ n0 + delta_refractive_index; 'permittivity' displays the solver's linearized
261
+ relative permittivity n0**2 + 2*n0*delta_refractive_index, not (n0+delta_refractive_index)**2.
262
+ length_unit : str, optional
263
+ Spatial display unit, default 'nanometer'. Stored SI data is unchanged.
264
+ surface_count : int, optional
265
+ Number of contours, from 1 to 32; default 8. More surfaces increase
266
+ browser rendering cost. Used only by Plotly volume and isosurface modes.
267
+ opacity : float, optional
268
+ Surface opacity in (0, 1]. Default is 1 for Matplotlib and 0.15
269
+ for Plotly. Slices are opaque in both backends.
270
+ opacity_scale : {'uniform', 'increasing'}, optional
271
+ Default 'uniform' gives all contours the same opacity. 'increasing'
272
+ is available only for Plotly volume mode and scales opacity from
273
+ zero at the lowest displayed contour to ``opacity`` at the highest.
274
+ Use field='refractive_index' to emphasize high refractive index, rather than
275
+ the magnitude of positive and negative fluctuations.
276
+ slice_indices : tuple of int, optional
277
+ One voxel index for each x, y, z plane, used only for slices.
278
+ Defaults to the middle voxel along each axis.
279
+
280
+ Returns
281
+ -------
282
+ figure : matplotlib.figure.Figure or plotly.graph_objects.Figure
283
+ Matplotlib: call pyplot.show() to display or figure.savefig() to
284
+ export. Rotation needs an interactive Matplotlib backend; gallery
285
+ images are static. Plotly: call figure.show() or figure.write_html().
286
+ Constructing a figure opens no window or browser.
287
+
288
+ Raises
289
+ ------
290
+ ValueError
291
+ If a mode, scalar field, display unit, or rendering setting is invalid.
292
+
293
+ Notes
294
+ -----
295
+ Matplotlib voxels preserve staircase interfaces; slices display every
296
+ selected cell with voxel-edge extents. Dense transparent scenes can have
297
+ depth-ordering limitations in Matplotlib. Plotly contours interpolate
298
+ between voxel centres and are visualization aids,
299
+ not exact curved interfaces or a replacement for grid refinement.
300
+ Slices show sampled values. Aspect ratio follows physical dimensions.
301
+ All voxels are retained, with at most 32**3 samples under Volume's grid
302
+ limit; browser performance also depends on surface count and graphics
303
+ support. This plots the input material, not an electromagnetic field.
304
+ """
305
+
306
+ from ._volume_plotting import _VolumePlotter
307
+
308
+ return _VolumePlotter(
309
+ volume=self,
310
+ ).plot_3d(
311
+ backend=backend,
312
+ mode=mode,
313
+ field=field,
314
+ length_unit=length_unit,
315
+ surface_count=surface_count,
316
+ opacity=opacity,
317
+ opacity_scale=opacity_scale,
318
+ slice_indices=slice_indices,
319
+ )