diff --git a/src/python_spectrometer/daq/simulator.py b/src/python_spectrometer/daq/simulator.py index ed8ebc179a4c6a793133a422a629b6e9506344f8..57b7ca3cde806adf8a455186113345c231badc7a 100644 --- a/src/python_spectrometer/daq/simulator.py +++ b/src/python_spectrometer/daq/simulator.py @@ -17,13 +17,17 @@ Add an artificial time delay to mimick finite data acquisition time: """ from __future__ import annotations +import copy import dataclasses import sys import time -from typing import Callable +from collections.abc import Callable +from typing import Literal import numpy as np from qutil.functools import partial +from qutil.math import cexp +from qutil.signal_processing import real_space from .base import DAQ, AcquisitionGenerator @@ -103,6 +107,77 @@ class QoptColoredNoise(DAQ): return {'qopt version': qopt.__version__} +class DemodulatorQoptColoredNoise(QoptColoredNoise): + DTYPE = np.complexfloating + + @staticmethod + def demodulate(signal: np.ndarray, IQ: np.ndarray, **settings): + # Don't highpass filter + settings = copy.deepcopy(settings) + settings.pop('f_min', None) + return real_space.RC_filter(signal * IQ, **settings) + + def acquire(self, *, n_avg: int, freq: float = 0, filter_order: int = 1, + delay: bool | float = False, modulate_signal: bool = False, + filter_method: Literal['forward', 'forward-backward'] = 'forward', + **settings) -> AcquisitionGenerator[DTYPE]: + r"""Simulate demodulated noisy data. + + See Ref. [1]_ for an introduction to Lock-in amplification. + + Parameters + ---------- + n_avg : int + Number of outer averages. + freq : float, optional + Modulation frequency. + filter_order : int, optional + RC filter order used to filter the demodulated signal. + delay : bool | float, optional + Simulate a realistic data acquisition duration. + modulate_signal : bool, optional + Add the simulated noise to the modulation signal to mimic + noise picked up by a Lock-In signal travelling through some + DUT. Otherwise, mimics the noise at the input of the + amplifier. + + In other words, simulate a Lock-In output connected to an + input, or just simulate the input. + + Note that if True, noise is assumed to be additive, that + is, + + .. math:: + + x(t) = s(t) + \delta(t) + + with $s(t)$ the output signal and $\delta(t)$ the noise. + filter_method : + See :func:`~qutil:qutil.signal_processing.real_space.RC_filter`. + + Yields + ------ + data : + Demodulated data in complex IQ-representation. + + References + ---------- + .. [1]: https://www.zhinst.com/europe/en/resources/principles-of-lock-in-detection + + """ + t = np.arange(0, settings['n_pts'] / settings['fs'], 1 / settings['fs']) + # demodulation by √2 exp(-iωt) (ZI convention) + IQ = np.sqrt(2) * cexp(-2 * np.pi * freq * t) + + yield from ( + self.demodulate(IQ.real + data if modulate_signal else data, IQ, + order=filter_order, method=filter_method, **settings) + for data in super().acquire(n_avg=n_avg, delay=delay, **settings) + ) + + return {'qopt version': qopt.__version__} + + @deprecated("Use QoptColoredNoise instead") class qopt_colored_noise(QoptColoredNoise): ...