Skip to main content

Hyperbolic Optics Simulation Package

License: MIT PyPI version Python 3.12+ Tests Issues

This package provides a comprehensive suite of tools to study the reflective properties of hyperbolic materials and anisotropic structures using the 4×4 transfer matrix method. It enables easy configuration of multilayer systems, calculation of reflection coefficients, and analysis using Mueller matrices.


Table of Contents


Features

  • 4×4 transfer-matrix engine for reflection coefficients of arbitrary anisotropic multilayers, plus an opt-in numerically-stable scattering-matrix backend for thick / lossy / evanescent stacks
  • Transmission, layer-resolved absorption, and field profiles (E, H, Sₙ vs depth) computed numerically from the Poynting flux — energy-conserving R + T + ΣA = 1
  • Polarization toolkit: Mueller and Jones calculus, ellipsometry (Ψ/Δ), eigenpolarizations / exceptional points, co- vs cross-polarized power, and Poincaré-sphere trajectories
  • Materials library: Quartz, Calcite, Sapphire, Ga₂O₃ (monoclinic), α-MoO₃ (biaxial), AlN, SiC, hBN, GaN — plus arbitrary permittivity/permeability tensors
  • Scenarios: incident-angle, azimuthal, k-space dispersion (kₓ–k_y), full 3-D sweep, single-point — and a swept layer-thickness axis
  • Fully vectorized over angle, azimuth, frequency, and thickness via a canonical batch convention
  • Publication-quality plotting for spectra, k-space maps, and polarization

Installation

The package is now available on PyPI and can be installed using pip or uv:

Using pip

pip install hyperbolic-optics
uv add hyperbolic-optics

Development Installation

For development or to get the latest features:

git clone https://github.com/hyperbolic-optics/hyperbolic-optics.git
cd hyperbolic_optics
pip install -e .

Quick Start

Here's a simple example to get you started:

import json
from hyperbolic_optics.structure import Structure
from hyperbolic_optics.mueller import Mueller

# Define a simple multilayer structure
payload = {
    "ScenarioData": {
        "type": "Simple",
        "incidentAngle": 45.0,  # degrees
        "azimuthal_angle": 0.0,  # degrees
        "frequency": 1460.0      # cm^-1
    },
    "Layers": [
        {
            "type": "Ambient Incident Layer",
            "permittivity": 50.0
        },
        {
            "type": "Semi Infinite Anisotropic Layer",
            "material": "Calcite",
            "rotationX": 0,
            "rotationY": 90,
            "rotationZ": 0
        }
    ]
}

# Create and execute the simulation
structure = Structure()
structure.execute(payload)

# Calculate reflectivities
R_pp = abs(structure.r_pp)**2
R_ss = abs(structure.r_ss)**2
print(f"p-polarized reflectivity: {R_pp}")
print(f"s-polarized reflectivity: {R_ss}")

# Perform Mueller matrix analysis
mueller = Mueller(structure)
mueller.set_incident_polarization('linear', angle=45)
mueller.add_optical_component('anisotropic_sample')

# Get Stokes parameters and polarization properties
all_params = mueller.get_all_parameters()
print(f"Reflectance (S0): {all_params['S0']}")
print(f"Degree of polarization: {all_params['DOP']}")

Usage Examples

The examples/ folder contains simple scripts demonstrating various capabilities.

Run any example with:

python examples/calcite.py

Advanced Features

Custom Materials

Define materials with arbitrary permittivity and permeability tensors:

custom_material = {
    "eps_xx": {"real": 2.27, "imag": 0.001},
    "eps_yy": {"real": -4.84, "imag": 0.755}, 
    "eps_zz": {"real": -4.84, "imag": 0.755},
    "eps_xy": {"real": 0.0, "imag": 0.0},
    "eps_xz": {"real": 0.0, "imag": 0.0},
    "eps_yz": {"real": 0.0, "imag": 0.0},
}

Multiple Scenario Types

  • Incident: Frequency vs incident angle analysis
  • Azimuthal: Frequency vs azimuthal rotation analysis
  • Dispersion: k-space dispersion at fixed frequency
  • Simple: Single-point calculation for specific conditions

