Medium visualization
====================
Interactive three-dimensional volumes
-------------------------------------
Start with the interactive volumes below. Drag to rotate, scroll to zoom,
and hover to inspect the refractive index. Plotly is included with BornSim and is the
default for 3D views. Angular and polar result plots use Matplotlib.
Interactive random-medium volumes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For random media, a volume view reveals connected high refractive index regions throughout
the sample. Set ``field="refractive_index"`` and ``opacity_scale="increasing"`` to make
low refractive index contours transparent and higher refractive index contours increasingly opaque.
``opacity`` sets the maximum contour opacity. This emphasizes high absolute
refractive index, rather than large positive and negative fluctuations equally. Opacity
is a display setting; it does not represent material absorption.
The opacity ramp uses each sample's displayed refractive index range, so equal opacity
in two different samples need not represent the same refractive index.
The following views use the same Gaussian and exponential samples as
:doc:`auto_examples/random_media/random_medium`: seed 42, 16 voxels per axis,
25 nm spacing, refractive index standard deviation 0.01, and correlation length 75 nm.
Drag to rotate and scroll to zoom. Each view includes Plotly and works offline.
Interactive views are generated during the documentation build alongside
interactive gallery examples.
Gaussian covariance
^^^^^^^^^^^^^^^^^^^
.. raw:: html
Exponential covariance
^^^^^^^^^^^^^^^^^^^^^^
.. raw:: html
.. code-block:: python
from bornsim.units import ureg
from bornsim import Grid
from bornsim.medium.random_medium import GaussianMedium
grid = Grid(
shape=(16, 16, 16),
spacing=2.5e-08 * ureg.meter,
)
medium = GaussianMedium(
refractive_index_std=0.01,
correlation_length=7.5e-08 * ureg.meter,
background_refractive_index=1.33,
)
volume = medium.to_volume(
grid=grid,
seed=42,
)
figure = volume.plot_3d(
mode='volume',
field='refractive_index',
surface_count=16,
opacity=0.2,
opacity_scale='increasing',
)
figure.write_html(file='medium.html', include_plotlyjs=True)
Plotly also supports ``mode="isosurface"``; its default mode is ``volume``.
Uniform fields render as volume data; isosurface mode requires a non-uniform
field. The HTML export includes Plotly itself and works offline. Call
``figure.show()`` to display a Plotly figure. See its official `3D isosurface documentation
`_ and
`rendering documentation `_.
Three-dimensional media
-----------------------
``Volume.plot_3d()`` returns a Plotly volume by default for generated random
fields, structured samples, and custom voxel arrays. Call ``figure.show()``
to display it or ``figure.write_html()`` to export an interactive view.
Select ``backend="matplotlib"`` for static figures, then use ``plt.show()``
or ``figure.savefig()``. The gallery embeds interactive 3D views alongside their plotting cells.
Constructing a figure opens no window.
Secondary views: voxel cross-sections
--------------------------------------
``Volume.plot_slice()`` builds a Matplotlib figure with physical axis labels,
an equal spatial aspect ratio, and a colorbar. It handles axis orientation and
voxel-edge extents automatically. The default plane is the middle z voxel;
on an even grid its centre is at positive half-spacing, not at zero.
.. code-block:: python
from bornsim.units import ureg
from bornsim import Grid, Sphere, StructuredMedium
structure = StructuredMedium()
structure.add_background(refractive_index=1.33)
sphere = Sphere(
radius=1.5e-07 * ureg.meter,
refractive_index=1.34,
)
structure.add_structures(sphere)
grid_configuration_1 = Grid(
shape=(24, 24, 24),
spacing=2e-08 * ureg.meter,
)
volume = structure.to_volume(grid=grid_configuration_1)
figure = volume.plot_slice(
normal='z',
field='refractive_index',
length_unit='nanometer',
)
figure.savefig('sphere-slice.png')
Use ``normal="x"`` or ``normal="y"`` for other orientations and ``index``
to select a voxel plane. Field choices match ``plot_3d()``. Color limits use
the entire volume, so different slices share the same scale.
Finite-sample cross sections
----------------------------
``Result.plot()`` plots differential scattering coefficients. For a finite
sample, ``Result.plot_cross_section()`` multiplies curves and ensemble standard
errors by the stored physical sample volume and converts the display area units. Both return
Matplotlib figures and accept a custom ``title`` and ``log_y=True``.
.. code-block:: python
from bornsim.units import ureg
from bornsim import Solver, Source
source_configuration_1 = Source(wavelength=6.33e-07 * ureg.meter)
solver = Solver(
source=source_configuration_1,
order=3,
)
result = solver.solve(target=volume)
figure = result.plot_cross_section(area_unit='nanometer**2')
figure.savefig('sphere-scattering.png')
The original Volume is not required for this plot. Normalization uses the entire
voxel box: :math:`d\sigma/d\Omega=V\,\mathrm{differential}`. Cumulative curves
preserve amplitude interference; ``terms=True`` shows isolated terms instead.
Analytical infinite-medium results have no finite-sample cross section.
Sphere offsets and phase functions
----------------------------------
``Sphere.centre`` specifies a three-component offset from the voxel-box centre,
as a quantity with explicit length units. For example,
``centre=np.array([40, 20, 0]) * ureg.nanometer`` shifts a sphere by 40 nm along x
and 20 nm along y. Keep its radius plus the absolute offset within each box
half-width to avoid clipping. Offsets aligned with the voxel spacing translate
the existing sampled mask; other offsets resample the interface.
The :doc:`auto_examples/structured_media/dielectric_sphere` example also plots the sphere's
directional 3D phase function. It calls ``Solver.solve`` with the
fixed generated volume to integrate a single deterministic sample over
polar Gauss nodes and azimuth. This supplies the solid-angle
normalization that a single meridian cut cannot determine. Refine both angular
quadratures to check the numerical normalization.
The phase surface represents probability density per steradian, with incidence
along +z, rather than spatial medium geometry. It retains both polar angle
and azimuth, so interference between multiple structures remains visible.
No azimuth averaging is applied to the 3D view. Increase ``azimuth_samples``
to resolve finer azimuthal features. One deterministic sample provides no
estimate of random sampling uncertainty.
Matplotlib voxel rendering
--------------------------
With ``backend="matplotlib"``, ``mode="voxels"`` renders the sampled material
using its voxel cells and physical extents. Use ``Volume.plot_slice()`` for a
two-dimensional cross-section of a selected voxel plane.
.. code-block:: python
from bornsim.units import ureg
import matplotlib.pyplot as plt
from bornsim import Grid
from bornsim.medium.random_medium import WhittleMaternMedium
medium = WhittleMaternMedium(
smoothness=1.5,
correlation_length=6e-08 * ureg.meter,
background_refractive_index=1.33,
refractive_index_std=0.01,
)
grid_configuration_1 = Grid(
shape=(16, 16, 16),
spacing=2.5e-08 * ureg.meter,
)
volume = medium.to_volume(
grid=grid_configuration_1,
seed=42,
)
figure = volume.plot_3d(
backend='matplotlib',
mode='voxels',
field='delta_refractive_index',
length_unit='nanometer',
)
plt.show()
Structured samples and voxels
-----------------------------
``mode="voxels"`` shows the actual cells with nonzero refractive index contrast. A sphere's
staircase boundary is therefore visible rather than smoothed by interpolation.
Only exposed faces are drawn. Use ``Volume.plot_slice()`` to inspect exact
voxel values in a two-dimensional cross-section. For an entirely zero field,
voxels display the full uniform box.
.. code-block:: python
from bornsim.units import ureg
import matplotlib.pyplot as plt
from bornsim import Grid, Sphere, StructuredMedium
sample = StructuredMedium()
sample.add_background(refractive_index=1.33)
sphere = Sphere(
radius=1.5e-07 * ureg.meter,
refractive_index=1.34,
)
sample.add_structures(sphere)
grid_configuration_1 = Grid(
shape=(24, 24, 24),
spacing=2e-08 * ureg.meter,
)
volume = sample.to_volume(grid=grid_configuration_1)
figure = volume.plot_3d(
backend='matplotlib',
mode='voxels',
field='refractive_index',
)
figure.savefig('sphere-3d.png')
plt.show()
Random backgrounds under structures
-----------------------------------
The composition methods update an existing ``StructuredMedium``. A random
background fills uncovered voxels; structures overwrite that background,
including its fluctuations, inside their masks. The exterior reference refractive index
is the random statistics' uniform background refractive index.
.. code-block:: python
from bornsim.units import ureg
from bornsim import Grid, Sphere, StructuredMedium
from bornsim.medium.random_medium import WhittleMaternMedium
statistics = WhittleMaternMedium(
refractive_index_std=0.002,
correlation_length=5e-08 * ureg.meter,
background_refractive_index=1.33,
smoothness=1.5,
)
sample = StructuredMedium()
sample.add_background(medium=statistics)
sphere = Sphere(
radius=7e-08 * ureg.meter,
refractive_index=1.345,
)
sample.add_structures(sphere)
grid_configuration_1 = Grid(
shape=(16, 16, 16),
spacing=2.5e-08 * ureg.meter,
)
volume = sample.to_volume(
grid=grid_configuration_1,
seed=42,
)
figure = volume.plot_3d(
field='refractive_index',
opacity_scale='increasing',
)
figure.show()
Interpretation and performance
------------------------------
``field="delta_refractive_index"`` plots fluctuations about the uniform background;
``field="refractive_index"`` plots :math:`n_0+\delta n`. ``field="permittivity"`` displays
the solver's linearized relative permittivity
:math:`n_0^2+2n_0\delta n`, which omits the quadratic fluctuation term.
These views represent the input material, not a calculated electromagnetic
field. Axis labels state the display unit; stored data remains in SI units.
The spatial aspect ratio follows physical dimensions.
Matplotlib voxel geometry and ``Volume.plot_slice()`` show the sampled
material. Plotly level surfaces interpolate between voxel centres and can make curved interfaces
appear smoother than the solver's staircase mask. Neither visualization
replaces checks of voxel refinement for quantitative calculations.
The current grid limit is 32 voxels per axis (32,768 samples). Interactive
volume views reveal high refractive index regions throughout random fields;
``Volume.plot_slice()`` is useful for inspecting exact voxel values. Filled
voxel views reveal only the outer faces.
Dense or transparent Matplotlib scenes can have depth-ordering limitations;
see the `mplot3d FAQ `_.
Use ``Volume.plot_slice()`` or opaque voxels when checking the sampled geometry. Plotly
rendering depends on browser graphics support and surface count; changing
surface count changes visualization, not numerical resolution.