diff --git a/README.md b/README.md index df1237f..7707090 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,31 @@ filepath = ts.run() ts.analyze() ``` +**Symmetric lock-in (optional):** For setups where the same frequencies are +used on input and output, you can use Presto’s `SymmetricLockin` backend by +setting `symmetric_lockin=True`. The database stores the lockin type so you +can filter later (e.g. `select_runs(lockin_type="SymmetricLockin")`). + +```python +from daq import TimeStream + +# TimeStream with SymmetricLockin (same frequencies on input and output) +ts_sym = TimeStream( + lo_freq=5e9, + if_freqs=[10e6, 20e6], + df=1e3, + pixel_counts=10000, + amp=[0.05, 0.05], + output_port=1, + input_port=1, + device="Detector_B", + symmetric_lockin=True, # Use SymmetricLockin backend + notes="Noise measurement (symmetric)" +) +filepath = ts_sym.run() +ts_sym.analyze() +``` + ### TwoTonePower Measurement ```python @@ -139,6 +164,7 @@ Each measurement creates a document with: - `file`: Full path to HDF5 data file - `output_port`, `input_port`: Port numbers - `amp`: Readout amplitude +- `lockin_type`: For TimeStream, `"Lockin"` or `"SymmetricLockin"` - All measurement-specific parameters (freq_center, lo_freq, etc.) **For Sweep measurements with automatic fitting enabled**, the document @@ -270,6 +296,12 @@ df = select_runs( freq_center=5e9, amp=0.1 ) + +# Find TimeStream runs that used SymmetricLockin +df = select_runs( + measurement_type="timestream", + lockin_type="SymmetricLockin" +) ``` **Filter by fit results (for Sweep measurements):** diff --git a/daq/measurements/timestream.py b/daq/measurements/timestream.py index 8ae481b..65f471b 100644 --- a/daq/measurements/timestream.py +++ b/daq/measurements/timestream.py @@ -2,7 +2,7 @@ """ TimeStream measurement class for acquiring time-domain data with multiple frequencies. """ -from typing import List, Optional, Union +from typing import Any, Dict, List, Optional, Union import h5py import numpy as np @@ -28,6 +28,7 @@ def __init__( output_port: int, input_port: int, dither: bool = True, + symmetric_lockin: bool = False, device: Optional[str] = None, filter: Optional[str] = None, notes: Optional[str] = None, @@ -43,6 +44,7 @@ def __init__( self.output_port = output_port self.input_port = input_port self.dither = dither + self.symmetric_lockin = symmetric_lockin self.device = device self.filter = filter self.notes = notes @@ -61,67 +63,158 @@ def __init__( def check_amp(self): assert self.amp.sum()<1.0, "Amplitude sum must be less than 1.0" - def run( + def _run_lockin( self, - presto_address: Optional[str] = None, - presto_port: Optional[int] = None, - ext_ref_clk: bool = False, - save_filename: Optional[str] = None, - ) -> str: - if presto_address is None: - presto_address = get_presto_address() - if presto_port is None: - presto_port = get_presto_port() + presto_address: str, + presto_port: int, + ext_ref_clk: bool, + save_filename: Optional[str], + ) -> None: with lockin.Lockin( address=presto_address, port=presto_port, ext_ref_clk=ext_ref_clk, **self.DC_PARAMS, ) as lck: - lck.hardware.set_adc_attenuation(self.input_port, self.ADC_ATTENUATION) + lck.hardware.set_adc_attenuation( + self.input_port, self.ADC_ATTENUATION + ) lck.hardware.set_dac_current(self.output_port, self.DAC_CURRENT) lck.hardware.set_inv_sinc(self.output_port, 0) lck.hardware.configure_mixer( - self.lo_freq, - out_ports=self.output_port, - in_ports=self.input_port + self.lo_freq, + out_ports=self.output_port, + in_ports=self.input_port, ) lck.set_dither(self.dither, self.output_port) _, self.df = lck.tune(0.0, self.df) lck.set_df(self.df) - # Configure output group - og = lck.add_output_group(self.output_port, len(self.if_freqs)) + og = lck.add_output_group( + self.output_port, len(self.if_freqs) + ) og.set_frequencies(self.if_freqs) og.set_amplitudes(self.amp) og.set_phases(self.phases_i, self.phases_q) - # Configure input group - ig = lck.add_input_group(self.input_port, len(self.if_freqs)) + ig = lck.add_input_group( + self.input_port, len(self.if_freqs) + ) ig.set_frequencies(self.if_freqs) lck.apply_settings() - # Acquire data pixel_dict = lck.get_pixels(self.pixel_counts) - - self.freq_arr, self.pixel_i, self.pixel_q = pixel_dict[self.input_port] - self.lsb, self.usb = untwist_downconversion(self.pixel_i, self.pixel_q) + self.freq_arr, self.pixel_i, self.pixel_q = pixel_dict[ + self.input_port + ] + self.lsb, self.usb = untwist_downconversion( + self.pixel_i, self.pixel_q + ) - # Calculate frequency arrays self.freqs_usb = self.lo_freq + self.if_freqs self.freqs_lsb = self.lo_freq - self.if_freqs - # Mute outputs at the end og.set_amplitudes(0.0) lck.apply_settings() + def _run_symmetric( + self, + presto_address: str, + presto_port: int, + ext_ref_clk: bool, + save_filename: Optional[str], + ) -> None: + with lockin.SymmetricLockin( + address=presto_address, + port=presto_port, + ext_ref_clk=ext_ref_clk, + **self.DC_PARAMS, + ) as lck: + lck.hardware.set_adc_attenuation( + self.input_port, self.ADC_ATTENUATION + ) + lck.hardware.set_dac_current(self.output_port, self.DAC_CURRENT) + lck.hardware.set_inv_sinc(self.output_port, 0) + lck.hardware.configure_mixer( + self.lo_freq, + out_ports=self.output_port, + in_ports=self.input_port, + ) + lck.set_dither(self.dither, self.output_port) + + _, self.df = lck.tune(0.0, self.df) + lck.set_df(self.df) + + sg = lck.add_symmetric_group( + self.input_port, self.output_port, len(self.if_freqs) + ) + sg.set_frequencies(self.if_freqs) + sg.set_amplitudes(self.amp) + sg.set_phases(self.phases_i) + + lck.apply_settings() + + pixel_dict = lck.get_pixels(self.pixel_counts) + self.freq_arr, pixels = pixel_dict[self.input_port] + self.pixel_i = np.real(pixels) + self.pixel_q = np.imag(pixels) + self.usb = pixels + self.lsb = None + self.freqs_usb = self.lo_freq + self.if_freqs + self.freqs_lsb = self.lo_freq - self.if_freqs + + sg.set_amplitudes(0.0) + lck.apply_settings() + + def run( + self, + presto_address: Optional[str] = None, + presto_port: Optional[int] = None, + ext_ref_clk: bool = False, + save_filename: Optional[str] = None, + ) -> str: + if presto_address is None: + presto_address = get_presto_address() + if presto_port is None: + presto_port = get_presto_port() + + if self.symmetric_lockin: + self._run_symmetric( + presto_address, presto_port, ext_ref_clk, save_filename + ) + else: + self._run_lockin( + presto_address, presto_port, ext_ref_clk, save_filename + ) return self.save(save_filename=save_filename) def save(self, save_filename: Optional[str] = None) -> str: return super()._save(__file__, save_filename=save_filename) + def _build_document( + self, + number: str, + measurement_type: str, + file_path: str, + device: str, + filter_name: Optional[str], + notes: Optional[str], + ) -> Dict[str, Any]: + document = super()._build_document( + number=number, + measurement_type=measurement_type, + file_path=file_path, + device=device, + filter_name=filter_name, + notes=notes, + ) + document["lockin_type"] = ( + "SymmetricLockin" if self.symmetric_lockin else "Lockin" + ) + return document + @classmethod def load(cls, load_filename: str) -> "TimeStream": with h5py.File(load_filename, "r") as h5f: @@ -131,6 +224,9 @@ def load(cls, load_filename: str) -> "TimeStream": output_port = int(h5f.attrs["output_port"]) # type: ignore input_port = int(h5f.attrs["input_port"]) # type: ignore dither = bool(h5f.attrs["dither"]) # type: ignore + symmetric_lockin = bool( + h5f.attrs["symmetric_lockin"] + ) if "symmetric_lockin" in h5f.attrs else False if_freqs: npt.NDArray[np.float64] = h5f["if_freqs"][()] # type: ignore amp: npt.NDArray[np.float64] = h5f["amp"][()] # type: ignore @@ -153,6 +249,7 @@ def load(cls, load_filename: str) -> "TimeStream": output_port=output_port, input_port=input_port, dither=dither, + symmetric_lockin=symmetric_lockin, ) # Restore data arrays