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