# --------------------------------------------------------------------------------------
# This code is part of SpinPulse.
#
# (C) Copyright Quobly 2025.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
# --------------------------------------------------------------------------------------
# Modified: add optional disk storage for pink-noise histories.
"""Description of the noisy environment associated to a hardware."""
import os
from typing import cast
import numpy as np
from joblib import Parallel, delayed
from ..transpilation.hardware_specs import HardwareSpecs
from .noise import (
NoiseType,
PinkNoiseTimeTrace,
QuasistaticNoiseTimeTrace,
WhiteNoiseTimeTrace,
)
[docs]
class ExperimentalEnvironment:
"""
Contain a quantum experimental environment with configurable noise models.
Attributes:
- hardware_specs (class): HardwareSpecs class, that defines the hardware settings.
- noise_type (NoiseType): Type of noise to simulate. Must be "pink", "white", or "quasistatic".
- T2S (float): Characteristic time of individual qubits.
- TJS (float or None): Characteristic time of coupled two-qubit system at maximal J coupling without noise on the qubit's frequency.
- duration (int): Total duration of the simulation.
- segment_duration (int): Duration of each noise segment; used to partition the time trace.
- only_idle (bool): Flag to apply noise only to idle qubits.
- time_traces (list[float]): List of time traces for each qubit.
- time_traces_coupling (list[float]): List of time traces for coupling noise for each pair of qubits (if TJS is set).
- seed (int or None): seed integer for random number generation. If not specified, no seed used.
"""
# noise_generator (class): Noise generator class based on noise_type. :no-index:
[docs]
noise_generator: (
type[WhiteNoiseTimeTrace]
| type[QuasistaticNoiseTimeTrace]
| type[PinkNoiseTimeTrace]
)
# Environments restored from older pickles did not store this option.
[docs]
noise_directory: str | os.PathLike | None = None
def __init__(
self,
hardware_specs: HardwareSpecs,
noise_type: NoiseType = NoiseType.PINK,
T2S: float = 100.0,
TJS: float | None = None,
duration: int = 2**10,
only_idle: bool = False,
segment_duration: int = 2**10,
seed: int | None = None,
*,
noise_directory: str | os.PathLike | None = None,
):
"""
Initialize the ExperimentalEnvironment with specified noise characteristics and simulation parameters.
Parameters:
hardware_specs (class): HardwareSpecs class, that defines the hardware settings.
noise_type (NoiseType): Type of noise to simulate. Must be "pink", "white", or "quasistatic".
T2S (float): Characteristic time of individual qubits.
TJS (float or None): Characteristic time of coupled two-qubit system at maximal J coupling with no noise on the qubit's frequency.
duration (int): Total duration of the simulation.
only_idle (bool): Flag to apply noise only to idle qubits.
segment_duration (int): Duration of each noise segment; used to partition the time trace.
seed (int or None): seed integer for random number generation. If not specified, no seed used.
noise_directory: Existing disk directory for temporary pink-noise
histories and FFT arrays. Files are reclaimed when the arrays
and their views are released. Parallel generation uses threads
to avoid copying mapped arrays between worker processes. Each
concurrent worker still needs its own FFT workspace.
Raises:
ValueError: If an invalid noise_type is provided.
"""
[docs]
self.hardware_specs: HardwareSpecs = hardware_specs
[docs]
self.noise_type: NoiseType = noise_type
[docs]
self.TJS: float | None = TJS
[docs]
self.duration: int = duration
[docs]
self.segment_duration: int = segment_duration
if noise_directory is not None and noise_type is not NoiseType.PINK:
raise ValueError("noise_directory is supported only for pink noise")
self.noise_directory = noise_directory
[docs]
self.seed: int | None = seed
self._seed_sequence = (
np.random.SeedSequence(self.seed)
if self.seed is not None
else np.random.SeedSequence()
)
match noise_type:
case NoiseType.PINK:
self.noise_generator = PinkNoiseTimeTrace
case NoiseType.WHITE:
self.noise_generator = WhiteNoiseTimeTrace
case NoiseType.QUASISTATIC:
self.noise_generator = QuasistaticNoiseTimeTrace
case _:
raise ValueError("unknown noise type")
[docs]
self.only_idle = only_idle
self.generate_time_traces()
def _generate_time_trace_on_qubit(self, seed: int):
"""Generate the time trace for one qubit."""
return self._generate_trace(self.T2S, seed)
def _generate_trace(self, coherence_time: float, seed):
if self.noise_directory is not None:
generator = cast(type[PinkNoiseTimeTrace], self.noise_generator)
return generator(
coherence_time,
self.duration,
self.segment_duration,
seed=seed,
noise_directory=self.noise_directory,
)
return self.noise_generator(
coherence_time,
self.duration,
self.segment_duration,
seed=seed, # type: ignore
)
def _generate_coupling_time_trace_on_qubit(self, seed: int):
"""Generate the coupling time trace for one qubit."""
assert self.TJS is not None, (
"self.TJS cannot be None when generating coupling traces."
)
return self._generate_trace(self.TJS, seed)
[docs]
def generate_time_traces(self, n_jobs: int = 1):
"""
Generate noise time traces for each qubit's frequency and J coupling to each pair of qubits if TJS is defined.
Parameters:
n_jobs (int): Number of core used to generate the time trace.
Behavior:
For each qubit, instantiate a noise generator using the selected noise_type.
The generator uses T2S, duration, and segment_duration to produce a time trace.
If TJS is provided, generate additional time traces for J coupling noise for each pair of qubits.
Effects:
Populate self.time_traces with one noise trace per qubit.
If TJS is set, populate self.time_traces_coupling with one trace per pair of qubits (n-1 traces for n qubits).
"""
n_jobs = min(n_jobs, os.cpu_count() or 1)
# Spawn two child SeedSequences (one for qubit traces, one for coupling traces).
# Each call to .spawn() yield independent streams, even though the root's
# entropy (derived from self.seed) is unchanged.
qubit_root, coupling_root = self._seed_sequence.spawn(2)
qubit_seeds: list[np.random.SeedSequence] = list(
qubit_root.spawn(self.hardware_specs.num_qubits)
)
parallel_options = (
{"require": "sharedmem"} if self.noise_directory is not None else {}
)
self.time_traces = Parallel(n_jobs=n_jobs, **parallel_options)(
delayed(self._generate_time_trace_on_qubit)(qubit_seeds[index])
for index in range(self.hardware_specs.num_qubits)
)
if self.TJS is not None:
coupling_seeds: list[np.random.SeedSequence] = list(
coupling_root.spawn(self.hardware_specs.num_qubits - 1)
)
self.time_traces_coupling = Parallel(n_jobs=n_jobs, **parallel_options)(
delayed(self._generate_coupling_time_trace_on_qubit)(
coupling_seeds[index]
)
for index in range(self.hardware_specs.num_qubits - 1)
)
def __str__(self):
"""
Return a string representation of the ExperimentalEnvironment instance.
Includes:
Number of qubits
Noise type
T2S and TJS values
Duration and segment duration
Whether noise is only appplied to idle qubits
The J coupling value set in HardwareSpecs
The total number of generated time traces
The total number of generated coupling time traces if there exists some
"""
summary = [
"ExperimentalEnvironment:",
f" Qubits: {self.hardware_specs.num_qubits}",
f" Noise Type: {self.noise_type}",
f" T2S (qubit dephasing): {self.T2S}",
f" TJS (coupling dephasing): {self.TJS if self.TJS is not None else 'None'}",
f" Duration: {self.duration}",
f" Segment Duration: {self.segment_duration}",
f" Only Idle: {self.only_idle}",
f" J Coupling: {self.hardware_specs.J_coupling if self.hardware_specs.J_coupling is not None else 'None'}",
f" Time Traces Generated: {len(self.time_traces)}",
f" Seed: {self.seed}",
]
if hasattr(self, "time_traces_coupling"):
summary.append(
f" Coupling Time Traces Generated: {len(self.time_traces_coupling)}"
)
return "\n".join(summary)
[docs]
def get_analytical_contrast(self, idle_duration: int | np.ndarray):
return self.time_traces[0].get_analytical_contrast(idle_duration)