A Differentiable Interferometer Simulator for the Compuational Design of Gravitational Wave Detectors
Project description
Differometor
Install guide | Getting started | Cavity example | Documentation | Development | Citing Differometor
Differometor is a differentiable frequency domain interferometer simulator implemented in Python using the JAX framework. Differometor’s implementation closely follows the established Finesse simulator and offers functionality to simulate plane waves in quasi-static, user-specified setup configurations including light field modulations, quantum noise calculations and optomechanical effects.
Differometor is designed to become a powerful tool for state-of-the-art AI-driven design of novel fundamental physics experiments.
The main difference to existing interferometer simulators is the speed of interferometer optimizations. Running auto-differentiated interferometer optimizations with Differometor on a Nvidia Quadro RTX 6000 GPU can be up to 160 times faster than running numerically differentiated optimizations with Finesse on a Xeon E5-2698 v4 CPU.
Figure 1: Left: The Differentiable Interferometer Simulator (Differometor) takes a user-defined configuration (e.g., a 2×2 universal interferometer, UIFO) and efficiently computes its properties by making use of just-in-time compilation, GPU acceleration, and automatic differentiation. This enables fast, gradient-based exploration of detector designs. Right: Gradient evaluation speedup over numerical approximation with Finesse for various optical setups, as a function of optimized parameter count.
Installation
This installation was tested on Ubuntu 24.04.1 LTS and on SUSE Linux Enterprise Server 15 SP6.
From PyPI
pip install differometor
Development mode
git clone https://github.com/artificial-scientist-lab/Differometor differometor
cd differometor
pip install -e .
GPU support
Both installation methods above automatically install the CPU version of JAX. To upgrade to the GPU version with CUDA 12, please use:
pip install --upgrade "jax[cuda12]==0.5.0"
Getting started
A good way to familiarize yourself with Differometor is to take a look at the examples:
- Optical cavity: A simple case of modelling a Fabry-Pérot cavity. Compare this setup with the corresponding Finesse example.
- Sensitivity of Advanced LIGO setup: A more advanced example of computing the strain sensitivity of a simplified Advanced LIGO setup. Compare this setup with the corresponding Finesse example.
- Sensitivity of Voyager setup: Same as for the Advanced LIGO setup above, but with a Voyager configuration using balanced homodyne readout.
- Sensitivity optimization of Voyager setup: This example showcases the potential of Differometor when it comes to interferometer optimization. It uses the Voyager setup from above, initializes its parameters randomly and then optimizes the parameters to achieve the best possible sensitivity.
Cavity Example
Optical cavity: A simple case of modelling a Fabry-Pérot cavity. Compare this setup with the corresponding Finesse example.
import differometor as df
import jax.numpy as jnp
from differometor.components import power_detector
import matplotlib.pyplot as plt
# define a simple cavity setup with three detectors
S = df.Setup()
S.add("laser", "l0", power=1)
S.add("mirror", "m0", reflectivity=0.99, loss=0)
S.add("mirror", "m1", reflectivity=0.991, loss=0)
S.space("l0", "m0", length=1)
S.space("m0", "m1", length=1)
S.add("detector", "refl", target="m0", port="left", direction="out")
S.add("detector", "circ", target="m1", port="left", direction="in")
S.add("detector", "trns", target="m1", port="right", direction="out")
# set the tuning range
tunings = jnp.linspace(-180, 180, 400)
# run the simulation with the tuning as the changing parameter
carrier, signal, noise, detector_ports, *_ = df.run(S, [("m0", "tuning")], tunings)
# calculate the power
powers = power_detector(carrier)
# plot the power at the detector ports
plt.figure()
plt.plot(tunings, powers[detector_ports[0]], label="refl")
plt.plot(tunings, powers[detector_ports[1]], label="circ")
plt.plot(tunings, powers[detector_ports[2]], label="trns")
plt.yscale("log")
plt.xlabel("Tuning (degrees)")
plt.ylabel("Power (W)")
plt.grid()
plt.legend()
plt.show()
Output:
Documentation
Differometor provides a Setup class to define the interferometer setup. You can add components and connect these components via spaces.
You can add the following components:
- mirror
setup.add("mirror", "m0")adds a mirror component with name m0 and default properties. Names cannot contain underscores.setup.add("mirror", "m1", reflectivity=0.5)adds a mirror component with name m1 which reflects half of the incoming light and transmits the other half. All other properties are set to default.setup.add("mirror", "abc", reflectivity=0.25, loss=0.5)adds a mirror component with name abc which reflects 25% of the incoming light, looses 50% of the incoming light and transmits the other 25%.setup.add("mirror", "m3", transmissivity=0.3, loss=0.6, tuning=90)adds a mirror component with name m3 which transmits 30% of the incoming light, looses 60% of the incoming light and is tuned by 90 degrees.- The default properties of a mirror are:
- reflectivity: 0.5
- loss: 5e-6
- tuning: 0
- A mirror has two ports: left and right. Each port has two directions: in and out.
- Reflectivity, transmissivity and loss always add up to 1 and are given as fractions of the total light amplitude. Differometor expresses reflectivity and transmissivity as one reflectivity parameter which is the fraction of the remaining light after loss that is reflected. Reflectivity and transmissivity of mirror m3 in the example above would therefore be expressed as the fraction: (1-0.3-0.6)/(1-0.6) = 0.25
- beamsplitter
- A beamsplitter is handled equivalent to a mirror, but has an additional alpha parameter describing the angle of incidence of the incoming light and two more ports.
setup.add("beamsplitter", "bs", alpha=50)adds a beamsplitter component with name bs which reflects half of the incoming light and transmits the other half at an angle of 50 degrees. All other properties are set to default.- A beamsplitter has four ports: left, right, top and bottom. Each port has two directions: in and out. Incoming light on the left port is transmitted to the right port and reflected to the top port. Incoming light at the top is transmitted to the bottom port and reflected to the left port. Incoming light at the right port is transmitted to the left port and reflected to the bottom port. Incoming light at the bottom port is transmitted to the top port and reflected to the right port.
- The default properties of a beamsplitter are:
- reflectivity: 0.5
- loss: 5e-6
- tuning: 0
- alpha: 45
- directional_beamsplitter
setup.add("directional_beamsplitter", "dbs")adds a directional beamsplitter component with name dbs- A directional beamsplitter forwards all light at the left port to the right port, all light at the top port to the left port, all light at the right port to the bottom port and all light at the bottom port to the top port.
- nothing
setup.add("nothing", "n0")adds a nothing component with name n0. These have two ports, left and right. Each port has two directions: in and out. Nothing components can be used to connect components without using a space.- The left input goes to the right output and the right input goes to the left output. Nothing changes.
- laser
setup.add("laser", "l0")adds a laser component with name l0 and default properties.setup.add("laser", "l1", power=0.5)adds a laser component with name l1 which has a power of 0.5. All other properties are set to default.- lasers can have a target if you want to connect them to a component without using a space.
setup.add("laser", "l2", target="m0", port="left", direction="in")adds a laser component with name l2 which is directly connected to the left port of m0 in the in direction. All other properties are set to default. - The default properties of a laser are:
- power: 1
- phase: 0
- squeezer
setup.add("squeezer", "sq", db=10, angle=0)adds a squeezer with name sq producing a squeezed-light beam with given squeezing in decibels db and angle.- Like a laser above, a squeezer can also have an additional target, port and direction.
- The default properties of a squeezer are:
- db: 0
- angle: 90
- free_mass
setup.add("free_mass", "sus", mass=10, target="m0")adds a simple free mass suspension with name sus and mass 10kg to the object with name m0. Masses can be added to mirrors or beamsplitters.- The default properties of a free mass are:
- mass: 40
- signal
setup.add("signal", "s0", target="l0_amplitude")adds a amplitude signal generator with name s0 to the laser l0.setup.add("signal", "s1", target="abc_frequency")adds a frequency signal generator with name s1 to the laser abc.setup.add("signal", "GW", target="m0_m1")adds a signal generator with name GW to the space between m0 and m1.setup.add("signal", "s2", target="l0_amplitude", amplitude=("l0_power", jnp.sqrt))adds a signal generator with name s2 to the laser l0. The signal amplitude is defined as the square root of the laser power. So instead of actual values, properties can be defined as functions of other properties.- Signal generators can be connected only to spaces or lasers. For lasers, they can only be applied as amplitude (l0_amplitude) or frequency (l0_frequency) modulation.
- The default properties of a signal are:
- amplitude: 1
- phase: 0
- qhd
setup.add("qhd", "qhd0", detector1="noise1", detector2="noise2", phase=45)adds a balanced homodyne quantum noise detector with name qhd0 and a phase of 45 degrees. It uses the two noise detectors noise1 and noise2.- The default properties of a qhd are:
- phase: 0
- frequency
setup.add("frequency", "f")sets the frequency used by all signals in the simulation to the default 1 Hz. It is a unique element and can only be set once in a given setup.- The default properties of a frequency are:
- frequency: 1
- detector
setup.add("detector", "d0", target="bs", port="top", direction="out")adds a detector with name d0 to the top port of the object bs in the out direction.setup.add("detector", "d1", target="m0")adds a detector with name d1 to the object m0. The default port and direction is left and in.
- qnoised
setup.add("qnoised", "1", target="m0")adds a noise detector with name 1 to the left port of the object m0 in the in direction.setup.add("qnoised", "noise", target="m0", port="left", direction="in", auxiliary=True)adds a noise detector with name noise to the left port of the object m0 in the in direction. This is an auxiliary detector that does not get its own noise output and can be used as detector1 or detector2 in a qhd.
You can connect components via spaces:
- space
setup.space("m0", "m1", length=0.5)adds a space between the objects m0 and m1 with a length of 0.5 meters. By default it connects the right port of m0 with the left port of m1.setup.space("bs1", "bs2", length=100, source_port="top", target_port="bottom")adds a space between the objects bs1 and bs2 with a length of 100 meters. It connects the top port of bs1 with the bottom port of bs2.setup.space("l0", "m0", length=1, refractive_index=1.5, target_port="right")adds a space between the laser l0 and the object m0 with a length of 1 meter and a refractive index of 1.5. It connects the laser l0 with the right port of m0, in the in direction.- The default properties of a space are:
- length: 0
- refractive_index: 1
For further details on the limits and inner workings of the simulator, please refer to the Finesse documentation.
Development
Differometor is developed at the Artificial Scientist Lab under Dr. Mario Krenn at the Max Planck Institute for the Science of Light in Erlangen, Germany.
Citing Differometor
To cite this repository, please use the following BibTeX entry:
@software{differometor2025github,
author = {Jonathan Klimesch and Yehonathan Drori and Rana X Adhikari and Mario Krenn},
title = {Differometor: A Differentiable Interferometer Simulator for the Computational Design of Gravitational Wave Detectors},
url = {http://github.com/artificial-scientist-lab/Differometor},
version = {0.1.0},
year = {2025},
}
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file differometor-0.0.1.tar.gz.
File metadata
- Download URL: differometor-0.0.1.tar.gz
- Upload date:
- Size: 37.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.11.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d76ee1c140ff1a2aefc639fa0f9ec78da59d08b74c56e3886b9ff29b93808925
|
|
| MD5 |
e223c5ed8259ffad455802001321b8a8
|
|
| BLAKE2b-256 |
945746ad2b0dca967041d41932ebe8bae419ea377e973e58aa3f5e2aa0c56ca7
|
File details
Details for the file differometor-0.0.1-py3-none-any.whl.
File metadata
- Download URL: differometor-0.0.1-py3-none-any.whl
- Upload date:
- Size: 34.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.11.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
89a33c41e2844d8fa3c162f338d88fe138c8f3de8be0c12b8c12c47be937234f
|
|
| MD5 |
6deea4b41a7a074716b38527c3af0425
|
|
| BLAKE2b-256 |
7b5288cb10161f9de650f55fcecd0192db8d17617893cc08a2ecced1d82d149a
|