Probing Environmental Memory¶
Open quantum systems in YAQS couple a probe qubit (site 0) to an environment simulated by the remaining chain. Environmental memory measures how long the environment keeps past control and measurement choices relevant for future probe responses, evaluated at a temporal cut \(c\) in a sequence of interventions.
Use characterize() to
probe operational memory: assemble the weighted response matrix
\(\widetilde{V}(c)\), then read \(S_V(c)\), \(R(c)=\exp(S_V(c))\), and the mode
spectrum.
Alternatively, build a process tensor (default: direct MPO) and call
compute_temporal_entropy for temporal entanglement \(S_{PT}(c)\) of the
multi-time process itself — a distinct quantity from \(S_V(c)\). For fast dynamics
under control sequences, see Memory Surrogate Training and Prediction.
Setup¶
1import matplotlib.pyplot as plt
2import numpy as np
3
4from mqt.yaqs import AnalogSimParams, Hamiltonian, MemoryCharacterizer
5from mqt.yaqs.characterization.memory.shared.utils import make_zero_psi
6
7length = 4
8ham = Hamiltonian.ising(length=length, J=1.0, g=1.0)
9params = AnalogSimParams(dt=0.1, max_bond_dim=16, order=1)
10mc = MemoryCharacterizer(show_progress=False)
11psi0 = make_zero_psi(length)
Throughout, num_interventions is the probe-sequence length \(k\) and cut is
the causal-break index \(c\) (the break sits at step \(c-1\); future legs use steps
\(c+1,\ldots,k\)). Use \(k>1\) and an interior cut so both past and future probe
legs contribute to \(\widetilde{V}(c)\).
Characterize with the Hamiltonian backend¶
The full chain (system + environment) is simulated for each probe sequence. This is the reference memory metric when you have a microscopic open-system model.
1cut, num_interventions = 4, 6
2ham_result = mc.characterize(
3 ham,
4 params,
5 cut=cut,
6 num_interventions=num_interventions,
7 n_pasts=8,
8 n_futures=8,
9 initial_psi=psi0,
10 rng=np.random.default_rng(42),
11)
12
13sv = ham_result.singular_values(cut)
14fig, axes = plt.subplots(1, 2, figsize=(8, 3))
15axes[0].semilogy(sv, "o-")
16axes[0].set_xlabel("mode index")
17axes[0].set_ylabel("singular value")
18axes[0].set_title(r"Memory spectrum at cut $c=4$")
19
20v = ham_result.response_matrix(cut)
21im = axes[1].imshow(np.abs(v), aspect="auto", cmap="viridis")
22axes[1].set_title(r"$|\widetilde{V}(c)|$")
23axes[1].set_xlabel("future probe index")
24axes[1].set_ylabel("past probe index")
25fig.colorbar(im, ax=axes[1], fraction=0.046, pad=0.04)
26fig.suptitle(
27 rf"$S_V(c={cut})={ham_result.entropy(cut):.3f}$, "
28 rf"$R(c)={ham_result.modes(cut):.2f}$",
29 y=1.02,
30)
31fig.tight_layout()
Use preset="quick", "balanced", or "accurate" for default probe-grid
sizes, or set n_pasts / n_futures explicitly.
Reading CharacterizationResult¶
Access |
Meaning |
|---|---|
|
Environmental memory entropy \(S_V(c)\) |
|
Effective memory modes \(R(c)=\exp(S_V(c))\) |
|
Mode spectrum at cut \(c\) (how many independent past branches remain visible) |
|
Cross-cut memory matrix \(\widetilde{V}(c)\) (see theory below) |
|
Probe arrays used at cut \(c\) (for reuse or inspection) |
|
Human-readable table of entropies and modes |
Theory: split-cut probing¶
Environmental memory asks: across a grid of past and future control settings on the probe, how many independent ways does the environment still correlate past choices with accessible future responses?
The split-cut protocol:
Sample past control legs \(\alpha=(U_1,\ldots,U_{c-1})\) and future legs \(\beta=(V_{c+1},\ldots,V_k)\) on the probe.
Insert a causal break at step \(c\): measure on the past side and prepare on the future side while the environment continues to evolve.
For each grid entry, simulate the open system, record probe weights and the output Pauli vector \(\mathbf{r}=(\langle X\rangle,\langle Y\rangle,\langle Z\rangle)\).
Assemble the weighted probe responses into \(\widetilde{V}(c)\) and compute \(S_V(c)\) from the normalized mode spectrum.
Hamiltonian characterize obtains weights from simulated intervention
probabilities through cut \(c\) (MCWF or TJM/MPS, per representation). Surrogate
and exact-reference backends use the same probing protocol with analytic weights
on the reference probe path.
Coupling strength and memory¶
Stronger Ising coupling \(J\) between the probe and the environment typically
increases cross-cut memory. Reuse one probe_set when sweeping \(J\):
1j_values = np.linspace(0.0, 2.0, 9)
2anchor = mc.characterize(
3 Hamiltonian.ising(length=length, J=0.0, g=1.0),
4 params,
5 num_interventions=num_interventions,
6 cut=cut,
7 n_pasts=8,
8 n_futures=8,
9 initial_psi=psi0,
10 rng=np.random.default_rng(42),
11)
12entropies = []
13for j in j_values:
14 result = mc.characterize(
15 Hamiltonian.ising(length=length, J=float(j), g=1.0),
16 params,
17 num_interventions=num_interventions,
18 cut=cut,
19 probe_set=anchor,
20 )
21 entropies.append(result.entropy(cut))
22
23fig, ax = plt.subplots(figsize=(5.5, 3))
24ax.plot(j_values, entropies, "o-")
25ax.set_xlabel(r"Ising coupling $J$")
26ax.set_ylabel(r"$S_V(c)$")
27ax.set_title(r"Environmental memory grows with probe-environment coupling")
28fig.tight_layout()
Intervention styles¶
characterize accepts intervention_style= (default "haar"):
"haar"— random unitaries on sequence legs; measure/prepare only at the causal cut."measure_prepare"— rank-1 measure–prepare maps on every leg."clifford"— random single-qubit Clifford gates on legs.
Pass probe_set= from a Hamiltonian run so surrogate or exact-reference
backends evaluate the same probe ensemble (Memory Surrogate Training and Prediction).
Memory persistence: reset delay at the causal break¶
Pass delay=N to insert \(N\) soft-reset slots
\((\lvert 0\rangle, \lvert 0\rangle)\) at the causal cut while the environment
keeps evolving. Extra reset time lets the environment decouple from the past
before future controls act, so \(S_V(c)\) often decreases at strong
probe-environment coupling (weaker coupling can show the opposite trend).
The logical num_interventions and cut are unchanged; the physical sequence
length becomes num_interventions + delay + 1. Reuse the same probe_set when
sweeping delay. delay > 0 is supported for Hamiltonian characterize only.
1delay_length = 6
2ham_delay = Hamiltonian.ising(length=delay_length, J=2.0, g=1.0)
3params_delay = AnalogSimParams(dt=0.1)
4mc_delay = MemoryCharacterizer(show_progress=False)
5delay_cut = 4
6delay_k = 6
7anchor_delay = mc_delay.characterize(
8 ham_delay,
9 params_delay,
10 num_interventions=delay_k,
11 cut=delay_cut,
12 delay=0,
13 n_pasts=6,
14 n_futures=6,
15 initial_psi=make_zero_psi(delay_length),
16 rng=np.random.default_rng(999_991),
17)
18delays = [0, 1, 2, 3]
19delay_entropies = []
20for delay in delays:
21 result = mc_delay.characterize(
22 ham_delay,
23 params_delay,
24 num_interventions=delay_k,
25 cut=delay_cut,
26 delay=delay,
27 probe_set=anchor_delay,
28 )
29 delay_entropies.append(result.entropy(delay_cut))
30
31fig, ax = plt.subplots(figsize=(4.5, 3))
32ax.plot(delays, delay_entropies, "s-")
33ax.set_xlabel("reset delay at causal cut")
34ax.set_ylabel(r"$S_V(c)$")
35ax.set_title(r"Strong coupling: memory erodes with longer reset delay")
36ax.set_xticks(delays)
37fig.tight_layout()
Representation¶
MemoryCharacterizer(representation="auto") mirrors Simulator: "vector"
selects MCWF, "mps" selects TJM for the environment chain. With "auto",
MCWF is used when hamiltonian.length <= vector_max_qubits (default 10).
Temporal entanglement from a process tensor¶
Operational memory (\(S_V\)) comes from probe responses. Temporal entanglement
\(S_{PT}(c)\) is computed directly from a process tensor at the same causal cut.
By default, build_process_tensor uses direct MPO construction
(return_type="mpo"). Pass return_type="dense" for exhaustive tomography
(required for noise_model):
1k = 3
2cut_pt = 2
3timesteps = [0.1] * (k + 1)
4
5pt_mpo = mc.build_process_tensor(ham, params, timesteps=timesteps)
6pt_dense = mc.build_process_tensor(
7 ham,
8 params,
9 timesteps=timesteps,
10 return_type="dense",
11)
12
13s_mpo = pt_mpo.compute_temporal_entropy(cut_pt)
14s_dense = pt_dense.compute_temporal_entropy(cut_pt)
15print(
16 f"S_PT(c={cut_pt}): mpo={s_mpo['entropy']:.4f}, "
17 f"dense={s_dense['entropy']:.4f}, schmidt_rank={s_mpo['schmidt_rank']}"
18)
19
20# Same process tensor also supports operational memory via characterize:
21pt_result = mc.characterize(
22 pt_mpo,
23 cut=cut_pt,
24 num_interventions=k,
25 n_pasts=6,
26 n_futures=6,
27 rng=np.random.default_rng(7),
28)
29print(f"S_V(c={cut_pt}) from process-tensor probes: {pt_result.entropy(cut_pt):.4f}")
S_PT(c=2): mpo=0.0000, dense=0.0002, schmidt_rank=3
S_V(c=2) from process-tensor probes: 0.0000
Dense and uncapped MPO construction (max_bond_dim=None) agree on \(S_{PT}\) for
small \(k\). Use return_type="dense" when you need noise. The default
max_bond_dim=64 keeps direct construction scalable; pass max_bond_dim=None
for an exact noiseless MPO. characterize(pt, ...) still builds \(S_V\) from
probe responses (native MPO evaluate_probes, without densifying for the
V-matrix path).