Merge branch 'main' of https://gitea.pkm.physik.tu-darmstadt.de/IPKM/python3-damaris
Build Debian Packages / build (bookworm, debian12) (push) Successful in 9m20s
Build Debian Packages / build (trixie, debian13) (push) Successful in 9m22s

This commit is contained in:
markusro committed 2026-10-19 09:19:39 +02:00
commit 9e42205a4d
48 files changed
+2184 -717

No files matched your search

+53 -13
View File
@@ -2,8 +2,7 @@ name: Build Debian Packages
on: on:
push: push:
branches: [ "main" ] tags: [ "v*" ] # releases are tag-driven now
pull_request:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@@ -16,14 +15,14 @@ jobs:
codename: bookworm codename: bookworm
- target: debian13 - target: debian13
codename: trixie codename: trixie
runs-on: ${{ matrix.target }} runs-on: ${{ matrix.target }}
steps: steps:
- name: Install Node.js and git - name: Install Node.js and git
run: | run: |
apt-get update apt-get update
apt-get -y install nodejs git lsb-release apt-get -y install nodejs git lsb-release jq
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3
@@ -31,20 +30,61 @@ jobs:
github-server-url: https://gitea.pkm.physik.tu-darmstadt.de github-server-url: https://gitea.pkm.physik.tu-darmstadt.de
- name: Install build dependencies - name: Install build dependencies
run: | run: bash requirements.debian
bash requirements.debian
- name: Build Package - name: Build Package
run: | run: dpkg-buildpackage -us -uc -b
dpkg-buildpackage -us -uc -b
- name: Collect Artifacts - name: Collect Artifacts
run: | run: |
mkdir -p artifacts mkdir -p artifacts
mv -v ../*.deb artifacts/ mv -v ../*.deb artifacts/
- name: Upload Artifacts - name: Publish release with assets
uses: actions/upload-artifact@v3 env:
with: GITEA_TOKEN: ${{ secrets.DEB_UPLOAD_TOKEN }}
name: python3-damaris-${{ matrix.codename }}.gz SLUG: ${{ gitea.repository }} # owner/repo
path: artifacts/*.deb TAG: ${{ gitea.ref_name }} # e.g. v1.2.3
CODENAME: ${{ matrix.codename }}
run: |
set -euo pipefail
API="https://gitea.pkm.physik.tu-darmstadt.de/api/v1"
AUTH="Authorization: token ${GITEA_TOKEN}"
# 1. Find an existing release for this tag, if any
release_id=$(curl -s -H "$AUTH" \
"$API/repos/$SLUG/releases/tags/$TAG" | jq -r '.id // empty')
# 2. Create the release if it does not exist yet
if [ -z "$release_id" ]; then
echo "Creating release $TAG"
release_id=$(curl -s -X POST -H "$AUTH" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg tag "$TAG" \
'{tag_name:$tag, name:$tag, draft:false, prerelease:false}')" \
"$API/repos/$SLUG/releases" | jq -r '.id')
else
echo "Reusing existing release $TAG (id=$release_id)"
fi
echo "release_id=$release_id"
# 3. Upload each .deb as an asset
for deb in artifacts/*.deb; do
asset="${CODENAME}_$(basename "$deb")" # avoid bookworm/trixie name clash
echo "Uploading $asset"
# delete any existing asset with the same name (idempotent re-runs)
existing=$(curl -s -H "$AUTH" \
"$API/repos/$SLUG/releases/$release_id/assets" \
| jq -r --arg n "$asset" '.[] | select(.name==$n) | .id')
if [ -n "$existing" ]; then
curl -s -X DELETE -H "$AUTH" \
"$API/repos/$SLUG/releases/$release_id/assets/$existing"
fi
curl --fail --show-error --silent \
-X POST -H "$AUTH" \
-F "attachment=@$deb;filename=$asset" \
"$API/repos/$SLUG/releases/$release_id/assets"
done
+2
View File
@@ -14,6 +14,8 @@ debian/.debhelper
# Sphinx documentation # Sphinx documentation
doc/_build/ doc/_build/
doc/_autosummary/
doc/damaris/
# Debian build artifacts # Debian build artifacts
debian/*.debhelper.log debian/*.debhelper.log
+14 -13
View File
@@ -4,16 +4,17 @@
This is the front end part of GUI for controlling NMR spectrometers. On "Start", the experiment script executes and creates XML job files. A backend, not in this code, executes these jobs sequentially adn writes the results as XML files. The front end consumes these result files using the result script. This is the front end part of GUI for controlling NMR spectrometers. On "Start", the experiment script executes and creates XML job files. A backend, not in this code, executes these jobs sequentially adn writes the results as XML files. The front end consumes these result files using the result script.
## Guidelines ## Guidelines
1. Use the .venv python3 environment 1. Prefer the system python and notify the user if libraries and packages are missing. Use uv for the python3 environment only if system packages are missing and the user does not want to install the needed packages.
5. Old code was ported from gtk2 and python2.7. This code has to work on python 3.11 and above, as well as GTK3 2. Old code was ported from gtk2 and python2.7. This code has to work on python 3.11 and above, as well as GTK3
2. Be aware the targeted Debian versions are bookworm, trixie, and forky. 3. Be aware the targeted Debian versions are bookworm, trixie, and forky.
3. Only use packages and libraries available in Debian bookworm, trixie, and forky. Make sure that the code is compatible with all of these versions. 4. Only use packages and libraries available in Debian bookworm, trixie, and forky. Make sure that the code is compatible with all of these versions.
3. Do not commit changes (with git) or write commit messages without asking the user. 5. Do not commit changes (with git) or write commit messages without asking the user.
4. Try to minimize code changes, do not use elaborate checks for defensive programming. Ask the user if something should be tested with unit tests. 6. Try to minimize code changes, do not use elaborate checks for defensive programming. Ask the user if something should be tested with unit tests.
6. Use the python unittest module for unit tests, not pytest 7. Use the python unittest module for unit tests, not pytest
7. Functions for the experiment script belong to the src/damaris/experiments/Experiment.py file 8. Functions for the experiment script belong to the src/damaris/experiments/Experiment.py file
8. Functions for the result script belong in the files in the src/damaris/data/ folder 9. Functions for the result script belong in the files in the src/damaris/data/ folder
9. The gitea workflow can be found at .gitea/workflows/debian-build.yml 10. The gitea workflow can be found at .gitea/workflows/debian-build.yml
10. to not edit anything in "build", it is regenerated 11. Do not edit anything in "build", it is regenerated
11. Use sphinx for documentation, autodoc and autosummary is used for references of all classes, functions and modules. 12. Use sphinx for documentation, autodoc and autosummary is used for references of all classes, functions and modules.
12. If a new function/method is created document a short usage the doc/api_reference.rst and a longer explanation in doc/manual.rst 13. If a new function/method is created document a short usage in doc/api_reference.rst and a longer explanation in doc/manual.rst
14. Do not use the Makefile in ./doc/, Doxygen is not used anymore.
+40
View File
@@ -3,3 +3,43 @@
## Version bump ## Version bump
Version is changed in src/damaris/__init__.py Version is changed in src/damaris/__init__.py
### Process
1. Update the version in `src/damaris/__init__.py`
2. Add a new changelog entry in `debian/changelog` using the `dch` (debchange) tool
### Using dch tool
The `dch` tool (from the `devscripts` package) is the standard way to maintain debian changelog files. Example workflow:
```bash
# Update version in Python package
sed -i 's/__version__ = "0.24"/__version__ = "0.25"/' src/damaris/__init__.py
# Add changelog entry with dch
dch --newversion 0.25
# This opens your editor to add the changes for this version
# Or use batch mode with message:
dch -v 0.25 "New features and bug fixes"
# Verify the changelog
tail -20 debian/changelog
```
The `dch` command will:
- Create a new entry with the specified version
- Set the distribution to "stable" (or "unstable" for development versions)
- Use the maintainer name and email from the previous entry or environment variables
- Open your editor to add the change descriptions
For development versions, use:
```bash
dch -v 0.25-1 --distribution unstable "Development version"
```
Remember to install devscripts if needed:
```bash
sudo apt-get install devscripts
```
+29
View File
@@ -1,3 +1,32 @@
python3-damaris (0.25) stable; urgency=medium
* fixed non working stop button
* added type hints to data classes, fixed get_ydata override conflict
* fixed y_square latent bug: kept always a list of per-channel arrays
* fixed time_period latent crashes: missing time information treated as None
* replaced remaining gtk2/PyGTK and Python 2 idioms in DamarisGUI
* fixed startup crash: register GtkSourceView GType for GtkBuilder
* fixed bug with counter updates
* FitResult fit overlays, leaked HDF5 files closed after experiment stop
* HTML documentation is built at package build time and opened from the Help menu
* crosshair cursor is fast on HiDPI screens (dirty-strip blitting)
-- Markus Rosenstihl <markus.rosenstihl@pkm.tu-darmstadt.de> Sun, 18 Oct 2026 19:13:20 +0200
python3-damaris (0.24) stable; urgency=medium
* fixed log dump race condition, no more crash at experiment end
* data pool HDF5 file is now always closed properly
* Signalpath.phase() no longer modifies data in place, returns object for phase=0 (fixes #43)
* fixed inconsistencies in returned objects: missing channels, is_clipped
* fixed accumulation lock leaks and deprecated API usage
* user plugin directory ~/.config/damaris/plugin added to Python path
* all output switched to logging module, experiment and result script prints show up in the GUI
* command line interface fixed and properly added to the deb package
* performance: incremental writing of HDF5 data pool, reduced polling in synchronize()
-- Markus Rosenstihl <markus.rosenstihl@pkm.tu-darmstadt.de> Sun, 18 Oct 2026 11:39:54 +0200
python3-damaris (0.22) stable; urgency=medium python3-damaris (0.22) stable; urgency=medium
* fixed autocomplete in the editor tabs * fixed autocomplete in the editor tabs
+2 -2
View File
@@ -2,7 +2,7 @@ Source: python3-damaris
Section: science Section: science
Priority: optional Priority: optional
Maintainer: Markus Rosenstihl <markus.rosenstihl@pkm.tu-darmstadt.de> Maintainer: Markus Rosenstihl <markus.rosenstihl@pkm.tu-darmstadt.de>
Build-Depends: python3, debhelper, python3-dev, dh-sequence-python3, dpkg-dev, pybuild-plugin-pyproject, sphinx-common, sphinx-rtd-theme-common, python3-sphinxcontrib.bibtex, python3-sphinx-rtd-theme Build-Depends: python3, debhelper, python3-dev, dh-sequence-python3, dpkg-dev, pybuild-plugin-pyproject, sphinx-common, python3-sphinx, sphinx-rtd-theme-common, python3-sphinxcontrib.bibtex, python3-sphinx-rtd-theme
Standards-Version: 3.7.3 Standards-Version: 3.7.3
X-Python3-Version: >= 3.5 X-Python3-Version: >= 3.5
Rules-Requires-Root: no Rules-Requires-Root: no
@@ -10,7 +10,7 @@ Rules-Requires-Root: no
Package: python3-damaris Package: python3-damaris
Architecture: all Architecture: all
Provides: damaris-frontend-py3 Provides: damaris-frontend-py3
Depends: ${python3:Depends}, python3-numpy, python3-scipy, python3-gi, python3-matplotlib, python3-tables, lzop, python3-xdg, libgtksourceview-3.0-1, libgirepository1.0-dev | libgirepository-2.0-dev, breeze-icon-theme Depends: ${python3:Depends}, ${sphinxdoc:Depends}, python3-numpy, python3-scipy, python3-gi, python3-matplotlib, python3-tables, lzop, python3-xdg, libgtksourceview-3.0-1, libgirepository1.0-dev | libgirepository-2.0-dev, breeze-icon-theme
Recommends: damaris-backends, python3-numpy-ext Recommends: damaris-backends, python3-numpy-ext
Suggests: python3-doc, python3-tables-doc, python3-numpy-doc Suggests: python3-doc, python3-tables-doc, python3-numpy-doc
Description: python frontend for DAMARIS project Description: python frontend for DAMARIS project
+1 -1
View File
@@ -1,4 +1,4 @@
doc/ usr/share/doc/python3-damaris/ build/html usr/share/doc/python3-damaris/
debian/damaris3.desktop usr/share/applications/ debian/damaris3.desktop usr/share/applications/
src/damaris/gui/DAMARIS3.png usr/share/pixmaps/ src/damaris/gui/DAMARIS3.png usr/share/pixmaps/
src/damaris/gui/DAMARIS3.ico usr/share/pixmaps/ src/damaris/gui/DAMARIS3.ico usr/share/pixmaps/
+1 -1
View File
@@ -17,7 +17,7 @@ DH_ALWAYS_EXCLUDE=CVS:.git:venv:.venv:*.h5
override_dh_auto_build: override_dh_auto_build:
dh_auto_build dh_auto_build
sphinx-build -N -bhtml doc/ build/doc sphinx-build -N -bhtml doc/ build/html
override_dh_auto_test: override_dh_auto_test:
+16
View File
@@ -26,11 +26,13 @@ The data module contains result types, data handling, and signal processing clas
damaris.data.ADC_Result damaris.data.ADC_Result
damaris.data.Accumulation damaris.data.Accumulation
damaris.data.Config_Result
damaris.data.DamarisFFT damaris.data.DamarisFFT
damaris.data.DataPool damaris.data.DataPool
damaris.data.Drawable damaris.data.Drawable
damaris.data.Errorable damaris.data.Errorable
damaris.data.Error_Result damaris.data.Error_Result
damaris.data.FitResult
damaris.data.MeasurementResult damaris.data.MeasurementResult
damaris.data.Persistance damaris.data.Persistance
damaris.data.Resultable damaris.data.Resultable
@@ -71,6 +73,20 @@ The GUI module contains the graphical interface, backend driver, and experiment/
damaris.gui.TemperatureMonitor damaris.gui.TemperatureMonitor
damaris.gui.logging_utils damaris.gui.logging_utils
A short usage example for the font helper of the GUI module (the non-deprecated
replacement for the removed ``Widget.modify_font()``):
.. code-block:: python
from damaris.gui.DamarisGUI import apply_font_css
apply_font_css(textview, "Monospace 10") # pango style font description
apply_font_css(label, "sans bold 16")
The font string is converted into a CSS rule (``font-family``/``font-size``/
``font-weight``) which is applied to the widget's style context with a
``Gtk.CssProvider``.
Tools Module Tools Module
============ ============
+54
View File
@@ -184,3 +184,57 @@ Stability of the DAC has been tested by measuring the shifts of resonance freque
The offset of the PFG amplifier was adjusted via the offset potentiometer on the DAC board. There is also an offset adjustment possible at the PFG amplifier directly. For adjustment, the signal of water in a sample tube with a 0.5 mm teflon stripe perpendicular to the magnetic field B0 was recorded. First, the PFG power supply has been switched off and the sample was shimmed with the room temperature shims until the longest decay time T*2 of the FID has been achieved. After this, the PFG was switched on and the offset has been adjusted to cause least distortion in the spectrum. The offset of the PFG amplifier was adjusted via the offset potentiometer on the DAC board. There is also an offset adjustment possible at the PFG amplifier directly. For adjustment, the signal of water in a sample tube with a 0.5 mm teflon stripe perpendicular to the magnetic field B0 was recorded. First, the PFG power supply has been switched off and the sample was shimmed with the room temperature shims until the longest decay time T*2 of the FID has been achieved. After this, the PFG was switched on and the offset has been adjusted to cause least distortion in the spectrum.
*******************
Publish new version
*******************
.. warning::
``__version__`` in ``src/damaris/__init__.py`` and the top entry of
``debian/changelog`` must carry the same version number at every
release. If they drift apart, the Debian package version no longer
matches the application version.
1. Commit all code changes intended for the release.
2. Bump ``__version__`` in ``src/damaris/__init__.py``. This constant
is the single source of truth for the application version and
follows the simple ``X.Y`` scheme used so far (a minor release
increments the second number, e.g. 0.22 -> 0.23).
3. Add a new entry at the top of ``debian/changelog`` with the same
version number and a bullet list of the user visible changes. The
Debian package version does not follow ``__version__``
automatically; it is defined solely by this file. Either edit
``debian/changelog`` directly or run ``dch`` (from the
``devscripts`` package), which prepends the entry for you. Match
the entry format already used in this file:
.. code-block:: text
python3-damaris (X.Y) stable; urgency=medium
* bullet list of user visible changes
-- Full Name <email> Day, DD Mon YYYY HH:MM:SS +0200
The maintainer line at the end is indented by two spaces and its
date must be in RFC-2822 format, which ``date -R`` produces.
4. Commit the version bump and the changelog entry together in one
commit. The repository currently carries no version tags, so no
tagging step is defined; confirm with ``git tag -l`` before
introducing one.
5. Everything else picks up the new version automatically. The Sphinx
documentation reads ``damaris.__version__`` in ``doc/conf.py``, and
``pyproject.toml`` declares the package version dynamically via
``[tool.setuptools.dynamic]`` with ``attr = "damaris.__version__"``,
so neither needs a manual edit. On a push to ``main`` the Debian CI
pipeline ``.gitea/workflows/debian-build.yml`` builds the package
with ``dpkg-buildpackage`` for bookworm and trixie; the version of
the resulting ``.deb`` is taken from ``debian/changelog``.
``DEVELOPER.md`` at the repository root keeps a short note about the
version bump in ``src/damaris/__init__.py``; this section expands it.
+45
View File
@@ -101,6 +101,12 @@ There are now two possibilities to export the figure:
2. Saving the data of the plot as CSV file (comma separated values) by pressing the button on the right side of the toolbar under the plot 2. Saving the data of the plot as CSV file (comma separated values) by pressing the button on the right side of the toolbar under the plot
Help Menu
=========
The **Help** button in the toolbar opens this documentation in the system's web browser. The arrow next to it offers additional local documentations, such as the Python documentation, if installed. When the Debian package is used, the HTML documentation is found in ``/usr/share/doc/python3-damaris/html/``.
Accumulation Accumulation
============ ============
@@ -285,6 +291,35 @@ A typical method to already extract data like amplitudes etc. is possible by usi
Essentially, a dictionary "measurement" is created with *tau* as the key. For each new key the current magnetization is added. This dictionary is then added to the data pool. The advantage over the previous listing is that the magnetization vs. τ curve can be directly displayed in order to gain an overview of the running experiment, because the "measurement" dictionary is added to the data pool. In this example, the value also stores statistics from the accumulation. The data dictionary is stored automatically as described before, including the result and experiment scripts, information about the used back end and a timeline of the experiment. Essentially, a dictionary "measurement" is created with *tau* as the key. For each new key the current magnetization is added. This dictionary is then added to the data pool. The advantage over the previous listing is that the magnetization vs. τ curve can be directly displayed in order to gain an overview of the running experiment, because the "measurement" dictionary is added to the data pool. In this example, the value also stores statistics from the accumulation. The data dictionary is stored automatically as described before, including the result and experiment scripts, information about the used back end and a timeline of the experiment.
**Displaying fits**
A fit to a measurement can be shown as an overlay on the measurement curve using the FitResult class. In contrast to the measurement itself, a fit has its own x-values, independent of the measured data points. Therefore the fit can be sampled on a much denser x-grid, which makes the plotted curve smooth. The fit is stored in the data pool under its own key and is drawn by the GUI on top of the measurement it belongs to. Fit parameters are shown in the plot and are stored together with the fit.
The fit is associated with its measurement in two ways: by the ``source`` argument (the data pool key of the measurement) and, optionally, by storing it under a nested key such as ``"Magnetization(tau)/fit"``, so that it is stored next to the measurement in the HDF5 data pool file. The GUI shows the fit only as an overlay on its measurement; fits do not get their own entry in the display tree.
.. code-block:: python
:linenos:
import numpy
from damaris import FitResult
def result(results):
# ... fill "measurement" as in the listing above ...
# fit an exponential decay to the measured points
from scipy.optimize import curve_fit
[k, v, e] = measurement.get_errorplotdata()
popt, pcov = curve_fit(lambda t, m0, t1: m0 * numpy.exp(-t / t1),
k, v, p0=(v[0], 1.0), sigma=e)
# sample the fit on a dense x-grid for a smooth curve
xfit = numpy.linspace(k.min(), k.max(), 200)
fit = FitResult(xfit, popt[0] * numpy.exp(-xfit / popt[1]),
title="Magnetization(tau) fit",
source="Magnetization(tau)",
params={"M0": popt[0], "T1": popt[1]})
data["Magnetization(tau)/fit"] = fit
The fit is written to the HDF5 data pool file as its own table (columns *x* and *y*, plus *y_err* if given), together with the fit parameters as attributes, and is restored when the data pool is read back.
Note that we can also create a HDF5 file from scratch! In this case experiment and result script are not saved in the resulting file. The advantage is that one can create arbitrary complex structured files at the cost of having a more complicated result script. Note that we can also create a HDF5 file from scratch! In this case experiment and result script are not saved in the resulting file. The advantage is that one can create arbitrary complex structured files at the cost of having a more complicated result script.
.. code-block:: python .. code-block:: python
@@ -313,6 +348,10 @@ The change is *in-place*, which means if the timesignal is needed later on, a co
- **clip(start=**\ *None* **, stop=**\ *None* **)** - **clip(start=**\ *None* **, stop=**\ *None* **)**
Only the data between start and stop is returned. Only the data between start and stop is returned.
The start and stop parameters can be either in time or frequency domain. The start and stop parameters can be either in time or frequency domain.
Calling **clip()** without any argument is a placeholder that leaves the
object unchanged (useful in scripted processing chains). If only one bound
is given, the missing one defaults to the first resp. last data point and
reversed bounds (start > stop) are swapped automatically.
- **baseline(lastpart=**\ *0.1* **)** - **baseline(lastpart=**\ *0.1* **)**
This method should be applied first as it corrects the baseline of the signal, taking the last part of the signal for determining the correction. This method should be applied first as it corrects the baseline of the signal, taking the last part of the signal for determining the correction.
@@ -374,6 +413,12 @@ To use this methods one has to apply consecutively the methods to an *timesignal
This applies baseline correction using the last 40% of the timesignal for determination of the signal. Then, an exponential window is applied. From this timesignal an FFT is calculated which is further clipped to view to the data between -1 kHz with 1 kHz width. This applies baseline correction using the last 40% of the timesignal for determination of the signal. Then, an exponential window is applied. From this timesignal an FFT is calculated which is further clipped to view to the data between -1 kHz with 1 kHz width.
Operators and Copies of Data Objects
====================================
The arithmetic operators (``+``, ``-``, ``*``, ``/``, ``//``, ``**``, unary ``-``) of **ADC_Result** and **Accumulation** objects return *new* objects: the operands are never modified and the data arrays of the result are independent copies, so later in-place operations (``+=``, ``*=``, ...) on the result cannot corrupt the operands. The channel metadata is carried over to the result: an **ADC_Result** result keeps the same ``get_nChannels()`` and ``is_clipped`` flag as its operand, and an **Accumulation** result keeps (and merges) the ``job_ids`` provenance of both operands. Adding an unsupported type or negating an empty accumulation raises a ``ValueError`` instead of silently returning *None*. In the same spirit, ``Config_Result(config=..., desc=...)`` stores the given dictionaries in the constructor, and ``measurementresult + 0`` returns an independent deep copy of the result object.
Good Practices Good Practices
============== ==============
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "python3-damaris" name = "python3-damaris"
version = "0.22" version = "0.25"
requires-python = ">= 3.11" requires-python = ">= 3.11"
description = "Python 3 frontend for DAMARIS (DArmstadt MAgnetic Resonance Instrument Software)" description = "Python 3 frontend for DAMARIS (DArmstadt MAgnetic Resonance Instrument Software)"
license = {text = "GPL-2.0"} license = {text = "GPL-2.0"}
+15
View File
@@ -39,6 +39,9 @@ maintainers = [
[project.scripts] [project.scripts]
DAMARIS3 = "damaris.__main__:main" DAMARIS3 = "damaris.__main__:main"
[dependency-groups]
dev = ["pyrefly"]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
where = ["src"] where = ["src"]
@@ -50,6 +53,18 @@ version = {attr = "damaris.__version__"}
[tool.setuptools.data-files] [tool.setuptools.data-files]
"share/python3-damaris/images" = ["src/damaris/gui/DAMARIS3.png", "src/damaris/gui/DAMARIS3.ico"] "share/python3-damaris/images" = ["src/damaris/gui/DAMARIS3.png", "src/damaris/gui/DAMARIS3.ico"]
[tool.pyrefly]
project-includes = [
"src/damaris/experiments/Experiment.py",
"src/damaris/experiments/dac.py",
"src/damaris/data/ADC_Result.py",
"src/damaris/data/MeasurementResult.py",
"src/damaris/data/Accumulation.py",
]
search-path = ["src"]
python-version = "3.11"
python-platform = "linux"
#"share/python3-damaris/doc" = ["doc/index.html"] #"share/python3-damaris/doc" = ["doc/index.html"]
#"share/python3-damaris/doc/reference-html" = ["doc/reference-html/*"] #"share/python3-damaris/doc/reference-html" = ["doc/reference-html/*"]
#"share/python3-damaris/doc/tutorial-html" = ["doc/tutorial-html/*"] #"share/python3-damaris/doc/tutorial-html" = ["doc/tutorial-html/*"]
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
apt-get -y install libcairo2-dev gir1.2-gtksource-3.0 hdf5-tools apt-get -y install libcairo2-dev gir1.2-gtksource-3.0 hdf5-tools
apt-get -y install python3-dev python3-scipy python3-lmfit python3-matplotlib python3-tables python3-xdg python3-setuptools python3-sphinx-rtd-theme apt-get -y install python3-dev python3-scipy python3-lmfit python3-matplotlib python3-tables python3-xdg python3-setuptools python3-sphinx-rtd-theme
apt-get -y install debhelper dh-sequence-python3 lsb-release sphinx-common pybuild-plugin-pyproject python3-sphinxcontrib.bibtex apt-get -y install debhelper dh-sequence-python3 lsb-release sphinx-common python3-sphinx pybuild-plugin-pyproject python3-sphinxcontrib.bibtex
DEBRELEASE=$(lsb_release -cs) DEBRELEASE=$(lsb_release -cs)
case $DEBRELEASE in case $DEBRELEASE in
"trixie") "trixie")
+1 -1
View File
@@ -15,7 +15,7 @@
## module contents ## module contents
# #
__version__ = "0.23" __version__ = "0.25"
__all__=["experiments", "data", "gui"] __all__=["experiments", "data", "gui"]
import sys import sys
+64 -42
View File
@@ -1,5 +1,7 @@
# -*- coding: iso-8859-1 -*- # -*- coding: iso-8859-1 -*-
from __future__ import annotations
from .Resultable import Resultable from .Resultable import Resultable
from .Drawable import Drawable from .Drawable import Drawable
from .Signalpath import Signalpath from .Signalpath import Signalpath
@@ -9,7 +11,13 @@ import numpy
import sys import sys
import datetime import datetime
import tables import tables
from typing import IO
from scipy.signal import filtfilt, remez from scipy.signal import filtfilt, remez
_XArray = numpy.ndarray | list
_YChannels = list[numpy.ndarray]
_IndexList = list[tuple[int, int]]
_Description = dict[str, str]
############################################################################# #############################################################################
# # # #
# Name: Class ADC_Result # # Name: Class ADC_Result #
@@ -39,7 +47,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
sampling_rate: Sampling frequency of the ADC data. sampling_rate: Sampling frequency of the ADC data.
job_id: identifier for the job that generated this result. job_id: identifier for the job that generated this result.
""" """
def __init__(self, x = None, y:list = None, index = None, sampl_freq = None, desc = None, job_id = None, job_date = None): def __init__(self, x: _XArray | None = None, y: _YChannels | None = None, index: _IndexList | None = None, sampl_freq: float | None = None, desc: _Description | None = None, job_id: int | None = None, job_date: datetime.datetime | None = None):
Resultable.__init__(self) Resultable.__init__(self)
Drawable.__init__(self) Drawable.__init__(self)
@@ -59,7 +67,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
self.cont_data = False self.cont_data = False
self.sampling_rate = 0 self.sampling_rate = 0
self.index = [] self.index = []
self.x = [] self.x = numpy.array([], dtype="float32")
self.y = [] self.y = []
# using all arguments for initialization # using all arguments for initialization
elif (x is not None) and (y is not None) and (index is not None) and (sampl_freq is not None) and (desc is not None) and (job_id is not None) and (job_date is not None): elif (x is not None) and (y is not None) and (index is not None) and (sampl_freq is not None) and (desc is not None) and (job_id is not None) and (job_date is not None):
@@ -67,8 +75,8 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
#for ch in y: #for ch in y:
# if not numpy.issubdtype(ch.dtype, numpy.integer): # if not numpy.issubdtype(ch.dtype, numpy.integer):
# raise TypeError("TypeError: ADC_Result y data must be a list with integer type channels") # raise TypeError("TypeError: ADC_Result y data must be a list with integer type channels")
self.x = x self.x = numpy.asarray(x)
self.y = y self.y = [numpy.asarray(ch) for ch in y]
self.index = index self.index = index
self.sampling_rate = sampl_freq self.sampling_rate = sampl_freq
self.cont_data = True self.cont_data = True
@@ -87,7 +95,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
raise ValueError("Wrong usage of __init__!") raise ValueError("Wrong usage of __init__!")
def create_data_space(self, channels, samples): def create_data_space(self, channels: int, samples: int) -> None:
"Initialises the internal data-structures" "Initialises the internal data-structures"
if self.contains_data(): if self.contains_data():
@@ -104,12 +112,12 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
self.cont_data = True self.cont_data = True
def contains_data(self): def contains_data(self) -> bool:
"""Returns true if ADC_Result contains data. (-> create_data_space() was called)""" """Returns true if ADC_Result contains data. (-> create_data_space() was called)"""
return self.cont_data return self.cont_data
def check_clipping(self, bit_depth=None): def check_clipping(self, bit_depth: int | None = None) -> bool:
""" """
Check if any data in y is clipped. Check if any data in y is clipped.
For a 14-bit ADC, clipping occurs at 2**13 (8192) or 2**13-1 (8191). For a 14-bit ADC, clipping occurs at 2**13 (8192) or 2**13-1 (8191).
@@ -147,7 +155,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
return self.is_clipped return self.is_clipped
def lowpass(self, cutoff, numtaps=None): def lowpass(self, cutoff: float, numtaps: int | None = None) -> ADC_Result:
""" """
Apply a zero-phase lowpass FIR filter to all channels. Apply a zero-phase lowpass FIR filter to all channels.
@@ -232,15 +240,14 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
job_id=self.job_id, job_id=self.job_id,
job_date=self.job_date, job_date=self.job_date,
) )
r.nChannels = self.nChannels self._copy_metadata(r)
r.is_clipped = self.is_clipped
finally: finally:
self.lock.release() self.lock.release()
return r return r
def add_sample_space(self, samples): def add_sample_space(self, samples: int) -> None:
"Adds space for n samples, where n can also be negative (deletes space). New space is filled up with \"0\"" "Adds space for n samples, where n can also be negative (deletes space). New space is filled up with \"0\""
self.lock.acquire() self.lock.acquire()
@@ -260,7 +267,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
self.lock.release() self.lock.release()
def get_result_by_index(self, index): def get_result_by_index(self, index: int) -> ADC_Result:
self.lock.acquire() self.lock.acquire()
try: try:
@@ -277,39 +284,38 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
tmp_y.append(self.y[i][start:end+1].copy()) tmp_y.append(self.y[i][start:end+1].copy())
r = ADC_Result(x = tmp_x, y = tmp_y, index = [(0,len(tmp_y[0])-1)], sampl_freq = self.sampling_rate, desc = self.description.copy() if self.description is not None else None, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = tmp_x, y = tmp_y, index = [(0,len(tmp_y[0])-1)], sampl_freq = self.sampling_rate, desc = self.description.copy() if self.description is not None else None, job_id = self.job_id, job_date = self.job_date)
r.nChannels = self.nChannels self._copy_metadata(r)
r.is_clipped = self.is_clipped
self.lock.release() self.lock.release()
return r return r
def get_sampling_rate(self): def get_sampling_rate(self) -> float:
"""Returns the samplingfrequency""" """Returns the samplingfrequency"""
return self.sampling_rate + 0 return self.sampling_rate + 0
def set_sampling_rate(self, hz): def set_sampling_rate(self, hz: float) -> None:
"""Sets the samplingfrequency in hz""" """Sets the samplingfrequency in hz"""
self.sampling_rate = float(hz) self.sampling_rate = float(hz)
def get_nChannels(self): def get_nChannels(self) -> int:
"""Gets the number of channels""" """Gets the number of channels"""
return self.nChannels + 0 return self.nChannels + 0
def set_nChannels(self, channels): def set_nChannels(self, channels: int) -> None:
"""Sets the number of channels""" """Sets the number of channels"""
self.nChannels = int(channels) self.nChannels = int(channels)
def get_index_bounds(self, index): def get_index_bounds(self, index: int) -> tuple[int, int]:
"Returns a tuple with (start, end) of the wanted result" "Returns a tuple with (start, end) of the wanted result"
return self.index[index] return self.index[index]
def uses_statistics(self): def uses_statistics(self) -> bool:
return False return False
def write_to_csv(self, destination=sys.stdout, delimiter=" "): def write_to_csv(self, destination: str | IO[str] = sys.stdout, delimiter: str = " ") -> None:
""" """
writes the data to a file or to sys.stdout writes the data to a file or to sys.stdout
destination can be a file or a filename destination can be a file or a filename
@@ -321,7 +327,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
else: else:
self._write_to_csv_dest(destination, delimiter) self._write_to_csv_dest(destination, delimiter)
def _write_to_csv_dest(self, dest, delimiter): def _write_to_csv_dest(self, dest: IO[str], delimiter: str) -> None:
dest.write("# adc_result\n") dest.write("# adc_result\n")
dest.write("# t y0 y1 ...\n") dest.write("# t y0 y1 ...\n")
self.lock.acquire() self.lock.acquire()
@@ -337,7 +343,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
finally: finally:
self.lock.release() self.lock.release()
def write_to_simpson(self, destination=sys.stdout, delimiter=" "): def write_to_simpson(self, destination: str | IO[str] = sys.stdout, delimiter: str = " ") -> None:
""" """
writes the data to a text file or sys.stdout in Simpson format, writes the data to a text file or sys.stdout in Simpson format,
for further processing with the NMRnotebook software; for further processing with the NMRnotebook software;
@@ -349,7 +355,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
else: else:
self._write_to_simpson_dest(destination, delimiter) self._write_to_simpson_dest(destination, delimiter)
def _write_to_simpson_dest(self, dest, delimiter): def _write_to_simpson_dest(self, dest: IO[str], delimiter: str) -> None:
self.lock.acquire() self.lock.acquire()
try: try:
xdata=self.get_xdata() xdata=self.get_xdata()
@@ -368,7 +374,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
finally: finally:
self.lock.release() self.lock.release()
def write_to_hdf(self, hdffile, where, name, title, complib=None, complevel=None): def write_to_hdf(self, hdffile: tables.File, where: str, name: str, title: str, complib: str | None = None, complevel: int | None = None) -> None:
accu_group=hdffile.create_group(where=where,name=name,title=title) accu_group=hdffile.create_group(where=where,name=name,title=title)
accu_group._v_attrs.damaris_type="ADC_Result" accu_group._v_attrs.damaris_type="ADC_Result"
if self.contains_data(): if self.contains_data():
@@ -465,13 +471,13 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
# Ueberladen von Operatoren und Built-Ins ------------------------------------------------------- # Ueberladen von Operatoren und Built-Ins -------------------------------------------------------
def __len__(self): def __len__(self) -> int:
"Redefining len(ADC_Result obj), returns the number of samples in one channel and 0 without data" "Redefining len(ADC_Result obj), returns the number of samples in one channel and 0 without data"
if len(self.y)>0: if len(self.y)>0:
return len(self.y[0]) return len(self.y[0])
return 0 return 0
def __repr__(self): def __repr__(self) -> str:
""" """
writes job meta data and data to string returned writes job meta data and data to string returned
""" """
@@ -488,7 +494,12 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
return tmp_string return tmp_string
def __add__(self, other): def _copy_metadata(self, r: ADC_Result) -> None:
"Copy channel metadata (nChannels, is_clipped) from self to the new result r"
r.nChannels = self.nChannels
r.is_clipped = self.is_clipped
def __add__(self, other: int | float) -> ADC_Result:
"Redefining self + other (scalar)" "Redefining self + other (scalar)"
if isinstance(other, int) or isinstance(other, float): if isinstance(other, int) or isinstance(other, float):
self.lock.acquire() self.lock.acquire()
@@ -498,6 +509,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
tmp_y.append(numpy.array(self.y[i], dtype="float32") + other) tmp_y.append(numpy.array(self.y[i], dtype="float32") + other)
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
self._copy_metadata(r)
self.lock.release() self.lock.release()
return r return r
else: else:
@@ -505,12 +517,12 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
def __radd__(self, other): def __radd__(self, other: int | float) -> ADC_Result:
"Redefining other (scalar) + self" "Redefining other (scalar) + self"
return self.__add__(other) return self.__add__(other)
def __sub__(self, other): def __sub__(self, other: int | float) -> ADC_Result:
"Redefining self - other (scalar)" "Redefining self - other (scalar)"
if isinstance(other, int) or isinstance(other, float): if isinstance(other, int) or isinstance(other, float):
self.lock.acquire() self.lock.acquire()
@@ -520,6 +532,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
tmp_y.append(numpy.array(self.y[i], dtype="float32") - other) tmp_y.append(numpy.array(self.y[i], dtype="float32") - other)
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
self._copy_metadata(r)
self.lock.release() self.lock.release()
return r return r
else: else:
@@ -527,7 +540,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
def __rsub__(self, other): def __rsub__(self, other: int | float) -> ADC_Result:
"Redefining other (scalar) - self" "Redefining other (scalar) - self"
if isinstance(other, int) or isinstance(other, float): if isinstance(other, int) or isinstance(other, float):
self.lock.acquire() self.lock.acquire()
@@ -537,6 +550,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
tmp_y.append(other - numpy.array(self.y[i], dtype="float32")) tmp_y.append(other - numpy.array(self.y[i], dtype="float32"))
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
self._copy_metadata(r)
self.lock.release() self.lock.release()
return r return r
else: else:
@@ -544,7 +558,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
def __mul__(self, other): def __mul__(self, other: int | float) -> ADC_Result:
"Redefining self * other (scalar)" "Redefining self * other (scalar)"
if isinstance(other, int) or isinstance(other, float): if isinstance(other, int) or isinstance(other, float):
self.lock.acquire() self.lock.acquire()
@@ -554,18 +568,19 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
tmp_y.append(numpy.array(self.y[i], dtype="float32") * other) tmp_y.append(numpy.array(self.y[i], dtype="float32") * other)
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
self._copy_metadata(r)
self.lock.release() self.lock.release()
return r return r
else: else:
raise ValueError(f"ValueError: Cannot multiply \"{other.__class__}\" to ADC-Result!") raise ValueError(f"ValueError: Cannot multiply \"{other.__class__}\" to ADC-Result!")
def __rmul__(self, other): def __rmul__(self, other: int | float) -> ADC_Result:
"Redefining other (scalar) * self" "Redefining other (scalar) * self"
return self.__mul__(other) return self.__mul__(other)
def __pow__(self, other): def __pow__(self, other: int | float) -> ADC_Result:
"Redefining self ** other (scalar)" "Redefining self ** other (scalar)"
if isinstance(other, int) or isinstance(other, float): if isinstance(other, int) or isinstance(other, float):
self.lock.acquire() self.lock.acquire()
@@ -575,12 +590,13 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
tmp_y.append(numpy.array(self.y[i], dtype="float32") ** other) tmp_y.append(numpy.array(self.y[i], dtype="float32") ** other)
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
self._copy_metadata(r)
self.lock.release() self.lock.release()
return r return r
else: else:
raise ValueError(f"ValueError: Cannot power raise \"{other.__class__}\" to ADC-Result!") raise ValueError(f"ValueError: Cannot power raise \"{other.__class__}\" to ADC-Result!")
def __truediv__(self, other): def __truediv__(self, other: int | float) -> ADC_Result:
"Redefining self / other (scalar)" "Redefining self / other (scalar)"
if isinstance(other, int) or isinstance(other, float): if isinstance(other, int) or isinstance(other, float):
self.lock.acquire() self.lock.acquire()
@@ -589,12 +605,13 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
tmp_y.append(numpy.array(self.y[i], dtype="float32") / other) tmp_y.append(numpy.array(self.y[i], dtype="float32") / other)
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
self._copy_metadata(r)
self.lock.release() self.lock.release()
return r return r
else: else:
raise ValueError(f"ValueError: Cannot divide \"{other.__class__}\" to ADC-Result!") raise ValueError(f"ValueError: Cannot divide \"{other.__class__}\" to ADC-Result!")
def __rtruediv__(self, other): def __rtruediv__(self, other: int | float) -> ADC_Result:
"Redefining other (scalar) / self" "Redefining other (scalar) / self"
if isinstance(other, int) or isinstance(other, float): if isinstance(other, int) or isinstance(other, float):
self.lock.acquire() self.lock.acquire()
@@ -602,45 +619,48 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
for i in range(self.get_number_of_channels()): for i in range(self.get_number_of_channels()):
tmp_y.append(other / numpy.array(self.y[i], dtype="float32")) tmp_y.append(other / numpy.array(self.y[i], dtype="float32"))
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
self._copy_metadata(r)
self.lock.release() self.lock.release()
return r return r
else: else:
raise ValueError(f"ValueError: Cannot divide \"{other.__class__}\" to ADC-Result!") raise ValueError(f"ValueError: Cannot divide \"{other.__class__}\" to ADC-Result!")
def __floordiv__(self, other): def __floordiv__(self, other: int) -> ADC_Result:
"Redefining self // other (scalar)" "Redefining self // other (scalar)"
if isinstance(other, float): if isinstance(other, float):
raise ValueError("ValueError: Cannot use floor division (//) on floats! Use \"/\" instead of \"//\"! ") raise ValueError("ValueError: Cannot use floor division (//) on floats! Use \"/\" instead of \"//\"! ")
if isinstance(other, int): if isinstance(other, int):
self.lock.acquire() self.lock.acquire()
tmp_y = [] tmp_y: list[numpy.ndarray] = []
for i in range(self.get_number_of_channels()): for i in range(self.get_number_of_channels()):
tmp_y.append(numpy.array(self.y[i], dtype="float32") // other) tmp_y.append(numpy.array(self.y[i], dtype="float32") // other)
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
self._copy_metadata(r)
self.lock.release() self.lock.release()
return r return r
else: else:
raise ValueError(f"ValueError: Cannot divide \"{other.__class__}\" to ADC-Result!") raise ValueError(f"ValueError: Cannot divide \"{other.__class__}\" to ADC-Result!")
def __rfloordiv__(self, other): def __rfloordiv__(self, other: int) -> ADC_Result:
"Redefining other (scalar) // self" "Redefining other (scalar) // self"
if isinstance(other, float): if isinstance(other, float):
raise ValueError("ValueError: Cannot use floor division (//) on floats! Use \"/\" instead of \"//\"! ") raise ValueError("ValueError: Cannot use floor division (//) on floats! Use \"/\" instead of \"//\"! ")
if isinstance(other, int): if isinstance(other, int):
self.lock.acquire() self.lock.acquire()
tmp_y = [] tmp_y: list[numpy.ndarray] = []
for i in range(self.get_number_of_channels()): for i in range(self.get_number_of_channels()):
tmp_y.append(other // numpy.array(self.y[i], dtype="float32")) tmp_y.append(other // numpy.array(self.y[i], dtype="float32"))
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
self._copy_metadata(r)
self.lock.release() self.lock.release()
return r return r
else: else:
raise ValueError(f"ValueError: Cannot divide \"{other.__class__}\" to ADC-Result!") raise ValueError(f"ValueError: Cannot divide \"{other.__class__}\" to ADC-Result!")
def __neg__(self): def __neg__(self) -> ADC_Result:
"Redefining -self" "Redefining -self"
self.lock.acquire() self.lock.acquire()
tmp_y = [] tmp_y = []
@@ -649,11 +669,12 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
tmp_y.append(numpy.array(-self.y[i])) tmp_y.append(numpy.array(-self.y[i]))
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
self._copy_metadata(r)
self.lock.release() self.lock.release()
return r return r
def read_from_hdf(hdf_node): def read_from_hdf(hdf_node: object) -> ADC_Result | None:
""" """
read accumulation data from HDF node and return it. read accumulation data from HDF node and return it.
""" """
@@ -703,6 +724,7 @@ def read_from_hdf(hdf_node):
adc.cont_data=True adc.cont_data=True
# now do the real data # now do the real data
adc_data=hdf_node.adc_data.read() adc_data=hdf_node.adc_data.read()
adc.set_nChannels(adc_data.shape[1])
adc.x=numpy.arange(adc_data.shape[0], dtype="float32")/adc.sampling_rate adc.x=numpy.arange(adc_data.shape[0], dtype="float32")/adc.sampling_rate
+114 -82
View File
@@ -25,6 +25,12 @@ import datetime # added by Oleg Petrov
import ctypes # added by Oleg Petrov import ctypes # added by Oleg Petrov
import struct # added by Oleg Petrov import struct # added by Oleg Petrov
import os # added by Oleg Petrov import os # added by Oleg Petrov
from typing import IO
_XArray = numpy.ndarray | list
_YChannels = list[numpy.ndarray]
_IndexList = list[tuple[int, int]]
_Description = dict[str, str]
class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath): class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
@@ -50,7 +56,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
x (list): The x-axis data points. x (list): The x-axis data points.
y (list): The y-axis data points. y (list): The y-axis data points.
""" """
def __init__(self, x = None, y = None, y_2 = None, n = None, index = None, sampl_freq = None, error = False): def __init__(self, x: _XArray | None = None, y: _YChannels | None = None, y_2: _YChannels | numpy.ndarray | None = None, n: int | None = None, index: _IndexList | None = None, sampl_freq: float | None = None, error: bool = False) -> None:
Errorable.__init__(self) Errorable.__init__(self)
Drawable.__init__(self) Drawable.__init__(self)
@@ -64,14 +70,15 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
self.is_clipped = False self.is_clipped = False
self.common_descriptions=None self.common_descriptions=None
self.time_period=[] self.time_period: list[datetime.datetime] | None = []
self.job_id = None # this does not make sense for an object accumulated from multiple job_ids self.job_id = None # this does not make sense for an object accumulated from multiple job_ids
self.job_ids = {} self.job_ids = {}
self.use_error = error self.use_error = error
if self.uses_statistics(): if self.uses_statistics():
if (y_2 is not None): if (y_2 is not None):
self.y_square = y_2 # y_square is always kept as a list of per-channel arrays
self.y_square: _YChannels = [numpy.asarray(ch) for ch in y_2]
elif (y_2 is None) : elif (y_2 is None) :
self.y_square = [] self.y_square = []
else: else:
@@ -86,12 +93,12 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
self.cont_data = False self.cont_data = False
self.index = [] self.index = []
self.x = [] self.x = numpy.array([], dtype="float32")
self.y = [] self.y = []
elif (x is not None) and (y is not None) and (index is not None) and (sampl_freq is not None) and (n is not None): elif (x is not None) and (y is not None) and (index is not None) and (sampl_freq is not None) and (n is not None):
self.x = x self.x = numpy.asarray(x)
self.y = y self.y = [numpy.asarray(ch) for ch in y]
self.sampling_rate = sampl_freq self.sampling_rate = sampl_freq
self.n = n self.n = n
@@ -102,7 +109,15 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
else: else:
raise ValueError("Wrong usage of __init__!") raise ValueError("Wrong usage of __init__!")
def get_accu_by_index(self, index: int): def _copy_provenance(self, r: "Accumulation") -> None:
r.is_clipped = self.is_clipped
r.job_id = self.job_id
r.job_ids = self.job_ids.copy()
r.time_period = self.time_period[:] if self.time_period is not None else None
if self.common_descriptions is not None:
r.common_descriptions = self.common_descriptions.copy()
def get_accu_by_index(self, index: int) -> "Accumulation":
self.lock.acquire() self.lock.acquire()
try: try:
start = self.index[index][0] start = self.index[index][0]
@@ -124,19 +139,19 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
r.y_square.append(self.y_square[i][start:end+1].copy()) r.y_square.append(self.y_square[i][start:end+1].copy())
if self.common_descriptions is not None: if self.common_descriptions is not None:
r.common_descriptions = self.common_descriptions.copy() r.common_descriptions = self.common_descriptions.copy()
r.time_period = self.time_period[:] r.time_period = self.time_period[:] if self.time_period is not None else None
r.job_ids = self.job_ids.copy() r.job_ids = self.job_ids.copy()
r.is_clipped = self.is_clipped r.is_clipped = self.is_clipped
self.lock.release() self.lock.release()
return r return r
def get_result_by_index(self, index): def get_result_by_index(self, index: int) -> "Accumulation":
return self.get_accu_by_index(index) return self.get_accu_by_index(index)
def get_Result_by_index(self, index): def get_Result_by_index(self, index: int) -> "Accumulation":
return self.get_accu_by_index(index) return self.get_accu_by_index(index)
def get_ysquare(self, channel): def get_ysquare(self, channel: int) -> numpy.ndarray | None:
if self.uses_statistics(): if self.uses_statistics():
try: try:
return self.y_square[channel] return self.y_square[channel]
@@ -164,15 +179,15 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
# external interface -------------------------------------------------------------------- # external interface --------------------------------------------------------------------
def get_yerr(self, channel): def get_yerr(self, channel: int) -> numpy.ndarray:
""" """
return error (std.dev/sqrt(n)) of mean return error (std.dev/sqrt(n)) of mean
""" """
if not self.contains_data():
return numpy.array([], dtype="float32")
if not self.uses_statistics(): if not self.uses_statistics():
return numpy.zeros((len(self.y[0]),),dtype="float32") return numpy.zeros((len(self.y[0]),),dtype="float32")
if not self.contains_data():
return []
self.lock.acquire() self.lock.acquire()
if self.n < 2: if self.n < 2:
@@ -188,13 +203,13 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
# sample standard deviation / sqrt(n) # sample standard deviation / sqrt(n)
return numpy.nan_to_num(numpy.sqrt(variance_over_n)) return numpy.nan_to_num(numpy.sqrt(variance_over_n))
def get_ydata(self, channel): def get_ydata(self, channel: int = 0) -> numpy.ndarray:
""" """
return mean data return mean data
""" """
if not self.contains_data(): if not self.contains_data():
return [] return numpy.array([], dtype="float32")
self.lock.acquire() self.lock.acquire()
try: try:
tmp_y = self.y[channel] / self.n tmp_y = self.y[channel] / self.n
@@ -206,7 +221,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
return tmp_y return tmp_y
def get_ymin(self): def get_ymin(self) -> float|int:
if not self.contains_data(): if not self.contains_data():
return 0 return 0
@@ -241,10 +256,10 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
self.lock.release() self.lock.release()
return max(tmp_max) return max(tmp_max)
def get_job_id(self) -> int: def get_job_id(self) -> int | None:
return self.job_id # modified by Oleg Petrov return self.job_id # modified by Oleg Petrov
def write_to_csv(self, destination=sys.stdout, delimiter=" "): def write_to_csv(self, destination: str | IO[str] = sys.stdout, delimiter: str = " ") -> None:
""" """
writes the data to a file. writes the data to a file.
destination can be a filehandle or a filename, default sys.stdout destination can be a filehandle or a filename, default sys.stdout
@@ -255,7 +270,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
else: else:
self._write_to_csv_dest(destination, delimiter) self._write_to_csv_dest(destination, delimiter)
def _write_to_csv_dest(self, dest, delimiter): def _write_to_csv_dest(self, dest: IO[str], delimiter: str) -> None:
dest.write("# accumulation %d\n"%self.n) dest.write("# accumulation %d\n"%self.n)
self.lock.acquire() self.lock.acquire()
try: try:
@@ -288,7 +303,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
self.lock.release() self.lock.release()
# ------------- added by Oleg Petrov, 14 Feb 2012 ---------------------- # ------------- added by Oleg Petrov, 14 Feb 2012 ----------------------
def write_to_simpson(self, destination=sys.stdout, delimiter=" ", frequency=100e6): def write_to_simpson(self, destination: str | IO[str] = sys.stdout, delimiter: str = " ", frequency: float = 100e6) -> None:
""" """
writes the data to a text file or sys.stdout in Simpson format, writes the data to a text file or sys.stdout in Simpson format,
for further processing with the NMRnotebook software; for further processing with the NMRnotebook software;
@@ -300,7 +315,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
else: else:
self._write_to_simpson_dest(destination, delimiter, frequency) self._write_to_simpson_dest(destination, delimiter, frequency)
def _write_to_simpson_dest(self, dest, delimiter, frequency): def _write_to_simpson_dest(self, dest: IO[str], delimiter: str, frequency: float) -> None:
self.lock.acquire() self.lock.acquire()
try: try:
xdata=self.get_xdata() xdata=self.get_xdata()
@@ -321,11 +336,11 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
self.lock.release() self.lock.release()
# ------------- added by Oleg Petrov, 10 Sep 2013 ----------------------- # ------------- added by Oleg Petrov, 10 Sep 2013 -----------------------
def write_to_tecmag(self, destination=sys.stdout, nrecords=1,\ def write_to_tecmag(self, destination = sys.stdout, nrecords: int = 1,\
frequency=100.,\ frequency: float = 100.,\
last_delay = 1.,\ last_delay: float = 1.,\
receiver_phase=0.,\ receiver_phase: float = 0.,\
nucleus='1H'): nucleus: str = '1H') -> None:
""" """
writes the data to a binary file in TecMag format; writes the data to a binary file in TecMag format;
destination can be a file object or a filename; destination can be a file object or a filename;
@@ -354,6 +369,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
base_freq = [frequency, frequency, 0., 0.] base_freq = [frequency, frequency, 0., 0.]
offset_freq = [0., 0., 0., 0.] offset_freq = [0., 0., 0., 0.]
ob_freq = [sum(x) for x in zip(base_freq, offset_freq)] ob_freq = [sum(x) for x in zip(base_freq, offset_freq)]
if self.time_period is None or len(self.time_period)==0:
raise ValueError("write_to_tecmag: no time information available")
date = self.time_period[0].strftime("%Y/%m/%d %H:%M:%S") date = self.time_period[0].strftime("%Y/%m/%d %H:%M:%S")
# data handling: # data handling:
@@ -448,7 +465,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
def write_to_hdf(self, hdffile, where, name, title, complib=None, complevel=None): def write_to_hdf(self, hdffile: tables.File, where: str, name: str, title: str, complib: str | None = None, complevel: int | None = None) -> None:
""" """
Write the accumulation data to an HDF5 file. Write the accumulation data to an HDF5 file.
@@ -609,7 +626,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
return tmp_string return tmp_string
def __add__(self, other): def __add__(self, other: "int | float | ADC_Result | Accumulation | None") -> "Accumulation":
""" """
Redefining self + other Redefining self + other
We define this for different types of other: We define this for different types of other:
@@ -637,12 +654,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = self.n, index = self.index, sampl_freq = self.sampling_rate, error = self.use_error) r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = self.n, index = self.index, sampl_freq = self.sampling_rate, error = self.use_error)
else: else:
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = self.n, index = self.index, sampl_freq = self.sampling_rate, error = self.use_error) r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = self.n, index = self.index, sampl_freq = self.sampling_rate, error = self.use_error)
r.is_clipped = self.is_clipped self._copy_provenance(r)
r.job_id = self.job_id
r.job_ids = self.job_ids.copy()
r.time_period = self.time_period[:] if self.time_period is not None else None
if self.common_descriptions is not None:
r.common_descriptions = self.common_descriptions.copy()
self.lock.release() self.lock.release()
return r return r
@@ -669,7 +681,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
r = Accumulation(x = numpy.array(other.x, dtype="float32"), y = tmp_y, index = other.index, sampl_freq = other.sampling_rate, n = 1, error = False) r = Accumulation(x = numpy.array(other.x, dtype="float32"), y = tmp_y, index = other.index, sampl_freq = other.sampling_rate, n = 1, error = False)
if hasattr(other, "is_clipped"): if hasattr(other, "is_clipped"):
r.is_clipped = other.is_clipped r.is_clipped = other.is_clipped
r.time_period=[other.job_date,other.job_date] r.time_period=[other.job_date,other.job_date] if other.job_date is not None else None
r.job_id = other.job_id r.job_id = other.job_id
r.common_descriptions=other.description.copy() r.common_descriptions=other.description.copy()
r.job_ids[other.job_id] = other.get_job_date() r.job_ids[other.job_id] = other.get_job_date()
@@ -678,8 +690,6 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
# Other and self not empty (self + other ADC_Result) # Other and self not empty (self + other ADC_Result)
else: else:
self.lock.acquire()
if self.sampling_rate != other.get_sampling_rate(): if self.sampling_rate != other.get_sampling_rate():
raise ValueError("Accumulation: You cant add ADC_Results with different sampling-rates") raise ValueError("Accumulation: You cant add ADC_Results with different sampling-rates")
if len(self.y[0]) != len(other): if len(self.y[0]) != len(other):
@@ -690,6 +700,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
if self.index[i] != other.get_index_bounds(i): if self.index[i] != other.get_index_bounds(i):
raise ValueError("Accumulation: You cant add ADC_Results with different indexing") raise ValueError("Accumulation: You cant add ADC_Results with different indexing")
self.lock.acquire()
tmp_y = [] tmp_y = []
tmp_ysquare = [] tmp_ysquare = []
@@ -703,10 +715,13 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
else: else:
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = self.n + 1, index = self.index, sampl_freq = self.sampling_rate, error = False) r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = self.n + 1, index = self.index, sampl_freq = self.sampling_rate, error = False)
r.is_clipped = self.is_clipped or (hasattr(other, "is_clipped") and other.is_clipped) r.is_clipped = self.is_clipped or (hasattr(other, "is_clipped") and other.is_clipped)
r.time_period=[min(self.time_period[0],other.job_date), if self.time_period and other.job_date is not None:
max(self.time_period[1],other.job_date)] r.time_period=[min(self.time_period[0],other.job_date),
max(self.time_period[1],other.job_date)]
else:
r.time_period=None
r.job_id = other.job_id r.job_id = other.job_id
r.job_ids = self.job_ids r.job_ids = self.job_ids.copy()
r.job_ids[other.job_id] = other.get_job_date() r.job_ids[other.job_id] = other.get_job_date()
if self.common_descriptions is not None: if self.common_descriptions is not None:
@@ -725,33 +740,29 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
# Self empty (copy) # Self empty (copy)
if not self.contains_data(): if not self.contains_data():
if self.uses_statistics() and not other.uses_statistics():
raise ValueError("Accumulation: You cant add non-error accumulations to accumulations with error")
self.lock.acquire()
tmp_y = [] tmp_y = []
tmp_ysquare = [] tmp_ysquare = []
self.lock.acquire() for i in range(other.get_number_of_channels()):
tmp_y.append(numpy.array(other.y[i], dtype="float32"))
if other.uses_statistics():
tmp_ysquare.append(numpy.array(other.y_square[i], dtype="float32"))
if self.uses_statistics(): if self.uses_statistics():
r = Accumulation(x = numpy.array(other.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = other.n, index = other.index, sampl_freq = other.sampling_rate, error = True) r = Accumulation(x = numpy.array(other.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = other.n, index = other.index, sampl_freq = other.sampling_rate, error = True)
else: else:
r = Accumulation(x = numpy.array(other.x, dtype="float32"), y = tmp_y, n = other.n, index = other.index, sampl_freq = other.sampling_rate, error = False) r = Accumulation(x = numpy.array(other.x, dtype="float32"), y = tmp_y, n = other.n, index = other.index, sampl_freq = other.sampling_rate, error = False)
for i in range(other.get_number_of_channels()): other._copy_provenance(r)
tmp_y.append(other.y[i])
tmp_ysquare.append(other.y_square[i])
r.time_period=other.time_period[:]
r.job_id = other.job_id
r.job_ids = other.job_ids
if other.common_descriptions is not None:
r.common_descriptions=other.common_descriptions.copy()
else:
r.common_descriptions=None
self.lock.release() self.lock.release()
return r return r
# Other and self not empty (self + other) # Other and self not empty (self + other)
else: else:
self.lock.acquire()
if self.sampling_rate != other.get_sampling_rate(): if self.sampling_rate != other.get_sampling_rate():
raise ValueError("Accumulation: You cant add accumulations with different sampling-rates") raise ValueError("Accumulation: You cant add accumulations with different sampling-rates")
if len(self.y[0]) != len(other): if len(self.y[0]) != len(other):
@@ -764,6 +775,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
if self.uses_statistics() and not other.uses_statistics(): if self.uses_statistics() and not other.uses_statistics():
raise ValueError("Accumulation: You cant add non-error accumulations to accumulations with error") raise ValueError("Accumulation: You cant add non-error accumulations to accumulations with error")
self.lock.acquire()
tmp_y = [] tmp_y = []
tmp_ysquare = [] tmp_ysquare = []
@@ -777,9 +790,13 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
else: else:
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = other.n + self.n, index = self.index, sampl_freq = self.sampling_rate, error = False) r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = other.n + self.n, index = self.index, sampl_freq = self.sampling_rate, error = False)
r.is_clipped = self.is_clipped or (hasattr(other, "is_clipped") and other.is_clipped) r.is_clipped = self.is_clipped or (hasattr(other, "is_clipped") and other.is_clipped)
r.time_period=[min(self.time_period[0],other.time_period[0]), if self.time_period and other.time_period:
max(self.time_period[1],other.time_period[1])] r.time_period=[min(self.time_period[0],other.time_period[0]),
max(self.time_period[1],other.time_period[1])]
else:
r.time_period=None
r.job_id = other.job_id r.job_id = other.job_id
r.job_ids = self.job_ids.copy()
for i in other.job_ids: for i in other.job_ids:
r.job_ids[i] = other.job_ids[i] r.job_ids[i] = other.job_ids[i]
r.common_descriptions={} r.common_descriptions={}
@@ -793,22 +810,28 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
return r return r
def __radd__(self, other): elif other is None:
# Convenience: ignore add of None
return self + 0
else:
raise ValueError("can not add "+repr(type(other))+" to Accumulation")
def __radd__(self, other: "int | float | ADC_Result | Accumulation | None") -> "Accumulation":
"""Redefining other + self""" """Redefining other + self"""
return self.__add__(other) return self.__add__(other)
def __sub__(self, other): def __sub__(self, other: "int | float | ADC_Result | Accumulation") -> "Accumulation":
"""Redefining self - other""" """Redefining self - other"""
return self.__add__(-other) return self.__add__(-other)
def __rsub__(self, other): def __rsub__(self, other: "int | float | ADC_Result | Accumulation") -> "Accumulation":
"""Redefining other - self""" """Redefining other - self"""
return (-self) + other return (-self) + other
def __mul__(self, other): def __mul__(self, other: int | float) -> "Accumulation":
"""Redefining self * other (scalar)""" """Redefining self * other (scalar)"""
if isinstance(other, (int, float)): if isinstance(other, (int, float)):
if not self.contains_data(): if not self.contains_data():
@@ -835,11 +858,11 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
else: else:
raise ValueError(f"ValueError: Cannot multiply \"{other.__class__}\" to Accumulation!") raise ValueError(f"ValueError: Cannot multiply \"{other.__class__}\" to Accumulation!")
def __rmul__(self, other): def __rmul__(self, other: int | float) -> "Accumulation":
"""Redefining other (scalar) * self""" """Redefining other (scalar) * self"""
return self.__mul__(other) return self.__mul__(other)
def __imul__(self, other): def __imul__(self, other: int | float) -> "Accumulation":
"""Redefining self *= other (scalar)""" """Redefining self *= other (scalar)"""
if isinstance(other, (int, float)): if isinstance(other, (int, float)):
if not self.contains_data(): if not self.contains_data():
@@ -854,7 +877,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
else: else:
raise ValueError(f"ValueError: Cannot multiply \"{other.__class__}\" to Accumulation!") raise ValueError(f"ValueError: Cannot multiply \"{other.__class__}\" to Accumulation!")
def __truediv__(self, other): def __truediv__(self, other: int | float) -> "Accumulation":
"""Redefining self / other (scalar)""" """Redefining self / other (scalar)"""
if isinstance(other, (int, float)): if isinstance(other, (int, float)):
if not self.contains_data(): if not self.contains_data():
@@ -881,7 +904,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
else: else:
raise ValueError(f"ValueError: Cannot divide \"{other.__class__}\" to Accumulation!") raise ValueError(f"ValueError: Cannot divide \"{other.__class__}\" to Accumulation!")
def __itruediv__(self, other): def __itruediv__(self, other: int | float) -> "Accumulation":
"""Redefining self /= other (scalar)""" """Redefining self /= other (scalar)"""
if isinstance(other, (int, float)): if isinstance(other, (int, float)):
if not self.contains_data(): if not self.contains_data():
@@ -896,7 +919,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
else: else:
raise ValueError(f"ValueError: Cannot divide \"{other.__class__}\" to Accumulation!") raise ValueError(f"ValueError: Cannot divide \"{other.__class__}\" to Accumulation!")
def __floordiv__(self, other): def __floordiv__(self, other: int) -> "Accumulation":
"""Redefining self // other (scalar)""" """Redefining self // other (scalar)"""
if isinstance(other, float): if isinstance(other, float):
raise ValueError("ValueError: Cannot use floor division (//) on floats! Use \"/\" instead of \"//\"! ") raise ValueError("ValueError: Cannot use floor division (//) on floats! Use \"/\" instead of \"//\"! ")
@@ -905,7 +928,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
if not self.contains_data(): if not self.contains_data():
raise ValueError("Accumulation: You cant divide an empty accumulation") raise ValueError("Accumulation: You cant divide an empty accumulation")
self.lock.acquire() self.lock.acquire()
tmp_y = [] tmp_y: list[numpy.ndarray] = []
for i in range(self.get_number_of_channels()): for i in range(self.get_number_of_channels()):
tmp_y.append(self.y[i] // other) tmp_y.append(self.y[i] // other)
@@ -921,7 +944,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
else: else:
raise ValueError(f"ValueError: Cannot divide \"{other.__class__}\" to Accumulation!") raise ValueError(f"ValueError: Cannot divide \"{other.__class__}\" to Accumulation!")
def __ifloordiv__(self, other): def __ifloordiv__(self, other: int) -> "Accumulation":
"""Redefining self //= other (scalar)""" """Redefining self //= other (scalar)"""
if isinstance(other, float): if isinstance(other, float):
raise ValueError("ValueError: Cannot use floor division (//) on floats! Use \"/\" instead of \"//\"! ") raise ValueError("ValueError: Cannot use floor division (//) on floats! Use \"/\" instead of \"//\"! ")
@@ -942,7 +965,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
raise ValueError(f"ValueError: Cannot divide \"{other.__class__}\" to Accumulation!") raise ValueError(f"ValueError: Cannot divide \"{other.__class__}\" to Accumulation!")
def __iadd__(self, other): def __iadd__(self, other: "int | float | ADC_Result | Accumulation | None") -> "Accumulation":
"""Redefining self += other""" """Redefining self += other"""
# Float or int # Float or int
if isinstance(other, int) or isinstance(other, float): if isinstance(other, int) or isinstance(other, float):
@@ -982,7 +1005,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
if hasattr(other, "is_clipped"): if hasattr(other, "is_clipped"):
self.is_clipped = other.is_clipped self.is_clipped = other.is_clipped
self.time_period=[other.job_date,other.job_date] self.time_period=[other.job_date,other.job_date] if other.job_date is not None else None
self.job_id = other.job_id # added by Oleg Petrov self.job_id = other.job_id # added by Oleg Petrov
self.job_ids[other.job_id] = other.get_job_date() self.job_ids[other.job_id] = other.get_job_date()
self.common_descriptions=other.description.copy() self.common_descriptions=other.description.copy()
@@ -992,8 +1015,6 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
# Other and self not empty (self + other/ADC_Result) # Other and self not empty (self + other/ADC_Result)
else: else:
self.lock.acquire()
if self.sampling_rate != other.get_sampling_rate(): if self.sampling_rate != other.get_sampling_rate():
raise ValueError("Accumulation: You can't add ADC-Results with different sampling-rates") raise ValueError("Accumulation: You can't add ADC-Results with different sampling-rates")
if len(self.y[0]) != len(other): if len(self.y[0]) != len(other):
@@ -1004,6 +1025,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
if self.index[i] != other.get_index_bounds(i): if self.index[i] != other.get_index_bounds(i):
raise ValueError("Accumulation: You can't add ADC-Results with different indexing") raise ValueError("Accumulation: You can't add ADC-Results with different indexing")
self.lock.acquire()
for i in range(self.get_number_of_channels()): for i in range(self.get_number_of_channels()):
self.y[i] += other.y[i] self.y[i] += other.y[i]
if self.uses_statistics(): if self.uses_statistics():
@@ -1012,8 +1035,11 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
self.n += 1 self.n += 1
if hasattr(other, "is_clipped"): if hasattr(other, "is_clipped"):
self.is_clipped = self.is_clipped or other.is_clipped self.is_clipped = self.is_clipped or other.is_clipped
self.time_period=[min(self.time_period[0],other.job_date), if self.time_period and other.job_date is not None:
max(self.time_period[1],other.job_date)] self.time_period=[min(self.time_period[0],other.job_date),
max(self.time_period[1],other.job_date)]
else:
self.time_period=None
self.job_id = other.job_id self.job_id = other.job_id
@@ -1053,7 +1079,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
if hasattr(other, "is_clipped"): if hasattr(other, "is_clipped"):
self.is_clipped = other.is_clipped self.is_clipped = other.is_clipped
self.common_descriptions=other.common_descriptions.copy() self.common_descriptions=other.common_descriptions.copy()
self.time_period=other.time_period[:] self.time_period=other.time_period[:] if other.time_period is not None else None
self.job_id = other.job_id # added by Oleg Petrov self.job_id = other.job_id # added by Oleg Petrov
self.job_ids.update(other.job_ids) self.job_ids.update(other.job_ids)
self.lock.release() self.lock.release()
@@ -1062,7 +1088,6 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
# Other and self not empty (self + other) # Other and self not empty (self + other)
else: else:
self.lock.acquire()
if self.sampling_rate != other.get_sampling_rate(): if self.sampling_rate != other.get_sampling_rate():
raise ValueError("Accumulation: You cant add accumulations with different sampling-rates") raise ValueError("Accumulation: You cant add accumulations with different sampling-rates")
if len(self.y[0]) != len(other): if len(self.y[0]) != len(other):
@@ -1074,6 +1099,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
raise ValueError("Accumulation: You cant add accumulations with different indexing") raise ValueError("Accumulation: You cant add accumulations with different indexing")
if self.uses_statistics() and not other.uses_statistics(): if self.uses_statistics() and not other.uses_statistics():
raise ValueError("Accumulation: You cant add non-error accumulations to accumulations with error") raise ValueError("Accumulation: You cant add non-error accumulations to accumulations with error")
self.lock.acquire()
for i in range(self.get_number_of_channels()): for i in range(self.get_number_of_channels()):
self.y[i] += other.y[i] self.y[i] += other.y[i]
@@ -1083,8 +1110,11 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
self.n += other.n self.n += other.n
if hasattr(other, "is_clipped"): if hasattr(other, "is_clipped"):
self.is_clipped = self.is_clipped or other.is_clipped self.is_clipped = self.is_clipped or other.is_clipped
self.time_period=[min(self.time_period[0],other.time_period[0]), if self.time_period and other.time_period:
max(self.time_period[1],other.time_period[1])] self.time_period=[min(self.time_period[0],other.time_period[0]),
max(self.time_period[1],other.time_period[1])]
else:
self.time_period=None
self.job_id = other.job_id self.job_id = other.job_id
self.job_ids.update(other.job_ids) self.job_ids.update(other.job_ids)
# Removes mismatched common description keys # Removes mismatched common description keys
@@ -1114,16 +1144,16 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
raise ValueError("can not add "+repr(type(other))+" to Accumulation") raise ValueError("can not add "+repr(type(other))+" to Accumulation")
def __isub__(self, other): def __isub__(self, other: "int | float | ADC_Result | Accumulation") -> "Accumulation":
"""Redefining self -= other""" """Redefining self -= other"""
return self.__iadd__(-other) return self.__iadd__(-other)
def __neg__(self): def __neg__(self) -> "Accumulation":
"""Redefining -self""" """Redefining -self"""
if not self.contains_data(): if not self.contains_data():
return raise ValueError("Accumulation: You cant negate an empty accumulation")
tmp_y = [] tmp_y = []
@@ -1132,14 +1162,16 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
tmp_y.append(numpy.array(-self.y[i], dtype="float32")) tmp_y.append(numpy.array(-self.y[i], dtype="float32"))
if self.uses_statistics(): if self.uses_statistics():
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, y_2 = numpy.array(self.y_square), n = self.n, index = self.index, sampl_freq = self.sampling_rate, error = True) tmp_y_square = [numpy.array(sq) for sq in self.y_square]
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, y_2 = tmp_y_square, n = self.n, index = self.index, sampl_freq = self.sampling_rate, error = True)
else: else:
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = self.n, index = self.index, sampl_freq = self.sampling_rate, error = False) r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = self.n, index = self.index, sampl_freq = self.sampling_rate, error = False)
self._copy_provenance(r)
self.lock.release() self.lock.release()
return r return r
def read_from_hdf(hdf_node): def read_from_hdf(hdf_node: object) -> "Accumulation | None":
""" """
read accumulation data from HDF node and return it. read accumulation data from HDF node and return it.
""" """
+4
View File
@@ -17,8 +17,12 @@ class Config_Result(Resultable):
if config is None: if config is None:
self.config = { } self.config = { }
else:
self.config = config
if desc is None: if desc is None:
self.description = { } self.description = { }
else:
self.description = desc
self.job_id = job_id self.job_id = job_id
self.job_date = job_date self.job_date = job_date
+4 -4
View File
@@ -19,10 +19,6 @@ class DamarisFFT:
:param float stop: end of clipping in s :param float stop: end of clipping in s
""" """
# check if start/stop order is properly
if start > stop:
start, stop = stop, start
# do nothing if one uses clip as a "placeholder" # do nothing if one uses clip as a "placeholder"
if start is None and stop is None: if start is None and stop is None:
return self return self
@@ -32,6 +28,10 @@ class DamarisFFT:
if stop is None: if stop is None:
stop = self.x[ -1 ] stop = self.x[ -1 ]
# check if start/stop order is properly
if start > stop:
start, stop = stop, start
# check if data is fft which changes the start/stop units # check if data is fft which changes the start/stop units
if self.xlabel == "Frequency / Hz": if self.xlabel == "Frequency / Hz":
start = self.x.size * (0.5 + start / self.sampling_rate) start = self.x.size * (0.5 + start / self.sampling_rate)
+6
View File
@@ -394,6 +394,12 @@ class DataPool(collections.abc.MutableMapping):
if obj is not None: if obj is not None:
self[full_path] = obj self[full_path] = obj
elif damaris_type == "FitResult":
from damaris.data.FitResult import read_from_hdf
obj = read_from_hdf(node)
if obj is not None:
self[full_path] = obj
elif damaris_type == "TemperatureResult": elif damaris_type == "TemperatureResult":
from damaris.data.Temperature import read_from_hdf from damaris.data.Temperature import read_from_hdf
obj = read_from_hdf(node) obj = read_from_hdf(node)
+74 -53
View File
@@ -1,5 +1,7 @@
# -*- coding: iso-8859-1 -*- # -*- coding: iso-8859-1 -*-
from __future__ import annotations
############################################################################# #############################################################################
# # # #
# Name: Class Drawable # # Name: Class Drawable #
@@ -8,23 +10,29 @@
# # # #
############################################################################# #############################################################################
import numpy
class Drawable: class Drawable:
def __init__(self): def __init__(self):
# Will be set correctly in one of the subclasses # Will be set correctly in one of the subclasses
self.x = [] # x is always a numpy.ndarray, y is always a list of numpy.ndarray
self.y = [] # (one array per channel); empty data is represented by zero-length
# arrays of the same types
self.x: numpy.ndarray = numpy.array([], dtype="float64")
self.y: list[numpy.ndarray] = []
self.styles = { } self.styles: dict = { }
self.xlabel = None self.xlabel: str | None = None
self.ylabel = None self.ylabel: str | None = None
self.title = None self.title: str | None = None
self.legend = { } self.legend: dict = { }
self.text = {} self.text: dict = {}
self.xmin = 0 self.xmin = 0
self.xmax = 0 self.xmax = 0
@@ -32,12 +40,12 @@ class Drawable:
self.ymax = 0 self.ymax = 0
def get_xdata(self): def get_xdata(self) -> numpy.ndarray:
"Returns a reference to the x-Plotdata (array)" "Returns a reference to the x-Plotdata (array)"
return self.x return self.x
def set_xdata(self, pos, value): def set_xdata(self, pos: int, value: float) -> None:
"Sets a point in x" "Sets a point in x"
try: try:
self.x[pos] = value self.x[pos] = value
@@ -45,7 +53,7 @@ class Drawable:
raise raise
def get_ydata(self, channel): def get_ydata(self, channel: int = 0) -> numpy.ndarray:
"Returns the y-Plotdata of channel n (array)" "Returns the y-Plotdata of channel n (array)"
try: try:
return self.y[channel] return self.y[channel]
@@ -53,51 +61,52 @@ class Drawable:
raise raise
def set_ydata(self, channel, pos, value): def set_ydata(self, channel: int, pos: int, value: float) -> None:
"Sets a point in y" "Sets a point in y"
try: try:
self.y[channel][pos] = value self.y[channel][pos] = value
except: except:
raise raise
def get_number_of_channels(self):
def get_number_of_channels(self) -> int:
"Returns the number of channels in y" "Returns the number of channels in y"
return len(self.y) return len(self.y)
def get_style(self): def get_style(self) -> dict:
"Returns a reference to plot-styles (dictionary)" "Returns a reference to plot-styles (dictionary)"
return self.styles return self.styles
def set_style(self, channel, value): def set_style(self, channel: int | str, value: object) -> None:
"Sets a channel to a certain plot-style" "Sets a channel to a certain plot-style"
if channel in self.styles: if channel in self.styles:
print("Drawable Warning: Style key \"%s\" will be overwritten with \"%s\"" % (str(channel), str(value))) print("Drawable Warning: Style key \"%s\" will be overwritten with \"%s\"" % (str(channel), str(value)))
self.styles[channel] = str(value) self.styles[channel] = str(value)
def get_xlabel(self): def get_xlabel(self) -> str | None:
"Returns the label for the x-axis" "Returns the label for the x-axis"
return self.xlabel return self.xlabel
def set_xlabel(self, label): def set_xlabel(self, label: object) -> None:
"Sets the label for the x-axis" "Sets the label for the x-axis"
self.xlabel = str(label) self.xlabel = str(label)
def get_ylabel(self): def get_ylabel(self) -> str | None:
"Gets the label for the y-axis" "Gets the label for the y-axis"
return self.ylabel return self.ylabel
def set_ylabel(self, label): def set_ylabel(self, label: object) -> None:
"Sets the label for the y-axis" "Sets the label for the y-axis"
self.ylabel = str(label) self.ylabel = str(label)
def get_text(self, index): def get_text(self, index: int | str) -> str | None:
"Returns labels to be plotted (List)" "Returns labels to be plotted (List)"
if index in self.text: if index in self.text:
return self.text[index] return self.text[index]
@@ -105,84 +114,96 @@ class Drawable:
return None return None
def set_text(self, index, text): def set_text(self, index: int | str, text: object) -> None:
"Sets labels to be plotted " "Sets labels to be plotted "
self.text[index] = str(text) self.text[index] = str(text)
def get_title(self): def get_title(self) -> str | None:
"Returns the title of the plot" "Returns the title of the plot"
return self.title return self.title
def set_title(self, title): def set_title(self, title: object) -> None:
"Sets the title of the plot" "Sets the title of the plot"
self.title = str(title) self.title = str(title)
def get_legend(self): def get_legend(self) -> dict:
"Returns the legend of the plot (Dictionary)" "Returns the legend of the plot (Dictionary)"
return self.legend return self.legend
def set_legend(self, channel, value): def set_legend(self, channel: int | str, value: object) -> None:
"Sets the legend of the plot" "Sets the legend of the plot"
if channel in self.legend: if channel in self.legend:
print("Drawable Warning: Legend key \"%s\" will be overwritten with \"%s\"" % (str(channel), str(value))) print("Drawable Warning: Legend key \"%s\" will be overwritten with \"%s\"" % (str(channel), str(value)))
self.legend[channel] = str(value) self.legend[channel] = str(value)
def get_xmin(self):
def get_xmin(self) -> float:
"Returns minimun of x" "Returns minimun of x"
if self.x.size == 0:
return 0
return self.x.min() return self.x.min()
def get_xminpos(self):
def get_xminpos(self) -> float:
"Returns smallest positive value of x" "Returns smallest positive value of x"
mask = self.x > 0 mask = self.x > 0
if not numpy.any(mask):
return 0
return self.x[mask].min() return self.x[mask].min()
def set_xmin(self, xmin):
def set_xmin(self, xmin: float) -> None:
"Sets minimum of x" "Sets minimum of x"
self.xmin = xmin self.xmin = xmin
def get_xmax(self):
def get_xmax(self) -> float:
"Returns maximum of x" "Returns maximum of x"
if self.x.size == 0:
return 0
return self.x.max() return self.x.max()
def set_xmax(self, xmax):
def set_xmax(self, xmax: float) -> None:
"Sets maximum of x" "Sets maximum of x"
self.xmax = xmax self.xmax = xmax
def get_ymin(self):
def get_ymin(self) -> float | int:
"Returns minimum of y" "Returns minimum of y"
if isinstance(self.y, list): if len(self.y) == 0:
return min([yarr.min() for yarr in self.y]) return 0
else: return min([yarr.min() for yarr in self.y])
return self.y.min()
def get_yminpos(self):
def get_yminpos(self) -> float | int:
"Returns smallest positive value of y" "Returns smallest positive value of y"
if isinstance(self.y, list): ymins = []
ymins = [] for ys in self.y:
for ys in self.y: mask = ys > 0
mask = ys > 0 if numpy.any(mask):
ymins.append(ys[mask].min()) ymins.append(ys[mask].min())
ymin = min(ymins) if len(ymins) == 0:
else: return 0
mask = self.y > 0 return min(ymins)
ymin = self.y[mask].min()
return ymin
def set_ymin(self, ymin):
def set_ymin(self, ymin: float) -> None:
"Sets minimum of y" "Sets minimum of y"
self.ymin = ymin self.ymin = ymin
def get_ymax(self):
"Returns maximimum of y"
if isinstance(self.y, list):
return max([yarr.max() for yarr in self.y])
else:
return self.y.max()
def set_ymax(self, ymax): def get_ymax(self) -> float | int:
"Returns maximimum of y"
if len(self.y) == 0:
return 0
return max([yarr.max() for yarr in self.y])
def set_ymax(self, ymax: float) -> None:
"Sets maximum of y" "Sets maximum of y"
self.ymax = ymax self.ymax = ymax
+4 -3
View File
@@ -2,6 +2,7 @@
from .Resultable import Resultable from .Resultable import Resultable
from .Drawable import Drawable from .Drawable import Drawable
import numpy
############################################################################# #############################################################################
# # # #
@@ -46,12 +47,12 @@ class Error_Result(Resultable, Drawable):
return False return False
# Nothing to plot # Nothing to plot
def get_ydata(self): def get_ydata(self, channel=0):
return [0.0] return numpy.array([0.0])
# Nothing to plot # Nothing to plot
def get_xdata(self): def get_xdata(self):
return [0.0] return numpy.array([0.0])
#overload of operators und built-ins ------------------------------------------------------- #overload of operators und built-ins -------------------------------------------------------
+127
View File
@@ -0,0 +1,127 @@
# fit curve with x-values independent of the measurement data points
import numpy
import tables
from . import Drawable
class FitResult(Drawable.Drawable):
"""
plottable fit curve belonging to a MeasurementResult
In contrast to MeasurementResult, the x-values are a plain array
and are independent of the measured data points, so a fit can be
sampled on a much denser x-grid for a smooth plot. The fit is
stored in the data pool under its own key, e.g. as a child of the
measurement ("Magnetization(tau)/fit"), and is drawn by the GUI
as an overlay on the measurement it belongs to.
"""
def __init__(self, x, y, title=None, source=None, y_err=None, params=None):
"""
x: array-like of x-values (independent of the measurement)
y: array-like of y-values, same length as x
title: plot title of the fit
source: data pool key of the MeasurementResult this fit belongs to
y_err: optional array-like of y errors, same length as x
params: optional dictionary of fit parameters, shown in the plot
"""
Drawable.Drawable.__init__(self)
self.x = numpy.array(x, dtype="float64").reshape(-1)
self.y = [numpy.array(y, dtype="float64").reshape(-1)]
if y_err is not None:
self.y_err = numpy.array(y_err, dtype="float64").reshape(-1)
else:
self.y_err = None
self.title = title
self.source = source
self.params = dict(params) if params is not None else {}
def get_lineplotdata(self):
"""
x and y data for plotting the fit curve
"""
return [self.x, self.y[0]]
def uses_statistics(self):
"""
drawable interface method returns False
"""
return False
def write_to_hdf(self, hdffile, where, name, title, complib=None, complevel=None):
h5_table_format = {
"x": tables.Float64Col(),
"y": tables.Float64Col()
}
if self.y_err is not None:
h5_table_format["y_err"] = tables.Float64Col()
filter = None
if complib is not None:
if complevel is None:
complevel = 9
filter = tables.Filters(complevel=complevel, complib=complib, shuffle=1)
fit_table = hdffile.create_table(where=where, name=name,
description=h5_table_format,
title=title,
filters=filter,
expectedrows=self.x.shape[0])
fit_table.flavor = "numpy"
fit_table.attrs.damaris_type = "FitResult"
fit_table.attrs.quantity_name = self.get_title()
if self.xlabel is not None:
fit_table.attrs.xlabel = self.xlabel
if self.ylabel is not None:
fit_table.attrs.ylabel = self.ylabel
if self.source is not None:
fit_table.attrs.source = self.source
for (key, value) in self.params.items():
if isinstance(value, (int, float)) and not isinstance(value, bool):
fit_table.attrs.__setattr__("param_" + str(key), float(value))
else:
fit_table.attrs.__setattr__("param_" + str(key), str(value))
row = fit_table.row
has_y_err = self.y_err is not None
for i in range(self.x.shape[0]):
row["x"] = self.x[i]
row["y"] = self.y[0][i]
if has_y_err:
row["y_err"] = self.y_err[i]
row.append()
fit_table.flush()
def read_from_hdf(hdf_node):
"""
reads a FitResult object from the hdf_node
or None if the node is not suitable
"""
if not isinstance(hdf_node, tables.Table):
return None
if hdf_node._v_attrs.damaris_type != "FitResult":
return None
xdata = hdf_node.col("x")
ydata = hdf_node.col("y")
if "y_err" in hdf_node.colnames:
y_err = hdf_node.col("y_err")
else:
y_err = None
fr = FitResult(xdata, ydata,
title=getattr(hdf_node._v_attrs, "quantity_name", None),
source=getattr(hdf_node._v_attrs, "source", None),
y_err=y_err)
fr.xlabel = getattr(hdf_node._v_attrs, "xlabel", None)
fr.ylabel = getattr(hdf_node._v_attrs, "ylabel", None)
params = {}
for attr_name in hdf_node._v_attrs._f_list("all"):
if attr_name.startswith("param_"):
params[attr_name[6:]] = getattr(hdf_node._v_attrs, attr_name)
fr.params = params
return fr
+49 -50
View File
@@ -4,6 +4,7 @@ import sys
import tables import tables
import numpy import numpy
import collections import collections
from typing import IO
from . import Drawable from . import Drawable
## provide gaussian statistics for a series of measured data points ## provide gaussian statistics for a series of measured data points
@@ -12,7 +13,7 @@ from . import Drawable
# internally it keeps the sum, the sum of squares and the number of data points # internally it keeps the sum, the sum of squares and the number of data points
class AccumulatedValue: class AccumulatedValue:
def __init__(self, mean=None, mean_err=None, n=None): def __init__(self, mean: float | None = None, mean_err: float | None = None, n: int | None = None):
""" """
one value with std. deviation one value with std. deviation
can be initialized by: can be initialized by:
@@ -42,7 +43,7 @@ class AccumulatedValue:
self.y=float(mean)*self.n self.y=float(mean)*self.n
self.y2=float(mean_err)**2*n*(n-1.0)+float(mean)**2*n self.y2=float(mean_err)**2*n*(n-1.0)+float(mean)**2*n
def __add__(self,y): def __add__(self, y: "AccumulatedValue | float") -> "AccumulatedValue":
new_one=AccumulatedValue() new_one=AccumulatedValue()
if isinstance(y, AccumulatedValue): if isinstance(y, AccumulatedValue):
new_one.y=self.y+y.y new_one.y=self.y+y.y
@@ -54,7 +55,7 @@ class AccumulatedValue:
new_one.n=self.n+1 new_one.n=self.n+1
return new_one return new_one
def __iadd__(self,y): def __iadd__(self, y: "AccumulatedValue | float") -> "AccumulatedValue":
if isinstance(y, AccumulatedValue): if isinstance(y, AccumulatedValue):
self.y+=y.y self.y+=y.y
self.y2+=y.y2 self.y2+=y.y2
@@ -65,14 +66,14 @@ class AccumulatedValue:
self.n+=1 self.n+=1
return self return self
def copy(self): def copy(self) -> "AccumulatedValue":
a=AccumulatedValue() a=AccumulatedValue()
a.y=self.y a.y=self.y
a.y2=self.y2 a.y2=self.y2
a.n=self.n a.n=self.n
return a return a
def mean(self): def mean(self) -> float | None:
""" """
returns the mean of all added/accumulated values returns the mean of all added/accumulated values
""" """
@@ -81,7 +82,7 @@ class AccumulatedValue:
else: else:
return self.y/self.n return self.y/self.n
def sigma(self): def sigma(self) -> float | None:
""" """
returns the standard deviation added/accumulated values returns the standard deviation added/accumulated values
""" """
@@ -97,7 +98,7 @@ class AccumulatedValue:
else: else:
return None return None
def mean_error(self): def mean_error(self) -> float | None:
""" """
returns the mean's error (=std.dev/sqrt(n)) of all added/accumulated values returns the mean's error (=std.dev/sqrt(n)) of all added/accumulated values
""" """
@@ -113,7 +114,7 @@ class AccumulatedValue:
else: else:
return None return None
def __str__(self): def __str__(self) -> str:
if self.n==0: if self.n==0:
return "no value" return "no value"
elif self.n==1: elif self.n==1:
@@ -121,12 +122,12 @@ class AccumulatedValue:
else: else:
return "%g +/- %g (%d accumulations)"%(self.mean(),self.mean_error(),self.n) return "%g +/- %g (%d accumulations)"%(self.mean(),self.mean_error(),self.n)
def __repr__(self): def __repr__(self) -> str:
return str(self) return str(self)
class MeasurementResult(Drawable.Drawable, collections.UserDict): class MeasurementResult(Drawable.Drawable, collections.UserDict):
def __init__(self, quantity_name): def __init__(self, quantity_name: str) -> None:
""" """
convenient accumulation and interface to plot functions convenient accumulation and interface to plot functions
@@ -138,7 +139,7 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
self.lock = threading.RLock() self.lock = threading.RLock()
# get the selected item, if it does not exist, create an empty one # get the selected item, if it does not exist, create an empty one
def __getitem__(self, key): def __getitem__(self, key: float) -> "AccumulatedValue":
if key not in self: if key not in self:
a=AccumulatedValue() a=AccumulatedValue()
self.data[float(key)]=a self.data[float(key)]=a
@@ -146,23 +147,26 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
else: else:
return self.data[float(key)] return self.data[float(key)]
def __setitem__(self,key,value): def __setitem__(self,key: float,item: "AccumulatedValue | float")-> None:
if not isinstance(value, AccumulatedValue): if not isinstance(item, AccumulatedValue):
value=AccumulatedValue(float(value)) item=AccumulatedValue(float(item))
return collections.UserDict.__setitem__(self, return collections.UserDict.__setitem__(self,
float(key), float(key),
value) item)
def __add__(self, right_value): def __add__(self, right_value: int) -> "MeasurementResult":
if right_value==0: if right_value==0:
return self.copy() new_one=MeasurementResult(self.quantity_name)
for key in self.data:
new_one[key]=self.data[key].copy()
return new_one
else: else:
raise Exception("not implemented") raise Exception("not implemented")
def get_title(self): def get_title(self) -> str:
return self.quantity_name return self.quantity_name
def get_xdata(self): def get_xdata(self) -> numpy.ndarray:
""" """
sorted array of all dictionary entries without Accumulated Value objects with n==0 sorted array of all dictionary entries without Accumulated Value objects with n==0
""" """
@@ -171,32 +175,35 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
keys.sort() keys.sort()
return keys return keys
def get_ydata(self): def get_ydata(self, channel: int = 0) -> numpy.ndarray:
"""
y data (mean values); the channel argument is accepted for
compatibility with the Drawable interface, MeasurementResult
is single-channel
"""
return self.get_xydata()[1] return self.get_xydata()[1]
def get_xydata(self): def get_xydata(self) -> list[numpy.ndarray]:
k=self.get_xdata() k=self.get_xdata()
v=numpy.array([self.data[key].mean() for key in k], dtype="float64") v=numpy.array([self.data[key].mean() for key in k], dtype="float64")
return [k,v] return [k,v]
def get_errorplotdata(self): def get_errorplotdata(self) -> list[numpy.ndarray]:
k=self.get_xdata() k=self.get_xdata()
v=numpy.array([self.data[key].mean() for key in k], dtype="float64") v=numpy.array([self.data[key].mean() for key in k], dtype="float64")
e=numpy.array([self.data[key].mean_error() for key in k], dtype="float64") e=numpy.array([self.data[key].mean_error() for key in k], dtype="float64")
return [k,v,e] return [k,v,e]
def get_lineplotdata(self): def get_lineplotdata(self) -> list[numpy.ndarray]:
k=self.get_xdata() return self.get_xydata()
v=numpy.array(self.y, dtype="float64")
return [k, v]
def uses_statistics(self): def uses_statistics(self) -> bool:
""" """
drawable interface method returns True drawable interface method returns True
""" """
return True return True
def write_to_csv(self,destination=sys.stdout, delimiter=" "): def write_to_csv(self,destination: str | IO[str] = sys.stdout, delimiter: str = " ") -> None:
""" """
writes the data to a file or to sys.stdout writes the data to a file or to sys.stdout
destination can be a file or a filename destination can be a file or a filename
@@ -208,24 +215,21 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
else: else:
self._write_to_csv_dest(destination, delimiter) self._write_to_csv_dest(destination, delimiter)
def _write_to_csv_dest(self, dest, delimiter): def _write_to_csv_dest(self, dest: IO[str], delimiter: str) -> None:
dest.write("# quantity:"+str(self.quantity_name)+"\n") dest.write("# quantity:"+str(self.quantity_name)+"\n")
dest.write("# x y ysigma n\n") dest.write("# x y ysigma n\n")
for x in self.get_xdata(): for x in self.get_xdata():
y = self.data[x] y = self.data[x]
if isinstance(y, (float, int)): dest.write("%e%s%e%s%e%s%d\n" % (x,
dest.write("%e%s%e%s0%s1\n" % (x, delimiter, y, delimiter, delimiter)) delimiter,
else: y.mean(),
dest.write("%e%s%e%s%e%s%d\n" % (x, delimiter,
delimiter, y.mean_error(),
y.mean(), delimiter,
delimiter, y.n))
y.mean_error(),
delimiter,
y.n))
def write_to_hdf(self, hdffile, where, name, title, complib=None, complevel=None): def write_to_hdf(self, hdffile: tables.File, where: str, name: str, title: str, complib: str | None = None, complevel: int | None = None) -> None:
h5_table_format= { h5_table_format= {
"x" : tables.Float32Col(), "x" : tables.Float32Col(),
"y" : tables.Float32Col(), "y" : tables.Float32Col(),
@@ -251,24 +255,19 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
row=mr_table.row row=mr_table.row
xdata=self.get_xdata() xdata=self.get_xdata()
if xdata.shape[0]!=0: if xdata.shape[0]!=0:
for x in self.get_xdata(): for x in xdata:
y=self.data[x] y=self.data[x]
row["x"]=x row["x"]=x
if type(y) in [float, int, int]: row["y"]=y.mean()
row["y"]=y row["y_err"]=y.mean_error()
row["y_err"]=0.0 row["n"]=y.n
row["n"]=1
else:
row["y"]=y.mean()
row["y_err"]=y.mean_error()
row["n"]=y.n
row.append() row.append()
finally: finally:
mr_table.flush() mr_table.flush()
self.lock.release() self.lock.release()
def read_from_hdf(hdf_node): def read_from_hdf(hdf_node: object) -> "MeasurementResult | None":
""" """
reads a MeasurementResult object from the hdf_node reads a MeasurementResult object from the hdf_node
or None if the node is not suitable or None if the node is not suitable
+3 -3
View File
@@ -21,16 +21,16 @@ class Signalpath:
if abs(reduced_angle[1]) < 1e-6: if abs(reduced_angle[1]) < 1e-6:
reduced_angle = reduced_angle[0] % 4 reduced_angle = reduced_angle[0] % 4
if reduced_angle == 0: if reduced_angle == 0:
rotated.y = list(self.y) rotated.y = [a.copy() for a in self.y]
elif reduced_angle == 1: elif reduced_angle == 1:
rotated.y = [-self.y[1], self.y[0]] rotated.y = [-self.y[1], self.y[0].copy()]
elif reduced_angle == 2: elif reduced_angle == 2:
rotated.y = [-self.y[0], -self.y[1]] rotated.y = [-self.y[0], -self.y[1]]
elif reduced_angle == 3: elif reduced_angle == 3:
rotated.y = [self.y[1], -self.y[0]] rotated.y = [self.y[1].copy(), -self.y[0]]
else: else:
rad_angle = N.deg2rad(degrees) rad_angle = N.deg2rad(degrees)
sin_angle = N.sin(rad_angle) sin_angle = N.sin(rad_angle)
+2 -1
View File
@@ -1,10 +1,11 @@
from damaris.data.ADC_Result import ADC_Result from damaris.data.ADC_Result import ADC_Result
from damaris.data.Accumulation import Accumulation from damaris.data.Accumulation import Accumulation
from damaris.data.MeasurementResult import MeasurementResult, AccumulatedValue from damaris.data.MeasurementResult import MeasurementResult, AccumulatedValue
from damaris.data.FitResult import FitResult
from damaris.data.DataPool import DataPool from damaris.data.DataPool import DataPool
from damaris.data.Error_Result import Error_Result from damaris.data.Error_Result import Error_Result
from damaris.data.Config_Result import Config_Result from damaris.data.Config_Result import Config_Result
from damaris.data.Temperature import TemperatureResult from damaris.data.Temperature import TemperatureResult
__all__=["ADC_Result", "Accumulation", "MeasurementResult", "AccumulatedValue", "DataPool", "Error_Result", "Config_Result", "TemperatureResult" ] __all__=["ADC_Result", "Accumulation", "MeasurementResult", "AccumulatedValue", "FitResult", "DataPool", "Error_Result", "Config_Result", "TemperatureResult" ]
+45 -43
View File
@@ -3,20 +3,20 @@ import types
import numpy import numpy
class StateBase(object): class StateBase(object):
def __init__(self): def __init__(self) -> None:
pass pass
def to_xml(self, indent = ""): def to_xml(self, indent: str = "") -> str:
return indent + "<!-- " + repr(self) + " -->" return indent + "<!-- " + repr(self) + " -->"
class StateSimple(StateBase): class StateSimple(StateBase):
def __init__(self, time, content=None): def __init__(self, time: float, content: object | None = None) -> None:
super(StateSimple, self).__init__() super(StateSimple, self).__init__()
if time < 0: if time < 0:
raise AssertionError("time for state is negative!") raise AssertionError("time for state is negative!")
self.time = float(time) self.time = float(time)
self.content = content self.content = content
def to_xml(self, indent = ""): def to_xml(self, indent: str = "") -> str:
s = indent + '<state time="%s"' % repr(self.time) s = indent + '<state time="%s"' % repr(self.time)
if self.content is None: if self.content is None:
return s + '/>\n' return s + '/>\n'
@@ -24,14 +24,14 @@ class StateSimple(StateBase):
s += indent + ' ' + str(self.content) + '\n' s += indent + ' ' + str(self.content) + '\n'
s += indent + '</state>\n' s += indent + '</state>\n'
return s return s
def __repr__(self): def __repr__(self) -> str:
return 'StateSimple(%s, %s)' % (self.time, repr(self.content)) return 'StateSimple(%s, %s)' % (self.time, repr(self.content))
class StateList(StateBase): class StateList(StateBase):
def __init__(self): def __init__(self) -> None:
super(StateList, self).__init__() super(StateList, self).__init__()
self.list = [] self.list: list[object] = []
def to_xml(self, indent = " "): def to_xml(self, indent: str = " ") -> str:
s = "" s = ""
for k in self.list: for k in self.list:
if hasattr(k, "to_xml"): if hasattr(k, "to_xml"):
@@ -39,21 +39,21 @@ class StateList(StateBase):
else: else:
s += indent + str(k) s += indent + str(k)
return s return s
def append(self, val): def append(self, val: object) -> None:
self.list.append(val) self.list.append(val)
class StateLoop(StateList): class StateLoop(StateList):
"""Represents a loop in the state tree""" """Represents a loop in the state tree"""
def __init__(self, repeat): def __init__(self, repeat: int) -> None:
super(StateLoop, self).__init__() super(StateLoop, self).__init__()
self.repeat = repeat self.repeat = repeat
def to_xml(self, indent = ""): def to_xml(self, indent: str = "") -> str:
s = indent + ('<sequent repeat="%d">\n' % self.repeat) s = indent + ('<sequent repeat="%d">\n' % self.repeat)
s += super(StateLoop, self).to_xml(indent + " ") s += super(StateLoop, self).to_xml(indent + " ")
s += indent + '</sequent>\n' s += indent + '</sequent>\n'
return s return s
def __repr__(self): def __repr__(self) -> str:
return 'StateLoop(repeat=%d, %s)' \ return 'StateLoop(repeat=%d, %s)' \
% (self.repeat, repr(self.list)) % (self.repeat, repr(self.list))
@@ -78,32 +78,32 @@ class Experiment:
:param list rf_gates: list of rf gates ttl channels len(rf_gates) == len(rf_sources) :param list rf_gates: list of rf gates ttl channels len(rf_gates) == len(rf_sources)
""" """
job_id = 0 job_id: int = 0
def __init__(self, gating=None, rf_sources=[], rf_gates=[]): def __init__(self, gating: float | None = None, rf_sources: list[int] = [], rf_gates: list[int] = []) -> None:
self.job_id = Experiment.job_id self.job_id: int = Experiment.job_id
Experiment.job_id += 1 Experiment.job_id += 1
self.state_list = StateList() self.state_list: StateList = StateList()
self.list_stack = [] self.list_stack: list[StateList] = []
self.description = { } self.description: dict[str, object] = { }
self.gating = gating self.gating: float | None = gating
assert type(rf_sources) == type(list()), "rf_sources needs to be a list with the channels" assert type(rf_sources) == type(list()), "rf_sources needs to be a list with the channels"
assert type(rf_gates) == type(list()), "rf_gates needs to be a list with the channels" assert type(rf_gates) == type(list()), "rf_gates needs to be a list with the channels"
assert len(rf_gates) == len(rf_sources), "rf_sources and rf_gates must have equal number of entries" assert len(rf_gates) == len(rf_sources), "rf_sources and rf_gates must have equal number of entries"
self.rf_sources = rf_sources self.rf_sources: list[int] = rf_sources
self.rf_gates = rf_gates self.rf_gates: list[int] = rf_gates
#for tracking the experiment length: #for tracking the experiment length:
#because loops are possible we need to track the length for each loop level #because loops are possible we need to track the length for each loop level
self.total_time=[0.0] self.total_time: list[float] = [0.0]
#and we need to know the number of iterations of the loops to multiply the state. #and we need to know the number of iterations of the loops to multiply the state.
self.loop_iterations=[1] self.loop_iterations: list[int] = [1]
# Commands ------------------------------------------------------------------------------------- # Commands -------------------------------------------------------------------------------------
def ttl_pulse(self, length, channel = None, value = None): def ttl_pulse(self, length: float, channel: int | None = None, value: int | None = None) -> None:
""" """
Creates a state with length **length** and switches requested bits of the pulse programmer to HIGH. Creates a state with length **length** and switches requested bits of the pulse programmer to HIGH.
@@ -151,7 +151,7 @@ class Experiment:
self.total_time[-1] += length self.total_time[-1] += length
## Same as ttl_pulse, but no *channel* keyword ## Same as ttl_pulse, but no *channel* keyword
def ttls(self, length = None, value = None): def ttls(self, length: float | None = None, value: int | None = None) -> None:
""" """
Same as ttl_pulse, but no *channel* keyword Same as ttl_pulse, but no *channel* keyword
@@ -159,6 +159,7 @@ class Experiment:
:param int value: lines to set (integer) :param int value: lines to set (integer)
:return: :return:
""" """
assert value is not None, "ttls needs an integer value"
the_value=int(value) the_value=int(value)
s_content = '<ttlout value="0x%06x"/>' % the_value s_content = '<ttlout value="0x%06x"/>' % the_value
if length is not None: if length is not None:
@@ -168,7 +169,7 @@ class Experiment:
self.state_list.append(s_content) self.state_list.append(s_content)
def rf_pulse(self, length=None, phase=0, source=0): def rf_pulse(self, length: float | None = None, phase: float = 0, source: int = 0) -> None:
""" """
Make an rf pulse, including gating and phase switching. Only possible if Experiment is configured. Make an rf pulse, including gating and phase switching. Only possible if Experiment is configured.
@@ -183,7 +184,7 @@ class Experiment:
self.ttls(length=length, value=self.rf_gates[source]+self.rf_sources[source]) self.ttls(length=length, value=self.rf_gates[source]+self.rf_sources[source])
## Beginning of a new state ## Beginning of a new state
def state_start(self, time): def state_start(self, time: float) -> None:
""" """
Starts a state in the pulse programs with duration *time*. Starts a state in the pulse programs with duration *time*.
This must be closed with :func:`state_end`. This must be closed with :func:`state_end`.
@@ -194,13 +195,13 @@ class Experiment:
self.total_time[-1] += time self.total_time[-1] += time
## End of *state_start* ## End of *state_start*
def state_end(self): def state_end(self) -> None:
""" """
Closes a previous :func:`state_start` Closes a previous :func:`state_start`
""" """
self.state_list.append('</state>\n') self.state_list.append('</state>\n')
def wait(self, time, ttls=None, gating=False): def wait(self, time: float, ttls: int | None = None, gating: bool = False) -> None:
""" """
Waits for a specified amount of time without performing any actions. Waits for a specified amount of time without performing any actions.
@@ -241,6 +242,7 @@ class Experiment:
""" """
if gating: if gating:
assert self.gating is not None, "gating time must be set for gating=True"
time -= self.gating time -= self.gating
if ttls is not None: if ttls is not None:
s_content = '<ttlout value="0x%06x"/>' % ttls s_content = '<ttlout value="0x%06x"/>' % ttls
@@ -251,7 +253,7 @@ class Experiment:
self.total_time[-1] += time self.total_time[-1] += time
def record(self, samples, frequency, timelength=None, sensitivity = None, ttls=None, channels = 3, offset = None, impedance = None): def record(self, samples: int, frequency: float, timelength: float | None = None, sensitivity: float | list[float] | None = None, ttls: int | None = None, channels: int = 3, offset: int | list[int] | None = None, impedance: float | list[float] | None = None) -> None:
""" """
Records data with a given number of samples, sampling frequency, and sensitivity. Records data with a given number of samples, sampling frequency, and sensitivity.
This command starts data acquisition from the ADC (Analog-to-Digital Converter). This command starts data acquisition from the ADC (Analog-to-Digital Converter).
@@ -365,7 +367,7 @@ class Experiment:
## Create a loop on the pulse programmer. Loop contents can not change inside the loop. ## Create a loop on the pulse programmer. Loop contents can not change inside the loop.
# @params iterations Number of loop iterations # @params iterations Number of loop iterations
def loop_start(self, iterations): def loop_start(self, iterations: int) -> None:
""" """
creates a loop of given number of iterations and has to be closed by loop_end(). creates a loop of given number of iterations and has to be closed by loop_end().
Commands inside the loop can not change, i.e., the parameters are the same for each loop run. Commands inside the loop can not change, i.e., the parameters are the same for each loop run.
@@ -384,7 +386,7 @@ class Experiment:
self.loop_iterations.append(iterations) self.loop_iterations.append(iterations)
## End loop state ## End loop state
def loop_end(self): def loop_end(self) -> None:
""" """
Closes a l:func:`loop_start` statement. Closes a l:func:`loop_start` statement.
""" """
@@ -396,7 +398,7 @@ class Experiment:
self.total_time[-1] += looptime*loopiterations self.total_time[-1] += looptime*loopiterations
def set_frequency(self, frequency, phase, ttls=0): def set_frequency(self, frequency: float, phase: float, ttls: int = 0) -> None:
""" """
Sets the frequency and phase of the frequency source and optionally further channels. Sets the frequency and phase of the frequency source and optionally further channels.
The state length is 2 µs. Stabilisation time depends on RF source. The state length is 2 µs. Stabilisation time depends on RF source.
@@ -412,7 +414,7 @@ class Experiment:
self.total_time[-1] += 2e-6 self.total_time[-1] += 2e-6
def set_pfg(self, dac_value=None, length=None, shape=('rec', 0), trigger=4, is_seq=False): def set_pfg(self, dac_value: int | None = None, length: float | None = None, shape: tuple[str, float] = ('rec', 0), trigger: int = 4, is_seq: bool = False) -> None:
""" """
This sets the value for the PFG, it also sets it back to dac_value=0 automatically. This sets the value for the PFG, it also sets it back to dac_value=0 automatically.
If you don't wish to do so (i.e. line shapes) set is_seq=1 If you don't wish to do so (i.e. line shapes) set is_seq=1
@@ -480,7 +482,7 @@ class Experiment:
else: # don't know what to do else: # don't know what to do
raise SyntaxError("form is unknown: %s"%form) raise SyntaxError("form is unknown: %s"%form)
def set_dac(self, dac_value, dac_id=1, length=None, is_seq=False, ttls=0): def set_dac(self, dac_value: int, dac_id: int = 1, length: float | None = None, is_seq: bool = False, ttls: int = 0) -> None:
""" """
Sets the value for the DAC (Digital-to-Analog Converter) and optionally additional TTL lines. Sets the value for the DAC (Digital-to-Analog Converter) and optionally additional TTL lines.
This command is used to control analog output signals, such as pulsed field gradients. This command is used to control analog output signals, such as pulsed field gradients.
@@ -544,7 +546,7 @@ class Experiment:
self.total_time[-1] += 42*9e-8 self.total_time[-1] += 42*9e-8
def set_phase(self, phase, ttls=0): def set_phase(self, phase: float, ttls: int = 0) -> None:
""" """
Sets the phase of the RF source to the specified value. Sets the phase of the RF source to the specified value.
@@ -590,7 +592,7 @@ class Experiment:
self.total_time[-1] += 0.5e-6 self.total_time[-1] += 0.5e-6
def set_description(self, key, value): def set_description(self, key: str, value: object) -> None:
""" """
Sets a description key-value pair in the experiment description dictionary. Sets a description key-value pair in the experiment description dictionary.
@@ -630,7 +632,7 @@ class Experiment:
print('Warning: Overwriting existing description "%s" = "%s" with "%s"' % (key, self.description[key], value)) print('Warning: Overwriting existing description "%s" = "%s" with "%s"' % (key, self.description[key], value))
self.description[key] = value self.description[key] = value
def set_pts_local(self): def set_pts_local(self) -> None:
""" """
Sets the PTS310/PTS500 frequency source to local mode. Sets the PTS310/PTS500 frequency source to local mode.
State length is 2 µs. State length is 2 µs.
@@ -654,7 +656,7 @@ class Experiment:
return self.job_id return self.job_id
def write_xml_string(self): def write_xml_string(self) -> str:
""" """
Returns the current program as a string Returns the current program as a string
@@ -707,14 +709,14 @@ class Experiment:
return xml_string return xml_string
def write_quit_job(self): def write_quit_job(self) -> str:
""" """
Returns a xml quit-job Returns a xml quit-job
:returns: XML quit-job :returns: XML quit-job
""" """
return '<?xml version="1.0" encoding="ISO-8859-1"?>\n<quit/>' return '<?xml version="1.0" encoding="ISO-8859-1"?>\n<quit/>'
def get_length(self): def get_length(self) -> float:
timelength = 0.0 timelength = 0.0
#calculate the correct timelength also for unclosed loops, if there is no unclosed loop #calculate the correct timelength also for unclosed loops, if there is no unclosed loop
#the timelength of this experiment is self.total_time[-1]. #the timelength of this experiment is self.total_time[-1].
@@ -724,13 +726,13 @@ class Experiment:
return timelength return timelength
class Quit(Experiment): class Quit(Experiment):
def write_xml_string(self): def write_xml_string(self) -> str:
return '<?xml version="1.0" encoding="ISO-8859-1"?>\n<quit no="%d"/>'%self.job_id return '<?xml version="1.0" encoding="ISO-8859-1"?>\n<quit no="%d"/>'%self.job_id
# /Public Methods ------------------------------------------------------------------------------ # /Public Methods ------------------------------------------------------------------------------
def self_test(): def self_test() -> None:
e = Experiment() e = Experiment()
e.set_description("key", "value") e.set_description("key", "value")
e.set_frequency(85e6, 90, ttls=16) e.set_frequency(85e6, 90, ttls=16)
+1 -1
View File
@@ -2,7 +2,7 @@
""" """
This module holds everything connected with the DAC and PFG This module holds everything connected with the DAC and PFG
""" """
def conv(I_out=0): def conv(I_out: float = 0) -> int:
""" """
converts the demanded Output current in Integer converts the demanded Output current in Integer
""" """
+3 -3
View File
@@ -120,7 +120,7 @@ class BackendDriver(threading.Thread):
self.statefilename=None self.statefilename=None
state_files=glob.glob(os.path.join(self.spool_dir,"*.state")) state_files=glob.glob(os.path.join(self.spool_dir,"*.state"))
while len(state_files)==0: while len(state_files)==0:
if timeout<0 or self.core_input is None or self.core_input.poll() is not None or self.quit_flag.isSet(): if timeout<0 or self.core_input is None or self.core_input.poll() is not None or self.quit_flag.is_set():
# look into core log file and include contents # look into core log file and include contents
logger.error("Backend failed to start: timeout=%s poll=%s", timeout, self.core_input.poll()) logger.error("Backend failed to start: timeout=%s poll=%s", timeout, self.core_input.poll())
log_message='' log_message=''
@@ -156,10 +156,10 @@ class BackendDriver(threading.Thread):
break break
# wait on flag and look after backend # wait on flag and look after backend
while not self.quit_flag.isSet() and self.is_busy(): while not self.quit_flag.is_set() and self.is_busy():
self.quit_flag.wait(0.1) self.quit_flag.wait(0.1)
if self.quit_flag.isSet(): if self.quit_flag.is_set():
self.stop_queue() self.stop_queue()
while self.is_busy(): while self.is_busy():
time.sleep(0.1) time.sleep(0.1)
File diff suppressed because it is too large. Load diff
+27 -16
View File
@@ -5,6 +5,7 @@ import sys
import time import time
import logging import logging
import ast import ast
import contextlib
from damaris.experiments.Experiment import Quit from damaris.experiments.Experiment import Quit
from damaris.experiments import Experiment from damaris.experiments import Experiment
@@ -36,6 +37,19 @@ class _SandboxStdout:
return False return False
@contextlib.contextmanager
def _sandbox_stdout_redirect(log_callback):
"""Temporarily redirect process-global stdout/stderr to the sandbox log."""
old_stdout, old_stderr = sys.stdout, sys.stderr
sandbox = _SandboxStdout(log_callback)
try:
sys.stdout = sandbox
sys.stderr = sandbox
yield sandbox
finally:
sys.stdout, sys.stderr = old_stdout, old_stderr
class ExperimentHandling(threading.Thread): class ExperimentHandling(threading.Thread):
""" """
runs the experiment script in sandbox runs the experiment script in sandbox
@@ -52,17 +66,25 @@ class ExperimentHandling(threading.Thread):
self.data["__recentexperiment"]=-1 self.data["__recentexperiment"]=-1
def synchronize(self, before=0, waitsteps=0.1): def synchronize(self, before=0, waitsteps=0.1):
while (self.data["__recentexperiment"]>self.data["__recentresult"]+before) and not self.quit_flag.isSet(): while (self.data["__recentexperiment"]>self.data["__recentresult"]+before) and not self.quit_flag.is_set():
self.quit_flag.wait(waitsteps) self.quit_flag.wait(waitsteps)
if self.quit_flag.isSet(): if self.quit_flag.is_set():
raise StopExperiment raise StopExperiment
def sleep(self, seconds): def sleep(self, seconds):
self.quit_flag.wait(seconds) self.quit_flag.wait(seconds)
if self.quit_flag.isSet(): if self.quit_flag.is_set():
raise StopExperiment raise StopExperiment
def run(self): def run(self):
# Redirect stdout/stderr so print() in sandboxed scripts goes to the GUI
if self._log_callback is not None:
with _sandbox_stdout_redirect(self._log_callback):
self._run_script()
else:
self._run_script()
def _run_script(self):
dataspace={} dataspace={}
exp_classes = __import__('damaris.experiments', dataspace, dataspace, ['Experiment']) exp_classes = __import__('damaris.experiments', dataspace, dataspace, ['Experiment'])
for name in dir(exp_classes): for name in dir(exp_classes):
@@ -77,13 +99,6 @@ class ExperimentHandling(threading.Thread):
self.location = None self.location = None
exp_iterator=None exp_iterator=None
# Redirect stdout/stderr so print() in sandboxed scripts goes to the GUI
_orig_stdout = sys.stdout
_orig_stderr = sys.stderr
if self._log_callback is not None:
sys.stdout = _SandboxStdout(self._log_callback)
sys.stderr = _SandboxStdout(self._log_callback)
# check for time.sleep() # check for time.sleep()
try: try:
tree = ast.parse(self.script) tree = ast.parse(self.script)
@@ -131,7 +146,7 @@ class ExperimentHandling(threading.Thread):
traceback_file=None traceback_file=None
return return
while exp_iterator is not None and not self.quit_flag.isSet(): while exp_iterator is not None and not self.quit_flag.is_set():
# get next experiment from script # get next experiment from script
try: try:
job=next(exp_iterator) job=next(exp_iterator)
@@ -171,7 +186,7 @@ class ExperimentHandling(threading.Thread):
# relax for a short time # relax for a short time
if "__resultsinadvance" in self.data and self.data["__resultsinadvance"]+100<job.job_id: if "__resultsinadvance" in self.data and self.data["__resultsinadvance"]+100<job.job_id:
self.quit_flag.wait(0.05) self.quit_flag.wait(0.05)
if self.quit_flag.isSet(): if self.quit_flag.is_set():
dataspace=None dataspace=None
exp_iterator=None exp_iterator=None
break break
@@ -181,7 +196,3 @@ class ExperimentHandling(threading.Thread):
dataspace=None dataspace=None
self.exp_iterator=None self.exp_iterator=None
self.writer=None self.writer=None
# Restore stdout/stderr
sys.stdout = _orig_stdout
sys.stderr = _orig_stderr
+10 -13
View File
@@ -7,7 +7,7 @@ import traceback
import logging import logging
import ast import ast
from damaris.data import Resultable from damaris.data import Resultable
from damaris.gui.ExperimentHandling import StopExperiment, _SandboxStdout from damaris.gui.ExperimentHandling import StopExperiment, _sandbox_stdout_redirect
logger = logging.getLogger("damaris.result_handling") logger = logging.getLogger("damaris.result_handling")
@@ -28,12 +28,13 @@ class ResultHandling(threading.Thread):
def run(self): def run(self):
# Redirect stdout/stderr so print() in sandboxed scripts goes to the GUI # Redirect stdout/stderr so print() in sandboxed scripts goes to the GUI
_orig_stdout = sys.stdout
_orig_stderr = sys.stderr
if self._log_callback is not None: if self._log_callback is not None:
sys.stdout = _SandboxStdout(self._log_callback) with _sandbox_stdout_redirect(self._log_callback):
sys.stderr = _SandboxStdout(self._log_callback) self._run_script()
else:
self._run_script()
def _run_script(self):
# execute it # execute it
dataspace={} dataspace={}
data_classes = __import__('damaris.data', dataspace, dataspace, ['*']) data_classes = __import__('damaris.data', dataspace, dataspace, ['*'])
@@ -96,30 +97,26 @@ class ResultHandling(threading.Thread):
traceback_file=None traceback_file=None
dataspace=None dataspace=None
# Restore stdout/stderr
sys.stdout = _orig_stdout
sys.stderr = _orig_stderr
def sleep(self, seconds): def sleep(self, seconds):
self.quit_flag.wait(seconds) self.quit_flag.wait(seconds)
if self.quit_flag.isSet(): if self.quit_flag.is_set():
raise StopExperiment raise StopExperiment
def __iter__(self): def __iter__(self):
if self.quit_flag.isSet(): if self.quit_flag.is_set():
self.results=None self.results=None
return return
for i in self.results: for i in self.results:
if hasattr(self.results, "in_advance"): if hasattr(self.results, "in_advance"):
self.data_space["__resultsinadvance"]=self.results.in_advance self.data_space["__resultsinadvance"]=self.results.in_advance
if self.quit_flag.isSet(): if self.quit_flag.is_set():
self.results=None self.results=None
return return
if isinstance(i, Resultable.Resultable): if isinstance(i, Resultable.Resultable):
if self.data_space is not None: if self.data_space is not None:
self.data_space["__recentresult"]=i.job_id+0 self.data_space["__recentresult"]=i.job_id+0
yield i yield i
if self.quit_flag.isSet(): if self.quit_flag.is_set():
self.results=None self.results=None
return return
+6 -6
View File
@@ -15,7 +15,7 @@ import base64
import numpy import numpy
try: try:
import xml.etree.cElementTree import xml.etree.ElementTree
ELEMENT_TREE = True ELEMENT_TREE = True
except: except:
import xml.parsers.expat import xml.parsers.expat
@@ -109,7 +109,7 @@ class ResultReader:
self.result_job_number = None self.result_job_number = None
# Job Date is set in __read_file() # Job Date is set in __read_file()
self.__filetype = None self.__filetype = None
for elem in xml.etree.cElementTree.ElementTree(file=in_file).iter(): for elem in xml.etree.ElementTree.ElementTree(file=in_file).iter():
if elem.tag == 'result': if elem.tag == 'result':
self.result_job_number = int(elem.get("job")) self.result_job_number = int(elem.get("job"))
pass pass
@@ -544,7 +544,7 @@ class BlockingResultReader(ResultReader):
block until result is available block until result is available
""" """
expected_filename=os.path.join(self.spool_dir,self.result_pattern%(self.no)) expected_filename=os.path.join(self.spool_dir,self.result_pattern%(self.no))
while (not self.quit_flag.isSet()) and (self.stop_no is None or self.stop_no>self.no): while (not self.quit_flag.is_set()) and (self.stop_no is None or self.stop_no>self.no):
if not os.access(expected_filename,os.R_OK): if not os.access(expected_filename,os.R_OK):
# stop polling, if required # stop polling, if required
if self.poll_time<0: break if self.poll_time<0: break
@@ -560,11 +560,11 @@ class BlockingResultReader(ResultReader):
self.in_advance+=1 self.in_advance+=1
in_advance_filename=os.path.join(self.spool_dir,self.result_pattern%(self.in_advance+1)) in_advance_filename=os.path.join(self.spool_dir,self.result_pattern%(self.in_advance+1))
if self.quit_flag.isSet(): break if self.quit_flag.is_set(): break
r=self.get_result_object(expected_filename) r=self.get_result_object(expected_filename)
if self.quit_flag.isSet(): break if self.quit_flag.is_set(): break
if self.quit_flag.isSet(): break if self.quit_flag.is_set(): break
yield r yield r
if self.clear_results: if self.clear_results:
+25 -32
View File
@@ -13,25 +13,6 @@ import datetime
from damaris.data import TemperatureResult from damaris.data import TemperatureResult
from damaris.tools.temperature import TemperatureController from damaris.tools.temperature import TemperatureController
# GTK imports are done lazily to avoid initialization issues
_HAS_GTK = None
_gdk = None
def _get_gdk():
"""Lazily import gdk to avoid GTK initialization issues."""
global _HAS_GTK, _gdk
if _HAS_GTK is None:
try:
import gi
gi.require_version('Gdk', '3.0')
from gi.repository import Gdk as gdk
_gdk = gdk
_HAS_GTK = True
except (ImportError, ValueError):
_HAS_GTK = False
return _gdk if _HAS_GTK else None
class TemperatureMonitor(threading.Thread): class TemperatureMonitor(threading.Thread):
""" """
@@ -72,6 +53,10 @@ class TemperatureMonitor(threading.Thread):
self.result = TemperatureResult() self.result = TemperatureResult()
self._running = False self._running = False
self._lock = threading.RLock() self._lock = threading.RLock()
self._consecutive_failures = 0
self._max_consecutive_failures = 10
self._last_error = None
self._connection_ok = True
# Initialize the result with metadata # Initialize the result with metadata
self.result.xlabel = "Time (s)" self.result.xlabel = "Time (s)"
@@ -124,21 +109,29 @@ class TemperatureMonitor(threading.Thread):
data_pool_result.job_id = self.result.job_id if hasattr(self.result, 'job_id') else None data_pool_result.job_id = self.result.job_id if hasattr(self.result, 'job_id') else None
data_pool_result.job_date = self.result.job_date if hasattr(self.result, 'job_date') else None data_pool_result.job_date = self.result.job_date if hasattr(self.result, 'job_date') else None
# Store in data_pool in GTK thread (if GTK available) # Store in data_pool (dict item assignment is atomic under the GIL)
gdk = _get_gdk() self.data_pool[self.data_key] = data_pool_result
if gdk is not None:
gdk.threads_enter() # Reset consecutive failure counter on successful read
try: self._consecutive_failures = 0
self.data_pool[self.data_key] = data_pool_result self._last_error = None
finally: self._connection_ok = True
gdk.threads_leave()
else:
self.data_pool[self.data_key] = data_pool_result
except Exception as e: except Exception as e:
print(f"TemperatureMonitor error: {e}") self._consecutive_failures += 1
import traceback self._last_error = str(e)
traceback.print_exc() if self._consecutive_failures <= 3:
print(f"TemperatureMonitor read error (attempt {self._consecutive_failures}): {e}")
elif self._consecutive_failures == 5:
print(f"TemperatureMonitor: repeated read errors, checking connection...")
if hasattr(self.controller, 'check_connection'):
self._connection_ok = self.controller.check_connection()
if not self._connection_ok:
print(f"TemperatureMonitor: WARNING - controller appears disconnected after {self._consecutive_failures} failures")
elif self._consecutive_failures >= self._max_consecutive_failures:
print(f"TemperatureMonitor: ERROR - {self._consecutive_failures} consecutive failures, controller may be disconnected")
else:
print(f"TemperatureMonitor error (failure #{self._consecutive_failures}): {e}")
# Sleep in small increments for responsive shutdown # Sleep in small increments for responsive shutdown
sleep_remaining = self.interval sleep_remaining = self.interval
+24 -14
View File
@@ -1,5 +1,6 @@
import serial import serial
import re import re
import time
import operator import operator
from functools import reduce from functools import reduce
@@ -57,21 +58,22 @@ class Eurotherm(TemperatureController):
def send_read_param(self, param): def send_read_param(self, param):
self.s.write(EOT + self.device + param + ENQ) self.s.write(EOT + self.device + param + ENQ)
def read_param(self, param): def read_param(self, param, retries=3, retry_delay=0.05):
self.s.flushInput() for attempt in range(retries):
self.send_read_param(param) self.s.flushInput()
answer = self.s.read(self._expect_len) self.send_read_param(param)
m = reply_pattern.search(answer) answer = self.s.read(self._expect_len)
if m is None:
# Reading _expect_len bytes was not enough...
answer += self.s.read(200)
m = reply_pattern.search(answer) m = reply_pattern.search(answer)
if m is not None: if m is None:
self._expect_len = len(answer) answer += self.s.read(200)
return m.group(1) m = reply_pattern.search(answer)
else: if m is not None:
print("received:", repr(answer)) self._expect_len = len(answer)
return None return m.group(1)
if attempt < retries - 1:
time.sleep(retry_delay)
print("received:", repr(answer))
return None
def write_param(self, mnemonic, data): def write_param(self, mnemonic, data):
if len(mnemonic) > 2: if len(mnemonic) > 2:
@@ -128,6 +130,14 @@ class Eurotherm(TemperatureController):
"""Set target temperature in Celsius.""" """Set target temperature in Celsius."""
return self.write_param('SL', str(temperature)) return self.write_param('SL', str(temperature))
def check_connection(self) -> bool:
"""Check if the Eurotherm device is connected and responsive."""
try:
result = self.read_param('PV')
return result is not None
except Exception:
return False
def close(self): def close(self):
"""Close the serial connection.""" """Close the serial connection."""
if hasattr(self, 's') and self.s is not None: if hasattr(self, 's') and self.s is not None:
+20 -7
View File
@@ -1,5 +1,6 @@
import serial import serial
import re import re
import time
import numpy as np import numpy as np
from .temperature import TemperatureController, register_controller from .temperature import TemperatureController, register_controller
@@ -91,13 +92,17 @@ class Lakeshore(TemperatureController):
else: else:
return 0.0 return 0.0
def read_param(self, param): def read_param(self, param, retries=3, retry_delay=0.05):
"""Read a parameter using simple ASCII commands.""" """Read a parameter using simple ASCII commands."""
self.s.write(f"{param}?\n") for attempt in range(retries):
answer = self.s.readline().strip() self.s.flushInput()
if answer and len(answer) > 1: self.s.write(f"{param}?\n")
# Remove the first character (command echo) and return the value answer = self.s.readline().strip()
return answer[1:] if answer and len(answer) > 1:
# Remove the first character (command echo) and return the value
return answer[1:]
if attempt < retries - 1:
time.sleep(retry_delay)
return None return None
def get_value(self, name): def get_value(self, name):
@@ -134,9 +139,17 @@ class Lakeshore(TemperatureController):
def set_setpoint(self, temperature: float) -> bool: def set_setpoint(self, temperature: float) -> bool:
"""Set target temperature in Celsius.""" """Set target temperature in Celsius."""
self.s.write(f"SETP {temperature}\n") self.s.write(f"SETP 1,{temperature}\n")
return True return True
def check_connection(self) -> bool:
"""Check if the Lakeshore device is connected and responsive."""
try:
result = self.read_param('KRDG')
return result is not None
except Exception:
return False
def close(self): def close(self):
"""Close the serial connection.""" """Close the serial connection."""
if hasattr(self, 's') and self.s is not None: if hasattr(self, 's') and self.s is not None:
+10
View File
@@ -149,3 +149,13 @@ class TemperatureController(ABC):
if they have resources to clean up. if they have resources to clean up.
""" """
pass pass
def check_connection(self) -> bool:
"""
Check if the controller is connected and responsive.
Returns:
bool: True if the controller is connected and responsive,
False otherwise. Subclasses should override this method.
"""
return False
+4 -3
View File
@@ -11,6 +11,7 @@ def test(i):
return e return e
def experiment(): def experiment():
for i in range(1400): for i in range(100):
sleep(0.1) #sleep(0.1)
yield test(i) yield test(i)
synchronize()
+16 -1
View File
@@ -1,6 +1,6 @@
import tables import tables
import numpy import numpy
# from damaris import FitResult
r = MeasurementResult("test") r = MeasurementResult("test")
def result(): def result():
@@ -9,14 +9,29 @@ def result():
except: except:
print("h5 already opened") print("h5 already opened")
accu = Accumulation(error=False) accu = Accumulation(error=False)
for num,ts in enumerate(results): for num,ts in enumerate(results):
data["accu"] = accu data["accu"] = accu
data["data/ts %i"%num] = ts+0 data["data/ts %i"%num] = ts+0
ts.write_to_hdf(h5, "/", f"adc_{num}", f"adc_{num}") ts.write_to_hdf(h5, "/", f"adc_{num}", f"adc_{num}")
data["fft"] = (ts+0).fft() data["fft"] = (ts+0).fft()
r[num] = ts.y[0][0] r[num] = ts.y[0][0]
if num > 4:
x = r.get_xdata()
y = r.get_ydata()
coeffs = numpy.polyfit(x, y, 2)
x_line = numpy.linspace(x.min(), x.max(), 100)
y_line = numpy.polyval(coeffs, x_line)
fit = FitResult(x=x_line,
y=y_line,
title="Magnetization(tau) fit",
source="test",
params={"a": coeffs[0], "b": coeffs[1], "c": coeffs[2]})
data["test/fit"] = fit
data["test"] = r data["test"] = r
accu+=ts accu+=ts
print(accu)
accu.write_to_hdf(h5, "/", "name", "title") accu.write_to_hdf(h5, "/", "name", "title")
try: try:
h5.close() h5.close()
+64 -11
View File
@@ -195,14 +195,14 @@ class TestADCResult(unittest.TestCase):
np.testing.assert_array_equal(adc.y[i]+10, y[i] + 10, strict=True) np.testing.assert_array_equal(adc.y[i]+10, y[i] + 10, strict=True)
adc += 10 adc += 10
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] + 10, strict=True) np.testing.assert_array_equal(adc.y[i], (y[i] + 10).astype("float32"), strict=True)
# test float addition # test float addition
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i] + 10., y[i] + 10., strict=True) np.testing.assert_array_equal(adc.y[i] + 10., (y[i] + 10 + 10.).astype("float32"), strict=True)
adc += 10. adc += 10.
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] + 10 + 10., strict=True) np.testing.assert_array_equal(adc.y[i], (y[i] + 10 + 10.).astype("float32"), strict=True)
def test_operator_sub_scalar(self): def test_operator_sub_scalar(self):
""" """
@@ -215,14 +215,14 @@ class TestADCResult(unittest.TestCase):
np.testing.assert_array_equal(adc.y[i] - 10, y[i] - 10, strict=True) np.testing.assert_array_equal(adc.y[i] - 10, y[i] - 10, strict=True)
adc -= 10 adc -= 10
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] - 10, strict=True) np.testing.assert_array_equal(adc.y[i], (y[i] - 10).astype("float32"), strict=True)
# test float subtraction # test float subtraction
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i] - 10., y[i] - 10., strict=True) np.testing.assert_array_equal(adc.y[i] - 10., (y[i] - 10 - 10.).astype("float32"), strict=True)
adc -= 10. adc -= 10.
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] - 10 - 10., strict=True) np.testing.assert_array_equal(adc.y[i], (y[i] - 10 - 10.).astype("float32"), strict=True)
def test_operator_mul_scalar(self): def test_operator_mul_scalar(self):
""" """
@@ -236,14 +236,14 @@ class TestADCResult(unittest.TestCase):
np.testing.assert_array_equal(adc.y[i] * 10, y[i] * 10, strict=True) np.testing.assert_array_equal(adc.y[i] * 10, y[i] * 10, strict=True)
adc *= 10 adc *= 10
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] * 10, strict=True) np.testing.assert_array_equal(adc.y[i], (y[i] * 10).astype("float32"), strict=True)
# test float multiplication # test float multiplication
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i] * 10.0, y[i] * 10.0, strict=True) np.testing.assert_array_equal(adc.y[i] * 10.0, (y[i] * 10 * 10.0).astype("float32"), strict=True)
adc *= 10.0 adc *= 10.0
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] * 10 * 10.0, strict=True) np.testing.assert_array_equal(adc.y[i], (y[i] * 10 * 10.0).astype("float32"), strict=True)
def test_operator_truediv_scalar(self): def test_operator_truediv_scalar(self):
""" """
@@ -255,7 +255,7 @@ class TestADCResult(unittest.TestCase):
np.testing.assert_array_equal(adc.y[i]/10, y[i] / 10, strict=True) np.testing.assert_array_equal(adc.y[i]/10, y[i] / 10, strict=True)
adc /= 10 adc /= 10
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] / 10, strict=True) np.testing.assert_array_equal(adc.y[i], (y[i] / 10).astype("float32"), strict=True)
def test_operator_floordiv_scalar(self): def test_operator_floordiv_scalar(self):
""" """
@@ -267,10 +267,37 @@ class TestADCResult(unittest.TestCase):
np.testing.assert_array_equal(adc.y[i]//10, y[i] // 10, strict=True) np.testing.assert_array_equal(adc.y[i]//10, y[i] // 10, strict=True)
adc //= 10 adc //= 10
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] // 10, strict=True) np.testing.assert_array_equal(adc.y[i], (y[i] // 10).astype("float32"), strict=True)
self.assertRaises(ValueError, adc.__floordiv__, 1.0) self.assertRaises(ValueError, adc.__floordiv__, 1.0)
self.assertRaises(ValueError, adc.__rfloordiv__, 1.0) self.assertRaises(ValueError, adc.__rfloordiv__, 1.0)
def test_operators_keep_channel_metadata(self):
"""operator results keep nChannels/is_clipped identical to the operand"""
adc = self.create_adc_result()
adc.is_clipped = True
results = [adc + 1, adc - 1, adc * 2, adc ** 2, adc / 2., adc // 2,
10 + adc, 10 - adc, 2 * adc, 100. / adc, 100 // adc, -adc]
for r in results:
self.assertEqual(r.get_nChannels(), adc.get_nChannels())
self.assertEqual(r.is_clipped, adc.is_clipped)
def test_hdf_roundtrip_keeps_nchannels(self):
"""read_from_hdf must restore the number of channels"""
import tempfile
import tables
from damaris.data.ADC_Result import read_from_hdf
adc = self.create_adc_result()
with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, "adc.h5")
with tables.open_file(path, "w") as h5:
adc.write_to_hdf(h5, h5.root, "adc", "test")
with tables.open_file(path, "r") as h5:
adc2 = read_from_hdf(h5.root.adc)
self.assertIsNotNone(adc2)
self.assertTrue(adc2.contains_data())
self.assertEqual(adc2.get_nChannels(), adc.get_nChannels())
def test_operator_overload_adc_result_exception(self): def test_operator_overload_adc_result_exception(self):
""" """
Test the ValueError raised when ADC_Result is used as an operand in an arithmetic operation Test the ValueError raised when ADC_Result is used as an operand in an arithmetic operation
@@ -289,5 +316,31 @@ class TestADCResult(unittest.TestCase):
self.assertRaises(ValueError, adc1.__rfloordiv__, adc2) self.assertRaises(ValueError, adc1.__rfloordiv__, adc2)
self.assertRaises(ValueError, adc1.__pow__, adc2) self.assertRaises(ValueError, adc1.__pow__, adc2)
def test_empty_state_types(self):
"""Empty ADC_Result must hold an ndarray x and a list-of-ndarray y."""
adc = ADC_Result()
self.assertIsInstance(adc.x, np.ndarray)
self.assertIsInstance(adc.y, list)
self.assertEqual(adc.x.size, 0)
self.assertEqual(adc.get_xmin(), 0)
self.assertEqual(adc.get_xmax(), 0)
self.assertEqual(adc.get_xminpos(), 0)
self.assertEqual(adc.get_ymin(), 0)
self.assertEqual(adc.get_ymax(), 0)
self.assertEqual(adc.get_yminpos(), 0)
def test_constructor_coerces_lists(self):
"""Plain lists passed to the constructor must become numpy arrays."""
x = [0.0, 1.0, 2.0]
y = [[10, 20, 30], [15, 25, 35]]
index = [(0, 2)]
adc = ADC_Result(x, y, index, 1000.0, {"key": "value"}, 1, datetime(2023, 1, 1))
self.assertIsInstance(adc.x, np.ndarray)
np.testing.assert_array_equal(adc.x, [0.0, 1.0, 2.0])
for ch in adc.y:
self.assertIsInstance(ch, np.ndarray)
np.testing.assert_array_equal(adc.y[0], [10, 20, 30])
np.testing.assert_array_equal(adc.y[1], [15, 25, 35])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+249 -1
View File
@@ -1,5 +1,6 @@
import os import os
import subprocess import subprocess
import threading
import unittest import unittest
from datetime import datetime from datetime import datetime
@@ -258,7 +259,7 @@ class TestAccumulation(unittest.TestCase):
np.testing.assert_array_equal(accu.y[i], y[i] - 10) np.testing.assert_array_equal(accu.y[i], y[i] - 10)
# test float subtraction # test float subtraction
adc -= 10. accu -= 10.
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(accu.y[i], y[i] - 10 - 10.) np.testing.assert_array_equal(accu.y[i], y[i] - 10 - 10.)
@@ -312,5 +313,252 @@ class TestAccumulation(unittest.TestCase):
self.assertRaises(ValueError, accu.__floordiv__, 1.0) self.assertRaises(ValueError, accu.__floordiv__, 1.0)
self.assertRaises(ValueError, accu.__ifloordiv__, 1.0) self.assertRaises(ValueError, accu.__ifloordiv__, 1.0)
def test_add_invalid_operand_raises(self):
"""accu + bogus must raise ValueError instead of returning None."""
adc = self.create_adc_result()
accu = Accumulation()
accu += adc
self.assertRaises(ValueError, accu.__add__, "foo")
self.assertRaises(ValueError, accu.__iadd__, "foo")
def test_neg_empty_raises(self):
"""-empty accumulation must raise ValueError."""
self.assertRaises(ValueError, Accumulation().__neg__)
def test_add_adc_result_does_not_modify_job_ids(self):
"""accu + adc must not insert the adc job id into accu.job_ids."""
accu = Accumulation()
accu += self.create_adc_result()
n_before = len(accu.job_ids)
adc2 = self.create_adc_result()
adc2.job_id = 12
r = accu + adc2
self.assertIn(12, r.job_ids)
self.assertNotIn(12, accu.job_ids)
self.assertEqual(len(accu.job_ids), n_before)
def test_empty_plus_accumulation_copies_data(self):
"""empty_accu + accu must copy arrays and metadata, not alias them."""
adc = self.create_adc_result()
accu = Accumulation()
accu += adc
accu.is_clipped = True
r = Accumulation() + accu
self.assertIsNot(r.y[0], accu.y[0])
self.assertIsNot(r.job_ids, accu.job_ids)
self.assertTrue(r.is_clipped)
# in-place operation on the result must leave the operand unchanged
r *= 2
np.testing.assert_array_equal(accu.y[0], np.array([10, 20, 30], dtype="float32"))
def test_accumulation_plus_accumulation_keeps_both_job_ids(self):
"""accu1 + accu2 must keep the provenance of both operands."""
adc1 = self.create_adc_result()
adc2 = self.create_adc_result()
adc2.job_id = 12
accu1 = Accumulation()
accu1 += adc1
accu2 = Accumulation()
accu2 += adc2
r = accu1 + accu2
self.assertEqual(set(r.job_ids.keys()), {11, 12})
self.assertEqual(set(accu1.job_ids.keys()), {11})
self.assertEqual(set(accu2.job_ids.keys()), {12})
def test_empty_error_plus_non_error_raises(self):
"""error accumulation + non-error accumulation must raise ValueError."""
accu = Accumulation()
accu += self.create_adc_result()
empty_err = Accumulation(error=True)
self.assertRaises(ValueError, empty_err.__add__, accu)
free = []
def check_lock():
if empty_err.lock.acquire(timeout=1):
free.append(True)
empty_err.lock.release()
t = threading.Thread(target=check_lock)
t.start()
t.join(3)
self.assertTrue(free, "lock leaked after ValueError in __add__")
def test_neg_keeps_y_square_as_list(self):
"""-accu must store y_square as a list of per-channel arrays."""
x = np.array([0.0, 1.0, 2.0])
y = [np.array([10, 20, 30], dtype="float32")]
y_square = [np.array([100.0, 400.0, 900.0], dtype="float32")]
accu = Accumulation(x=x, y=y, y_2=y_square, n=2, index=[(0, 2)], sampl_freq=1000.0, error=True)
r = -accu
self.assertIsInstance(r.y_square, list)
# sums of squares are invariant under negation
np.testing.assert_array_equal(r.get_ysquare(0), y_square[0])
def test_iadd_adc_after_ndarray_y_2(self):
"""empty accu with ndarray y_square += adc must not crash (was AttributeError)."""
y_square_2d = np.array([[100.0, 400.0, 900.0]], dtype="float32")
accu = Accumulation(y_2=y_square_2d, error=True)
accu += self.create_adc_result()
self.assertIsInstance(accu.y_square, list)
np.testing.assert_array_equal(accu.get_ydata(0), np.array([10, 20, 30], dtype="float32"))
np.testing.assert_array_equal(accu.get_ysquare(0), np.array([10, 20, 30], dtype="float32") ** 2)
def test_iadd_accumulation_after_ndarray_y_2(self):
"""empty accu with ndarray y_square += accu must not crash (was AttributeError)."""
full = Accumulation(error=True)
full += self.create_adc_result()
y_square_2d = np.array([[100.0, 400.0, 900.0]], dtype="float32")
accu = Accumulation(y_2=y_square_2d, error=True)
accu += full
self.assertIsInstance(accu.y_square, list)
np.testing.assert_array_equal(accu.get_ydata(0), np.array([10, 20, 30], dtype="float32"))
np.testing.assert_array_equal(accu.get_ysquare(0), np.array([10, 20, 30], dtype="float32") ** 2)
def test_constructor_normalizes_ndarray_y_2(self):
"""an ndarray passed as y_2 must be stored as a list of channel arrays."""
x = np.array([0.0, 1.0, 2.0])
y = [np.array([10, 20, 30], dtype="float32")]
y_square_2d = np.array([[100.0, 400.0, 900.0]], dtype="float32")
accu = Accumulation(x=x, y=y, y_2=y_square_2d, n=2, index=[(0, 2)], sampl_freq=1000.0, error=True)
self.assertIsInstance(accu.y_square, list)
np.testing.assert_array_equal(accu.get_ysquare(0), y_square_2d[0])
def test_add_accumulations_without_time_period(self):
"""accu + accu without time information must not crash."""
accu1 = Accumulation()
accu1 += self.create_adc_result()
accu2 = Accumulation()
accu2 += self.create_adc_result()
accu1.time_period = None
accu2.time_period = []
r = accu1 + accu2
self.assertIsNone(r.time_period)
def test_iadd_accumulation_without_time_period(self):
"""accu += accu without time information must not crash (was IndexError)."""
accu1 = Accumulation()
accu1 += self.create_adc_result()
accu2 = Accumulation()
accu2 += self.create_adc_result()
accu1.time_period = []
accu2.time_period = []
accu1 += accu2
self.assertIsNone(accu1.time_period)
def test_add_adc_without_time_period(self):
"""accu + adc without time information must not crash."""
accu = Accumulation()
accu += self.create_adc_result()
accu.time_period = []
r = accu + self.create_adc_result()
self.assertIsNone(r.time_period)
def test_time_period_merged_on_add(self):
"""accu + accu with both time periods set merges to min/max."""
accu1 = Accumulation()
accu1 += self.create_adc_result()
accu2 = Accumulation()
accu2 += self.create_adc_result()
accu1.time_period = [datetime(2023, 1, 3), datetime(2023, 1, 5)]
accu2.time_period = [datetime(2023, 1, 2), datetime(2023, 1, 7)]
r = accu1 + accu2
self.assertEqual(r.time_period, [datetime(2023, 1, 2), datetime(2023, 1, 7)])
def test_time_period_merged_with_adc_job_date(self):
"""accu + adc merges self.time_period with other.job_date."""
accu = Accumulation()
accu += self.create_adc_result()
accu.time_period = [datetime(2023, 1, 3), datetime(2023, 1, 5)]
adc = self.create_adc_result()
adc.job_date = datetime(2023, 1, 1)
r = accu + adc
self.assertEqual(r.time_period, [datetime(2023, 1, 1), datetime(2023, 1, 5)])
def test_iadd_adc_without_job_date_sets_time_period_none(self):
"""empty accu += adc without job_date gets time_period None."""
accu = Accumulation()
adc = self.create_adc_result()
adc.job_date = None
accu += adc
self.assertIsNone(accu.time_period)
def test_get_accu_by_index_without_time_period(self):
"""get_accu_by_index without time information must not crash."""
accu = Accumulation()
adc = self.create_adc_result()
adc.index = [(0, 1), (1, 2)]
accu += adc
accu.time_period = None
r = accu.get_accu_by_index(0)
self.assertIsNone(r.time_period)
def test_write_to_tecmag_without_time_period_raises(self):
"""write_to_tecmag without time information must raise ValueError."""
accu = Accumulation()
accu += self.create_adc_result()
accu.time_period = None
self.assertRaises(ValueError, accu.write_to_tecmag, "/tmp/damaris_tecmag_no_time.tnt")
self.assertTrue(accu.lock.acquire(timeout=1), "lock leaked after ValueError in write_to_tecmag")
accu.lock.release()
def test_empty_state_accessors(self):
"""Empty Accumulation accessors must return ndarrays, min/max helpers 0."""
accu = Accumulation()
self.assertIsInstance(accu.x, np.ndarray)
self.assertEqual(accu.x.size, 0)
self.assertIsInstance(accu.get_ydata(), np.ndarray)
self.assertEqual(accu.get_ydata().size, 0)
self.assertIsInstance(accu.get_yerr(0), np.ndarray)
self.assertEqual(accu.get_yerr(0).size, 0)
self.assertEqual(accu.get_xmin(), 0)
self.assertEqual(accu.get_xmax(), 0)
self.assertEqual(accu.get_ymin(), 0)
self.assertEqual(accu.get_ymax(), 0)
def test_get_yerr_without_statistics_on_empty(self):
"""get_yerr on an empty accumulation without statistics must not crash."""
accu = Accumulation(error=False)
err = accu.get_yerr(0)
self.assertIsInstance(err, np.ndarray)
self.assertEqual(err.size, 0)
def test_constructor_coerces_lists(self):
"""Plain lists passed to the constructor must become numpy arrays."""
x = [0.0, 1.0, 2.0]
y = [[10, 20, 30], [15, 25, 35]]
y_2 = [[100.0, 400.0, 900.0], [225.0, 625.0, 1225.0]]
accu = Accumulation(x=x, y=y, y_2=y_2, n=1,
index=[(0, 2)], sampl_freq=1000.0, error=True)
self.assertIsInstance(accu.x, np.ndarray)
np.testing.assert_array_equal(accu.x, [0.0, 1.0, 2.0])
for ch in accu.y:
self.assertIsInstance(ch, np.ndarray)
for ch in accu.y_square:
self.assertIsInstance(ch, np.ndarray)
np.testing.assert_array_equal(accu.y[0], [10, 20, 30])
np.testing.assert_array_equal(accu.y_square[0], [100.0, 400.0, 900.0])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+29
View File
@@ -0,0 +1,29 @@
import unittest
from damaris.data.Config_Result import Config_Result
class TestConfigResult(unittest.TestCase):
def test_constructor_with_no_arguments(self):
"""Test initializer with no arguments."""
cr = Config_Result()
self.assertEqual(cr.get_config_dictionary(), {})
self.assertEqual(cr.description, {})
def test_constructor_with_arguments(self):
"""Test initializer with config and description arguments."""
config = {"a": 1}
desc = {"b": 2}
cr = Config_Result(config=config, desc=desc)
self.assertEqual(cr.get_config_dictionary(), config)
self.assertEqual(cr.description, desc)
def test_get_set_config(self):
cr = Config_Result()
cr.set_config("key", "value")
self.assertEqual(cr.get_config("key"), "value")
self.assertIsNone(cr.get_config("missing"))
if __name__ == "__main__":
unittest.main()
+44
View File
@@ -0,0 +1,44 @@
import unittest
import numpy as np
from damaris.data.DamarisFFT import DamarisFFT
class TestDamarisFFT(unittest.TestCase):
@staticmethod
def make_fft():
fft = DamarisFFT()
fft.sampling_rate = 100.0
fft.x = np.arange(10, dtype="float32") / 100.0
fft.y = [np.arange(10, dtype="float32")]
fft.xlabel = "Time / s"
return fft
def test_clip_without_arguments_is_placeholder(self):
"""clip() without arguments returns self unchanged (placeholder use)."""
fft = self.make_fft()
self.assertIs(fft.clip(), fft)
self.assertEqual(len(fft.x), 10)
np.testing.assert_array_equal(fft.y[0], np.arange(10, dtype="float32"))
def test_clip_swaps_reversed_bounds(self):
fft = self.make_fft()
fft.clip(start=0.05, stop=0.01)
expected_x = (np.arange(10, dtype="float32") / 100.0)[1:5]
np.testing.assert_array_equal(fft.x, expected_x)
np.testing.assert_array_equal(fft.y[0], np.arange(1, 5, dtype="float32"))
def test_clip_single_bound(self):
fft = self.make_fft()
fft.clip(start=0.02)
self.assertEqual(len(fft.x), 7)
self.assertEqual(len(fft.y[0]), 7)
def test_clip_too_close_raises(self):
fft = self.make_fft()
self.assertRaises(ValueError, fft.clip, 0.021, 0.029)
if __name__ == "__main__":
unittest.main()
+54
View File
@@ -0,0 +1,54 @@
import unittest
import numpy as np
from damaris.data.Drawable import Drawable
class TestDrawable(unittest.TestCase):
def test_initial_types(self):
"""x must start as an empty ndarray, y as a list of arrays."""
d = Drawable()
self.assertIsInstance(d.x, np.ndarray)
self.assertEqual(d.x.size, 0)
self.assertIsInstance(d.y, list)
def test_empty_min_max_helpers(self):
"""min/max helpers must return 0 instead of raising on empty data."""
d = Drawable()
self.assertEqual(d.get_xmin(), 0)
self.assertEqual(d.get_xmax(), 0)
self.assertEqual(d.get_xminpos(), 0)
self.assertEqual(d.get_ymin(), 0)
self.assertEqual(d.get_ymax(), 0)
self.assertEqual(d.get_yminpos(), 0)
def test_min_max_helpers_with_data(self):
d = Drawable()
d.x = np.array([-1.0, 0.0, 2.0, 5.0])
d.y = [np.array([1.0, -2.0, 3.0]), np.array([0.5, 4.0, -0.5])]
self.assertEqual(d.get_xmin(), -1.0)
self.assertEqual(d.get_xmax(), 5.0)
self.assertEqual(d.get_xminpos(), 2.0)
self.assertEqual(d.get_ymin(), -2.0)
self.assertEqual(d.get_ymax(), 4.0)
self.assertEqual(d.get_yminpos(), 0.5)
def test_yminpos_without_positive_values(self):
d = Drawable()
d.x = np.array([1.0])
d.y = [np.array([-1.0, -2.0])]
self.assertEqual(d.get_yminpos(), 0)
def test_set_xdata_set_ydata(self):
d = Drawable()
d.x = np.zeros(3)
d.y = [np.zeros(3)]
d.set_xdata(1, 5.0)
d.set_ydata(0, 1, 7.0)
self.assertEqual(d.get_xdata()[1], 5.0)
self.assertEqual(d.get_ydata(0)[1], 7.0)
if __name__ == "__main__":
unittest.main()
+23
View File
@@ -0,0 +1,23 @@
import unittest
import numpy as np
from damaris.data.Error_Result import Error_Result
class TestErrorResult(unittest.TestCase):
def test_plotdata_are_ndarrays(self):
"""get_xdata/get_ydata must return numpy arrays like every Drawable."""
er = Error_Result("core failed")
self.assertIsInstance(er.get_xdata(), np.ndarray)
self.assertIsInstance(er.get_ydata(), np.ndarray)
np.testing.assert_array_equal(er.get_xdata(), [0.0])
np.testing.assert_array_equal(er.get_ydata(), [0.0])
def test_add_returns_self(self):
er = Error_Result("msg")
self.assertIs(er + 1, er)
if __name__ == "__main__":
unittest.main()
+70
View File
@@ -0,0 +1,70 @@
import sys
import threading
import unittest
from damaris.gui.ExperimentHandling import ExperimentHandling
from damaris.gui.ResultHandling import ResultHandling
class _FakeWriter:
def __init__(self):
self.sent = []
def send_next(self, job, quit=False):
self.sent.append(job)
class _FakeResultIterator:
def __init__(self):
self.quit_flag = threading.Event()
def __iter__(self):
return iter([])
class TestSandboxStdoutRestore(unittest.TestCase):
"""
sys.stdout/sys.stderr are process-global: the sandbox redirect in
run() must be undone even when the script raises or returns early.
"""
def test_experiment_stdout_restored_after_script_exception(self):
orig_stdout = sys.stdout
orig_stderr = sys.stderr
handled = ExperimentHandling("raise ValueError('boom')", None, None,
log_callback=lambda line: None)
handled.run()
self.assertIs(sys.stdout, orig_stdout)
self.assertIs(sys.stderr, orig_stderr)
self.assertIsNotNone(handled.raised_exception)
def test_experiment_stdout_restored_on_success(self):
orig_stdout = sys.stdout
writer = _FakeWriter()
handled = ExperimentHandling("print('hello')\nexperiment = lambda: iter([])",
writer, None, log_callback=lambda line: None)
handled.run()
self.assertIs(sys.stdout, orig_stdout)
self.assertIsNone(handled.raised_exception)
def test_result_stdout_restored_after_script_exception(self):
orig_stdout = sys.stdout
orig_stderr = sys.stderr
handling = ResultHandling("raise ValueError('boom')", _FakeResultIterator(),
None, log_callback=lambda line: None)
handling.run()
self.assertIs(sys.stdout, orig_stdout)
self.assertIs(sys.stderr, orig_stderr)
self.assertIsNotNone(handling.raised_exception)
def test_result_stdout_restored_without_result_function(self):
orig_stdout = sys.stdout
handling = ResultHandling("x = 1", _FakeResultIterator(), None,
log_callback=lambda line: None)
handling.run()
self.assertIs(sys.stdout, orig_stdout)
self.assertIsNone(handling.raised_exception)
if __name__ == "__main__":
unittest.main()
+86
View File
@@ -0,0 +1,86 @@
import io
import os
import tempfile
import unittest
import numpy as np
import tables
from damaris.data.MeasurementResult import MeasurementResult, read_from_hdf
class TestMeasurementResult(unittest.TestCase):
def test_add_zero_preserves_data(self):
"""mr + 0 must return a MeasurementResult with identical data."""
mr = MeasurementResult("test_quantity")
for i in range(3):
mr[i] = float(i * i)
mr2 = mr + 0
self.assertIsInstance(mr2, MeasurementResult)
self.assertEqual(mr2.quantity_name, "test_quantity")
np.testing.assert_array_equal(mr2.get_xdata(), mr.get_xdata())
np.testing.assert_array_equal(mr2.get_ydata(), mr.get_ydata())
def test_add_zero_returns_independent_copy(self):
"""Changing the copy must not change the original."""
mr = MeasurementResult("q")
mr[1.0] = 5.0
mr2 = mr + 0
self.assertIsNot(mr2[1.0], mr[1.0])
mr2[1.0] = mr2[1.0] + 100.0
self.assertEqual(mr[1.0].mean(), 5.0)
def test_add_nonzero_raises(self):
mr = MeasurementResult("q")
self.assertRaises(Exception, mr.__add__, 1)
def test_get_lineplotdata_returns_means(self):
"""get_lineplotdata must return the accumulated means, not self.y."""
mr = MeasurementResult("test_quantity")
for i in range(3):
mr[float(i)] = float(i * i)
k, v = mr.get_lineplotdata()
self.assertIsInstance(k, np.ndarray)
self.assertIsInstance(v, np.ndarray)
self.assertEqual(k.shape, v.shape)
np.testing.assert_array_equal(v, mr.get_ydata())
np.testing.assert_array_equal(v, [0.0, 1.0, 4.0])
def test_get_lineplotdata_empty(self):
"""get_lineplotdata of an empty result must return empty arrays."""
mr = MeasurementResult("q")
k, v = mr.get_lineplotdata()
self.assertEqual(k.shape[0], 0)
self.assertEqual(v.shape[0], 0)
def test_write_to_csv(self):
mr = MeasurementResult("q")
mr[1.0] = 5.0
mr[2.0] = 7.0
buf = io.StringIO()
mr.write_to_csv(buf)
lines = buf.getvalue().splitlines()
self.assertEqual(lines[0], "# quantity:q")
self.assertEqual(lines[1], "# x y ysigma n")
self.assertEqual(len(lines), 4)
self.assertEqual(lines[2].split()[3], "1")
self.assertEqual(lines[3].split()[3], "1")
def test_write_to_hdf_roundtrip(self):
mr = MeasurementResult("q")
mr[1.0] = 5.0
mr[2.0] = 7.0
with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, "test.h5")
with tables.open_file(path, mode="w") as h5file:
mr.write_to_hdf(h5file, "/", "mr", "test result")
with tables.open_file(path, mode="r") as h5file:
mr2 = read_from_hdf(h5file.get_node("/mr"))
self.assertIsNotNone(mr2)
self.assertEqual(mr2.quantity_name, "q")
np.testing.assert_array_equal(mr2.get_xdata(), mr.get_xdata())
np.testing.assert_array_equal(mr2.get_ydata(), mr.get_ydata())
if __name__ == "__main__":
unittest.main()
Generated
+27
View File
@@ -938,6 +938,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
] ]
[[package]]
name = "pyrefly"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b3/3e/ea6893be167fa14307d7f9a924bed9ecbbbd91b01953890b9b3c58ed5359/pyrefly-1.4.0.tar.gz", hash = "sha256:bb57c004a4842cfe93f93aebb5203ca6209a5f0c8bda47e8bc289e70940fd910", size = 6962062, upload-time = "2026-10-15T16:54:11.819Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/17/dfce6611acc00cb26667e5e1011438cc510f3067f961e0f07f7a0c68f416/pyrefly-1.4.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:460cec39bc8743915e9c2c8c10ba926ad0b29b099d89c1859fb97e0059f42fa7", size = 15705624, upload-time = "2026-10-15T16:53:43.412Z" },
{ url = "https://files.pythonhosted.org/packages/a6/78/a7b96442123fa6ed733de0d1ea933e35d6fc6b7c9cf16e05279abb493493/pyrefly-1.4.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5a6370d686b6cbbac8869832f09a52012888371956fbc68d446a349bc85ccd43", size = 14978669, upload-time = "2026-10-15T16:53:46.546Z" },
{ url = "https://files.pythonhosted.org/packages/23/97/dfcfec6a9d8948ab4b2746f7e87558a82bffabb7a54c22e7d3ce4bdc1932/pyrefly-1.4.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f1b16923a6583a6764e8aec07787e2ef4df644c7030294cba71339783a9451c", size = 15438858, upload-time = "2026-10-15T16:53:48.904Z" },
{ url = "https://files.pythonhosted.org/packages/31/12/6fa414aa87e398d6ddc7145a5d993b0975fc3511198dbb72c47873653ab6/pyrefly-1.4.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:688ad4cb4d6e711d67026fdcfb37b599f86061efe11ba5704287330e7041a69e", size = 16852241, upload-time = "2026-10-15T16:53:51.496Z" },
{ url = "https://files.pythonhosted.org/packages/a8/73/2d7312262cf6481d6ad46eed55b8f2592c3cff60f95cc2f30a114cd1484e/pyrefly-1.4.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5f3c9dddb6f3128beb49cf4e0f5cb88d4d17f039008d8e0c2be653966acd234", size = 16751917, upload-time = "2026-10-15T16:53:54.39Z" },
{ url = "https://files.pythonhosted.org/packages/2c/65/b44a1cb23667bcbcdc88558cbef79fa9e07a1152c2a1da148036b1a2e0d1/pyrefly-1.4.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6951f51687304caebfedf13dd1a943aeafdbc95abcc4e24a683b97467a77007", size = 16066345, upload-time = "2026-10-15T16:53:57.436Z" },
{ url = "https://files.pythonhosted.org/packages/a6/07/7ca6ff0c53e3a90ecdab05790d33f430fca5c48687613a0b182744166d98/pyrefly-1.4.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:838de4d4a8b9b8ed39bedd0b8b55629c69661d73a6fa3a3a4adff24b9dbeb399", size = 15461223, upload-time = "2026-10-15T16:54:00.364Z" },
{ url = "https://files.pythonhosted.org/packages/61/be/ec7b2f38a898ce5b2219dda730e87d31fc8cfb2108a3d5844f9d590f1281/pyrefly-1.4.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:afb9ce2e0ae3e6106fb45f6d47536ac92b6423e601e08401cdefa850066444f5", size = 16119099, upload-time = "2026-10-15T16:54:02.664Z" },
{ url = "https://files.pythonhosted.org/packages/6e/16/1b9549168b38a575d9c168e17b4b4ee30028d602ec0f7b07c198ef265dbe/pyrefly-1.4.0-py3-none-win32.whl", hash = "sha256:839db536d0ac29f1b23e2d974fb911075c2283e3e623755c45d826aafac175bc", size = 14858880, upload-time = "2026-10-15T16:54:05.114Z" },
{ url = "https://files.pythonhosted.org/packages/03/02/18f075d4de0e255f184f42fc28c1f0ecc9f49dd528678479fc90cf12246c/pyrefly-1.4.0-py3-none-win_amd64.whl", hash = "sha256:7aff37c2b711c8240ed650da0c6148237b327bf7e5dfacee65884f1adea22305", size = 15726220, upload-time = "2026-10-15T16:54:07.452Z" },
{ url = "https://files.pythonhosted.org/packages/92/62/92947076681550904c72a9dd03b24e92694ede9a48548f5e487c849e66ea/pyrefly-1.4.0-py3-none-win_arm64.whl", hash = "sha256:88879f0cb749e5fea81d9ff4c2d5431a70349c458bcdf0b61de72c79214d90a5", size = 14943690, upload-time = "2026-10-15T16:54:09.689Z" },
]
[[package]] [[package]]
name = "pyserial" name = "pyserial"
version = "3.5" version = "3.5"
@@ -976,6 +995,11 @@ dependencies = [
{ name = "tables" }, { name = "tables" },
] ]
[package.dev-dependencies]
dev = [
{ name = "pyrefly" },
]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "matplotlib", specifier = ">=1.4.1,<=3.6" }, { name = "matplotlib", specifier = ">=1.4.1,<=3.6" },
@@ -990,6 +1014,9 @@ requires-dist = [
{ name = "tables", specifier = ">=3.1.1" }, { name = "tables", specifier = ">=3.1.1" },
] ]
[package.metadata.requires-dev]
dev = [{ name = "pyrefly" }]
[[package]] [[package]]
name = "pyxdg" name = "pyxdg"
version = "0.28" version = "0.28"