r"""
ODMR fitting tools.
.. code-block:: python
import nvtools as nv
fitter = nv.ODMRFitter()
# assume you have some ODMR spectrum
mw_frequency = np.array(...)
odmr = np.array(...)
# tries to automatically guess the starting parameters for the fit
fit_results = fitter.fit_odmr_full(mw_frequency, odmr)
# if that does not work, try inputting the frequencies manually:
fit_results = fitter.fit_odmr_full(mw_frequency, odmr, p0_frequency=[<list of eight values>])
# fit_results contains all kinds of useful values
print(fit_results.frequency) # resonance frequency
print(fit_results.contrast) # ODMR contrast
print(fit_results.linewidth) # ODMR linewidth
print(fit_results.r_squared) # R^2 value of the fit
print(fit_results.sensitivity(photon_rate=1e6 * unit.Hz)) # CW ODMR sensitivity
plt.figure()
plt.plot(mw_frequency, odmr)
plt.plot(mw_frequency, fit_results.fit_func) # plot fit function
plt.show()
"""
import pathlib
import sys
from dataclasses import dataclass
from typing import Callable
import matplotlib.pyplot as plt
import numpy as np
import scipy
from colorama import Fore, Style
from prettytable import SINGLE_BORDER, PrettyTable
import nvtools
import nvtools.fit
from nvtools import unit
from nvtools.fit.odmr_model import ModelVoigt1, ModelVoigt8, ModelVoigtDerivative1
from nvtools.fit.utils import r_squared
from nvtools.utils import critical, success
[docs]
@dataclass
class ODMRFitResult:
"""ODMR fit results."""
mw_frequency: list
odmr: list
popt: list
pcov: list
perr: list
model: Callable
frequency: float | list[float]
contrast: float | list[float]
linewidth: float | list[float]
@property
def fit_func(self):
"""Fit function values."""
return self.model(self.mw_frequency, *self.popt)
@property
def r_squared(self):
r"""
R^2 value of fit.
.. math::
R^2 = 1 - \frac{SS_\text{res}}{SS_\text{tot}}
"""
return r_squared(self.model, self.mw_frequency, self.odmr, self.popt)
[docs]
def sensitivity(self, photon_rate: float) -> float:
r"""
Calculate CW ODMR sensitivity.
:param float photon_rate: Photon rate I_0
.. math::
\eta = \frac{1}{\text{max}\frac{\partial V(f)}{\partial f}} \frac{1}{\gamma_\text{NV}} \frac{1}{\sqrt{I_0}}
"""
dx = self.mw_frequency[1] - self.mw_frequency[0]
grad = np.gradient(self.fit_func, dx)
max_grad = np.max(grad) / nvtools.unit.MHz
return 1 / max_grad / nvtools.GAMMA / np.sqrt(photon_rate)
[docs]
class ODMRFitter:
"""ODMR fitter."""
[docs]
@staticmethod
def fit_odmr_single(mw_frequency, odmr):
"""
Fit ODMR with one resonance.
:param mw_frequency: MW frequency
:param odmr: ODMR
"""
ten_mhz_distance = len(mw_frequency) / (np.max(mw_frequency - np.min(mw_frequency))) * 10
one_percent_prominence = (np.max(odmr) - np.min(odmr)) * 0.3
peaks, __ = scipy.signal.find_peaks(-odmr, distance=ten_mhz_distance, prominence=one_percent_prominence)
fit_model = ModelVoigt1()
popt, pcov = scipy.optimize.curve_fit(
f=fit_model.f,
xdata=mw_frequency,
ydata=odmr,
p0=[1, 5, 5, mw_frequency[peaks[0]], 1],
bounds=([-np.inf, 0, 0, 0, -np.inf], [np.inf, np.inf, np.inf, np.inf, np.inf]),
)
perr = np.sqrt(np.diag(pcov))
# plt.figure()
# plt.plot(mw_frequency, odmr)
# plt.plot(mw_frequency[peaks], odmr[peaks], "x")
# plt.plot(mw_frequency, fit_model.f(mw_frequency, 1, 5, 5, mw_frequency[peaks[0]], 1))
# plt.plot(mw_frequency, fit_model.f(mw_frequency, *popt))
# plt.show()
area = (popt[0] * unit.MHz * unit.MHz).plus_minus(perr[0])
sigma = (popt[1] * unit.MHz).plus_minus(perr[1])
nu = (popt[2] * unit.MHz).plus_minus(perr[2])
frequency = (popt[3] * unit.MHz).plus_minus(perr[3])
alpha_l, alpha_g, alpha_v, d, contrast = fit_model.voigt_parameters(area, sigma, nu)
return ODMRFitResult(
mw_frequency=mw_frequency,
odmr=odmr,
popt=popt,
pcov=pcov,
perr=perr,
frequency=frequency,
model=fit_model.f,
contrast=contrast,
linewidth=alpha_v,
)
[docs]
@staticmethod
def fit_odmr_derivative_single(mw_frequency, odmr, p0_frequency):
"""
Fit ODMR derivative with one resonance.
:param mw_frequency: MW frequency
:param odmr: ODMR
"""
# ten_mhz_distance = len(mw_frequency) / (np.max(mw_frequency - np.min(mw_frequency))) * 10
# one_percent_prominence = (np.max(odmr) - np.min(odmr)) * 0.3
# peaks, __ = scipy.signal.find_peaks(-odmr, distance=ten_mhz_distance, prominence=one_percent_prominence)
fit_model = ModelVoigtDerivative1()
popt, pcov = scipy.optimize.curve_fit(
f=fit_model.f,
xdata=mw_frequency,
ydata=odmr,
p0=[1, 5, 5, p0_frequency[0]],
bounds=([-np.inf, 0, 0, 0], [np.inf, np.inf, np.inf, np.inf]),
)
perr = np.sqrt(np.diag(pcov))
# plt.figure()
# plt.plot(mw_frequency, odmr)
# # plt.plot(mw_frequency[peaks], odmr[peaks], "x")
# # plt.plot(mw_frequency, fit_model.f(mw_frequency, 1, 5, 5, p0_frequency[0]))
# plt.plot(mw_frequency, fit_model.f(mw_frequency, *popt))
# plt.show()
area = (popt[0] * unit.MHz * unit.MHz).plus_minus(perr[0])
sigma = (popt[1] * unit.MHz).plus_minus(perr[1])
nu = (popt[2] * unit.MHz).plus_minus(perr[2])
frequency = (popt[3] * unit.MHz).plus_minus(perr[3])
alpha_l, alpha_g, alpha_v, d, contrast = fit_model.voigt_parameters(area, sigma, nu)
return ODMRFitResult(
mw_frequency=mw_frequency,
odmr=odmr,
popt=popt,
pcov=pcov,
perr=perr,
frequency=frequency,
model=fit_model.f,
contrast=contrast,
linewidth=alpha_v,
)
[docs]
@staticmethod
def fit_odmr_full(mw_frequency, odmr, p0_frequency=None, units=True):
"""
Fit ODMR with eight resonances.
:param mw_frequency: MW frequency
:param odmr: ODMR
:param list | None p0_frequency: estimates for resonance frequencies
:raises: RuntimeError when peaks can not be found
"""
if p0_frequency is None:
ten_mhz_distance = len(mw_frequency) / (np.max(mw_frequency - np.min(mw_frequency))) * 10
one_percent_prominence = (np.max(odmr) - np.min(odmr)) * 0.1
peaks, __ = scipy.signal.find_peaks(-odmr, distance=ten_mhz_distance, prominence=one_percent_prominence)
p0_frequency = mw_frequency[peaks]
else:
peaks = None
if len(p0_frequency) != 8:
peaks_str = ", ".join([f"{x:.2f}" for x in p0_frequency])
critical("Could not find eight peaks. Try to set the starting parameters manually with 'p0_frequency'.")
critical(f"Found {len(p0_frequency)} peaks at {peaks_str} MHz")
print("\n")
plt.figure()
plt.plot(mw_frequency, odmr, label="data")
if peaks is not None:
plt.plot(p0_frequency, odmr[peaks], "x", label="peak estimates")
plt.legend(loc="best")
plt.show()
raise RuntimeError(
"Could not find eight peaks. Try to set the starting parameters manually with 'p0_frequency'."
)
# c_est = np.max(odmr) - np.min(odmr)
# print("contrast", c_est)
areas = np.full(8, 0.1)
sigmas = np.full(8, 2)
nus = np.full(8, 2)
start_params = np.column_stack((areas, sigmas, nus, p0_frequency)).flatten()
start_params = np.append(start_params, np.max(odmr))
if nvtools.ODMR_MODEL is None:
fit_model = ModelVoigt8()
# plt.figure()
# plt.plot(mw_frequency, odmr)
# plt.plot(mw_frequency, fit_model.f(mw_frequency, *start_params))
# plt.show()
lower_bounds = [-np.inf, 0, 0, 0] * 8 + [-np.inf]
upper_bounds = [np.inf, np.inf, np.inf, np.inf] * 8 + [np.inf]
popt, pcov = scipy.optimize.curve_fit(
f=fit_model.f,
xdata=mw_frequency,
ydata=odmr,
p0=start_params,
bounds=(lower_bounds, upper_bounds),
)
perr = np.sqrt(np.diag(pcov))
areas, sigmas, nus, frequencies, alpha_ls, alpha_gs, alpha_vs, contrasts = [], [], [], [], [], [], [], []
for i in range(8):
if units:
area = (popt[4 * i + 0] * unit.MHz * unit.MHz).plus_minus(perr[4 * i + 0])
sigma = (popt[4 * i + 1] * unit.MHz).plus_minus(perr[4 * i + 1])
nu = (popt[4 * i + 2] * unit.MHz).plus_minus(perr[4 * i + 2])
frequency = (popt[4 * i + 3] * unit.MHz).plus_minus(perr[4 * i + 3])
else:
area = popt[4 * i + 0]
sigma = popt[4 * i + 1]
nu = popt[4 * i + 2]
frequency = popt[4 * i + 3]
alpha_l, alpha_g, alpha_v, d, c = fit_model.voigt_parameters(area, sigma, nu, units=units)
areas.append(area)
sigmas.append(sigma)
nus.append(nus)
frequencies.append(frequency)
alpha_ls.append(alpha_l)
alpha_gs.append(alpha_g)
alpha_vs.append(alpha_v)
contrasts.append(c)
if nvtools.VERBOSE or nvtools.EXPORT:
resonance_names = ["lower 1", "lower 2", "lower 3", "lower 4", "upper 4", "upper 3", "upper 2", "upper 1"]
table = PrettyTable()
table.set_style(SINGLE_BORDER)
table.field_names = ["Resonance", "Frequency", "Contrast", "Linewidth"]
for i in range(8):
table.add_row(
[resonance_names[i], f"{frequencies[i]:~P}", f"{contrasts[i] * 100:.2f} %", f"{alpha_vs[i]:~P}"]
)
if nvtools.VERBOSE:
print(
Fore.GREEN
+ Style.BRIGHT
+ "───────────────────────── ODMR Fitter ─────────────────────────"
+ Style.RESET_ALL
)
print(table)
print("\n")
if nvtools.EXPORT:
folder = pathlib.Path(sys.argv[0]).parent
file_path = pathlib.Path(folder, "odmr_fit.csv")
success(f"saving fit results in {file_path}")
print("\n")
with open(file_path, "w") as file:
file.write(table.get_csv_string())
return ODMRFitResult(
mw_frequency=mw_frequency,
odmr=odmr,
popt=popt,
pcov=pcov,
perr=perr,
model=fit_model.f,
frequency=frequencies,
contrast=contrasts,
linewidth=alpha_vs,
)