fibre_walk_project_code/scripts/experiments/013_step_through.py

362 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""A demonstration of the ringdown spectroscopy protocol."""
from rabifun.system import *
from rabifun.plots import *
from rabifun.utilities import *
from ringfit.utils import WelfordAggregator
from rabifun.analysis import *
import multiprocessing
import copy
def solve_shot(
params: Params, t: np.ndarray, t_before: np.ndarray, t_after: np.ndarray
):
"""A worker function to solve for the time evolution in separate processes.
:param params: The parameters of the system.
:param t: The time axis.
:param t_before: The time axis before the EOM is switched off.
:param t_after: The time axis after the EOM is switched off.
"""
solution = solve(t, params)
amps = solution.y[::, len(t_before) - 1 :]
return t_after, amps
def make_shots(
params: Params,
total_lifetimes: float,
eom_range: tuple[float, float],
eom_steps: int,
num_freq: int = 1,
):
"""Generate a series of shots with varying EOM frequencies.
The implementation here slightly varies the off time of the laser
so as to introduce some random relative phases of the modes.
:param params: The parameters of the system.
:param total_lifetimes: The total time of the experiment in
lifetimes.
:param eom_range: The range of EOM frequencies in units of
:any:`params.Ω`.
:param eom_steps: The number of steps in the EOM frequency range.
:param num_freq: The number of frequencies to drive. If a number
greater than 1 is given, the EOM will be driven at multiple
frequencies where the highest frequency is the base frequency
plus an consecutive integer multiples of :any:`params.Ω`.
"""
solutions = []
shot_params = []
rng = np.random.default_rng(seed=0)
off_time = params.laser_off_time or 0
analyze_time = params.lifetimes(total_lifetimes) - off_time
t_after = time_axis(params, total_time=analyze_time, resolution=0.01)
for step in range(eom_steps):
base = params.Ω * (
eom_range[0] + (eom_range[1] - eom_range[0]) * step / eom_steps
)
current_params = copy.deepcopy(params)
current_params.drive_override = (
base + params.Ω * np.arange(num_freq),
np.ones(num_freq),
)
current_params.drive_phases = rng.uniform(0, 2 * np.pi, size=num_freq)
off_time = rng.normal(off_time, 0.1 * params.laser_off_time)
current_params.laser_off_time = None # off_time
current_params.drive_off_time = off_time
t_before = time_axis(params, total_time=off_time, resolution=0.01)
t = np.concatenate([t_before[:-1], t_after + t_before[-1]])
shot_params.append((current_params, t, t_before, t_after))
pool = multiprocessing.Pool()
solutions = pool.starmap(solve_shot, shot_params)
return solutions
def process_shots(
params: Params,
solutions: list[tuple[np.ndarray, np.ndarray]],
noise_amplitude: float,
num_freq: int,
):
"""
Calculates the normalized average Fourier power spectrum of a
series of experimental (simulated) shots.
:param params: The parameters of the system.
:param solutions: A list of solutions to process returned by
:any:`solve_shot`.
:param noise_amplitude: The amplitude of the noise to add to the
signal.
The amplitude is normalized by 2/η which is roughly the steady
state signal amplitude if a bath mode is excited resonantly by
a unit-strength input.
:param num_freq: The number of frequencies to drive. See
:any:`make_shots` for details.
"""
rng = np.random.default_rng(seed=0)
noise_amplitude /= params.η * np.pi
aggregate = None
for t, amps in solutions:
signal = output_signal(t, amps, params)
signal += rng.normal(scale=noise_amplitude, size=len(signal))
window = (0, t[-1])
freq, fft = fourier_transform(
t,
signal,
low_cutoff=0.3 * params.Ω,
high_cutoff=params.Ω * (1 + num_freq),
window=window,
)
power = np.abs(fft) ** 2
# ugly hack because shape is hard to predict
if aggregate is None:
aggregate = WelfordAggregator(power)
else:
aggregate.update(power)
assert aggregate is not None # appease pyright
max_power = np.max(aggregate.mean)
return (freq, aggregate.mean / max_power, aggregate.ensemble_std / max_power)
def process_and_plot_results(
params: Params,
ax: plt.Axes,
freq: np.ndarray,
average_power_spectrum: np.ndarray,
σ_power_spectrum: np.ndarray,
annotate: bool = True,
):
"""
Fits the ringdown spectrum and plots the results.
:param params: The parameters of the system.
:param ax: The axis to plot on.
:param freq: The frequency array.
:param average_power_spectrum: The average power spectrum obtained from :any:`process_shots`.
:param σ_power_spectrum: The standard deviation of the power
spectrum.
:param annotate: Whether to annotate the plot with peak and mode positions.
"""
ringdown_params = RingdownParams(
fω_shift=params.measurement_detuning,
mode_window=(params.N, params.N),
fΩ_guess=params.Ω,
fδ_guess=params.Ω * params.δ,
η_guess=0.5,
absolute_low_cutoff=0.3 * params.Ω,
)
peak_info = find_peaks(
freq,
average_power_spectrum,
ringdown_params,
prominence=0.05,
height=0.1,
σ_power=σ_power_spectrum,
)
peak_info = refine_peaks(peak_info, ringdown_params, height_cutoff=0.05)
plot_spectrum_and_peak_info(ax, peak_info, annotate=annotate)
if peak_info.lm_result is not None:
fine_freq = np.linspace(freq.min(), freq.max(), 5000)
fine_fit = peak_info.lm_result.eval(ω=fine_freq)
ax.plot(fine_freq, fine_fit - peak_info.noise_floor, color="C3", zorder=-100)
ax.set_ylim(-0.1, max(1, fine_fit.max() * 1.1))
ax.set_xlabel("Frequency (MHz)")
if annotate:
annotate_ringodown_mode_positions(params, ax)
def generate_data(
Ω=13,
η=0.2,
g_0=0.5,
η_factor=5,
noise_amplitude=0.3,
laser_detuning=0,
laser_on_mode=0,
N=10,
eom_ranges=(0.5, 2.5),
eom_steps=20,
excitation_lifetimes=2,
measurement_lifetimes=4,
num_freq=3,
extra_title="",
):
"""Simulate and plot the ringdown spectroscopy protocol.
The idea is to have the laser on ``laser_on_mode`` and to sweep
the EOM frequency over a range of values given in ``eom_ranges``
in ``eom_steps`` steps. For each step, the laser and EOM are
inputting into the system for a time given by
``excitation_lifetimes``. Then, the ringdown signal is collected
for a time given by ``measurement_lifetimes``. (Lifetime units
are given by ``η``.) The resulting power spectra are averaged and
then fitted.
:param Ω: The FSR of the system.
:param η: The decay rate of the system.
:param g_0: The coupling strength of the system in units of
:any:`Ω`. Note that the effective coupling strength between
the ``A`` site and the bath modes is reduced by a factor of
:math:`\sqrt{2}`.
:param η_factor: The factor by which the decay rate of the A site
is greater.
:param noise_amplitude: The amplitude of the noise to add to the
signal. See :any:`process_shots` for details.
:param laser_detuning: The detuning of the laser from the the mode
it is exciting.
:param laser_on_mode: The mode that the laser is exciting.
:param N: The number of bath modes.
:param eom_ranges: The range of EOM frequencies in units of
:any:`Ω`.
:param eom_steps: The number of steps in the EOM frequency range.
:param excitation_lifetimes: The time the EOM is driving the
system.
:param measurement_lifetimes: The time the system is left to ring
down.
Note that the laser is not turned off during the ringdown.
:param num_freq: The number of frequencies to drive. See
:any:`make_shots` for details.
:param extra_title: A string to add to the title of the plot.
:returns: The figure containing the plot.
"""
final_laser_detuning = laser_detuning + (
0 if laser_on_mode == 0 else (laser_on_mode - 1 / 4) * Ω
)
params = Params(
η=η,
η_hybrid=η_factor * η,
Ω=Ω,
δ=1 / 4,
ω_c=0.1,
g_0=g_0 * num_freq, # as it would be normalized otherwise
laser_detuning=final_laser_detuning,
N=N,
N_couplings=N,
measurement_detuning=0,
α=0,
rwa=False,
flat_energies=False,
correct_lamb_shift=0,
laser_off_time=None,
small_loop_detuning=0,
drive_override=(np.array([]), np.array([])),
)
params.laser_off_time = params.lifetimes(excitation_lifetimes)
params.drive_off_time = params.lifetimes(excitation_lifetimes)
solutions = make_shots(
params,
excitation_lifetimes + measurement_lifetimes,
eom_ranges,
eom_steps,
num_freq,
)
freq, average_power_spectrum, σ_power_spectrum = process_shots(
params,
solutions,
noise_amplitude,
num_freq,
)
fig = make_figure(extra_title, figsize=(10, 6))
fig.clear()
ax = fig.subplots()
process_and_plot_results(params, ax, freq, average_power_spectrum, σ_power_spectrum)
ax.text(
0.01,
0.95,
f"""$Ω/2π = {params.Ω}$MHz
$η/2π = {params.η}MHz$
$g_0 = {params.g_0}Ω$
$N = {params.N}$
noise = ${noise_amplitude * 2}$
$η_A = {η_factor}η$
EOM range = {eom_ranges[0]:.2f}Ω to {eom_ranges[1]:.2f}Ω
EOM steps = {eom_steps}
excitation time = {excitation_lifetimes} lifetimes
measurement time = {measurement_lifetimes} lifetimes
on mode = {laser_on_mode}
laser detuning = {laser_detuning}
num freq = {num_freq}
total time = {(excitation_lifetimes + measurement_lifetimes) * eom_steps / (params.η * 1e6)}s""",
transform=ax.transAxes,
ha="left",
va="top",
size=10,
bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.5),
)
ax.set_title(extra_title)
fig.tight_layout()
return fig
# %% save
if __name__ == "__main__":
fig = generate_data(
g_0=0.2,
η_factor=5,
noise_amplitude=0.3,
N=5,
eom_ranges=(0.7, 0.9),
eom_steps=100,
laser_detuning=0,
laser_on_mode=0,
excitation_lifetimes=2,
measurement_lifetimes=4,
num_freq=4,
extra_title="Laser on A site",
)
fig = generate_data(
g_0=0.2,
η_factor=5,
noise_amplitude=0.3,
N=5,
eom_ranges=(1.2, 1.3),
eom_steps=100,
laser_detuning=0,
laser_on_mode=-1,
excitation_lifetimes=2,
measurement_lifetimes=4,
num_freq=1,
extra_title="Laser on Bath Mode",
)