Skip to content
Snippets Groups Projects
Commit 09fd77b5 authored by Tobias Hangleiter's avatar Tobias Hangleiter
Browse files

Merge branch 'bugfixes_and_improvements' into 'main'

Bugfixes and improvements

See merge request !68
parents be7e6a5f 85459232
Branches
Tags
1 merge request!68Bugfixes and improvements
Pipeline #1707660 waiting for manual action
......@@ -31,7 +31,7 @@ classifiers = [
dependencies = [
"packaging",
"lazy-loader",
"qutech-util >= 2025.03.1",
"qutech-util >= 2025.05.1",
"numpy",
"scipy",
"matplotlib >= 3.7",
......@@ -87,7 +87,7 @@ features = [
]
[tool.hatch.envs.tests.scripts]
run = [
"python -m pytest --doctest-modules --junitxml=./pytest_report.xml"
"python -m pytest --doctest-modules --junitxml=./pytest_report.xml src/ tests/"
]
[tool.hatch.envs.doc]
......
......
This diff is collapsed.
This diff is collapsed.
......@@ -466,16 +466,26 @@ class DAQSettings(dict):
fs = self._domain_df.next_smallest(fs / self['nperseg']) * self['nperseg']
if not self._isclose(fs, fs_prev):
self['df'] = self._domain_df.next_closest(fs_prev / self['nperseg'])
self._make_compatible_nperseg(ceil(fs_prev / self['df']))
self._make_compatible_nperseg(ceil(self._domain_df.round(fs_prev / self['df'])))
return self._to_allowed_fs(fs_prev)
# Constraints on fs itself
if not isinf(df) and not (fs / df) % 1:
if not isinf(df) and not self._domain_fs.round(fs / df) % 1:
# fs might be due to ceil-ing when inferring nperseg. Use next_closest
fs = self._domain_fs.next_closest(fs)
else:
fs = self._domain_fs.next_largest(fs)
if not self._isclose(fs, fs_prev):
self._make_compatible_fs(fs)
return fs
# Finally, as a last resort test if the parameters match. If not, try to adjust nperseg
if not isinf(df) and 'nperseg' in self:
fs = df / self['nperseg']
if not self._isclose(fs, fs_prev):
self['fs'] = self._domain_fs.next_closest(fs_prev)
self._make_compatible_nperseg(
self._domain_nperseg.next_closest(self['fs'] / df)
)
return self['fs']
return fs
......@@ -490,7 +500,7 @@ class DAQSettings(dict):
if not self._isclose(df, df_prev):
self['fs'] = self._domain_fs.next_closest(df_prev * self['nperseg'])
# Use df instead of df_prev here because we preferentially adjust df over fs or nperseg
self._make_compatible_nperseg(ceil(self['fs'] / df))
self._make_compatible_nperseg(ceil(self._domain_fs.round(self['fs'] / df)))
return self._to_allowed_df(df)
if not isinf(fs := self.get('fs', self.get('f_max', inf) * 2)):
# Constraints on nperseg constrain df
......@@ -503,6 +513,16 @@ class DAQSettings(dict):
df = self._domain_df.next_smallest(df)
if not self._isclose(df, df_prev):
self._make_compatible_df(df)
return df
# Finally, as a last resort test if the parameters match. If not, try to adjust nperseg
if not isinf(fs) and 'nperseg' in self:
df = fs / self['nperseg']
if not self._isclose(df, df_prev):
self['df'] = self._domain_df.next_closest(df_prev)
self._make_compatible_nperseg(
self._domain_nperseg.next_closest(fs / self['df'])
)
return self['df']
return df
......@@ -522,14 +542,14 @@ class DAQSettings(dict):
df = self.get('df', self.get('f_min', inf))
if not isinf(df):
# Constraints on fs constrain nperseg through df/f_min
nperseg = ceil(self._domain_fs.next_largest(df * nperseg) / df)
nperseg = ceil(self._domain_df.round(self._domain_fs.next_largest(df * nperseg) / df))
if nperseg != nperseg_prev:
self['fs'] = self._domain_fs.next_largest(fs if not isinf(fs) else df * nperseg_prev)
self._make_compatible_df(self['fs'] / nperseg_prev)
return self._to_allowed_nperseg(nperseg_prev)
if not isinf(fs):
# Constraints on df constrain nperseg through fs/f_max
nperseg = ceil(fs / self._domain_df.next_smallest(fs / nperseg))
nperseg = ceil(self._domain_fs.round(fs / self._domain_df.next_smallest(fs / nperseg)))
if nperseg != nperseg_prev:
self['df'] = self._domain_df.next_closest(df if not isinf(df) else fs * nperseg_prev)
self._make_compatible_fs(self['df'] * nperseg_prev)
......@@ -538,6 +558,14 @@ class DAQSettings(dict):
nperseg = self._domain_nperseg.next_largest(nperseg)
if nperseg != nperseg_prev:
self._make_compatible_nperseg(nperseg)
return nperseg
# Finally, as a last resort test if the parameters match. If not, try to adjust df
if not isinf(df) and not isinf(fs):
nperseg = ceil(self._domain_fs.round(fs / df))
if nperseg != nperseg_prev:
self['nperseg'] = self._domain_nperseg.next_closest(nperseg_prev)
self._make_compatible_df(self._domain_df.next_closest(self['fs'] / self['nperseg']))
return self['nperseg']
return nperseg
......@@ -630,7 +658,7 @@ class DAQSettings(dict):
def _infer_nperseg(self, default: bool = False) -> int | None:
# user-set fs or df take precedence over noverlap
if 'fs' in self and 'df' in self:
return ceil(self['fs'] / self['df'])
return ceil(self._domain_fs.round(self['fs'] / self['df']))
if 'n_pts' in self:
if 'noverlap' in self and 'n_seg' in self:
return int((self['n_pts'] + (self['n_seg'] - 1) * self['noverlap'])
......@@ -648,7 +676,7 @@ class DAQSettings(dict):
if not isinf(df) and not isinf(fs):
# In principle this should be self._domain_nperseg.next_largest(), but that recurses
# infinitely. So we do what we can.
return min(ceil(fs / df), self._upper_bound_nperseg())
return min(ceil(self._domain_fs.round(fs / df)), self._upper_bound_nperseg())
return None
def _infer_noverlap(self, default: bool = False) -> int | None:
......@@ -775,7 +803,7 @@ class DAQSettings(dict):
return self['nperseg']
if (nperseg := self._infer_nperseg()) is not None:
return nperseg
return self.setdefault('nperseg', ceil(self.fs / self.df))
return self.setdefault('nperseg', ceil(self._domain_fs.round(self.fs / self.df)))
@interdependent_daq_property
def noverlap(self) -> int:
......
......
......@@ -194,14 +194,14 @@ class DemodulatorQoptColoredNoise(QoptColoredNoise):
"""
# Don't highpass filter
settings = copy.deepcopy(settings)
settings = copy.copy(settings)
settings.pop('f_min', None)
return real_space.RC_filter(signal * IQ, **settings)
@with_delay
def acquire(self, *, n_avg: int, freq: float = 50, filter_order: int = 1,
modulate_signal: bool = False,
filter_method: Literal['forward', 'forward-backward'] = 'forward',
def acquire(self, *, n_avg: int, freq: float = 50, modulate_signal: bool = False,
filter_order: int = 1,
filter_method: Literal['forward', 'forward-backward'] = 'forward-backward',
**settings) -> AcquisitionGenerator[DTYPE]:
r"""Simulate demodulated noisy data.
......@@ -217,8 +217,6 @@ class DemodulatorQoptColoredNoise(QoptColoredNoise):
Number of outer averages.
freq :
Modulation frequency.
filter_order :
RC filter order used to filter the demodulated signal.
modulate_signal :
Add the simulated noise to the modulation signal to mimic
noise picked up by a Lock-In signal travelling through some
......@@ -237,6 +235,8 @@ class DemodulatorQoptColoredNoise(QoptColoredNoise):
with :math:`s(t)` the output signal and :math:`\delta(t)`
the noise.
filter_order :
RC filter order used to filter the demodulated signal.
filter_method :
See :func:`~qutil:qutil.signal_processing.real_space.RC_filter`.
......
......
......@@ -58,7 +58,7 @@ import sys
import time
import warnings
from abc import ABC
from typing import Any, Dict, Mapping, Optional, Type, Union
from typing import Any, Dict, Mapping, Optional, Type
import numpy as np
from packaging import version
......
......
......@@ -39,10 +39,14 @@ def test_daq_settings_warnings():
s = DAQSettings(nperseg=1000, df=0.999)
s.fs = 1000
with pytest.warns(UserWarning, match='Need to change fs from 1001 to 1000.'):
with pytest.warns(UserWarning, match='Need to change df from 1 to 1.001'):
s = DAQSettings(fs=1001, df=1)
s.nperseg = 1000
with pytest.warns(UserWarning, match='Need to change nperseg from 1000 to 1001'):
s = DAQSettings(fs=1001, nperseg=1000)
s.df = 1
def test_daq_settings_exceptions():
......@@ -134,6 +138,10 @@ def test_mfli_daq_settings(mock_zi_daq):
t = mock_zi_daq.DAQSettings(**s.to_consistent_dict())
t.to_consistent_dict()
s = mock_zi_daq.DAQSettings(f_min=1e1, f_max=1e5)
t = mock_zi_daq.DAQSettings(s.to_consistent_dict())
t.to_consistent_dict()
def test_reproducibility():
"""Test if there are no exceptions or infinite recursions for a
......
......
......@@ -2,6 +2,7 @@ import os
import pathlib
import random
import string
import time
from tempfile import mkdtemp
import pytest
......@@ -79,7 +80,8 @@ def spectrometer(monkeypatch, relative_paths: bool, threaded_acquisition: bool):
@pytest.fixture
def serialized(spectrometer: Spectrometer):
def serialized(done_saving: Spectrometer):
spectrometer = done_saving
stem = ''.join(random.choices(string.ascii_letters, k=10))
try:
......@@ -96,8 +98,18 @@ def serialized(spectrometer: Spectrometer):
remove_file_if_exists(spectrometer.savepath / f'{stem}{ext}')
def test_saving(spectrometer: Spectrometer, relative_paths: bool):
@pytest.fixture
def done_saving(spectrometer: Spectrometer):
while spectrometer._datasaver.jobs:
time.sleep(0.1)
return spectrometer
def test_saving(done_saving: Spectrometer, relative_paths: bool):
spectrometer = done_saving
assert spectrometer.savepath.exists()
for file in spectrometer.files:
if relative_paths:
assert os.path.exists(spectrometer.savepath / file)
......@@ -105,16 +117,14 @@ def test_saving(spectrometer: Spectrometer, relative_paths: bool):
assert os.path.exists(file)
def test_serialization(spectrometer: Spectrometer):
spectrometer.serialize_to_disk('blub')
def test_serialization(spectrometer: Spectrometer, serialized: pathlib.Path):
exts = ['_files.txt']
if (spectrometer.savepath / 'blub').is_file():
assert os.path.exists(spectrometer.savepath / 'blub')
if serialized.is_file():
assert os.path.exists(serialized)
else:
exts.extend(['.bak', '.dat', '.dir'])
for ext in exts:
assert os.path.exists(spectrometer.savepath / f'blub{ext}')
assert os.path.exists(serialized.with_name(serialized.name + ext))
def test_deserialization(serialized: pathlib.Path):
......
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment