Source code for squadds.simulations.drivenmodal.coupled_postprocess
"""Pure helpers for driven-modal coupled-system post-processing."""
from __future__ import annotations
import math
import numpy as np
[docs]
def calculate_chi_hz(f_ground_hz: float, f_excited_hz: float) -> float:
"""Return the dispersive shift in Hz from the embedded ground/excited resonances."""
return float(f_excited_hz - f_ground_hz)
[docs]
def calculate_loaded_q(*, f_res_hz: float, fwhm_hz: float) -> float:
"""Compute loaded quality factor from resonance frequency and FWHM."""
if f_res_hz <= 0:
raise ValueError("f_res_hz must be positive.")
if fwhm_hz <= 0:
raise ValueError("fwhm_hz must be positive.")
return float(f_res_hz / fwhm_hz)
[docs]
def calculate_kappa_hz(*, f_res_hz: float, loaded_q: float) -> float:
"""Compute kappa / 2pi in Hz from loaded quality factor."""
if f_res_hz <= 0:
raise ValueError("f_res_hz must be positive.")
if loaded_q <= 0:
raise ValueError("loaded_q must be positive.")
return float(f_res_hz / loaded_q)
[docs]
def calculate_g_from_chi(*, f_r_hz: float, f_q_hz: float, chi_hz: float, alpha_hz: float) -> float:
"""Back-calculate coupling strength magnitude using the dispersive transmon relation with the non-RWA term."""
omega_r = 2 * np.pi * f_r_hz
omega_q = 2 * np.pi * f_q_hz
chi = 2 * np.pi * chi_hz
alpha = 2 * np.pi * alpha_hz
delta = omega_q - omega_r
sigma = omega_q + omega_r
rwa_term = alpha / (delta * (delta - alpha))
non_rwa_term = alpha / (sigma * (sigma + alpha))
denominator = 2 * (rwa_term + non_rwa_term)
if denominator == 0:
raise ValueError("The dispersive denominator is zero; cannot compute coupling.")
# ``chi`` sign conventions vary across workflows. We expose a positive coupling
# magnitude while preserving the user's chosen resonance ordering elsewhere.
g_squared_magnitude = abs(chi / denominator)
return float(math.sqrt(g_squared_magnitude))
[docs]
def terminate_port_y(y_matrices, *, terminated_port: int, load_impedance_ohms) -> np.ndarray:
"""Terminate a single port of an N-port admittance sweep using a load impedance."""
matrices = np.asarray(y_matrices, dtype=complex)
if matrices.ndim != 3:
raise ValueError("y_matrices must be a 3D array of shape (freq, nport, nport).")
nports = matrices.shape[1]
if matrices.shape[2] != nports:
raise ValueError("y_matrices must be square in the last two dimensions.")
if not 0 <= terminated_port < nports:
raise ValueError("terminated_port is out of range.")
load_impedance = np.asarray(load_impedance_ohms, dtype=complex)
if load_impedance.ndim == 0:
load_impedance = np.full(matrices.shape[0], load_impedance, dtype=complex)
if load_impedance.shape != (matrices.shape[0],):
raise ValueError("load_impedance_ohms must be a scalar or one value per frequency.")
keep = [index for index in range(nports) if index != terminated_port]
y_aa = matrices[:, keep][:, :, keep]
y_ab = matrices[:, keep, terminated_port][:, :, np.newaxis]
y_ba = matrices[:, terminated_port, keep][:, np.newaxis, :]
y_bb = matrices[:, terminated_port, terminated_port]
y_load = 1.0 / load_impedance
denominator = (y_bb + y_load)[:, np.newaxis, np.newaxis]
return y_aa - (y_ab * y_ba) / denominator
[docs]
def y_to_s(y_matrices, *, z0_ohms: float = 50.0) -> np.ndarray:
"""Convert a sweep of admittance matrices into scattering matrices for a uniform reference impedance."""
matrices = np.asarray(y_matrices, dtype=complex)
if matrices.ndim != 3:
raise ValueError("y_matrices must be a 3D array of shape (freq, nport, nport).")
nports = matrices.shape[1]
if matrices.shape[2] != nports:
raise ValueError("y_matrices must be square in the last two dimensions.")
identity = np.eye(nports, dtype=complex)
s_matrices = np.empty_like(matrices)
for index, y_matrix in enumerate(matrices):
left = identity - z0_ohms * y_matrix
right = identity + z0_ohms * y_matrix
s_matrices[index] = left @ np.linalg.inv(right)
return s_matrices