Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion docs/explanation/backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Backends are used to execute the AtomicCircuit.
## Supported Backends

- [QuTiP](https://qutip.readthedocs.io/en/latest/) <div style="float:right;"> [![](https://img.shields.io/badge/Implementation-7C4DFF)][oqd_trical.backend.qutip.QutipBackend] </div>
- [Dynamiqs](https://qutip.readthedocs.io/en/latest/) <div style="float:right;"> [![](https://img.shields.io/badge/Implementation-7C4DFF)][oqd_trical.backend.dynamiqs.DynamiqsBackend] </div>
- [Dynamiqs](https://www.dynamiqs.org/) <div style="float:right;"> [![](https://img.shields.io/badge/Implementation-7C4DFF)][oqd_trical.backend.dynamiqs.DynamiqsBackend] </div>

## Compile

Expand All @@ -32,3 +32,42 @@ Executes the compatible form of the AtomicCircuit with the backend using a tree
- Dynamiqs uses [`DynamiqsVM`][oqd_trical.backend.dynamiqs.vm.DynamiqsVM] as its tree walking interpreter.

///

## Units and solver options (Dynamiqs)

The Dynamiqs backend integrates the Schrödinger equation
$\frac{d}{dt}\lvert\psi\rangle = -i H \lvert\psi\rangle$ with $\hbar = 1$. Units flow
consistently from the atomic interface to [`dq.sesolve`](https://www.dynamiqs.org/):

- Level energies, beam Rabi frequencies and detunings are **angular frequencies
(rad/s)**, so the lowered Hamiltonian is in rad/s.
- Pulse durations and the save `timestep` are in **seconds**.
- The phase accumulated by a time-dependent coefficient is `frequency * t`
(rad/s × s), which is dimensionless.

Because the bare optical carrier ($\sim 2\pi\cdot 10^{15}$ rad/s) makes the equation
stiff, drive it through the
[`RotatingReferenceFrame`][oqd_trical.light_matter.compiler.approximate.RotatingReferenceFrame]
and [`RotatingWaveApprox`][oqd_trical.light_matter.compiler.approximate.RotatingWaveApprox]
passes, which remove the carrier and leave a slowly varying Hamiltonian. Accuracy
is then controlled by the explicit Diffrax tolerances rather than by rescaling time.

The Diffrax solver, its tolerances and the step-size controller are set explicitly via
[`DynamiqsSolverOptions`][oqd_trical.backend.dynamiqs.solver_options.DynamiqsSolverOptions]
(default: `Tsit5` with `rtol = atol = 1e-8`), passed as `solver_options` to the
backend. The progress meter is disabled by default to avoid the `ZMQError` raised by
`tqdm` inside Jupyter (issue #26).

<!-- prettier-ignore -->
/// admonition | Example
type: example

```python
from oqd_trical.backend import DynamiqsBackend, DynamiqsSolverOptions

backend = DynamiqsBackend(
solver_options=DynamiqsSolverOptions(rtol=1e-8, atol=1e-8),
)
```

///
4 changes: 3 additions & 1 deletion src/oqd_trical/backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
# limitations under the License.

from . import dynamiqs, qutip
from .dynamiqs import DynamiqsBackend
from .dynamiqs import DynamiqsBackend, DynamiqsSolverOptions, TaskArgsAtomicEmulator
from .qutip import QutipBackend

__all__ = [
"qutip",
"dynamiqs",
"DynamiqsBackend",
"DynamiqsSolverOptions",
"TaskArgsAtomicEmulator",
"QutipBackend",
]
4 changes: 4 additions & 0 deletions src/oqd_trical/backend/dynamiqs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@

from .base import DynamiqsBackend
from .codegen import DynamiqsCodeGeneration
from .solver_options import DynamiqsSolverOptions
from .task import TaskArgsAtomicEmulator
from .vm import DynamiqsVM

__all__ = [
"DynamiqsBackend",
"DynamiqsCodeGeneration",
"DynamiqsSolverOptions",
"TaskArgsAtomicEmulator",
"DynamiqsVM",
]
49 changes: 44 additions & 5 deletions src/oqd_trical/backend/dynamiqs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from oqd_core.interface.atomic import AtomicCircuit

from oqd_trical.backend.dynamiqs.codegen import DynamiqsCodeGeneration
from oqd_trical.backend.dynamiqs.solver_options import DynamiqsSolverOptions
from oqd_trical.backend.dynamiqs.vm import DynamiqsVM
from oqd_trical.light_matter.compiler.analysis import GetHilbertSpace, HilbertSpace
from oqd_trical.light_matter.compiler.canonicalize import (
Expand All @@ -37,7 +38,9 @@ class DynamiqsBackend(BackendBase):
save_intermediate (bool): Whether compiler saves the intermediate representation of the atomic circuit
approx_pass (PassBase): Pass of approximations to apply to the system.
solver (Literal["SESolver","MESolver"]): Dynamiqs solver to use.
solver_options (Dict[str,Any]): Dynamiqs solver options
solver_options (DynamiqsSolverOptions): Dynamiqs solver options. Accepts a
[`DynamiqsSolverOptions`][oqd_trical.backend.dynamiqs.solver_options.DynamiqsSolverOptions],
a dict of its fields, or None for the documented defaults.
intermediate (AtomicEmulatorCircuit): Intermediate representation of the atomic circuit during compilation
"""

Expand All @@ -46,15 +49,15 @@ def __init__(
save_intermediate=True,
approx_pass=None,
solver="SESolver",
solver_options={},
solver_options=None,
):
super().__init__()

self.save_intermediate = save_intermediate
self.intermediate = None
self.approx_pass = approx_pass
self.solver = solver
self.solver_options = solver_options
self.solver_options = DynamiqsSolverOptions.from_obj(solver_options)

def compile(self, circuit, fock_cutoff, *, relabel=True):
"""
Expand Down Expand Up @@ -87,7 +90,6 @@ def compile(self, circuit, fock_cutoff, *, relabel=True):

get_hilbert_space = GetHilbertSpace()
analysis = Post(get_hilbert_space)
analysis(intermediate)

if relabel:
analysis(intermediate)
Expand All @@ -105,7 +107,7 @@ def compile(self, circuit, fock_cutoff, *, relabel=True):
hilbert_space = HilbertSpace(hilbert_space=_hilbert_space)

if any(map(lambda x: x is None, hilbert_space.hilbert_space.values())):
raise "Hilbert space not fully specified."
raise ValueError("Hilbert space not fully specified.")

relabeller = Post(RelabelStates(hilbert_space.get_relabel_rules()))
intermediate = relabeller(intermediate)
Expand Down Expand Up @@ -143,3 +145,40 @@ def run(self, experiment, hilbert_space, timestep, *, initial_state=None):
vm(experiment)

return vm.children[0].result

def run_task(self, task, *, initial_state=None):
"""
Runs an AtomicCircuit carried by a Task.

Mirrors the Task entry point of oqd-analog-emulator's QutipBackend. The
Fock cutoff and timestep are taken from the task args' fock_trunc and dt;
the Diffrax options come from the
[`TaskArgsAtomicEmulator`][oqd_trical.backend.dynamiqs.task.TaskArgsAtomicEmulator]
solver_options, falling back to the backend's own solver_options when unset.

Args:
task (Task): Task whose program is an AtomicCircuit and whose args are a
[`TaskArgsAtomicEmulator`][oqd_trical.backend.dynamiqs.task.TaskArgsAtomicEmulator].

Returns:
result (Dict[str,Any]): Result of execution, as returned by run().
"""
args = task.args
solver = getattr(args, "solver", self.solver)
solver_options = getattr(args, "solver_options", None) or self.solver_options

experiment, hilbert_space = self.compile(task.program, args.fock_trunc)

vm = Pre(
DynamiqsVM(
hilbert_space=hilbert_space,
timestep=args.dt,
solver=solver,
solver_options=solver_options,
initial_state=initial_state,
)
)

vm(experiment)

return vm.children[0].result
4 changes: 4 additions & 0 deletions src/oqd_trical/backend/dynamiqs/codegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ class DynamiqsCodeGeneration(ConversionRule):
Rule that converts an [`AtomicEmulatorCircuit`][oqd_trical.light_matter.interface.emulator.AtomicEmulatorCircuit]
to a [`DynamiqsExperiment`][oqd_trical.backend.dynamiqs.interface.DynamiqsExperiment]

Operators and coefficients are in angular frequency (rad/s) and time t is in
seconds, matching the atomic interface; the Hamiltonian is integrated with
hbar = 1.

Attributes:
hilbert_space (Dict[str, int]): Hilbert space of the system.
"""
Expand Down
4 changes: 2 additions & 2 deletions src/oqd_trical/backend/dynamiqs/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ class DynamiqsExperiment(TypeReflectBaseModel):
Class representing a Dynamiqs experiment represented in terms of atomic operations expressed in terms of their Hamiltonians.

Attributes:
base (Operator): Free Hamiltonian.
sequence (List[AtomicEmulatorGate]): List of gates to apply.
frame (Optional[dq.TimeQArray]): Time-dependent frame transformation, if any.
sequence (List[DynamiqsGate]): List of gates to apply.

"""

Expand Down
70 changes: 70 additions & 0 deletions src/oqd_trical/backend/dynamiqs/solver_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Copyright 2024-2025 Open Quantum Design

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Literal

import dynamiqs as dq
from pydantic import BaseModel, ConfigDict

########################################################################################


class DynamiqsSolverOptions(BaseModel):
"""Explicit Diffrax solver options for the Dynamiqs backend.

Builds the Dynamiqs method and options passed to each sesolve call. Tolerances
live on the integration method, from which Dynamiqs configures the Diffrax
PIDController.

Attributes:
method (str): Adaptive ODE method, one of Tsit5, Dopri5, Dopri8, Kvaerno3
or Kvaerno5. Defaults to Tsit5.
rtol (float): Relative tolerance of the step-size controller. Defaults to 1e-8.
atol (float): Absolute tolerance of the step-size controller. Defaults to 1e-8.
max_steps (int): Maximum number of solver steps. Defaults to 1000000.
progress_meter (bool): Show the Dynamiqs progress meter. Defaults to False to
avoid a tqdm/ZMQError under Jupyter (issue #26).
"""

model_config = ConfigDict(frozen=True)

method: Literal["Tsit5", "Dopri5", "Dopri8", "Kvaerno3", "Kvaerno5"] = "Tsit5"
rtol: float = 1e-8
atol: float = 1e-8
max_steps: int = 1_000_000
progress_meter: bool = False

def to_method(self):
"""Build the Dynamiqs adaptive integration method instance."""
return getattr(dq.method, self.method)(
rtol=self.rtol, atol=self.atol, max_steps=self.max_steps
)

def to_options(self):
"""Build the Dynamiqs solver options."""
return dq.Options(progress_meter=self.progress_meter)

@classmethod
def from_obj(cls, obj):
"""Normalize None, a dict or a DynamiqsSolverOptions into an instance."""
if obj is None:
return cls()
if isinstance(obj, cls):
return obj
if isinstance(obj, dict):
return cls(**obj)
raise TypeError(
"solver_options must be None, a dict, or a DynamiqsSolverOptions, "
f"got {type(obj).__name__}"
)
38 changes: 38 additions & 0 deletions src/oqd_trical/backend/dynamiqs/task.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright 2024-2025 Open Quantum Design

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Literal, Optional

from oqd_core.backend.task import TaskArgsAtomic

from oqd_trical.backend.dynamiqs.solver_options import DynamiqsSolverOptions

########################################################################################


class TaskArgsAtomicEmulator(TaskArgsAtomic):
"""Task arguments for the Dynamiqs backend.

Extends TaskArgsAtomic with the Diffrax solver and its options, so a Task can
carry them to
[`DynamiqsBackend.run_task`][oqd_trical.backend.dynamiqs.DynamiqsBackend.run_task].
The inherited fock_trunc and dt set the Fock cutoff and the timestep.

Attributes:
solver (Literal["SESolver","MESolver"]): Dynamiqs solver to use.
solver_options (Optional[DynamiqsSolverOptions]): Dynamiqs solver options.
"""

solver: Literal["SESolver", "MESolver"] = "SESolver"
solver_options: Optional[DynamiqsSolverOptions] = None
Loading