Analytical Optimization Digital Twin from Experimental Trajectories¶
Build a digital twin of an open quantum system using analytical optimization: learn unknown Lindblad jump rates from observable time series via simulator forward modeling and CMA-ES, validate the fit on the measured traces, then deploy the learned model in Simulator to predict held-out observables.
The entry point is NoiseCharacterizer.
Note
A machine-learning pipeline with the same I/O (reference trajectories in, fitted
NoiseModel out) is planned for a future release.
Note
Rates are not always uniquely identifiable from a sparse observable set. Judge a fit by trajectory overlap first; rate bars are secondary validation.
Note
Forward backends: representation="auto" (default) prefers deterministic Lindblad on small chains, then MCWF ("vector"), then TJM ("mps"). See Representation Comparison for cross-backend validation.
1. Minimal fit¶
Three-site transverse-field Ising chain with homogeneous Pauli noise. Pass reference_model= to simulate target trajectories internally (benchmark shortcut); for lab data use ref_expectations= instead (section 3).
1import warnings
2
3import matplotlib.pyplot as plt
4import numpy as np
5
6warnings.filterwarnings("ignore", message=".*special injected samples.*")
7
8from mqt.yaqs import AnalogSimParams, Hamiltonian, NoiseCharacterizer, NoiseModel, Observable, Simulator, State
9
10n_sites = 3
11j_coupling = 1.0
12transverse_field = 2.0
13gamma_true = 0.08
14gamma_init = 0.35
15cma_seed = 42
16sites = list(range(n_sites))
17
18hamiltonian = Hamiltonian.ising(n_sites, J=j_coupling, g=transverse_field)
19init_state = State(n_sites, initial="zeros")
20
21fitting_observables = [
22 Observable("y", 0),
23 Observable("z", 0),
24 Observable("y", 1),
25]
26prediction_observables = [
27 Observable("x", 0),
28 Observable("x", 1),
29 Observable("x", 2),
30 Observable("z", 2),
31]
32
33sim_params = AnalogSimParams(
34 observables=fitting_observables,
35 elapsed_time=0.8,
36 dt=0.1,
37 order=1,
38 sample_timesteps=True,
39)
40
41reference_model = NoiseModel(
42 [{"name": "pauli_x", "sites": [s], "strength": gamma_true} for s in sites]
43 + [{"name": "pauli_y", "sites": [s], "strength": gamma_true} for s in sites]
44 + [{"name": "pauli_z", "sites": [s], "strength": gamma_true} for s in sites]
45)
46
47init_guess = NoiseModel(
48 [{"name": "pauli_x", "sites": [s], "strength": gamma_init} for s in sites]
49 + [{"name": "pauli_y", "sites": [s], "strength": gamma_init} for s in sites]
50 + [{"name": "pauli_z", "sites": [s], "strength": gamma_init} for s in sites]
51)
52
53rate_bounds_low = np.zeros(len(init_guess.processes))
54rate_bounds_high = np.full(len(init_guess.processes), 0.5)
55pauli_labels = ["X", "Y", "Z"]
56
57nc = NoiseCharacterizer(show_progress=False)
58result = nc.characterize(
59 hamiltonian,
60 sim_params,
61 init_state=init_state,
62 init_guess=init_guess,
63 observables=fitting_observables,
64 reference_model=reference_model,
65 x_low=rate_bounds_low,
66 x_up=rate_bounds_high,
67 sigma0=0.05,
68 popsize=8,
69 max_iter=40,
70 seed=cma_seed,
71)
72
73gamma_learned = np.array([
74 result.best_parameters[0:n_sites].mean(),
75 result.best_parameters[n_sites : 2 * n_sites].mean(),
76 result.best_parameters[2 * n_sites : 3 * n_sites].mean(),
77])
78times = result.times
79print(f"√J: {result.sqrt_loss_before():.3f} → {result.sqrt_loss_after():.2e}")
80print(f"fitting trajectory RMSE: {result.trajectory_rmse():.2e}")
√J: 0.178 → 6.05e-03
fitting trajectory RMSE: 6.05e-03
2. Validate fitted dynamics and rates¶
1gamma_reference = np.full(len(pauli_labels), gamma_true)
2ref_traj = result.ref_traj
3fit_traj = result.fit_traj
4
5fig, axes = plt.subplots(1, 3, figsize=(9, 2.8), gridspec_kw={"width_ratios": [1.1, 1.0, 1.0]})
6
7x_pos = np.arange(len(pauli_labels))
8bar_width = 0.35
9axes[0].bar(x_pos - bar_width / 2, gamma_reference, bar_width, label=r"$\gamma_{\mathrm{true}}$", color="0.35")
10axes[0].bar(x_pos + bar_width / 2, gamma_learned, bar_width, label="learned twin", color="C0")
11axes[0].set_xticks(x_pos, pauli_labels)
12axes[0].set_ylabel(r"$\gamma$")
13axes[0].set_title("Learned rates vs. hidden truth")
14axes[0].legend(loc="upper right", fontsize=8)
15
16fit_panels = [(0, r"$\langle Y_0\rangle$"), (1, r"$\langle Z_0\rangle$")]
17for ax, (obs_idx, ylabel) in zip(axes[1:], fit_panels, strict=True):
18 ax.plot(times, fit_traj[obs_idx], color="C0", lw=2.5, label="twin", zorder=1)
19 ax.plot(times, ref_traj[obs_idx], color="0.2", ls=":", lw=2.5, label="experiment", zorder=2)
20 ax.set_xlabel("time")
21 ax.set_ylabel(ylabel)
22 ax.set_ylim(-1.05, 1.05)
23 panel_rmse = float(np.sqrt(np.mean((fit_traj[obs_idx] - ref_traj[obs_idx]) ** 2)))
24 ax.text(0.03, 0.06, rf"RMSE={panel_rmse:.1e}", transform=ax.transAxes, fontsize=8)
25 ax.legend(loc="upper right", fontsize=8)
26
27fig.suptitle("Twin reproduces the experimental fitting observables", y=1.05, fontsize=11)
28fig.tight_layout()
3. Experimental data¶
When trajectories come from the lab (or an external simulator), pass them as ref_expectations with shape (n_obs, n_times) matching observables and sim_params.times. Below we reuse the reference trajectories from section 1 as a stand-in for measured data.
1experimental_data = np.asarray(result.ref_traj, dtype=float)
2
3lab_result = NoiseCharacterizer(show_progress=False).characterize(
4 hamiltonian,
5 sim_params,
6 init_state=init_state,
7 init_guess=init_guess,
8 observables=fitting_observables,
9 ref_expectations=experimental_data,
10 x_low=rate_bounds_low,
11 x_up=rate_bounds_high,
12 sigma0=0.05,
13 popsize=8,
14 max_iter=40,
15 seed=cma_seed,
16)
17print(f"lab-data fit RMSE: {lab_result.trajectory_rmse():.2e}")
lab-data fit RMSE: 6.05e-03
4. Predict held-out observables with the twin¶
Plug result.optimal_model into Simulator and compare to the hidden reference on observables not used during fitting.
1pred_params = AnalogSimParams(
2 observables=prediction_observables,
3 elapsed_time=sim_params.elapsed_time,
4 dt=sim_params.dt,
5 order=sim_params.order,
6 sample_timesteps=True,
7)
8simulator = Simulator(show_progress=False)
9
10twin_result = simulator.run(init_state, hamiltonian, pred_params, result.optimal_model)
11truth_result = simulator.run(init_state, hamiltonian, pred_params, reference_model)
12twin_traj = np.asarray(twin_result.expectation_values, dtype=float)
13truth_traj = np.asarray(truth_result.expectation_values, dtype=float)
14
15fig, axes = plt.subplots(1, 2, figsize=(7, 2.8))
16holdout_panels = [(0, r"$\langle X_0\rangle$"), (3, r"$\langle Z_2\rangle$")]
17for ax, (obs_idx, ylabel) in zip(axes, holdout_panels, strict=True):
18 ax.plot(times, twin_traj[obs_idx], color="C0", lw=2.5, label="twin", zorder=1)
19 ax.plot(times, truth_traj[obs_idx], color="0.2", ls=":", lw=2.5, label="reference", zorder=2)
20 ax.set_xlabel("time")
21 ax.set_ylabel(ylabel)
22 ax.set_ylim(-1.05, 1.05)
23 ax.legend(loc="upper right", fontsize=8)
24
25fig.suptitle("Twin predicts observables outside the fitting set", y=1.05, fontsize=11)
26fig.tight_layout()
5. Stochastic experimental data (MCWF)¶
The same workflow works with trajectory-averaged MCWF data. Increase num_traj until observables stabilize; the objective becomes stochastic.
1mcwf_sim_params = AnalogSimParams(
2 observables=fitting_observables,
3 elapsed_time=0.8,
4 dt=0.1,
5 order=1,
6 num_traj=32,
7 sample_timesteps=True,
8)
9
10mcwf_result = NoiseCharacterizer(show_progress=False, representation="vector").characterize(
11 hamiltonian,
12 mcwf_sim_params,
13 init_state=init_state,
14 init_guess=init_guess,
15 observables=fitting_observables,
16 reference_model=reference_model,
17 x_low=rate_bounds_low,
18 x_up=rate_bounds_high,
19 sigma0=0.05,
20 popsize=8,
21 max_iter=20,
22 seed=cma_seed,
23)
24
25fig, ax = plt.subplots(figsize=(4.5, 2.8))
26obs_idx = 1
27ax.plot(times, mcwf_result.fit_traj[obs_idx], color="C0", lw=2.5, label="MCWF twin", zorder=1)
28ax.plot(times, mcwf_result.ref_traj[obs_idx], color="0.2", ls=":", lw=2.5, label="experiment", zorder=2)
29ax.set_xlabel("time")
30ax.set_ylabel(r"$\langle Z_0\rangle$")
31ax.set_ylim(-1.05, 1.05)
32ax.legend(loc="upper right", fontsize=8)
33ax.set_title(f"MCWF fit: √J → {mcwf_result.sqrt_loss_after():.2e}")
34fig.tight_layout()
Workflow summary¶
Step |
Action |
|---|---|
1 |
Collect experimental trajectories on a fitting observable set |
2 |
|
3 |
Compare learned rates and fitted-observable dynamics to reference |
4 |
|
See also¶
Representation Comparison — Lindblad vs MCWF vs TJM on the same benchmark
Noisy Analog Simulation — open-system simulation overview
Probing Environmental Memory — non-Markovian memory characterization (the memory twin submodule)