Structure API¶
structure
¶
Main simulation structure and workflow orchestration.
The Structure class is the primary interface for running optical simulations. It coordinates the entire calculation workflow:
- Parse scenario configuration
- Create multilayer stack
- Calculate transfer matrices for each layer
- Multiply matrices to get total system response
- Extract reflection coefficients
The transfer matrix method is a recursive approach where each layer's effect is represented by a 4×4 matrix relating field components at its boundaries. The total response is the product of all layer matrices.
Reference: Passler & Paarmann, JOSA B 34, 2128-2139 (2017)
Classes:
-
Structure–Main interface for optical simulations.
Structure
¶
Main interface for optical simulations.
Examples:
Basic single-point calculation:
>>> payload = {
... "ScenarioData": {
... "type": "Simple",
... "incidentAngle": 45.0,
... "azimuthal_angle": 0.0,
... "frequency": 1460.0
... },
... "Layers": [
... {"type": "Ambient Incident Layer", "permittivity": 50.0},
... {"type": "Semi Infinite Anisotropic Layer",
... "material": "Calcite", "rotationY": 90}
... ]
... }
>>> structure = Structure()
>>> structure.execute(payload)
>>> print(f"R_pp = {abs(structure.r_pp)**2:.4f}")
Multi-layer structure with air gap:
>>> payload = {
... "ScenarioData": {"type": "Incident"},
... "Layers": [
... {"type": "Ambient Incident Layer", "permittivity": 50.0},
... {"type": "Isotropic Middle-Stack Layer",
... "thickness": 0.5, "permittivity": 1.0},
... {"type": "Crystal Layer", "material": "Quartz",
... "thickness": 1.0, "rotationY": 70},
... {"type": "Semi Infinite Anisotropic Layer",
... "material": "Sapphire", "rotationY": 90}
... ]
... }
>>> structure = Structure()
>>> structure.execute(payload)
>>> # Results are arrays: structure.r_pp.shape = (410, 360)
Custom material with complex permittivity:
>>> custom_material = {
... "eps_xx": {"real": 2.5, "imag": 0.1},
... "eps_yy": {"real": 3.0, "imag": 0.05},
... "eps_zz": {"real": -4.0, "imag": 0.5}
... }
>>> payload = {
... "ScenarioData": {"type": "Simple", "incidentAngle": 45.0,
... "azimuthal_angle": 0.0, "frequency": 1000.0},
... "Layers": [
... {"type": "Ambient Incident Layer", "permittivity": 25.0},
... {"type": "Semi Infinite Anisotropic Layer",
... "material": custom_material, "rotationY": 45}
... ]
... }
>>> structure = Structure()
>>> structure.execute(payload)
Initialize empty optical structure for simulation.
Creates placeholder attributes for scenario, layers, reflection coefficients, and intermediate calculation results.
Methods:
-
get_scenario–Parse and initialize scenario from configuration data.
-
setup_attributes–Transfer scenario attributes to structure for easy access.
-
resolve_frequency–Resolve the frequency array (cm⁻¹) for the simulation.
-
calculate_kx_k0–Calculate parallel wavevector and free-space wavenumber.
-
get_layers–Create all layers in the structure from configuration.
-
calculate–Calculate total transfer matrix by multiplying layer matrices.
-
calculate_reflectivity–Extract reflection coefficients from total transfer matrix.
-
calculate_transmissivity–Extract transmission amplitude coefficients from the transfer matrix.
-
display_layer_info–Print information about all layers in the structure.
-
calculate_scattering–Fill reflection/transmission coefficients via the scattering backend.
-
execute–Execute complete simulation from configuration payload.
Source code in hyperbolic_optics/structure.py
get_scenario
¶
Parse and initialize scenario from configuration data.
Parameters:
-
scenario_data(dict[str, Any]) –Dictionary with scenario type and parameters
Note
Automatically sets up angle and frequency arrays based on scenario type.
Source code in hyperbolic_optics/structure.py
setup_attributes
¶
Transfer scenario attributes to structure for easy access.
Copies incident_angle, azimuthal_angle, and frequency from scenario to structure attributes.
Source code in hyperbolic_optics/structure.py
resolve_frequency
¶
Resolve the frequency array (cm⁻¹) for the simulation.
Precedence: an explicit ScenarioData['frequency'] (scalar or list)
wins; otherwise fall back to the default range of the last
material-bearing layer (the bulk crystal — an isotropic exit layer has
no dispersive range, so it is skipped automatically).
Parameters:
-
layer_data_list(list[dict[str, Any]]) –The raw layer configuration dicts.
Returns:
-
ndarray–A 1-D frequency array (length 1 for single-frequency scenarios).
Raises:
-
ValueError–If no frequency is given and no material can supply a range.
Source code in hyperbolic_optics/structure.py
calculate_kx_k0
¶
Calculate parallel wavevector and free-space wavenumber.
Computes
kx = n_prism · sin(θ) where n_prism = √ε_prism k0 = ω / c = 2π · frequency
Note
kx is conserved across all interfaces (phase matching condition).
Source code in hyperbolic_optics/structure.py
get_layers
¶
Create all layers in the structure from configuration.
Parameters:
-
layer_data_list(list[dict[str, Any]]) –List of layer configuration dictionaries
Note
First layer must be Ambient Incident Layer (prism). Automatically determines frequency range if not specified.
Source code in hyperbolic_optics/structure.py
calculate
¶
Calculate total transfer matrix by multiplying layer matrices.
Performs matrix multiplication of all layer transfer matrices from incident to exit medium: M_total = M_exit · ... · M_2 · M_1 · M_prism
Note
Uses functools.reduce with operator.matmul for efficient sequential multiplication.
Source code in hyperbolic_optics/structure.py
calculate_reflectivity
¶
Extract reflection coefficients from total transfer matrix.
Solves the system of equations to obtain r_pp, r_ss, r_ps, r_sp from the boundary conditions encoded in the transfer matrix.
Note
Reflection coefficients are complex and relate incident field amplitudes to reflected field amplitudes: E_reflected = r · E_incident
Source code in hyperbolic_optics/structure.py
calculate_transmissivity
¶
Extract transmission amplitude coefficients from the transfer matrix.
Computes t_pp, t_ps, t_sp, t_ss. The results are returned in the same
presentation layout as the reflection coefficients (see :meth:_present),
so t_* and r_* share axis ordering. Not called by execute() by
default — invoke explicitly after calculate() if transmission is needed.
Note
These are bare amplitude coefficients. For power transmittance,
layer-resolved absorption, and field profiles computed numerically
from the propagated fields (energy-conserving R + T + ΣA = 1), use
:class:hyperbolic_optics.fields.FieldProfile, which is the blessed path.
Source code in hyperbolic_optics/structure.py
display_layer_info
¶
Print information about all layers in the structure.
Debugging utility to display layer configuration and properties.
calculate_scattering
¶
Fill reflection/transmission coefficients via the scattering backend.
Uses :func:hyperbolic_optics.scattering.scattering_coefficients — a
numerically-stable Redheffer scattering-matrix cascade built from each
layer's eigenmodes — instead of the transfer-matrix product. Sets
r_pp … t_ss in the same presentation layout as
:meth:calculate_reflectivity.
Source code in hyperbolic_optics/structure.py
execute
¶
Execute complete simulation from configuration payload.
Parameters:
-
payload(dict[str, Any]) –Dictionary with 'ScenarioData' and 'Layers' keys.
-
backend(str, default:'transfer') –"transfer"(default) for the 4×4 transfer-matrix product, or"scattering"for the numerically-stable scattering-matrix backend (correct for thick / lossy / strongly-evanescent stacks where the transfer product overflows). Both yield the same coefficients where the transfer method is well-conditioned.
Example
payload = { ... "ScenarioData": {"type": "Simple", "incidentAngle": 45.0, ... "azimuthal_angle": 0.0, "frequency": 1460.0}, ... "Layers": [ ... {"type": "Ambient Incident Layer", "permittivity": 50.0}, ... {"type": "Semi Infinite Anisotropic Layer", ... "material": "Calcite", "rotationY": 90} ... ] ... } structure = Structure() structure.execute(payload) R_pp = abs(structure.r_pp)**2