API Reference
A high-level Python API is provided for the programmatic execution of quantum control optimizations. The principal interface is the CtrlFreeQAPI class, defined in the ctrl_freeq.api module, which encapsulates the complete workflow from configuration loading through optimization to result retrieval.
Quick Start
from ctrl_freeq.api import CtrlFreeQAPI, load_single_qubit_config
# Load a built-in example configuration
api = load_single_qubit_config()
# View configuration summary
print(api.get_config_summary())
# Run optimization (returns optimized pulse parameters as a torch.Tensor)
solution = api.run_optimization()
# Access results stored on the parameters object
print(f"Final fidelity: {api.parameters.final_fidelity}")
print(f"Iterations: {api.parameters.iterations}")
CtrlFreeQAPI Class
The CtrlFreeQAPI class serves as the primary interface for interacting with the ctrl-freeq optimization framework.
Constructor
An API instance may be created from a path to a JSON configuration file (provided as a string or Path object) or from a dictionary containing the configuration parameters directly. An optional hamiltonian_model argument allows a pre-built HamiltonianModel instance to be injected directly, bypassing the registry lookup.
Parameters:
| Parameter | Type | Description |
|---|---|---|
config |
str, Path, or dict |
Configuration source |
hamiltonian_model |
HamiltonianModel or None |
Optional pre-built model instance (overrides hamiltonian_type in config) |
Examples:
from ctrl_freeq.api import CtrlFreeQAPI
config = {
"qubits": ["q1"],
"hamiltonian_type": "spin_chain", # or "superconducting"
"compute_resource": "cpu",
"parameters": {
"Delta": [0.0],
"Omega_R_max": [10000.0],
"pulse_duration": [0.001],
"point_in_pulse": [100],
# ... other parameters
},
"initial_states": [["Z"]],
"target_states": {"Axis": [["-Z"]]},
"optimization": {
"space": "hilbert",
"algorithm": "bfgs",
"max_iter": 100,
"targ_fid": 0.99
}
}
api = CtrlFreeQAPI(config)
Methods
run_optimization()
Executes the quantum control optimization with the loaded configuration.
Returns: torch.Tensor — The optimized pulse parameters.
Upon completion of the optimization, the following attributes are stored on api.parameters:
| Attribute | Type | Description |
|---|---|---|
final_fidelity |
float |
Physical fidelity of the returned solution |
final_penalty |
float |
Amplitude penalty of the returned solution |
final_score |
float |
Penalized score, final_fidelity - final_penalty |
iterations |
int |
Number of iterations performed |
fidelity_history |
list[float] |
Physical fidelity at each iteration |
penalty_history |
list[float] |
Amplitude penalty at each iteration |
score_history |
list[float] |
Penalized score at each iteration |
objective_mode |
str |
"gate" or "state_transfer" — see Objectives and Fidelity |
amplitude_report |
list[dict] |
Per-qubit peak amplitude against Omega_R_max |
Final metrics are evaluated on the solution that is returned, not on the optimizer's last trial point.
Example:
from ctrl_freeq.api import load_single_qubit_config
api = load_single_qubit_config()
solution = api.run_optimization()
# Access results from the parameters object
print(f"Final fidelity: {api.parameters.final_fidelity}")
print(f"Penalty: {api.parameters.final_penalty}")
print(f"Penalized score: {api.parameters.final_score}")
print(f"Iterations: {api.parameters.iterations}")
print(f"Fidelity history: {api.parameters.fidelity_history}")
fidelity_history semantics changed in 0.4.0
It previously stored fidelity - penalty. It now stores the physical
fidelity; the penalized series is score_history.
get_config_summary()
Returns a human-readable summary of the current configuration.
Returns: str — A formatted string containing the number of qubits, optimization space (Hilbert/Liouville), algorithm name, maximum iterations, target fidelity, and the initial and target states.
Example:
from ctrl_freeq.api import load_two_qubit_config
api = load_two_qubit_config()
print(api.get_config_summary())
Output:
Number of qubits: 2
Optimization space: hilbert
Algorithm: newton-cg
Max iterations: 1000
Target fidelity: 0.999
Initial states: [['Z', '-Z']]
Target Gate: ['CNOT']
update_parameter(parameter_path, value)
Updates a specific parameter in the configuration and reinitializes the internal state accordingly.
Parameters:
| Parameter | Type | Description |
|---|---|---|
parameter_path |
str |
Dot-separated path to the parameter |
value |
Any |
New value for the parameter |
Raises: KeyError if the parameter path is not found.
Example:
from ctrl_freeq.api import load_single_qubit_config
api = load_single_qubit_config()
# Update optimization settings
api.update_parameter("optimization.max_iter", 200)
api.update_parameter("optimization.algorithm", "qiskit-cobyla")
api.update_parameter("optimization.targ_fid", 0.999)
# Update pulse parameters
api.update_parameter("parameters.pulse_duration", [0.002])
# Run with updated configuration
solution = api.run_optimization()
Common parameter paths:
| Path | Description |
|---|---|
hamiltonian_type |
spin_chain or superconducting |
optimization.algorithm |
Optimization algorithm |
optimization.max_iter |
Maximum iterations |
optimization.targ_fid |
Target penalized score (fidelity − penalty) at which to stop |
optimization.space |
hilbert or liouville |
optimization.dissipation_mode |
non-dissipative or dissipative |
parameters.Delta |
Detuning / qubit frequency (Hz) |
parameters.Omega_R_max |
Maximum Rabi frequency / drive amplitude (Hz) |
parameters.pulse_duration |
Pulse duration (seconds) |
parameters.point_in_pulse |
Discretization points |
parameters.T1 |
Amplitude damping time (seconds) |
parameters.T2 |
Pure dephasing time (seconds) |
compute_resource |
cpu or gpu |
Properties
config
The raw configuration dictionary.
config_path
The path to the configuration file, if the instance was created from a file; otherwise None.
parameters
The initialized parameter object used internally by the optimization engine.
Helper Functions
run_from_config(config, hamiltonian_model=None)
A convenience function that creates a CtrlFreeQAPI instance and executes the optimization in a single call.
Parameters:
| Parameter | Type | Description |
|---|---|---|
config |
str, Path, or dict |
Configuration source |
hamiltonian_model |
HamiltonianModel or None |
Optional pre-built model instance |
Returns: torch.Tensor — The optimized pulse parameters.
Example with dictionary:
from ctrl_freeq.api import run_from_config
solution = run_from_config({
"qubits": ["q1"],
"compute_resource": "cpu",
"parameters": { ... },
"initial_states": [["Z"]],
"target_states": {"Axis": [["-Z"]]},
"optimization": {
"space": "hilbert",
"algorithm": "bfgs",
"max_iter": 100,
"targ_fid": 0.99
}
})
load_config(config_path)
Loads a JSON configuration file and returns a CtrlFreeQAPI instance.
Parameters:
| Parameter | Type | Description |
|---|---|---|
config_path |
str or Path |
Path to JSON configuration file |
Returns: CtrlFreeQAPI instance.
Built-in Example Configurations
Several pre-configured examples are bundled with the package for common use cases. These correspond to JSON files located under src/ctrl_freeq/data/json_input/.
Single-Qubit Configurations
load_single_qubit_config()
A basic single-qubit optimization configuration.
from ctrl_freeq.api import load_single_qubit_config
api = load_single_qubit_config()
solution = api.run_optimization()
load_single_qubit_multiple_config()
A single-qubit configuration with multiple initial/target state pairs for universal rotation design.
from ctrl_freeq.api import load_single_qubit_multiple_config
api = load_single_qubit_multiple_config()
load_single_qubit_polar_phase_config()
A single-qubit configuration using the polar/phase waveform mode.
from ctrl_freeq.api import load_single_qubit_polar_phase_config
api = load_single_qubit_polar_phase_config()
load_single_qubit_dissipative_config()
A single-qubit configuration with dissipative (Lindblad) dynamics enabled. The evolution includes amplitude damping (T1 = 1 ms) and pure dephasing (T2 = 500 μs), and the optimization is performed in Liouville (density matrix) space.
from ctrl_freeq.api import load_single_qubit_dissipative_config
api = load_single_qubit_dissipative_config()
Two-Qubit Configurations
load_two_qubit_config()
A basic two-qubit optimization configuration with inter-qubit coupling.
load_two_qubit_multiple_config()
A two-qubit configuration with multiple initial/target state pairs.
load_two_qubit_polar_phase_config()
A two-qubit configuration using the polar/phase waveform mode.
from ctrl_freeq.api import load_two_qubit_polar_phase_config
api = load_two_qubit_polar_phase_config()
Multi-Qubit Configurations
load_four_qubit_polar_phase_config()
A four-qubit optimization configuration using the polar/phase waveform mode.
from ctrl_freeq.api import load_four_qubit_polar_phase_config
api = load_four_qubit_polar_phase_config()
Complete Workflow Example
The following example illustrates a complete optimization workflow, from configuration loading through parameter customization, optimization, and result visualization:
from ctrl_freeq.api import CtrlFreeQAPI, load_single_qubit_config
from ctrl_freeq.visualisation.plotter import process_and_plot
from ctrl_freeq.visualisation.dashboard import create_dashboard
from datetime import datetime
# 1. Load configuration
api = load_single_qubit_config()
# 2. Customize parameters
api.update_parameter("optimization.algorithm", "qiskit-cobyla")
api.update_parameter("optimization.max_iter", 150)
api.update_parameter("optimization.targ_fid", 0.995)
# 3. Review configuration
print(api.get_config_summary())
# 4. Run optimization
solution = api.run_optimization()
# 5. Check results
print(f"Final fidelity: {api.parameters.final_fidelity}")
print(f"Iterations: {api.parameters.iterations}")
# 6. Generate plots
# process_and_plot takes the solution tensor and the parameters object
waveforms, figures = process_and_plot(solution, api.parameters, save_plots=True)
# 7. Create dashboard (pass parameters for a richer sidebar)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
dashboard_path = create_dashboard(figures, timestamp, parameters=api.parameters)
print(f"Dashboard saved to: {dashboard_path}")
Dissipative (Lindblad) Workflow Example
The following example demonstrates a complete optimization workflow for an open quantum system subject to amplitude damping and pure dephasing. The Lindblad master equation is employed to model the dissipative channels, and the optimization is performed in Liouville (density matrix) space.
from ctrl_freeq.api import load_single_qubit_dissipative_config
from ctrl_freeq.visualisation.plotter import process_and_plot
# Load the pre-configured dissipative example
api = load_single_qubit_dissipative_config()
# Review configuration (includes T1/T2 information)
print(api.get_config_summary())
# Run optimization
solution = api.run_optimization()
# Check results
print(f"Final fidelity: {api.parameters.final_fidelity}")
print(f"Iterations: {api.parameters.iterations}")
# Generate plots
waveforms, figures = process_and_plot(solution, api.parameters, show_plots=True)
from ctrl_freeq.api import CtrlFreeQAPI
from ctrl_freeq.visualisation.plotter import process_and_plot
config = {
"qubits": ["q1"],
"compute_resource": "cpu",
"parameters": {
"Delta": [10000000.0],
"Omega_R_max": [40000000.0],
"pulse_duration": [2e-07],
"point_in_pulse": [100],
"wf_type": ["cheb"],
"wf_mode": ["cart"],
"amplitude_envelope": ["gn"],
"amplitude_order": [1],
"coverage": ["broadband"],
"sw": [5000000.0],
"pulse_offset": [0.0],
"pulse_bandwidth": [500000.0],
"ratio_factor": [0.5],
"sigma_Delta": [0.0],
"sigma_Omega_R_max": [0.0],
"profile_order": [2],
"n_para": [16],
"J": [[0.0]],
"T1": [0.001], # 1 ms amplitude damping
"T2": [0.0005] # 500 μs pure dephasing
},
"initial_states": [["Z"]],
"target_states": {"Axis": [["-Z"]]},
"optimization": {
"space": "liouville",
"dissipation_mode": "dissipative",
"H0_snapshots": 100,
"Omega_R_snapshots": 1,
"algorithm": "qiskit-cobyla",
"max_iter": 1000,
"targ_fid": 0.99
}
}
api = CtrlFreeQAPI(config)
solution = api.run_optimization()
print(f"Final fidelity: {api.parameters.final_fidelity}")
Dissipation Requirements
When dissipation_mode is set to "dissipative", the following conditions must be satisfied:
- The optimization
spacemust be"liouville"(density matrix mode) - Per-qubit
T1andT2values must be provided in theparameterssection - The physical constraint \(T_2 \leq 2\,T_1\) must hold for each qubit
For a detailed description of the dissipation parameters, see Optimization → Parameters → Dissipation Parameters.
Hamiltonian Models
Ctrl-freeq provides a platform-agnostic Hamiltonian model abstraction for defining quantum control problems. All models implement the HamiltonianModel abstract base class, which follows the standard bilinear control formulation:
The framework uses a plugin architecture based on a model registry. New Hamiltonian types can be added by subclassing HamiltonianModel and decorating with @register_hamiltonian — no framework files need to be modified.
HamiltonianModel (Abstract Base Class)
All Hamiltonian models implement the following interface:
| Method / Property | Description |
|---|---|
build_drift(frequency_instances, coupling_instances=None, **kwargs) |
Construct drift Hamiltonian \(H_0\) snapshots (list of \((D, D)\) numpy arrays) |
build_control_ops() |
Return fixed control operators \(H_{\text{ctrl},k}\) (list of \((D, D)\) numpy arrays) |
control_amplitudes(cx, cy, rabi_freq, n_h0) |
Map waveform outputs to control amplitudes \(u_k(t)\) |
from_config(n_qubits, params) (classmethod) |
Construct a model instance from a configuration dictionary |
default_config(n_qubits) (classmethod) |
Return a complete runnable configuration dictionary |
dim (property) |
Hilbert space dimension \(D\) (e.g. \(2^n\) for qubit models, \(3^n\) for 3-level models) |
n_controls (property) |
Number of independent control channels |
control_ops_tensor() |
Control operators as a stacked torch tensor \((n_\text{controls}, D, D)\) |
embed_computational_state(state) |
Embed a \(2^n\) state into the model's Hilbert space (identity for qubit models) |
embed_computational_gate(gate) |
Embed a \(2^n \times 2^n\) gate into the model's Hilbert space (identity for qubit models) |
The build_drift method uses a standardized signature across all models: frequency_instances contains per-qubit frequency arrays (detunings, qubit frequencies, etc.), and coupling_instances contains coupling matrices. Model-specific parameters are passed via **kwargs.
Model Registry
The registry provides automatic discovery and lookup of Hamiltonian models:
from ctrl_freeq.setup.hamiltonian_generation import (
register_hamiltonian,
get_hamiltonian_class,
list_hamiltonians,
)
# List all registered models
print(list_hamiltonians()) # ['duffing_transmon', 'spin_chain', 'superconducting']
# Look up a model class by name
cls = get_hamiltonian_class("spin_chain")
model = cls.from_config(n_qubits=2, params={"coupling_type": "XY"})
| Function | Description |
|---|---|
register_hamiltonian(name) |
Decorator that registers a HamiltonianModel subclass under the given name |
get_hamiltonian_class(name) |
Look up and return the model class for a given name |
list_hamiltonians() |
Return a sorted list of all registered model names |
SpinChainModel
Models spin-chain qubits with detuning-based drift and configurable inter-qubit coupling (Ising, XY, Heisenberg).
from ctrl_freeq.setup.hamiltonian_generation import SpinChainModel
model = SpinChainModel(n_qubits=2, coupling_type="XY")
# Build drift Hamiltonian
import numpy as np
Delta = np.array([2 * np.pi * 1e7, 2 * np.pi * 2e7])
J = np.array([[0.0, 2 * np.pi * 1e6], [0.0, 0.0]])
H0_list = model.build_drift(frequency_instances=[Delta], coupling_instances=[J])
# Get control operators
ctrl_ops = model.build_control_ops() # [X_0, Y_0, X_1, Y_1]
| Parameter | Description |
|---|---|
n_qubits |
Number of spin-1/2 qubits |
coupling_type |
"Z" (Ising), "XY" (exchange), or "XYZ" (Heisenberg) |
SuperconductingQubitModel
Models fixed-frequency transmon qubits in the per-qubit rotating frame (RWA applied) with capacitive coupling, supporting exchange (XY), static ZZ, and combined (XY+ZZ) coupling. The frequency_instances values are rotating-frame detunings \(\delta_i\), not bare transition frequencies.
from ctrl_freeq.setup.hamiltonian_generation import SuperconductingQubitModel
model = SuperconductingQubitModel(
n_qubits=2,
coupling_type="XY+ZZ",
anharmonicities=np.array([2 * np.pi * -330e6, 2 * np.pi * -330e6]),
)
# Build drift Hamiltonian
omega = np.array([2 * np.pi * 5e9, 2 * np.pi * 5.2e9])
g = np.array([[0.0, 2 * np.pi * 5e6], [0.0, 0.0]])
H0_list = model.build_drift(frequency_instances=[omega], coupling_instances=[g])
| Parameter | Description |
|---|---|
n_qubits |
Number of transmon qubits |
coupling_type |
"XY" (exchange), "ZZ" (static ZZ), or "XY+ZZ" (both) |
anharmonicities |
Per-qubit anharmonicities \(\alpha_i\) (rad/s), used for perturbative ZZ computation |
zz_crosstalk |
Calibrated static ZZ coupling matrix (rad/s). Overrides the perturbative formula when provided |
stark_shift_coeffs |
Per-qubit AC Stark shift coefficients \(s_i\). Adds a Z control channel per qubit with amplitude \(-s_i (I^2+Q^2) \Omega_d^2\) — \(s_i\) shifts the qubit frequency, so it enters the drift with the same minus sign as the detuning |
ZZ coupling priority chain
When coupling_type includes "ZZ", the ZZ interaction is determined by the first available source:
- Runtime
zz_instanceskwarg tobuild_drift(per-snapshot, highest priority) - Constructor
zz_crosstalk(calibrated, fixed across snapshots) - Perturbative formula \(\zeta_{ij} = 2 g_{ij}^2 (\alpha_i + \alpha_j) / \bigl[(\Delta + \alpha_i)(\Delta - \alpha_j)\bigr]\) with \(\Delta = \delta_i - \delta_j\), evaluated per snapshot. Refused when the \(|11\rangle\)-\(|20\rangle\)/\(|02\rangle\) mixing \(\sqrt{2}|g| / \min(|\Delta+\alpha_i|, |\Delta-\alpha_j|)\) exceeds
0.1 - Zero matrix
DuffingTransmonModel
A 3-level (Duffing oscillator) transmon model that captures leakage to the \(|2\rangle\) state. Each transmon is modelled as a 3-level system, giving a Hilbert space of dimension \(3^{n_\text{qubits}}\). Anharmonicities are required.
from ctrl_freeq.setup.hamiltonian_generation import DuffingTransmonModel
model = DuffingTransmonModel(
n_qubits=2,
coupling_type="XY",
anharmonicities=np.array([2 * np.pi * -330e6, 2 * np.pi * -330e6]),
)
# Drift: ω n̂ + (α/2) n̂(n̂−I) + g (a†a + aa†)
omega = np.array([2 * np.pi * 5e9, 2 * np.pi * 5.2e9])
g = np.array([[0.0, 2 * np.pi * 5e6], [0.0, 0.0]])
H0_list = model.build_drift(frequency_instances=[omega], coupling_instances=[g])
# Embed 2-level states/gates into the 3-level space
import numpy as np
state_2 = np.array([1, 0, 0, 0], dtype=complex) # |00>
state_3 = model.embed_computational_state(state_2) # (9,) vector
X_gate = np.array([[0,1],[1,0]], dtype=complex)
X_embed = model.embed_computational_gate(X_gate) # (3,3) — identity on |2>
# Compute leakage
leakage = model.leakage(state_3) # 0.0 for computational states
| Parameter | Description |
|---|---|
n_qubits |
Number of transmon qubits |
coupling_type |
Currently "XY" (exchange coupling via \(a^\dagger_i a_j + a_i a^\dagger_j\)) |
anharmonicities |
Per-qubit anharmonicities \(\alpha_i\) (rad/s). Required. |
| Method | Description |
|---|---|
embed_computational_state(state) |
Map a \(2^n\) state vector into the \(3^n\) Hilbert space |
embed_computational_gate(gate) |
Embed a \(2^n \times 2^n\) gate (identity on leakage subspace) |
leakage(state) |
Compute population outside the computational subspace ({ |
Using Models in Configuration
To use a Hamiltonian model, set the hamiltonian_type field in the configuration:
config = {
"qubits": ["q1", "q2"],
"hamiltonian_type": "superconducting", # or "spin_chain"
"compute_resource": "cpu",
"parameters": { ... },
"initial_states": [["Z", "Z"]],
"target_states": {"Gate": ["iSWAP"]},
"optimization": { ... }
}
Default Configurations
Each model provides a default_config classmethod that returns a complete, ready-to-run configuration dictionary with sensible physical defaults. This is useful for quick experiments and will also serve as the basis for GUI form population.
from ctrl_freeq.setup.hamiltonian_generation import (
SpinChainModel, SuperconductingQubitModel, DuffingTransmonModel
)
from ctrl_freeq.api import CtrlFreeQAPI
# Get a complete spin-chain config for 2 qubits (CNOT gate, XY coupling)
config = SpinChainModel.default_config(n_qubits=2)
api = CtrlFreeQAPI(config)
# Get a complete superconducting config for 2 qubits (iSWAP gate, XY coupling)
config = SuperconductingQubitModel.default_config(n_qubits=2)
api = CtrlFreeQAPI(config)
# Get a complete 3-level Duffing config for 2 qubits (leakage-aware)
config = DuffingTransmonModel.default_config(n_qubits=2)
api = CtrlFreeQAPI(config)
Adding Custom Hamiltonian Models
A user who wants to add a new Hamiltonian needs to do just one thing: write a Python class that subclasses HamiltonianModel and decorate it with @register_hamiltonian("name"). No framework files need to be touched.
Step 1 — Define the model class
Implement six required members (build_drift, build_control_ops, control_amplitudes, from_config, dim, n_controls) and optionally default_config:
from ctrl_freeq.setup.hamiltonian_generation import HamiltonianModel, register_hamiltonian
import numpy as np
@register_hamiltonian("trapped_ion")
class TrappedIonModel(HamiltonianModel):
def __init__(self, n_qubits, laser_wavelength=729e-9):
self.n_qubits = n_qubits
self.laser_wavelength = laser_wavelength
# ... pre-compute operators, Lamb-Dicke params, etc.
@property
def dim(self):
return 2 ** self.n_qubits
@property
def n_controls(self):
return 2 * self.n_qubits # I, Q per qubit
def build_drift(self, frequency_instances, coupling_instances=None, **kwargs):
# frequency_instances: list of arrays with per-qubit trap frequencies
# coupling_instances: list of coupling matrices (Mølmer-Sørensen, etc.)
# Return: list of (D, D) numpy arrays — one per snapshot
...
def build_control_ops(self):
# Return list of (D, D) numpy arrays: [X_0, Y_0, X_1, Y_1, ...]
...
def control_amplitudes(self, cx, cy, rabi_freq, n_h0):
# Map waveform (cx, cy) and Rabi frequency to control amplitudes u_k(t)
...
@classmethod
def from_config(cls, n_qubits, params):
# Extract model-specific constructor args from the config dict
return cls(n_qubits, laser_wavelength=params.get("laser_wavelength", 729e-9))
@classmethod
def default_config(cls, n_qubits):
# Optional: return a complete, ready-to-run config with sensible defaults
return {
"hamiltonian_type": "trapped_ion",
"qubits": [f"q{i+1}" for i in range(n_qubits)],
"parameters": { ... },
"initial_states": [["Z", "Z"]],
"target_states": {"Gate": ["XX"]},
"optimization": {"algorithm": "l-bfgs", "max_iter": 300, "targ_fid": 0.999, ...},
}
Step 2 — Make sure the class is imported
The @register_hamiltonian decorator fires at import time, so the module just needs to be imported before the config is loaded. Define it in the same script that runs the optimization, or import it from your own package.
Step 3 — Use it
Two workflows are available:
Reference the model by name in any configuration dictionary or JSON file:
For one-off experiments, inject a model instance directly without registration:
model = TrappedIonModel(n_qubits=2, laser_wavelength=729e-9)
api = CtrlFreeQAPI(config_dict, hamiltonian_model=model)
solution = api.run_optimization()
The injected model overrides any hamiltonian_type specified in the configuration.
What you get for free
Everything else in the pipeline works automatically — waveform parameterisation, gradient computation (autograd), fidelity evaluation, plotting, and dashboards — because it all goes through the generic pulse_hamiltonian_generic path using build_control_ops and control_amplitudes. The user only defines the physics; the optimisation machinery is inherited.
Analysis and Plotting
process_and_plot(solution, parameters) replays an optimised pulse and draws
the waveforms, trajectories, observables, leakage, excitation profiles and,
for a single qubit, the Bloch-sphere video. The
replay uses the optimizer's own control mapping, propagator and dissipative
channel, so plotted final states match the objective's.
Time grids
Waveform samples sit at the midpoints of the propagation intervals,
\((k + \tfrac{1}{2})\,T/N\), while propagated states exist at the \(N+1\)
interval boundaries \(k\,T/N\), starting with the initial state. Plot
trajectories against parameters.state_boundary_times(), not
parameters.t:
p = api.parameters
p.t # N waveform sample times (midpoints)
p.state_boundary_times() # N+1 state times, ending exactly at T
p.dt # T / N
Leakage
For models with a local dimension above 2 (e.g. duffing_transmon),
compute_and_store_evolution also returns the total leakage
\(L(t) = 1 - \mathrm{Tr}(\Pi_{\text{comp}}\,\rho(t))\) at every state boundary,
as a nominal trace plus the snapshot minimum/maximum envelope. This is a single
total for the whole register — per-qubit leakages are not summed.
cxs, cys, history, history_mean, leakage = compute_and_store_evolution(
solution, p, p.init[0]
)
leakage["nominal"] # (N+1,)
leakage["snapshots"] # (N+1, n_snapshots)
Bloch-sphere video
For a single qubit, with the video extra installed (pip install
"ctrl-freeq[video]"), process_and_plot adds the Bloch-sphere dynamics to
figures as a nutate.Trajectory, and create_dashboard renders it into the
dashboard. With save_plots=True the video is rendered straight away to
results/plots/ and its MP4 path takes the trajectory's place; with
show_plots=True nutate's interactive viewer opens in its own window. The two
steps are also available on their own:
from ctrl_freeq.visualisation.plotter import bloch_trajectory, render_bloch_video
p = api.parameters
traj = bloch_trajectory(solution, p, p.init[0]) # 31 offsets across Delta ± sw/2
video = render_bloch_video(traj, "bloch.mp4") # Path, or None if rendering failed
bloch_trajectory replays the pulse with the optimizer's physics (including
dissipation) at the nominal Rabi amplitude, and takes num_points= and
waveform_spec= like the other analysis functions. Times are in seconds and
offsets in Hz. render_bloch_video writes a 720p MP4 in a separate process, so
a crash in VTK (for example without a display) cannot take your session down.
The trajectory is plain nutate data, so nutate's own functions accept it too,
e.g. nutate.snapshot(traj, "bloch.png") for a still; those run in your
process.
Waveform representation
A solution vector is meaningless without the representation it was optimised in: a basis-mode solution holds basis coefficients, a piecewise solution holds one value per pulse segment against an identity basis. Each optimizer records its representation on the parameters object, and analysis uses it automatically.
When two optimizers share one parameters object, the recorded representation tracks the most recent run only. Pass the owning run's representation explicitly:
basis_spec = api.waveform_spec() # stays valid across later runs
piecewise = PiecewiseAPI(api)
pw_solution = piecewise.run_optimization()
pw_spec = piecewise.waveform_spec()
process_and_plot(basis_solution, api.parameters, waveform_spec=basis_spec)
process_and_plot(pw_solution, piecewise.parameters, waveform_spec=pw_spec)
waveform_spec= is accepted by process_and_plot,
compute_and_store_evolution, get_final_rho_for_excitation_profile and
plot_excitation_profiles. Omitting it keeps the implicit behaviour.
Equal parameter counts are indistinguishable
Analysing a solution whose parameter count disagrees with the recorded
representation raises a named error. Representations of the same size
cannot be told apart — for example a two-point basis solution and a
two-point piecewise cart solution, both 4 parameters — and will replay
with the latest run's representation. Optimisation metrics such as
final_fidelity live on the same shared object and are overwritten the
same way.
Configuration Schema
For a detailed treatment of all configuration parameters, the reader is referred to:
- Optimization Parameters — Full parameter reference
- Objectives and Fidelity — What is optimised, and which number is reported
- Algorithms — Available optimization algorithms
- Compute (CPU/GPU) — CPU and GPU configuration
Error Handling
from ctrl_freeq.api import CtrlFreeQAPI
try:
api = CtrlFreeQAPI("nonexistent.json")
except FileNotFoundError as e:
print(f"Configuration file not found: {e}")
try:
api.update_parameter("invalid.path", 100)
except KeyError as e:
print(f"Invalid parameter path: {e}")
Next Steps
- GUI Guide — Configure and run optimization interactively
- Dashboard — Visualize and export results
- Optimization Parameters — Detailed parameter reference