Built-in Visualization

from hyperbolic_optics.plots import plot_mueller_dispersion, plot_kx_frequency

# Generate publication-quality plots
plot_kx_frequency(structure, reflectivity, save_name="my_plot")

Contributing

We welcome contributions! Please see our contributing guidelines for details on:

  • Reporting bugs and requesting features
  • Setting up a development environment
  • Code style and testing requirements
  • Submitting pull requests

Citation

If you use this package in your research, please cite:

Software Citation

@software{cunningham2025hyperbolic,
  title={Hyperbolic Optics Simulation Package},
  author={Mark Cunningham},
  year={2026},
  version={0.3.0},
  doi={10.5281/zenodo.14946556},
  url={https://pypi.org/project/hyperbolic-optics/},
  howpublished={PyPI},
  note={Python package for 4×4 transfer matrix method simulations}
}

This package was used to generate results in:

M. Cunningham et al., "Optical footprint of ghost and leaky hyperbolic polaritons," Photonics Research, vol. 13, no. 8, pp. 2291-2305 (2025). DOI: 10.1364/PRJ.558334


Transmission, Absorption & Field Profiles

Reflection is computed automatically by Structure.execute. Power transmittance, layer-resolved absorption, and field profiles are computed numerically from the propagated fields (energy-conserving R + T + ΣA = 1) via FieldProfile:

from hyperbolic_optics.structure import Structure
from hyperbolic_optics.fields import FieldProfile

structure = Structure()
structure.execute(payload)

fp = FieldProfile(structure)
print(fp.summary("p"))            # R, T, per-layer absorption, conservation residual
T = fp.transmittance("p")          # power transmittance (same shape as r_pp)
A = fp.layer_absorption("p")       # per-interior-layer absorptance
prof = fp.field_profile("p")       # z, Ex..Hz, S_z(z), cumulative absorption

For a single semi-infinite anisotropic layer there are no interior layers to resolve, so T = 1 − R is the power delivered into the bulk, and field_profile shows it being absorbed with depth.

Polarization conversion

Cross-polarization (the r_ps/t_ps channels) can be analysed several ways:

  • FieldProfile.polarization_resolved("p") (experimental) — splits R and T into co- and cross-polarized power and reports the conversion fractions. The transmitted s/p split is rigorous for an isotropic exit.
  • FieldProfile.stokes_from_field_profile("p") — the Stokes vector and polarization ellipse (azimuth ψ, ellipticity χ) of the transverse field versus depth, so you can watch the state evolve through a birefringent layer.
  • Mueller.calculate_transmission_mueller_matrix() — the transmission Mueller matrix from the t coefficients. Its |t|² intensity equals true power transmittance only for a symmetric prism = substrate system; into a lower-index medium it breaks in the evanescent regime (use FieldProfile.transmittance for power there).

examples/layer_resolved_absorption.py reproduces the azimuthal layer-resolved absorption of a MoO₃/AlN/SiC heterostructure in the Otto geometry (Passler, Jeannin & Paarmann, J. Opt. Soc. Am. B 37, 1060 (2020)). The amplitude transmission coefficients are also available via FieldProfile.transmission_coefficients() (and Structure.calculate_transmissivity()).

Numerical backends

execute defaults to the 4×4 transfer-matrix method, which is fast but numerically unstable for thick / lossy / strongly-evanescent layers (the propagation terms grow exponentially and the matrix product overflows to NaN). For those cases, opt into the numerically-stable scattering-matrix backend:

structure.execute(payload, backend="scattering")

It cascades per-layer scattering matrices with the Redheffer star product so only decaying exponentials ever appear. It returns the same reflection/transmission coefficients as the transfer method where that is well-conditioned, and correct ones where the transfer matrix is (near-)singular — e.g. a thick evanescent Otto gap, where backend="transfer" gives NaN but backend="scattering" correctly gives total reflection. Method/conventions follow PyLlama (Bay et al., J. Opt. Soc. Am. A 39, 1431 (2022)).

Sweeping Layer Thickness

A layer's thickness may be a list instead of a scalar, which sweeps it as a fourth canonical axis (T) alongside incident angle, azimuth, and frequency — computed in a single execute (the eigendecomposition is thickness-independent; only the propagation phase broadcasts over T). It composes with any scenario and is absent (size 1) by default:

{"type": "Crystal Layer", "material": "Calcite",
 "thickness": [0.5, 1.0, 1.5, 2.0], "rotationY": 90}   # -> appends a length-4 T axis

Outputs append the trailing T axis (e.g. Simple → r_pp.shape == (4,), Incident → (F, angle, 4)). At most one layer may carry a list thickness. For a 2-D thickness × thickness grid, combine a list thickness on one layer with the ThicknessSweep helper (from hyperbolic_optics import ThicknessSweep), which re-runs the stack and stacks results along a leading index.

Known Issues / Limitations

  • Multiple Optical Components: While you can place multiple Mueller matrix components in series, matching incident angles between them isn't yet implemented
  • Field profiles over full sweeps: field_profile is batched but intended for Simple / single-point use — a full angle/frequency sweep times the depth axis is memory-heavy.

Testing

This package includes a comprehensive test suite covering all major functionality. Run tests locally with:

pytest
pytest --cov=hyperbolic_optics --cov-report=html  # with coverage report

Please open an issue if you encounter any bugs or have suggestions for improvements.


Papers & Further Reading

For background on the physics and mathematical methods, see:

Key References:

  • N. C. Passler and A. Paarmann, "Generalized 4 × 4 matrix formalism for light propagation in anisotropic stratified media," J. Opt. Soc. Am. B 34, 2128-2139 (2017)
  • P. Yeh, "Electromagnetic propagation in birefringent layered media," J. Opt. Soc. Am. 69, 742-756 (1979)
  • N. C. Passler et al., "Layer-resolved resonance intensity of evanescent polariton modes in anisotropic multilayers," Phys. Rev. B 107, 235426 (2023)
  • M. Cunningham et al., "Optical footprint of ghost and leaky hyperbolic polaritons," Photonics Research 13, 2291-2305 (2025)

License

This project is licensed under the MIT License – see the LICENSE file for details.


Getting Help

  • Documentation: Check the examples/ folder and docstrings
  • Issues: Report bugs or request features via GitHub Issues
  • Discussions: Start a discussion for usage questions or feature ideas

Thank you for your interest in the Hyperbolic Optics Simulation Package!

Happy simulating! 🔬✨

Metadata

Release files for hyperbolic-optics 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hyperbolic-optics 0.3.0
File Size Uploaded Provenance Actions
hyperbolic_optics-0.3.0.tar.gz 7.0 MB Without provenance Details for hyperbolic_optics-0.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for hyperbolic-optics 0.3.0
File Interpreter ABI Platform Actions
hyperbolic_optics-0.3.0-py3-none-any.whl Python 3 none any Details for hyperbolic_optics-0.3.0-py3-none-any.whl

Total release size: 7.0 MB

Release files / hyperbolic_optics-0.3.0.tar.gz

Download URL hyperbolic_optics-0.3.0.tar.gz
Size 7.0 MB
Tags Source
SHA-256 checksum
How to use checksums
078d0c80f41b40bb85f94acfb4c6a759fe6a49b3358ac72853b4f01e1b98f68e
BLAKE2b-256 checksum
How to use checksums
28b0b77a3566b64654a744b443da178a642cd636f7aaa3f5cb2965531e41b5f9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / hyperbolic_optics-0.3.0-py3-none-any.whl

Download URL hyperbolic_optics-0.3.0-py3-none-any.whl
Size 77.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
97d86db4df82dced0013ac807300aa10bfaf7b50f0886dbe9ba5e726555828b9
BLAKE2b-256 checksum
How to use checksums
a7c736012c2d66af92d84e59fa2eb8cb9405fd73be43467c7e75cd9a42b667c0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for hyperbolic-optics 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page