Source code for rydiqule.experiments

"""
Standard methods for converting results to physical values.
"""

from .sensor import Sensor
from .solvers import solve_steady_state

import numpy as np
import warnings

from typing import Tuple, List

from .exceptions import RydiquleError, TimeDependenceWarning


[docs] def get_snr(sensor: Sensor, param_label: str, phase_quadrature: bool = False, diff_nearest: bool = False, **kwargs ) -> Tuple[np.ndarray, List[np.ndarray]]: """ Calculate a Sensor's signal-to-noise ratio in standard deviation, in a 1Hz bandwidth, to a specified signal parameter, assuming a homodyne measurement of optical field. SNR is calculated with respect to the signal parameter, relative to the initial value of the signal parameter. The returned mesh is similarly transformed from the typical sensor mesh, by replacing the total value of the signal parameter with the deviation in the signal parameter. The conventions used follow that of [1]_. Note ---- The default is to return the SNR of an amplitude quadrature measurement. To convert to a power measurement (i.e. :math:`\\Omega_p^2`), the amplitude quadrature SNR must be divided by 2. To get the SNR in variance, square the result. Parameters ---------- sensor : :class:`Sensor` sensor for which SNR should be calculated. The definition of sensor.couplings should contain at least one coupling with a list-like parameter. For the list-like parameter, the first array element is the "base" against which SNR for each other value is calculated. param_label : str Label of the axis with respect to which SNR is calculated. See :meth:`Sensor.axis_labels` for more details on axis labeling. The value corresponding to this label should be the list-like parameter with respect to which SNR should be calculated. This parameter list must have at least two elements, and SNR is calculated relative to the first element in the list for all other elements in the list. phase_quadrature : :obj:`bool`, optional Whether the sensor is measured in the phase quadrature of the probe laser. False denotes measurement in the amplitude quadrature. Default is False. diff_nearest: bool, optional Controls method by which the SNR is calculated. The default (False) calculates the SNR with respect to the 0 index value. Setting True calculates the SNR with respect to nearest neighbor differences. kwargs : dict, optional Additional keyword arguments to pass to `rq.solve_steady_state()`. Returns ------- snrs : numpy.ndarray Array of SNRs for the sensor with respect to the change in the signal parameter. Calculated in units of amplitude relative to noise standard deviation. SNR referenced to 1 second BW. mesh : tuple(numpy.ndarray) Numpy meshgrid of the coupling parameters that yield each snr. The signal parameter axis now shows the signal change. Raises ------ RydiquleError If the specified param_label is not in `Sensor.axis_labels()` Examples -------- >>> atom = "Rb85" >>> [g,e] = rq.D2_states("Rb85") >>> c = rq.Cell('Rb85', [g,e], cell_length=0.0001) >>> c.add_coupling(states=(g,e), rabi_frequency=np.linspace(1e-6, 1, 5), detuning=1, label="probe") >>> snr, mesh = rq.get_snr(c, 'probe_rabi_frequency') >>> print(snr) [ 0. 13654034.1 27301261.5 40934886.9 54548137.6] >>> print(mesh) # doctest: +SKIP [array([0. , 0.25 , 0.499999, 0.749999, 0.999999])] References ---------- .. [1] D. H. Meyer, C. O'Brien, D. P. Fahey, K. C. Cox, and P. D. Kunz, "Optimal atomic quantum sensing using electromagnetically-induced-transparency readout," Phys. Rev. A, vol. 104, p. 043103, 2021. """ labels = sensor.axis_labels() try: sensitivity_axis = labels.index(param_label) except ValueError as err: raise RydiquleError(f"{param_label} label is not in sensor.axis_labels()") from err if len(sensor.couplings_with('time_dependence')): warnings.warn(TimeDependenceWarning('At least one coupling has time dependence. ' 'get_snr() only solves in steady-state; ' 'results may not be as expected.')) full_sols = solve_steady_state(sensor, **kwargs) rhos_ij = full_sols.coupling_coefficient_observable() _ = full_sols.get_OD() if diff_nearest: rho_diffs = np.diff(rhos_ij, axis = sensitivity_axis) else: rho_diffs = rhos_ij - np.take(rhos_ij, [0], sensitivity_axis) if phase_quadrature: rho_diffs_quadrature = np.abs(np.real(rho_diffs)) else: rho_diffs_quadrature = np.abs(np.imag(rho_diffs)) rho_ij_noise = full_sols.eta/full_sols.kappa # follows directly from eqns 4 and 6 of 2105.10494 # factor of 1e3 converts from root(MHz) to root(Hz) snrs: np.ndarray = rho_diffs_quadrature/rho_ij_noise*full_sols.cell_length*1e3 mesh = sensor.get_parameter_mesh() mesh = [np.array(a) for a in np.broadcast_arrays(*mesh)] # avoid writing to views by copying mesh[sensitivity_axis] -= np.take(mesh[sensitivity_axis], [0], sensitivity_axis) return snrs, mesh