Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e42205a4d | ||
|
|
e5e1a111d8 | ||
|
|
458715c19a | ||
|
|
18e085d3ee | ||
|
|
fe65073988 | ||
|
|
9cc449d6e1 | ||
|
|
ab943c30ae | ||
|
|
947c4c7f8d | ||
|
|
35d040426b | ||
|
|
f18bbdf48c | ||
|
|
6d46e8ca1a | ||
|
|
6987ee31b7 | ||
|
|
804996cbd4 | ||
|
|
27d05496ab | ||
|
|
c797aeb387 | ||
|
|
e5472484cb | ||
|
|
f450075d24 | ||
|
|
0ca78cc566 | ||
|
|
da1dc4a088 | ||
|
|
efb01250c8 | ||
|
|
f9d8de7697 | ||
|
|
91ade4376c | ||
|
|
c9af33bff0 | ||
|
|
1f6763f9de | ||
|
|
6668ac5b2b | ||
|
|
ee453c03a7 | ||
|
|
1fb56e20f9 | ||
|
|
ef11824f8b | ||
|
|
8d7d241ec7 | ||
|
|
e963c2ef3c | ||
|
|
bafbc1b1a0 | ||
|
|
cb5752d20c | ||
|
|
227ddda684 | ||
|
|
6d598e9da9 | ||
|
|
d8d7b9853a | ||
|
|
ddb3f00abb | ||
|
|
8bd088d97f | ||
|
|
f06af28827 | ||
|
|
b6ca2cbba3 | ||
|
|
acd3352967 | ||
|
|
7d6b7855e3 | ||
|
|
442db8e287 | ||
|
|
5ca54b7c39 | ||
|
|
523c544750 | ||
|
|
f048e0dced | ||
|
|
8f89778434 | ||
|
|
9b9a0ec1df | ||
|
|
1bb92a9157 | ||
|
|
0eb44039f0 | ||
|
|
fa064c1727 | ||
|
|
696cdfb556 | ||
|
|
65649bb307 | ||
|
|
6f7ca7ecaf | ||
|
|
b04a4274e0 | ||
|
|
02409488b8 | ||
|
|
34bbd68894 | ||
|
|
ebc2daf170 | ||
|
|
a75917efd5 | ||
|
|
3d847588c4 | ||
|
|
a1323a7a5d | ||
|
|
1c3527b945 | ||
|
|
75863ffc19 | ||
|
|
6f0b090d9d | ||
|
|
d9f2cc3f2c | ||
|
|
43cfaf633e | ||
|
|
5ac56446b3 | ||
|
|
7dc3f6560e | ||
|
|
e4cdea3b07 | ||
|
|
eaf23abec6 | ||
|
|
6efcc7ce0c | ||
|
|
97f811d904 | ||
|
|
e9e3f5a5f5 | ||
|
|
9441b5a4d9 | ||
|
|
bf2706ec80 | ||
|
|
baaf6b90b6 | ||
|
|
3c069a559d | ||
|
|
4e833fe49b | ||
|
|
5cd8df89be | ||
|
|
ef6bd32d68 | ||
|
|
7f29f2d173 | ||
|
|
847ab4d89c | ||
|
|
d6da2f2bda | ||
|
|
5338304ee8 | ||
|
|
d48f96e498 | ||
|
|
058716f3c0 | ||
|
|
6d9f55f788 | ||
|
|
e8d0e45bfb | ||
|
|
08e1fe2a1a | ||
|
|
064036241b | ||
|
|
97aa99c239 | ||
|
|
f26d203e79 | ||
|
|
594c0ee8d4 | ||
|
|
68da261ab4 | ||
|
|
00466cb559 | ||
|
|
7ba5c7d56a | ||
|
|
c181e15aa8 | ||
|
|
26970ba201 | ||
|
|
eab49db8d4 | ||
|
|
3bfb2670eb | ||
|
|
83a01f1d83 |
No files matched your search
@@ -2,30 +2,27 @@ 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:
|
||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
target: [debian11, debian12, debian13]
|
target: [debian12, debian13]
|
||||||
include:
|
include:
|
||||||
- target: debian11
|
|
||||||
codename: bullseye
|
|
||||||
- target: debian12
|
- target: debian12
|
||||||
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
|
||||||
@@ -33,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
|
||||||
|
|
||||||
+16
@@ -6,3 +6,19 @@ __pycache__
|
|||||||
*.egg-info
|
*.egg-info
|
||||||
*.whl
|
*.whl
|
||||||
.idea
|
.idea
|
||||||
|
debian/files
|
||||||
|
.pybuild
|
||||||
|
.junie
|
||||||
|
debian/debhelper-build-stamp
|
||||||
|
debian/.debhelper
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
doc/_build/
|
||||||
|
doc/_autosummary/
|
||||||
|
doc/damaris/
|
||||||
|
|
||||||
|
# Debian build artifacts
|
||||||
|
debian/*.debhelper.log
|
||||||
|
debian/*.debhelper
|
||||||
|
debian/*.substvars
|
||||||
|
debian/python3-damaris/
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# python3-damaris
|
||||||
|
|
||||||
|
## GUI Purpose and Operation
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
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
|
||||||
|
3. Be aware the targeted Debian versions are bookworm, trixie, and forky.
|
||||||
|
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.
|
||||||
|
5. Do not commit changes (with git) or write commit messages without asking the user.
|
||||||
|
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.
|
||||||
|
7. Use the python unittest module for unit tests, not pytest
|
||||||
|
8. Functions for the experiment script belong to the src/damaris/experiments/Experiment.py file
|
||||||
|
9. Functions for the result script belong in the files in the src/damaris/data/ folder
|
||||||
|
10. The gitea workflow can be found at .gitea/workflows/debian-build.yml
|
||||||
|
11. Do not edit anything in "build", it is regenerated
|
||||||
|
12. Use sphinx for documentation, autodoc and autosummary is used for references of all classes, functions and modules.
|
||||||
|
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.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Developing
|
||||||
|
|
||||||
|
## Version bump
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||||
|
<https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your
|
||||||
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
License is intended to guarantee your freedom to share and change free
|
||||||
|
software--to make sure the software is free for all its users. This
|
||||||
|
General Public License applies to most of the Free Software
|
||||||
|
Foundation's software and to any other program whose authors commit to
|
||||||
|
using it. (Some other Free Software Foundation software is covered by
|
||||||
|
the GNU Lesser General Public License instead.) You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
this service if you wish), that you receive source code or can get it
|
||||||
|
if you want it, that you can change the software or use pieces of it
|
||||||
|
in new free programs; and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid
|
||||||
|
anyone to deny you these rights or to ask you to surrender the rights.
|
||||||
|
These restrictions translate to certain responsibilities for you if you
|
||||||
|
distribute copies of the software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must give the recipients all the rights that
|
||||||
|
you have. You must make sure that they, too, receive or can get the
|
||||||
|
source code. And you must show them these terms so they know their
|
||||||
|
rights.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and
|
||||||
|
(2) offer you this license which gives you legal permission to copy,
|
||||||
|
distribute and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain
|
||||||
|
that everyone understands that there is no warranty for this free
|
||||||
|
software. If the software is modified by someone else and passed on, we
|
||||||
|
want its recipients to know that what they have is not the original, so
|
||||||
|
that any problems introduced by others will not reflect on the original
|
||||||
|
authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software
|
||||||
|
patents. We wish to avoid the danger that redistributors of a free
|
||||||
|
program will individually obtain patent licenses, in effect making the
|
||||||
|
program proprietary. To prevent this, we have made it clear that any
|
||||||
|
patent must be licensed for everyone's free use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains
|
||||||
|
a notice placed by the copyright holder saying it may be distributed
|
||||||
|
under the terms of this General Public License. The "Program", below,
|
||||||
|
refers to any such program or work, and a "work based on the Program"
|
||||||
|
means either the Program or any derivative work under copyright law:
|
||||||
|
that is to say, a work containing the Program or a portion of it,
|
||||||
|
either verbatim or with modifications and/or translated into another
|
||||||
|
language. (Hereinafter, translation is included without limitation in
|
||||||
|
the term "modification".) Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of
|
||||||
|
running the Program is not restricted, and the output from the Program
|
||||||
|
is covered only if its contents constitute a work based on the
|
||||||
|
Program (independent of having been made by running the Program).
|
||||||
|
Whether that is true depends on what the Program does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Program's
|
||||||
|
source code as you receive it, in any medium, provided that you
|
||||||
|
conspicuously and appropriately publish on each copy an appropriate
|
||||||
|
copyright notice and disclaimer of warranty; keep intact all the
|
||||||
|
notices that refer to this License and to the absence of any warranty;
|
||||||
|
and give any other recipients of the Program a copy of this License
|
||||||
|
along with the Program.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and
|
||||||
|
you may at your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion
|
||||||
|
of it, thus forming a work based on the Program, and copy and
|
||||||
|
distribute such modifications or work under the terms of Section 1
|
||||||
|
above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices
|
||||||
|
stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in
|
||||||
|
whole or in part contains or is derived from the Program or any
|
||||||
|
part thereof, to be licensed as a whole at no charge to all third
|
||||||
|
parties under the terms of this License.
|
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively
|
||||||
|
when run, you must cause it, when started running for such
|
||||||
|
interactive use in the most ordinary way, to print or display an
|
||||||
|
announcement including an appropriate copyright notice and a
|
||||||
|
notice that there is no warranty (or else, saying that you provide
|
||||||
|
a warranty) and that users may redistribute the program under
|
||||||
|
these conditions, and telling the user how to view a copy of this
|
||||||
|
License. (Exception: if the Program itself is interactive but
|
||||||
|
does not normally print such an announcement, your work based on
|
||||||
|
the Program is not required to print an announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Program,
|
||||||
|
and can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based
|
||||||
|
on the Program, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the
|
||||||
|
entire whole, and thus to each and every part regardless of who wrote it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or
|
||||||
|
collective works based on the Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program
|
||||||
|
with the Program (or with a work based on the Program) on a volume of
|
||||||
|
a storage or distribution medium does not bring the other work under
|
||||||
|
the scope of this License.
|
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it,
|
||||||
|
under Section 2) in object code or executable form under the terms of
|
||||||
|
Sections 1 and 2 above provided that you also do one of the following:
|
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable
|
||||||
|
source code, which must be distributed under the terms of Sections
|
||||||
|
1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three
|
||||||
|
years, to give any third party, for a charge no more than your
|
||||||
|
cost of physically performing source distribution, a complete
|
||||||
|
machine-readable copy of the corresponding source code, to be
|
||||||
|
distributed under the terms of Sections 1 and 2 above on a medium
|
||||||
|
customarily used for software interchange; or,
|
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer
|
||||||
|
to distribute corresponding source code. (This alternative is
|
||||||
|
allowed only for noncommercial distribution and only if you
|
||||||
|
received the program in object code or executable form with such
|
||||||
|
an offer, in accord with Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For an executable work, complete source
|
||||||
|
code means all the source code for all modules it contains, plus any
|
||||||
|
associated interface definition files, plus the scripts used to
|
||||||
|
control compilation and installation of the executable. However, as a
|
||||||
|
special exception, the source code distributed need not include
|
||||||
|
anything that is normally distributed (in either source or binary
|
||||||
|
form) with the major components (compiler, kernel, and so on) of the
|
||||||
|
operating system on which the executable runs, unless that component
|
||||||
|
itself accompanies the executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering
|
||||||
|
access to copy from a designated place, then offering equivalent
|
||||||
|
access to copy the source code from the same place counts as
|
||||||
|
distribution of the source code, even though third parties are not
|
||||||
|
compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program
|
||||||
|
except as expressly provided under this License. Any attempt
|
||||||
|
otherwise to copy, modify, sublicense or distribute the Program is
|
||||||
|
void, and will automatically terminate your rights under this License.
|
||||||
|
However, parties who have received copies, or rights, from you under
|
||||||
|
this License will not have their licenses terminated so long as such
|
||||||
|
parties remain in full compliance.
|
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not
|
||||||
|
signed it. However, nothing else grants you permission to modify or
|
||||||
|
distribute the Program or its derivative works. These actions are
|
||||||
|
prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
modifying or distributing the Program (or any work based on the
|
||||||
|
Program), you indicate your acceptance of this License to do so, and
|
||||||
|
all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Program or works based on it.
|
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the
|
||||||
|
Program), the recipient automatically receives a license from the
|
||||||
|
original licensor to copy, distribute or modify the Program subject to
|
||||||
|
these terms and conditions. You may not impose any further
|
||||||
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
You are not responsible for enforcing compliance by third parties to
|
||||||
|
this License.
|
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot
|
||||||
|
distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you
|
||||||
|
may not distribute the Program at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Program by
|
||||||
|
all those who receive copies directly or indirectly through you, then
|
||||||
|
the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under
|
||||||
|
any particular circumstance, the balance of the section is intended to
|
||||||
|
apply and the section as a whole is intended to apply in other
|
||||||
|
circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any
|
||||||
|
such claims; this section has the sole purpose of protecting the
|
||||||
|
integrity of the free software distribution system, which is
|
||||||
|
implemented by public license practices. Many people have made
|
||||||
|
generous contributions to the wide range of software distributed
|
||||||
|
through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
to distribute software through any other system and a licensee cannot
|
||||||
|
impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to
|
||||||
|
be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Program under this License
|
||||||
|
may add an explicit geographical distribution limitation excluding
|
||||||
|
those countries, so that distribution is permitted only in or among
|
||||||
|
countries not thus excluded. In such case, this License incorporates
|
||||||
|
the limitation as if written in the body of this License.
|
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program
|
||||||
|
specifies a version number of this License which applies to it and "any
|
||||||
|
later version", you have the option of following the terms and conditions
|
||||||
|
either of that version or of any later version published by the Free
|
||||||
|
Software Foundation. If the Program does not specify a version number of
|
||||||
|
this License, you may choose any version ever published by the Free Software
|
||||||
|
Foundation.
|
||||||
|
|
||||||
|
10. If you wish to incorporate parts of the Program into other free
|
||||||
|
programs whose distribution conditions are different, write to the author
|
||||||
|
to ask for permission. For software which is copyrighted by the Free
|
||||||
|
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||||
|
make exceptions for this. Our decision will be guided by the two goals
|
||||||
|
of preserving the free status of all derivatives of our free software and
|
||||||
|
of promoting the sharing and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||||
|
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||||
|
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||||
|
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||||
|
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||||
|
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||||
|
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||||
|
REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||||
|
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||||
|
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||||
|
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||||
|
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||||
|
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
convey the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along
|
||||||
|
with this program; if not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program is interactive, make it output a short notice like this
|
||||||
|
when it starts in an interactive mode:
|
||||||
|
|
||||||
|
Gnomovision version 69, Copyright (C) year name of author
|
||||||
|
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, the commands you use may
|
||||||
|
be called something other than `show w' and `show c'; they could even be
|
||||||
|
mouse-clicks or menu items--whatever suits your program.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||||
|
necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||||
|
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||||
|
|
||||||
|
<signature of Moe Ghoul>, 1 April 1989
|
||||||
|
Moe Ghoul, President of Vice
|
||||||
|
|
||||||
|
This General Public License does not permit incorporating your program into
|
||||||
|
proprietary programs. If your program is a subroutine library, you may
|
||||||
|
consider it more useful to permit linking proprietary applications with the
|
||||||
|
library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License.
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
# GNU General Public License, version 2
|
||||||
|
|
||||||
|
**Version 2, June 1991**
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
## Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and modification follow.
|
||||||
|
|
||||||
|
## TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
### Section 0
|
||||||
|
This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.
|
||||||
|
|
||||||
|
### Section 1
|
||||||
|
You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
### Section 2
|
||||||
|
You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
* **a)** You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.
|
||||||
|
* **b)** You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.
|
||||||
|
* **c)** If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
|
||||||
|
|
||||||
|
### Section 3
|
||||||
|
You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:
|
||||||
|
|
||||||
|
* **a)** Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
* **b)** Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
* **c)** Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
### Section 4
|
||||||
|
You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
|
||||||
|
|
||||||
|
### Section 5
|
||||||
|
You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.
|
||||||
|
|
||||||
|
### Section 6
|
||||||
|
Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.
|
||||||
|
|
||||||
|
### Section 7
|
||||||
|
If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
### Section 8
|
||||||
|
If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
|
||||||
|
|
||||||
|
### Section 9
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.
|
||||||
|
|
||||||
|
### Section 10
|
||||||
|
If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be led by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
|
||||||
|
|
||||||
|
## NO WARRANTY
|
||||||
|
|
||||||
|
### Section 11
|
||||||
|
BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
### Section 12
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
**END OF TERMS AND CONDITIONS**
|
||||||
+5
-4
@@ -1,6 +1,7 @@
|
|||||||
include src/gui/DAMARIS3.png
|
include src/damaris/gui/DAMARIS3.png
|
||||||
include src/gui/DAMARIS3.ico
|
include src/damaris/gui/DAMARIS3.ico
|
||||||
include src/gui/damaris.glade
|
include src/damaris/gui/damaris3.glade
|
||||||
include src/gui/damaris.gladep
|
include src/damaris/gui/python.xml
|
||||||
|
include scripts/damaris-cli
|
||||||
#recursive-include doc/reference-html *.html *.gif *.png *.css
|
#recursive-include doc/reference-html *.html *.gif *.png *.css
|
||||||
#recursive-include doc/tutorial-html *.html *.gif *.png *.css *.tar.gz *.sh
|
#recursive-include doc/tutorial-html *.html *.gif *.png *.css *.tar.gz *.sh
|
||||||
@@ -15,3 +15,12 @@ Alternatively you can install withoout the system-site-packages:
|
|||||||
pip install -e .
|
pip install -e .
|
||||||
|
|
||||||
This will download newer packages and install them in the venv.
|
This will download newer packages and install them in the venv.
|
||||||
|
|
||||||
|
|
||||||
|
Using uv
|
||||||
|
========
|
||||||
|
Dependicies needed:
|
||||||
|
sudo apt install libgirepository1.0-dev pkg-config
|
||||||
|
|
||||||
|
Then you can start DAMARIS3:
|
||||||
|
uv run DAMARIS3
|
||||||
Vendored
+34
-2
@@ -1,8 +1,40 @@
|
|||||||
python3-damaris (0.21) UNRELEASED; urgency=medium
|
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
|
||||||
|
|
||||||
* fixed autocomplete in the editor tabs
|
* fixed autocomplete in the editor tabs
|
||||||
|
* added clipping warning on ADC overload, ADC bit-depth settable in config
|
||||||
|
* Accumulation object keeps track of job_ids
|
||||||
|
* get_result_by_index keeps common description
|
||||||
|
|
||||||
-- Markus Rosenstihl <markus.rosenstihl@pkm.tu-darmstadt.de> Sat, 04 Jul 2026 20:00:13 +0200
|
-- Markus Rosenstihl <markus.rosenstihl@pkm.tu-darmstadt.de> Sun, 05 Jul 2026 14:09:34 +0200
|
||||||
|
|
||||||
python3-damaris (0.20) stable; urgency=medium
|
python3-damaris (0.20) stable; urgency=medium
|
||||||
|
|
||||||
|
|||||||
Vendored
+2
-2
@@ -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
|
||||||
Vendored
-2
@@ -1,2 +0,0 @@
|
|||||||
python3-damaris_0.20_all.deb science optional
|
|
||||||
python3-damaris_0.20_amd64.buildinfo science optional
|
|
||||||
Vendored
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
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/
|
||||||
|
scripts/damaris-cli usr/bin/
|
||||||
Vendored
+1
-1
@@ -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:
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
#############
|
||||||
|
API Reference
|
||||||
|
#############
|
||||||
|
|
||||||
|
This page provides an auto-generated reference of all DAMARIS classes, functions, and modules.
|
||||||
|
It is built from docstrings using Sphinx autodoc and autosummary.
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
:caption: Modules:
|
||||||
|
|
||||||
|
damaris/data
|
||||||
|
damaris/experiments
|
||||||
|
damaris/gui
|
||||||
|
damaris/tools
|
||||||
|
|
||||||
|
|
||||||
|
Data Module
|
||||||
|
===========
|
||||||
|
|
||||||
|
The data module contains result types, data handling, and signal processing classes.
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:toctree: _autosummary
|
||||||
|
:recursive:
|
||||||
|
|
||||||
|
damaris.data.ADC_Result
|
||||||
|
damaris.data.Accumulation
|
||||||
|
damaris.data.Config_Result
|
||||||
|
damaris.data.DamarisFFT
|
||||||
|
damaris.data.DataPool
|
||||||
|
damaris.data.Drawable
|
||||||
|
damaris.data.Errorable
|
||||||
|
damaris.data.Error_Result
|
||||||
|
damaris.data.FitResult
|
||||||
|
damaris.data.MeasurementResult
|
||||||
|
damaris.data.Persistance
|
||||||
|
damaris.data.Resultable
|
||||||
|
damaris.data.Signalpath
|
||||||
|
damaris.data.Temperature
|
||||||
|
damaris.data.autophase
|
||||||
|
|
||||||
|
|
||||||
|
Experiments Module
|
||||||
|
==================
|
||||||
|
|
||||||
|
The experiments module contains the Experiment class and related utilities for defining pulse sequences.
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:toctree: _autosummary
|
||||||
|
:recursive:
|
||||||
|
|
||||||
|
damaris.experiments.Experiment
|
||||||
|
damaris.experiments.DurationEstimator
|
||||||
|
damaris.experiments.dac
|
||||||
|
|
||||||
|
|
||||||
|
GUI Module
|
||||||
|
==========
|
||||||
|
|
||||||
|
The GUI module contains the graphical interface, backend driver, and experiment/result handling.
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:toctree: _autosummary
|
||||||
|
:recursive:
|
||||||
|
|
||||||
|
damaris.gui.BackendDriver
|
||||||
|
damaris.gui.DamarisGUI
|
||||||
|
damaris.gui.ExperimentHandling
|
||||||
|
damaris.gui.ExperimentWriter
|
||||||
|
damaris.gui.ResultHandling
|
||||||
|
damaris.gui.ResultReader
|
||||||
|
damaris.gui.TemperatureMonitor
|
||||||
|
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
|
||||||
|
============
|
||||||
|
|
||||||
|
The tools module contains hardware driver interfaces and utility functions.
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:toctree: _autosummary
|
||||||
|
:recursive:
|
||||||
|
|
||||||
|
damaris.tools.eurotherm
|
||||||
|
damaris.tools.flow_control
|
||||||
|
damaris.tools.goniometer
|
||||||
|
damaris.tools.lakeshore
|
||||||
|
damaris.tools.ranges
|
||||||
|
damaris.tools.signal
|
||||||
|
damaris.tools.temperature
|
||||||
|
damaris.tools.temperature_simulator
|
||||||
@@ -21,9 +21,13 @@ release = damaris.__version__
|
|||||||
|
|
||||||
extensions = [
|
extensions = [
|
||||||
'sphinx.ext.autodoc',
|
'sphinx.ext.autodoc',
|
||||||
|
'sphinx.ext.autosummary',
|
||||||
'sphinx.ext.napoleon',
|
'sphinx.ext.napoleon',
|
||||||
|
'sphinx.ext.viewcode',
|
||||||
'sphinxcontrib.bibtex',
|
'sphinxcontrib.bibtex',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
autosummary_generate = True
|
||||||
bibtex_bibfiles = ['bibdesk_db.bib']
|
bibtex_bibfiles = ['bibdesk_db.bib']
|
||||||
bibtex_bibstyles = ['numeric']
|
bibtex_bibstyles = ['numeric']
|
||||||
bibtex_citestyle = 'numeric'
|
bibtex_citestyle = 'numeric'
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
@@ -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
|
||||||
==============
|
==============
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "python3-damaris"
|
||||||
|
version = "0.25"
|
||||||
|
requires-python = ">= 3.11"
|
||||||
|
description = "Python 3 frontend for DAMARIS (DArmstadt MAgnetic Resonance Instrument Software)"
|
||||||
|
license = {text = "GPL-2.0"}
|
||||||
|
keywords = ["NMR", "data-processing"]
|
||||||
|
readme = {file = "README", content-type = "text/plain"}
|
||||||
|
dependencies = [
|
||||||
|
"scipy>=1.7",
|
||||||
|
"numpy>=1.21,<2.0",
|
||||||
|
"tables>=3.7",
|
||||||
|
"matplotlib>=3.5,<3.11",
|
||||||
|
"pyxdg",
|
||||||
|
# PyGObject must come from system packages:
|
||||||
|
# apt-get install python3-gi gir1.2-gtk-3.0 gir1.2-gtksource-3.0
|
||||||
|
]
|
||||||
|
authors = [
|
||||||
|
{name = "Achim Gädke"},
|
||||||
|
{name = "Christopher Schmitt"},
|
||||||
|
{name = "Christian Tacke"},
|
||||||
|
{name = "Joachim Beerwerth"},
|
||||||
|
{name = "Markus Rosenstihl", email = "markus.rosenstihl@pkm.tu-darmstadt.de"}
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
DAMARIS3 = "damaris.__main__:main"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
"damaris.gui" = ["DAMARIS3.png", "DAMARIS3.ico", "python.xml", "damaris3.glade"]
|
||||||
+24
-5
@@ -6,7 +6,7 @@ requires = [
|
|||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
requires-python = ">= 3.9"
|
requires-python = ">= 3.11"
|
||||||
name = "python3-damaris"
|
name = "python3-damaris"
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
description = "Python version 3 frontend for DAMARIS (DArmstadt MAgnetic Resonance Instrument Software)"
|
description = "Python version 3 frontend for DAMARIS (DArmstadt MAgnetic Resonance Instrument Software)"
|
||||||
@@ -14,12 +14,16 @@ license = {text = "GPL-2.0"}
|
|||||||
keywords = ["NMR", "data-processing"]
|
keywords = ["NMR", "data-processing"]
|
||||||
readme = {file = "README", content-type = "text/plain"}
|
readme = {file = "README", content-type = "text/plain"}
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"scipy >= 0.14.0",
|
"scipy>=0.14.0",
|
||||||
"numpy >= 1.8.2, < 2.0",
|
"numpy>=1.8.2,<2.0",
|
||||||
"tables >= 3.1.1",
|
"tables >= 3.1.1",
|
||||||
"matplotlib >= 1.4.1, <= 3.6",
|
"matplotlib >= 1.4.1, <= 3.6",
|
||||||
"pyxdg",
|
"pyxdg",
|
||||||
"PyGObject >= 3.14.0, <= 3.51.0"
|
"PyGObject >= 3.14.0, <= 3.51.0",
|
||||||
|
"pyserial>=3.5",
|
||||||
|
"sphinx>=9.0.4",
|
||||||
|
"sphinxcontrib-bibtex>=2.7.0",
|
||||||
|
"sphinx-rtd-theme>=3.1.0",
|
||||||
]
|
]
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Achim Gädke"},
|
{name = "Achim Gädke"},
|
||||||
@@ -35,17 +39,32 @@ 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"]
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
"damaris.gui" = ["DAMARIS3.png", "DAMARIS3.ico", "damaris.xml", "python.xml", "damaris3.glade"]
|
"damaris.gui" = ["DAMARIS3.png", "DAMARIS3.ico", "python.xml", "damaris3.glade"]
|
||||||
|
|
||||||
[tool.setuptools.dynamic]
|
[tool.setuptools.dynamic]
|
||||||
version = {attr = "damaris.__version__"}
|
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
@@ -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")
|
||||||
|
|||||||
Regular → Executable
+52
-32
@@ -1,9 +1,11 @@
|
|||||||
#! /usr/bin/env python
|
#! /usr/bin/env python3
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
import tables
|
import tables
|
||||||
import damaris.data.DataPool as DataPool
|
import damaris.data.DataPool as DataPool
|
||||||
import damaris.gui.ResultReader as ResultReader
|
import damaris.gui.ResultReader as ResultReader
|
||||||
@@ -12,7 +14,9 @@ import damaris.gui.BackendDriver as BackendDriver
|
|||||||
import damaris.gui.ResultHandling as ResultHandling
|
import damaris.gui.ResultHandling as ResultHandling
|
||||||
import damaris.gui.ExperimentHandling as ExperimentHandling
|
import damaris.gui.ExperimentHandling as ExperimentHandling
|
||||||
|
|
||||||
def some_listener(event):
|
logger = logging.getLogger("damaris.cli")
|
||||||
|
|
||||||
|
def progressbar(event):
|
||||||
if event.subject=="__recentexperiment" or event.subject=="__recentresult":
|
if event.subject=="__recentexperiment" or event.subject=="__recentresult":
|
||||||
r=event.origin.get("__recentresult",-1)+1
|
r=event.origin.get("__recentresult",-1)+1
|
||||||
e=event.origin.get("__recentexperiment",-1)+1
|
e=event.origin.get("__recentexperiment",-1)+1
|
||||||
@@ -20,17 +24,19 @@ def some_listener(event):
|
|||||||
ratio=100.0*r/e
|
ratio=100.0*r/e
|
||||||
else:
|
else:
|
||||||
ratio=100.0
|
ratio=100.0
|
||||||
print("\r%d/%d (%.0f%%)"%(r,e,ratio), end=' ')
|
sys.stderr.write("\r%d/%d (%.0f%%)"%(r,e,ratio))
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
|
||||||
class ScriptInterface:
|
class ScriptInterface:
|
||||||
|
|
||||||
def __init__(self, exp_script=None, res_script=None, backend_executable=None, spool_dir="spool"):
|
def __init__(self, exp_script=None, res_script=None, backend_executable=None, spool_dir="spool", log_callback=None):
|
||||||
self.exp_script=exp_script
|
self.exp_script=exp_script
|
||||||
self.res_script=res_script
|
self.res_script=res_script
|
||||||
self.backend_executable=backend_executable
|
self.backend_executable=backend_executable
|
||||||
self.spool_dir=os.path.abspath(spool_dir)
|
self.spool_dir=os.path.abspath(spool_dir)
|
||||||
self.exp_handling=self.res_handling=None
|
self.exp_handling=self.res_handling=None
|
||||||
|
self._log_callback = log_callback
|
||||||
|
|
||||||
self.exp_writer=self.res_reader=self.back_driver=None
|
self.exp_writer=self.res_reader=self.back_driver=None
|
||||||
if self.backend_executable is not None:
|
if self.backend_executable is not None:
|
||||||
@@ -48,9 +54,9 @@ class ScriptInterface:
|
|||||||
def runScripts(self):
|
def runScripts(self):
|
||||||
# get script engines
|
# get script engines
|
||||||
if self.exp_script and self.exp_writer:
|
if self.exp_script and self.exp_writer:
|
||||||
self.exp_handling=ExperimentHandling.ExperimentHandling(self.exp_script, self.exp_writer, self.data)
|
self.exp_handling=ExperimentHandling.ExperimentHandling(self.exp_script, self.exp_writer, self.data, self._log_callback)
|
||||||
if self.res_script and self.res_reader:
|
if self.res_script and self.res_reader:
|
||||||
self.res_handling=ResultHandling.ResultHandling(self.res_script, self.res_reader, self.data)
|
self.res_handling=ResultHandling.ResultHandling(self.res_script, self.res_reader, self.data, self._log_callback)
|
||||||
|
|
||||||
# start them
|
# start them
|
||||||
if self.exp_handling: self.exp_handling.start()
|
if self.exp_handling: self.exp_handling.start()
|
||||||
@@ -73,27 +79,29 @@ class ScriptInterface:
|
|||||||
if not self.exp_handling.is_alive():
|
if not self.exp_handling.is_alive():
|
||||||
self.exp_handling.join()
|
self.exp_handling.join()
|
||||||
if self.exp_handling.raised_exception:
|
if self.exp_handling.raised_exception:
|
||||||
print(": experiment script failed at line %d (function %s): %s"%(self.exp_handling.location[0],
|
logger.error(": experiment script failed at line %d (function %s): %s",
|
||||||
self.exp_handling.location[1],
|
self.exp_handling.location[0],
|
||||||
self.exp_handling.raised_exception))
|
self.exp_handling.location[1],
|
||||||
|
self.exp_handling.raised_exception)
|
||||||
else:
|
else:
|
||||||
print(": experiment script finished")
|
logger.info(": experiment script finished")
|
||||||
self.exp_handling = None
|
self.exp_handling = None
|
||||||
|
|
||||||
if self.res_handling is not None:
|
if self.res_handling is not None:
|
||||||
if not self.res_handling.is_alive():
|
if not self.res_handling.is_alive():
|
||||||
self.res_handling.join()
|
self.res_handling.join()
|
||||||
if self.res_handling.raised_exception:
|
if self.res_handling.raised_exception:
|
||||||
print(": result script failed at line %d (function %s): %s"%(self.res_handling.location[0],
|
logger.error(": result script failed at line %d (function %s): %s",
|
||||||
self.res_handling.location[1],
|
self.res_handling.location[0],
|
||||||
self.res_handling.raised_exception))
|
self.res_handling.location[1],
|
||||||
|
self.res_handling.raised_exception)
|
||||||
else:
|
else:
|
||||||
print(": result script finished")
|
logger.info(": result script finished")
|
||||||
self.res_handling = None
|
self.res_handling = None
|
||||||
|
|
||||||
if self.back_driver is not None:
|
if self.back_driver is not None:
|
||||||
if not self.back_driver.is_alive():
|
if not self.back_driver.is_alive():
|
||||||
print(": backend finished")
|
logger.info(": backend finished")
|
||||||
self.back_driver=None
|
self.back_driver=None
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
@@ -120,33 +128,45 @@ class ScriptInterface:
|
|||||||
dump_file=None
|
dump_file=None
|
||||||
# todo
|
# todo
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("dump failed", e)
|
logger.error("Dump failed: %s", e)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__=="__main__":
|
if __name__=="__main__":
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Run DAMARIS experiment and result scripts."
|
||||||
|
)
|
||||||
|
parser.add_argument("experiment_script", help="Path to the experiment script file")
|
||||||
|
parser.add_argument("result_script", help="Path to the result script file")
|
||||||
|
parser.add_argument(
|
||||||
|
"spool_dir", nargs="?", default=None,
|
||||||
|
help="Spool directory (defaults to current working directory)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--backend", default="/usr/lib/damaris/backends/Mobilecore",
|
||||||
|
help="Path to the backend executable"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output", default="data_pool.h5",
|
||||||
|
help="Output HDF5 file path (default: data_pool.h5)"
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
if len(sys.argv)==1:
|
spool_dir = args.spool_dir if args.spool_dir else os.getcwd()
|
||||||
print("%s: data_handling_script [spool directory]"%sys.argv[0])
|
|
||||||
sys.exit(1)
|
|
||||||
if len(sys.argv)==3:
|
|
||||||
spool_dir=os.getcwd()
|
|
||||||
else:
|
|
||||||
spool_dir=sys.argv[3]
|
|
||||||
|
|
||||||
expscriptfile=open(sys.argv[1])
|
|
||||||
expscript=expscriptfile.read()
|
|
||||||
resscriptfile=open(sys.argv[2])
|
|
||||||
resscript=resscriptfile.read()
|
|
||||||
|
|
||||||
si=ScriptInterface(expscript, resscript,"/usr/lib/damaris/backends/Mobilecore", spool_dir)
|
with open(args.experiment_script) as f:
|
||||||
|
expscript = f.read()
|
||||||
|
with open(args.result_script) as f:
|
||||||
|
resscript = f.read()
|
||||||
|
|
||||||
si.data.register_listener(some_listener)
|
si = ScriptInterface(expscript, resscript, args.backend, spool_dir)
|
||||||
|
|
||||||
|
si.data.register_listener(progressbar)
|
||||||
|
|
||||||
si.runScripts()
|
si.runScripts()
|
||||||
|
|
||||||
si.waitForScriptsEnding()
|
si.waitForScriptsEnding()
|
||||||
|
|
||||||
si.dump_data("data_pool.h5")
|
si.dump_data(args.output)
|
||||||
|
|
||||||
si=None
|
si = None
|
||||||
@@ -15,5 +15,13 @@
|
|||||||
|
|
||||||
## module contents
|
## module contents
|
||||||
#
|
#
|
||||||
__version__ = "0.21"
|
__version__ = "0.25"
|
||||||
__all__=["experiments", "data", "gui"]
|
__all__=["experiments", "data", "gui"]
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Add user plugin directory to Python path (highest priority)
|
||||||
|
_plugin_dir = os.path.expanduser("~/.config/damaris/plugin")
|
||||||
|
if os.path.isdir(_plugin_dir) and _plugin_dir not in sys.path:
|
||||||
|
sys.path.insert(0, _plugin_dir)
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
// -*- C++ -*-
|
||||||
|
/**
|
||||||
|
* @file logging_helper.h
|
||||||
|
* @brief Lightweight logging helper for the DAMARIS C++ backend.
|
||||||
|
*
|
||||||
|
* Writes structured log messages to stdout in a format parseable by the
|
||||||
|
* Python FileTailerHandler:
|
||||||
|
*
|
||||||
|
* [LEVEL] message
|
||||||
|
*
|
||||||
|
* Levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||||
|
*
|
||||||
|
* Usage in C++ code:
|
||||||
|
* #include "logging_helper.h"
|
||||||
|
* log_info("Starting backend, spool=%s", spool_dir.c_str());
|
||||||
|
* log_error("Failed to open device: %s", strerror(errno));
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DAMARIS_LOGGING_HELPER_H
|
||||||
|
#define DAMARIS_LOGGING_HELPER_H
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdarg>
|
||||||
|
#include <cstring>
|
||||||
|
#include <ctime>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace damaris {
|
||||||
|
namespace logging {
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Internal helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
inline std::string timestamp() {
|
||||||
|
char buf[32];
|
||||||
|
time_t now = time(nullptr);
|
||||||
|
struct tm tm_buf;
|
||||||
|
#ifdef _WIN32
|
||||||
|
localtime_s(&tm_buf, &now);
|
||||||
|
#else
|
||||||
|
localtime_r(&now, &tm_buf);
|
||||||
|
#endif
|
||||||
|
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm_buf);
|
||||||
|
return std::string(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void log_message(int level, const char* fmt, ...) {
|
||||||
|
const char* level_str = "INFO";
|
||||||
|
switch (level) {
|
||||||
|
case 0: level_str = "DEBUG"; break;
|
||||||
|
case 1: level_str = "INFO"; break;
|
||||||
|
case 2: level_str = "WARNING"; break;
|
||||||
|
case 3: level_str = "ERROR"; break;
|
||||||
|
case 4: level_str = "CRITICAL"; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
char msg[4096];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, fmt);
|
||||||
|
vsnprintf(msg, sizeof(msg), fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
|
||||||
|
fprintf(stdout, "[%s] %s\n", level_str, msg);
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Convenience macros / inline functions
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
inline void log_debug(const char* fmt, ...) {
|
||||||
|
char msg[4096];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, fmt);
|
||||||
|
vsnprintf(msg, sizeof(msg), fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
fprintf(stdout, "[DEBUG] %s\n", msg);
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void log_info(const char* fmt, ...) {
|
||||||
|
char msg[4096];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, fmt);
|
||||||
|
vsnprintf(msg, sizeof(msg), fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
fprintf(stdout, "[INFO] %s\n", msg);
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void log_warning(const char* fmt, ...) {
|
||||||
|
char msg[4096];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, fmt);
|
||||||
|
vsnprintf(msg, sizeof(msg), fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
fprintf(stdout, "[WARNING] %s\n", msg);
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void log_error(const char* fmt, ...) {
|
||||||
|
char msg[4096];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, fmt);
|
||||||
|
vsnprintf(msg, sizeof(msg), fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
fprintf(stdout, "[ERROR] %s\n", msg);
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void log_critical(const char* fmt, ...) {
|
||||||
|
char msg[4096];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, fmt);
|
||||||
|
vsnprintf(msg, sizeof(msg), fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
fprintf(stdout, "[CRITICAL] %s\n", msg);
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace logging
|
||||||
|
} // namespace damaris
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Optional C-style macros for convenience
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#define DAMARIS_LOG_DEBUG(fmt, ...) damaris::logging::log_debug(fmt, ##__VA_ARGS__)
|
||||||
|
#define DAMARIS_LOG_INFO(fmt, ...) damaris::logging::log_info(fmt, ##__VA_ARGS__)
|
||||||
|
#define DAMARIS_LOG_WARNING(fmt, ...) damaris::logging::log_warning(fmt, ##__VA_ARGS__)
|
||||||
|
#define DAMARIS_LOG_ERROR(fmt, ...) damaris::logging::log_error(fmt, ##__VA_ARGS__)
|
||||||
|
#define DAMARIS_LOG_CRITICAL(fmt, ...) damaris::logging::log_critical(fmt, ##__VA_ARGS__)
|
||||||
|
|
||||||
|
#endif // DAMARIS_LOGGING_HELPER_H
|
||||||
+179
-63
@@ -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,6 +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
|
||||||
|
|
||||||
|
_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 #
|
||||||
@@ -38,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)
|
||||||
|
|
||||||
@@ -58,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):
|
||||||
@@ -66,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
|
||||||
@@ -86,30 +95,29 @@ 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():
|
||||||
print("Warning ADC-Result: Tried to run \"create_data_space()\" more than once.")
|
print("Warning ADC-Result: Tried to run \"create_data_space()\" more than once.")
|
||||||
return
|
return
|
||||||
|
|
||||||
raise ValueError("ValueError: You cant create an ADC-Result with less than 1 sample!")
|
|
||||||
|
|
||||||
for i in range(channels):
|
for i in range(channels):
|
||||||
self.y.append(numpy.zeros((samples,), dtype="int16"))
|
self.y.append(numpy.zeros((samples,), dtype="int16"))
|
||||||
|
|
||||||
self.x = numpy.zeros((samples,), dtype="float32")
|
self.x = numpy.zeros((samples,), dtype="float32")
|
||||||
|
self.nChannels = channels
|
||||||
|
|
||||||
self.index.append((0, samples-1))
|
self.index.append((0, samples-1))
|
||||||
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,99 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
|||||||
return self.is_clipped
|
return self.is_clipped
|
||||||
|
|
||||||
|
|
||||||
def add_sample_space(self, samples):
|
def lowpass(self, cutoff: float, numtaps: int | None = None) -> ADC_Result:
|
||||||
|
"""
|
||||||
|
Apply a zero-phase lowpass FIR filter to all channels.
|
||||||
|
|
||||||
|
Uses scipy.signal.filtfilt (forward-backward filtering) with a Parks-McClellan
|
||||||
|
FIR filter (remez). The forward-backward pass guarantees zero phase distortion
|
||||||
|
and doubles the effective filter order.
|
||||||
|
|
||||||
|
The stopband is automatically set to 0.1 * Nyquist beyond the cutoff. If
|
||||||
|
numtaps is not provided, it is estimated from the transition width to yield
|
||||||
|
approximately 40-50 dB stopband attenuation (typically ~40 taps).
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cutoff : float
|
||||||
|
Passband edge frequency in Hz. Frequencies above this value are attenuated.
|
||||||
|
Must be a positive number below the Nyquist frequency.
|
||||||
|
numtaps : int, optional
|
||||||
|
Number of FIR taps. If omitted, computed automatically from the transition
|
||||||
|
width.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
ADC_Result
|
||||||
|
A new ADC_Result with the filtered data (float64 per channel).
|
||||||
|
The original object is not modified.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
RuntimeError
|
||||||
|
If the result contains no data.
|
||||||
|
ValueError
|
||||||
|
If cutoff is invalid for the current sampling rate.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
>>> # Lowpass at 1 MHz (taps auto-computed, ~40)
|
||||||
|
... filtered = adc.lowpass(cutoff=1e6)
|
||||||
|
>>> # Lowpass at 500 kHz with fixed 63 taps
|
||||||
|
... filtered = adc.lowpass(cutoff=500e3, numtaps=63)
|
||||||
|
"""
|
||||||
|
if not self.contains_data():
|
||||||
|
raise RuntimeError("lowpass: no data present")
|
||||||
|
|
||||||
|
nyquist = self.sampling_rate / 2.0
|
||||||
|
cutoff = float(cutoff)
|
||||||
|
|
||||||
|
if cutoff <= 0 or cutoff >= nyquist:
|
||||||
|
raise ValueError(
|
||||||
|
f"cutoff {cutoff} Hz is outside valid range (0, {nyquist})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Transition width: 0.1 * Nyquist
|
||||||
|
trans_width = 0.1 * nyquist
|
||||||
|
stopband = min(cutoff + trans_width, nyquist * 0.99)
|
||||||
|
|
||||||
|
# Auto-estimate taps if not given
|
||||||
|
if numtaps is None:
|
||||||
|
numtaps = max(21, int(round(8.0 / (stopband - cutoff) * nyquist)))
|
||||||
|
if numtaps % 2 == 0:
|
||||||
|
numtaps += 1
|
||||||
|
|
||||||
|
coeffs = remez(numtaps, [0, cutoff, stopband, nyquist], [1, 0], fs=self.sampling_rate)
|
||||||
|
|
||||||
|
self.lock.acquire()
|
||||||
|
try:
|
||||||
|
tmp_y = []
|
||||||
|
for i in range(self.get_number_of_channels()):
|
||||||
|
data = numpy.asarray(self.y[i], dtype='float64')
|
||||||
|
filtered = filtfilt(
|
||||||
|
coeffs, [1.0], data,
|
||||||
|
padtype='odd',
|
||||||
|
padlen=(numtaps - 1) * 3
|
||||||
|
)
|
||||||
|
tmp_y.append(filtered)
|
||||||
|
|
||||||
|
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)
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
@@ -167,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:
|
||||||
@@ -184,97 +284,97 @@ 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)
|
||||||
|
self._copy_metadata(r)
|
||||||
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
|
||||||
suitable for further processing
|
suitable for further processing
|
||||||
"""
|
"""
|
||||||
# write sorted
|
if isinstance(destination, str):
|
||||||
the_destination=destination
|
with open(destination, "w") as f:
|
||||||
if type(destination) in (str,):
|
self._write_to_csv_dest(f, delimiter)
|
||||||
the_destination=open(destination, "w")
|
else:
|
||||||
|
self._write_to_csv_dest(destination, delimiter)
|
||||||
|
|
||||||
the_destination.write("# adc_result\n")
|
def _write_to_csv_dest(self, dest: IO[str], delimiter: str) -> None:
|
||||||
the_destination.write("# t y0 y1 ...\n")
|
dest.write("# adc_result\n")
|
||||||
|
dest.write("# t y0 y1 ...\n")
|
||||||
self.lock.acquire()
|
self.lock.acquire()
|
||||||
try:
|
try:
|
||||||
xdata=self.get_xdata()
|
xdata=self.get_xdata()
|
||||||
ch_no=self.get_number_of_channels()
|
ch_no=self.get_number_of_channels()
|
||||||
ydata=list(map(self.get_ydata, range(ch_no)))
|
ydata=list(map(self.get_ydata, range(ch_no)))
|
||||||
#yerr=map(self.get_yerr, xrange(ch_no))
|
|
||||||
for i in range(len(xdata)):
|
for i in range(len(xdata)):
|
||||||
the_destination.write("%e"%xdata[i])
|
dest.write("%e"%xdata[i])
|
||||||
for j in range(ch_no):
|
for j in range(ch_no):
|
||||||
the_destination.write("%s%e"%(delimiter, ydata[j][i]))
|
dest.write("%s%e"%(delimiter, ydata[j][i]))
|
||||||
the_destination.write("\n")
|
dest.write("\n")
|
||||||
the_destination=None
|
|
||||||
xdata=ydata=None
|
|
||||||
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;
|
||||||
destination can be a file or a filename
|
destination can be a file or a filename
|
||||||
"""
|
"""
|
||||||
# write sorted
|
if isinstance(destination, str):
|
||||||
the_destination=destination
|
with open(destination, "w") as f:
|
||||||
if type(destination) in (str,):
|
self._write_to_simpson_dest(f, delimiter)
|
||||||
the_destination=open(destination, "w")
|
else:
|
||||||
|
self._write_to_simpson_dest(destination, 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()
|
||||||
the_destination.write("SIMP\n")
|
dest.write("SIMP\n")
|
||||||
the_destination.write("%s%i%s"%("NP=", len(xdata), "\n"))
|
dest.write("%s%i%s"%("NP=", len(xdata), "\n"))
|
||||||
the_destination.write("%s%i%s"%("SW=", self.get_sampling_rate(), "\n"))
|
dest.write("%s%i%s"%("SW=", self.get_sampling_rate(), "\n"))
|
||||||
the_destination.write("TYPE=FID\n")
|
dest.write("TYPE=FID\n")
|
||||||
the_destination.write("DATA\n")
|
dest.write("DATA\n")
|
||||||
ch_no=self.get_number_of_channels()
|
ch_no=self.get_number_of_channels()
|
||||||
ydata=list(map(self.get_ydata, range(ch_no)))
|
ydata=list(map(self.get_ydata, range(ch_no)))
|
||||||
for i in range(len(xdata)):
|
for i in range(len(xdata)):
|
||||||
for j in range(ch_no):
|
for j in range(ch_no):
|
||||||
the_destination.write("%g%s"%(ydata[j][i], delimiter))
|
dest.write("%g%s"%(ydata[j][i], delimiter))
|
||||||
the_destination.write("\n")
|
dest.write("\n")
|
||||||
the_destination.write("END\n")
|
dest.write("END\n")
|
||||||
the_destination=None
|
|
||||||
xdata=ydata=None
|
|
||||||
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():
|
||||||
@@ -371,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
|
||||||
"""
|
"""
|
||||||
@@ -394,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()
|
||||||
@@ -404,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:
|
||||||
@@ -411,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()
|
||||||
@@ -426,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:
|
||||||
@@ -433,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()
|
||||||
@@ -443,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:
|
||||||
@@ -450,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()
|
||||||
@@ -460,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()
|
||||||
@@ -481,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()
|
||||||
@@ -495,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()
|
||||||
@@ -508,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 = []
|
||||||
@@ -555,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.
|
||||||
"""
|
"""
|
||||||
@@ -609,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
|
||||||
|
|
||||||
|
|||||||
+207
-129
@@ -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]
|
||||||
@@ -111,31 +126,32 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
|||||||
self.lock.release()
|
self.lock.release()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
tmp_x = self.x[start:end+1]
|
tmp_x = self.x[start:end+1].copy()
|
||||||
tmp_y = []
|
tmp_y = []
|
||||||
|
|
||||||
for i in range(self.get_number_of_channels()):
|
for i in range(self.get_number_of_channels()):
|
||||||
tmp_y.append(self.y[i][start:end+1])
|
tmp_y.append(self.y[i][start:end+1].copy())
|
||||||
|
|
||||||
r = Accumulation(x = tmp_x, y = tmp_y, n = self.n, index = [(0,len(tmp_y[0])-1)], sampl_freq = self.sampling_rate, error = self.use_error)
|
r = Accumulation(x = tmp_x, y = tmp_y, n = self.n, index = [(0,len(tmp_y[0])-1)], sampl_freq = self.sampling_rate, error = self.use_error)
|
||||||
if self.uses_statistics():
|
if self.uses_statistics():
|
||||||
r.y_square = []
|
r.y_square = []
|
||||||
for i in range(self.get_number_of_channels()):
|
for i in range(self.get_number_of_channels()):
|
||||||
r.y_square.append(self.y_square[i][start:end+1])
|
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
|
||||||
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]
|
||||||
@@ -163,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:
|
||||||
@@ -187,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
|
||||||
@@ -205,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
|
||||||
|
|
||||||
@@ -240,86 +256,91 @@ 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
|
||||||
"""
|
"""
|
||||||
the_destination=destination
|
|
||||||
if isinstance(destination, str):
|
if isinstance(destination, str):
|
||||||
the_destination=open(destination, "w")
|
with open(destination, "w") as f:
|
||||||
|
self._write_to_csv_dest(f, delimiter)
|
||||||
|
else:
|
||||||
|
self._write_to_csv_dest(destination, delimiter)
|
||||||
|
|
||||||
the_destination.write("# accumulation %d\n"%self.n)
|
def _write_to_csv_dest(self, dest: IO[str], delimiter: str) -> None:
|
||||||
|
dest.write("# accumulation %d\n"%self.n)
|
||||||
self.lock.acquire()
|
self.lock.acquire()
|
||||||
try:
|
try:
|
||||||
if self.common_descriptions is not None:
|
if self.common_descriptions is not None:
|
||||||
for (key,value) in self.common_descriptions.items():
|
for (key,value) in self.common_descriptions.items():
|
||||||
the_destination.write("# %s : %s\n"%(key, str(value)))
|
dest.write("# %s : %s\n"%(key, str(value)))
|
||||||
the_destination.write("# t")
|
dest.write("# t")
|
||||||
ch_no=self.get_number_of_channels()
|
ch_no=self.get_number_of_channels()
|
||||||
if self.use_error:
|
if self.use_error:
|
||||||
for i in range(ch_no):
|
for i in range(ch_no):
|
||||||
the_destination.write(" ch%d_mean ch%d_err"%(i,i))
|
dest.write(" ch%d_mean ch%d_err"%(i,i))
|
||||||
else:
|
else:
|
||||||
for i in range(ch_no):
|
for i in range(ch_no):
|
||||||
the_destination.write(" ch%d_mean"%i)
|
dest.write(" ch%d_mean"%i)
|
||||||
the_destination.write("\n")
|
dest.write("\n")
|
||||||
xdata=self.get_xdata()
|
xdata=self.get_xdata()
|
||||||
ydata=list(map(self.get_ydata, range(ch_no)))
|
ydata=list(map(self.get_ydata, range(ch_no)))
|
||||||
yerr=None
|
yerr=None
|
||||||
if self.use_error:
|
if self.use_error:
|
||||||
yerr=list(map(self.get_yerr, range(ch_no)))
|
yerr=list(map(self.get_yerr, range(ch_no)))
|
||||||
for i in range(len(xdata)):
|
for i in range(len(xdata)):
|
||||||
the_destination.write("%e"%xdata[i])
|
dest.write("%e"%xdata[i])
|
||||||
for j in range(ch_no):
|
for j in range(ch_no):
|
||||||
if self.use_error:
|
if self.use_error:
|
||||||
the_destination.write("%s%e%s%e"%(delimiter, ydata[j][i], delimiter, yerr[j][i]))
|
dest.write("%s%e%s%e"%(delimiter, ydata[j][i], delimiter, yerr[j][i]))
|
||||||
else:
|
else:
|
||||||
the_destination.write("%s%e"%(delimiter,ydata[j][i]))
|
dest.write("%s%e"%(delimiter,ydata[j][i]))
|
||||||
the_destination.write("\n")
|
dest.write("\n")
|
||||||
finally:
|
finally:
|
||||||
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;
|
||||||
destination can be a file or a filename
|
destination can be a file or a filename
|
||||||
"""
|
"""
|
||||||
the_destination=destination
|
|
||||||
if isinstance(destination, str):
|
if isinstance(destination, str):
|
||||||
the_destination=open(destination, "w")
|
with open(destination, "w") as f:
|
||||||
|
self._write_to_simpson_dest(f, delimiter, frequency)
|
||||||
|
else:
|
||||||
|
self._write_to_simpson_dest(destination, 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()
|
||||||
the_destination.write("SIMP\n")
|
dest.write("SIMP\n")
|
||||||
the_destination.write("%s%i%s"%("NP=", len(xdata), "\n"))
|
dest.write("%s%i%s"%("NP=", len(xdata), "\n"))
|
||||||
the_destination.write("%s%i%s"%("SW=", self.get_sampling_rate(), "\n"))
|
dest.write("%s%i%s"%("SW=", self.get_sampling_rate(), "\n"))
|
||||||
the_destination.write("%s%i%s"%("REF=", frequency, "\n"))
|
dest.write("%s%i%s"%("REF=", frequency, "\n"))
|
||||||
the_destination.write("TYPE=FID\n")
|
dest.write("TYPE=FID\n")
|
||||||
the_destination.write("DATA\n")
|
dest.write("DATA\n")
|
||||||
ch_no=self.get_number_of_channels()
|
ch_no=self.get_number_of_channels()
|
||||||
ydata=list(map(self.get_ydata, range(ch_no)))
|
ydata=list(map(self.get_ydata, range(ch_no)))
|
||||||
for i in range(len(xdata)):
|
for i in range(len(xdata)):
|
||||||
for j in range(ch_no):
|
for j in range(ch_no):
|
||||||
the_destination.write("%g%s"%(ydata[j][i], delimiter))
|
dest.write("%g%s"%(ydata[j][i], delimiter))
|
||||||
the_destination.write("\n")
|
dest.write("\n")
|
||||||
the_destination.write("END\n")
|
dest.write("END\n")
|
||||||
the_destination.close()
|
|
||||||
finally:
|
finally:
|
||||||
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;
|
||||||
@@ -348,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:
|
||||||
@@ -442,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.
|
||||||
|
|
||||||
@@ -603,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:
|
||||||
@@ -631,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
|
||||||
|
|
||||||
@@ -663,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()
|
||||||
@@ -672,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):
|
||||||
@@ -684,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 = []
|
||||||
|
|
||||||
@@ -697,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:
|
||||||
@@ -719,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):
|
||||||
@@ -758,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 = []
|
||||||
|
|
||||||
@@ -771,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={}
|
||||||
@@ -787,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():
|
||||||
@@ -829,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():
|
||||||
@@ -848,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():
|
||||||
@@ -875,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():
|
||||||
@@ -890,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 \"//\"! ")
|
||||||
@@ -899,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)
|
||||||
|
|
||||||
@@ -915,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 \"//\"! ")
|
||||||
@@ -936,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):
|
||||||
@@ -976,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()
|
||||||
@@ -986,17 +1015,17 @@ 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 diffrent 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):
|
||||||
raise ValueError("Accumulation: You cant add ADC-Results with diffrent number of samples")
|
raise ValueError("Accumulation: You can't add ADC-Results with different number of samples")
|
||||||
if len(self.y) != other.get_number_of_channels():
|
if len(self.y) != other.get_number_of_channels():
|
||||||
raise ValueError("Accumulation: You cant add ADC-Results with diffrent number of channels")
|
raise ValueError("Accumulation: You can't add ADC-Results with different number of channels")
|
||||||
for i in range(len(self.index)):
|
for i in range(len(self.index)):
|
||||||
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 diffrent 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]
|
||||||
@@ -1006,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
|
||||||
|
|
||||||
@@ -1047,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()
|
||||||
@@ -1056,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):
|
||||||
@@ -1068,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]
|
||||||
@@ -1077,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
|
||||||
@@ -1108,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 = []
|
||||||
|
|
||||||
@@ -1126,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.
|
||||||
"""
|
"""
|
||||||
@@ -1160,26 +1198,66 @@ def read_from_hdf(hdf_node):
|
|||||||
earliest_time=None
|
earliest_time=None
|
||||||
if "earliest_time" in dir(hdf_node._v_attrs):
|
if "earliest_time" in dir(hdf_node._v_attrs):
|
||||||
timestring=hdf_node._v_attrs.__getattr__("earliest_time")
|
timestring=hdf_node._v_attrs.__getattr__("earliest_time")
|
||||||
earliest_time=datetime.datetime(int(timestring[:4]), # year
|
# Handle both formats: "20260705 19:13:59.162" and "2026-07-05 19:13:59.162"
|
||||||
int(timestring[4:6]), # month
|
if '-' in timestring:
|
||||||
int(timestring[6:8]), # day
|
# New format: "2026-07-05 19:13:59.162"
|
||||||
int(timestring[9:11]), # hour
|
try:
|
||||||
int(timestring[12:14]), # minute
|
earliest_time=datetime.datetime.strptime(str(timestring), '%Y-%m-%d %H:%M:%S.%f')
|
||||||
int(timestring[15:17]), # second
|
except ValueError:
|
||||||
int(timestring[18:21])*1000 # microsecond
|
# Fallback to manual parsing
|
||||||
)
|
parts = timestring.split()
|
||||||
|
if len(parts) >= 2:
|
||||||
|
date_parts = parts[0].split('-')
|
||||||
|
time_parts = parts[1].split(':')
|
||||||
|
if len(date_parts) == 3 and len(time_parts) >= 2:
|
||||||
|
year, month, day = int(date_parts[0]), int(date_parts[1]), int(date_parts[2])
|
||||||
|
hour, minute = int(time_parts[0]), int(time_parts[1])
|
||||||
|
second_parts = time_parts[2].split('.') if len(time_parts) > 2 else ['0']
|
||||||
|
second = int(second_parts[0])
|
||||||
|
microsecond = int(second_parts[1].ljust(6, '0')[:6]) if len(second_parts) > 1 else 0
|
||||||
|
earliest_time=datetime.datetime(year, month, day, hour, minute, second, microsecond)
|
||||||
|
else:
|
||||||
|
# Old format: "20260705 19:13:59.162"
|
||||||
|
earliest_time=datetime.datetime(int(timestring[:4]), # year
|
||||||
|
int(timestring[4:6]), # month
|
||||||
|
int(timestring[6:8]), # day
|
||||||
|
int(timestring[9:11]), # hour
|
||||||
|
int(timestring[12:14]), # minute
|
||||||
|
int(timestring[15:17]), # second
|
||||||
|
int(timestring[18:21])*1000 # microsecond
|
||||||
|
)
|
||||||
|
|
||||||
oldest_time=None
|
oldest_time=None
|
||||||
if "oldest_time" in dir(hdf_node._v_attrs):
|
if "oldest_time" in dir(hdf_node._v_attrs):
|
||||||
timestring=hdf_node._v_attrs.__getattr__("oldest_time")
|
timestring=hdf_node._v_attrs.__getattr__("oldest_time")
|
||||||
oldest_time=datetime.datetime(int(timestring[:4]), # year
|
# Handle both formats: "20260705 19:13:59.162" and "2026-07-05 19:13:59.162"
|
||||||
int(timestring[4:6]), # month
|
if '-' in timestring:
|
||||||
int(timestring[6:8]), # day
|
# New format: "2026-07-05 19:13:59.162"
|
||||||
int(timestring[9:11]), # hour
|
try:
|
||||||
int(timestring[12:14]), # minute
|
oldest_time=datetime.datetime.strptime(str(timestring), '%Y-%m-%d %H:%M:%S.%f')
|
||||||
int(timestring[15:17]), # second
|
except ValueError:
|
||||||
int(timestring[18:21])*1000 # microsecond
|
# Fallback to manual parsing
|
||||||
)
|
parts = timestring.split()
|
||||||
|
if len(parts) >= 2:
|
||||||
|
date_parts = parts[0].split('-')
|
||||||
|
time_parts = parts[1].split(':')
|
||||||
|
if len(date_parts) == 3 and len(time_parts) >= 2:
|
||||||
|
year, month, day = int(date_parts[0]), int(date_parts[1]), int(date_parts[2])
|
||||||
|
hour, minute = int(time_parts[0]), int(time_parts[1])
|
||||||
|
second_parts = time_parts[2].split('.') if len(time_parts) > 2 else ['0']
|
||||||
|
second = int(second_parts[0])
|
||||||
|
microsecond = int(second_parts[1].ljust(6, '0')[:6]) if len(second_parts) > 1 else 0
|
||||||
|
oldest_time=datetime.datetime(year, month, day, hour, minute, second, microsecond)
|
||||||
|
else:
|
||||||
|
# Old format: "20260705 19:13:59.162"
|
||||||
|
oldest_time=datetime.datetime(int(timestring[:4]), # year
|
||||||
|
int(timestring[4:6]), # month
|
||||||
|
int(timestring[6:8]), # day
|
||||||
|
int(timestring[9:11]), # hour
|
||||||
|
int(timestring[12:14]), # minute
|
||||||
|
int(timestring[15:17]), # second
|
||||||
|
int(timestring[18:21])*1000 # microsecond
|
||||||
|
)
|
||||||
|
|
||||||
if oldest_time is None or earliest_time is None:
|
if oldest_time is None or earliest_time is None:
|
||||||
accu.time_period=None
|
accu.time_period=None
|
||||||
@@ -1212,7 +1290,7 @@ def read_from_hdf(hdf_node):
|
|||||||
accu.y_square=[]
|
accu.y_square=[]
|
||||||
accu.use_error=False
|
accu.use_error=False
|
||||||
|
|
||||||
for ch in range(accu_data.shape[1]/2):
|
for ch in range(accu_data.shape[1] // 2):
|
||||||
accu.y.append(accu_data[:,ch*2]*accu.n)
|
accu.y.append(accu_data[:,ch*2]*accu.n)
|
||||||
if accu.n<2 or numpy.all(accu_data[:,ch*2+1]==0.0):
|
if accu.n<2 or numpy.all(accu_data[:,ch*2+1]==0.0):
|
||||||
accu.y_square.append(numpy.zeros((accu_data.shape[0]) ,dtype="float32"))
|
accu.y_square.append(numpy.zeros((accu_data.shape[0]) ,dtype="float32"))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
+400
-40
@@ -7,15 +7,16 @@ import collections
|
|||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
import io
|
import io
|
||||||
|
import numpy
|
||||||
|
|
||||||
class DataPool(collections.abc.MutableMapping):
|
class DataPool(collections.abc.MutableMapping):
|
||||||
"""
|
"""
|
||||||
dictionary with sending change events
|
dictionary with sending change events
|
||||||
"""
|
|
||||||
|
# supports translation from dictionary keys to pytables hdf node names
|
||||||
# supports tranlation from dictionary keys to pytables hdf node names
|
|
||||||
# taken from: Python Ref Manual Section 2.3: Identifiers and keywords
|
# taken from: Python Ref Manual Section 2.3: Identifiers and keywords
|
||||||
# things are always prefixed by "dir_" or "dict_"
|
# things are always prefixed by "dir_" or "dict_"
|
||||||
|
"""
|
||||||
translation_table=""
|
translation_table=""
|
||||||
for i in range(256):
|
for i in range(256):
|
||||||
c=chr(i)
|
c=chr(i)
|
||||||
@@ -46,78 +47,146 @@ class DataPool(collections.abc.MutableMapping):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.__mydict={}
|
self.__mydict={}
|
||||||
self.__dictlock=threading.Lock()
|
self._dictlock=threading.RLock()
|
||||||
self.__registered_listeners=[]
|
self._registered_listeners=[]
|
||||||
|
self.__dirty_keys=set()
|
||||||
|
|
||||||
def __getitem__(self, name):
|
def __getitem__(self, name):
|
||||||
try:
|
try:
|
||||||
self.__dictlock.acquire()
|
self._dictlock.acquire()
|
||||||
return self.__mydict[name]
|
return self.__mydict[name]
|
||||||
finally:
|
finally:
|
||||||
self.__dictlock.release()
|
self._dictlock.release()
|
||||||
|
|
||||||
def __setitem__(self, name, value):
|
def __setitem__(self, name, value):
|
||||||
try:
|
try:
|
||||||
self.__dictlock.acquire()
|
self._dictlock.acquire()
|
||||||
if name in self.__mydict:
|
if name in self.__mydict:
|
||||||
e=DataPool.Event(DataPool.Event.updated_value,name,self)
|
e=DataPool.Event(DataPool.Event.updated_value,name,self)
|
||||||
else:
|
else:
|
||||||
e=DataPool.Event(DataPool.Event.new_key, name,self)
|
e=DataPool.Event(DataPool.Event.new_key, name,self)
|
||||||
self.__mydict[name]=value
|
self.__mydict[name]=value
|
||||||
|
self.__dirty_keys.add(name)
|
||||||
finally:
|
finally:
|
||||||
self.__dictlock.release()
|
self._dictlock.release()
|
||||||
self.__send_event(e)
|
self.__send_event(e)
|
||||||
|
|
||||||
|
|
||||||
def __delitem__(self, name):
|
def __delitem__(self, name):
|
||||||
try:
|
try:
|
||||||
self.__dictlock.acquire()
|
self._dictlock.acquire()
|
||||||
del self.__mydict[name]
|
del self.__mydict[name]
|
||||||
|
self.__dirty_keys.add(name)
|
||||||
finally:
|
finally:
|
||||||
self.__dictlock.release()
|
self._dictlock.release()
|
||||||
self.__send_event(DataPool.Event(DataPool.Event.deleted_key,name,self))
|
self.__send_event(DataPool.Event(DataPool.Event.deleted_key,name,self))
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
try:
|
try:
|
||||||
self.__dictlock.acquire()
|
self._dictlock.acquire()
|
||||||
return iter(self.__mydict)
|
return iter(self.__mydict)
|
||||||
finally:
|
finally:
|
||||||
self.__dictlock.release()
|
self._dictlock.release()
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
try:
|
try:
|
||||||
self.__dictlock.acquire()
|
self._dictlock.acquire()
|
||||||
return len(self.__mydict)
|
return len(self.__mydict)
|
||||||
finally:
|
finally:
|
||||||
self.__dictlock.release()
|
self._dictlock.release()
|
||||||
|
|
||||||
def keys(self):
|
def keys(self):
|
||||||
try:
|
try:
|
||||||
self.__dictlock.acquire()
|
self._dictlock.acquire()
|
||||||
return list(self.__mydict.keys())
|
return list(self.__mydict.keys())
|
||||||
finally:
|
finally:
|
||||||
self.__dictlock.release()
|
self._dictlock.release()
|
||||||
|
|
||||||
def __send_event(self, _event):
|
def __send_event(self, _event):
|
||||||
for listeners in self.__registered_listeners:
|
for listeners in self._registered_listeners:
|
||||||
listeners(_event.copy())
|
listeners(_event.copy())
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
self.__send_event(DataPool.Event(DataPool.Event.destroy))
|
self.__send_event(DataPool.Event(DataPool.Event.destroy))
|
||||||
self.__registered_listeners=None
|
self._registered_listeners=None
|
||||||
|
|
||||||
def write_hdf5(self,hdffile,where="/",name="data_pool", complib=None, complevel=None):
|
def write_hdf5(self,hdffile,where="/",name="data_pool", complib=None, complevel=None, incremental=False, write_interval=0):
|
||||||
if type(hdffile) is bytes:
|
|
||||||
|
"""
|
||||||
|
Writes the content of a DataPool object to an HDF5 file using the `tables` library.
|
||||||
|
|
||||||
|
This method serializes the entire content of the data pool into an HDF5 file, creating
|
||||||
|
appropriate groups and arrays based on the keys and values stored in the data pool.
|
||||||
|
Nested storage paths are supported, and group or array names are
|
||||||
|
converted into valid HDF5 names. Paths are prepended with "dir_", results with "dict_".
|
||||||
|
If duplicate names arise, extensions (like `_001`, `_002`)
|
||||||
|
are appended to ensure uniqueness.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
hdffile : str | bytes | tables.File
|
||||||
|
Path to the HDF5 file or an already open `tables.File` object where the
|
||||||
|
data should be written. Strings or bytes are interpreted as file paths.
|
||||||
|
|
||||||
|
where : str, optional
|
||||||
|
The root HDF5 group path under which the data pool will be stored.
|
||||||
|
Defaults to "/".
|
||||||
|
|
||||||
|
name : str, optional
|
||||||
|
The name of the root HDF5 group under which the data pool is saved.
|
||||||
|
Defaults to "data_pool".
|
||||||
|
|
||||||
|
complib : str, optional
|
||||||
|
Compression library to be used when creating HDF5 arrays. Optional, defaults
|
||||||
|
to None, meaning no compression is applied.
|
||||||
|
|
||||||
|
complevel : int, optional
|
||||||
|
Compression level to use when applying compression. Has no effect if `complib`
|
||||||
|
is set to None.
|
||||||
|
|
||||||
|
incremental : bool, optional
|
||||||
|
If True, only write keys that have been modified since the last write (dirty keys).
|
||||||
|
If False, write all keys. Defaults to False.
|
||||||
|
|
||||||
|
write_interval : int, optional
|
||||||
|
Time interval in seconds between periodic writes.
|
||||||
|
Defaults to 0.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception
|
||||||
|
If `hdffile` is neither a string nor a valid `tables.File` object.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- This method assumes that the content of the data pool does not change during the
|
||||||
|
write operation.
|
||||||
|
- Keys in the data pool starting with "__" (double underscores) are skipped.
|
||||||
|
- If a key contains nested paths (e.g., "group1/subgroup2/item"), intermediate
|
||||||
|
groups are created as necessary.
|
||||||
|
- Group or array names are converted to valid HDF5 names, and name conflicts
|
||||||
|
are resolved by appending extensions (e.g., "_001").
|
||||||
|
- Errors during the writing process are logged to the console, and traceback
|
||||||
|
information is displayed for debugging purposes.
|
||||||
|
"""
|
||||||
|
if isinstance(hdffile, (bytes, str)):
|
||||||
dump_file=tables.open_file(hdffile, mode="a")
|
dump_file=tables.open_file(hdffile, mode="a")
|
||||||
elif isinstance(hdffile,tables.File):
|
elif isinstance(hdffile,tables.File):
|
||||||
dump_file=hdffile
|
dump_file=hdffile
|
||||||
else:
|
else:
|
||||||
raise Exception("expecting hdffile or string")
|
raise Exception("expecting hdffile or string")
|
||||||
|
|
||||||
dump_group=dump_file.create_group(where, name, "DAMARIS data pool")
|
if where in dump_file and name in dump_file.get_node(where):
|
||||||
self.__dictlock.acquire()
|
dump_group = dump_file.get_node(where, name)
|
||||||
dict_keys=list(self.__mydict.keys())
|
else:
|
||||||
self.__dictlock.release()
|
dump_group = dump_file.create_group(where, name, "DAMARIS data pool")
|
||||||
|
|
||||||
|
self._dictlock.acquire()
|
||||||
|
if incremental:
|
||||||
|
dict_keys=list(self.__dirty_keys)
|
||||||
|
else:
|
||||||
|
# Full sync: current keys + any other dirty keys (e.g. deletions)
|
||||||
|
dict_keys=list(set(self.__mydict.keys()) | self.__dirty_keys)
|
||||||
|
|
||||||
|
self._dictlock.release()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for key in dict_keys:
|
for key in dict_keys:
|
||||||
if key.startswith("__"):
|
if key.startswith("__"):
|
||||||
@@ -142,21 +211,43 @@ class DataPool(collections.abc.MutableMapping):
|
|||||||
|
|
||||||
# convert last part of key to a valid name
|
# convert last part of key to a valid name
|
||||||
group_keyname="dict_"+str(namelist[-1]).translate(DataPool.translation_table)
|
group_keyname="dict_"+str(namelist[-1]).translate(DataPool.translation_table)
|
||||||
# avoid double names by adding number extension
|
|
||||||
|
# find existing node for this key
|
||||||
|
node_found = False
|
||||||
if group_keyname in dump_dir:
|
if group_keyname in dump_dir:
|
||||||
extension_count=0
|
if dump_dir._v_children[group_keyname]._v_title == key:
|
||||||
while group_keyname+"_%03d"%extension_count in dump_dir:
|
node_found = True
|
||||||
extension_count+=1
|
else:
|
||||||
group_keyname+="_%03d"%extension_count
|
extension_count = 0
|
||||||
self.__dictlock.acquire()
|
while group_keyname + "_%03d" % extension_count in dump_dir:
|
||||||
|
suffix_name = group_keyname + "_%03d" % extension_count
|
||||||
|
if dump_dir._v_children[suffix_name]._v_title == key:
|
||||||
|
group_keyname = suffix_name
|
||||||
|
node_found = True
|
||||||
|
break
|
||||||
|
extension_count += 1
|
||||||
|
else:
|
||||||
|
# truly new key with collision
|
||||||
|
group_keyname += "_%03d" % extension_count
|
||||||
|
|
||||||
|
if node_found:
|
||||||
|
dump_file.remove_node(dump_dir, group_keyname, recursive=True)
|
||||||
|
|
||||||
|
self._dictlock.acquire()
|
||||||
if key not in self.__mydict:
|
if key not in self.__mydict:
|
||||||
# outdated ...
|
# Key was deleted
|
||||||
self.__dictlock.release()
|
if key in self.__dirty_keys:
|
||||||
|
self.__dirty_keys.remove(key)
|
||||||
|
self._dictlock.release()
|
||||||
continue
|
continue
|
||||||
value=self.__mydict[key]
|
value=self.__mydict[key]
|
||||||
self.__dictlock.release()
|
# We will clear the dirty flag only after successful write
|
||||||
|
# to handle potential transient failures or re-dirtied items
|
||||||
|
self._dictlock.release()
|
||||||
|
|
||||||
# now write data, assuming, the object is constant during write operation
|
# now write data, assuming, the object is constant during write operation
|
||||||
if "write_to_hdf" in dir(value):
|
write_success = False
|
||||||
|
if hasattr(value, "write_to_hdf"):
|
||||||
try:
|
try:
|
||||||
value.write_to_hdf(hdffile=dump_file,
|
value.write_to_hdf(hdffile=dump_file,
|
||||||
where=dump_dir,
|
where=dump_dir,
|
||||||
@@ -164,25 +255,294 @@ class DataPool(collections.abc.MutableMapping):
|
|||||||
title=key,
|
title=key,
|
||||||
complib=complib,
|
complib=complib,
|
||||||
complevel=complevel)
|
complevel=complevel)
|
||||||
|
write_success = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("failed to write data_pool[\"%s\"]: %s"%(key,str(e)))
|
print("failed to write data_pool[\"%s\"]: %s"%(key,str(e)))
|
||||||
traceback_file=io.StringIO()
|
traceback_file=io.StringIO()
|
||||||
traceback.print_tb(sys.exc_info()[2], None, traceback_file)
|
traceback.print_tb(sys.exc_info()[2], None, traceback_file)
|
||||||
print("detailed traceback: %s\n"%str(e)+traceback_file.getvalue())
|
print("detailed traceback: %s\n"%str(e)+traceback_file.getvalue())
|
||||||
traceback_file=None
|
traceback_file=None
|
||||||
|
elif isinstance(value, (numpy.ndarray, int, float, str, bytes)):
|
||||||
|
try:
|
||||||
|
obj_to_save = value
|
||||||
|
if isinstance(value, str):
|
||||||
|
obj_to_save = value.encode('utf-8')
|
||||||
|
dump_file.create_array(dump_dir,
|
||||||
|
name=group_keyname,
|
||||||
|
obj=obj_to_save,
|
||||||
|
title=key)
|
||||||
|
write_success = True
|
||||||
|
except Exception as e:
|
||||||
|
print("failed to write data_pool[\"%s\"]: %s"%(key,str(e)))
|
||||||
else:
|
else:
|
||||||
print("don't know how to store data_pool[\"%s\"]"%key)
|
print("don't know how to store data_pool[\"%s\"] (type: %s)"%(key, type(value)))
|
||||||
|
|
||||||
|
if write_success:
|
||||||
|
self._dictlock.acquire()
|
||||||
|
if key in self.__dirty_keys:
|
||||||
|
# Only remove if it hasn't been modified again in the meantime
|
||||||
|
# (Checking identity is a simple way for objects)
|
||||||
|
if self.__mydict.get(key) is value:
|
||||||
|
self.__dirty_keys.remove(key)
|
||||||
|
self._dictlock.release()
|
||||||
value=None
|
value=None
|
||||||
|
|
||||||
|
dump_file.flush()
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
dump_group=None
|
dump_group=None
|
||||||
if type(hdffile) is bytes:
|
if isinstance(hdffile, (bytes, str)):
|
||||||
dump_file.close()
|
dump_file.close()
|
||||||
dump_file=None
|
dump_file=None
|
||||||
|
|
||||||
|
def read_from_hdf(self, hdffile, where="/data_pool"):
|
||||||
|
"""
|
||||||
|
Read specified data from HDF5 file and populate the DataPool.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- hdffile: HDF5 file object or filename (bytes/str)
|
||||||
|
- where: Location within HDF5 file to read from (default: "/data_pool")
|
||||||
|
"""
|
||||||
|
# Handle both filename and file object
|
||||||
|
if type(hdffile) is bytes or isinstance(hdffile, str):
|
||||||
|
hdf_file = tables.open_file(hdffile, mode="r")
|
||||||
|
close_file = True
|
||||||
|
elif isinstance(hdffile, tables.File):
|
||||||
|
hdf_file = hdffile
|
||||||
|
close_file = False
|
||||||
|
else:
|
||||||
|
raise Exception("expecting hdffile or string")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Navigate to the data pool group
|
||||||
|
if where not in hdf_file:
|
||||||
|
raise Exception(f"HDF5 location '{where}' not found")
|
||||||
|
|
||||||
|
data_pool_group = hdf_file.get_node(where)
|
||||||
|
|
||||||
|
# Recursively process the group structure
|
||||||
|
self._read_hdf_group(data_pool_group, "")
|
||||||
|
|
||||||
|
# Clear dirty keys as they are now in sync with HDF5
|
||||||
|
self._dictlock.acquire()
|
||||||
|
self.__dirty_keys.clear()
|
||||||
|
self._dictlock.release()
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if close_file:
|
||||||
|
hdf_file.close()
|
||||||
|
|
||||||
|
def _read_hdf_group(self, group, path_prefix):
|
||||||
|
"""
|
||||||
|
Recursively read HDF5 group and populate DataPool.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- group: HDF5 group object
|
||||||
|
- path_prefix: Current path in DataPool hierarchy
|
||||||
|
"""
|
||||||
|
# Process all nodes in this group
|
||||||
|
for node in group:
|
||||||
|
# Get the node name (last part of the path)
|
||||||
|
node_name = node._v_name
|
||||||
|
|
||||||
|
# Try to get the original name from title
|
||||||
|
title = node._v_title
|
||||||
|
|
||||||
|
if title:
|
||||||
|
# For DAMARIS objects, the title is usually the full path
|
||||||
|
# For intermediate groups, it's just the part name
|
||||||
|
if "/" in title:
|
||||||
|
full_path = title
|
||||||
|
else:
|
||||||
|
if path_prefix:
|
||||||
|
full_path = f"{path_prefix}/{title}"
|
||||||
|
else:
|
||||||
|
full_path = title
|
||||||
|
else:
|
||||||
|
# Strip dir_ and dict_ prefixes from node name
|
||||||
|
clean_name = self._clean_hdf_name(node_name)
|
||||||
|
|
||||||
|
# Build the full path in DataPool hierarchy
|
||||||
|
if path_prefix:
|
||||||
|
full_path = f"{path_prefix}/{clean_name}"
|
||||||
|
else:
|
||||||
|
full_path = clean_name
|
||||||
|
|
||||||
|
full_path = full_path.replace("//", "/")
|
||||||
|
|
||||||
|
if isinstance(node, (tables.Group, tables.Table)):
|
||||||
|
# Check if this is a known DAMARIS object type
|
||||||
|
if hasattr(node._v_attrs, 'damaris_type'):
|
||||||
|
damaris_type = node._v_attrs.damaris_type
|
||||||
|
|
||||||
|
# Handle different DAMARIS object types
|
||||||
|
if damaris_type == "Accumulation":
|
||||||
|
from damaris.data.Accumulation import read_from_hdf
|
||||||
|
obj = read_from_hdf(node)
|
||||||
|
if obj is not None:
|
||||||
|
self[full_path] = obj
|
||||||
|
|
||||||
|
elif damaris_type == "ADC_Result":
|
||||||
|
from damaris.data.ADC_Result import read_from_hdf
|
||||||
|
obj = read_from_hdf(node)
|
||||||
|
if obj is not None:
|
||||||
|
self[full_path] = obj
|
||||||
|
|
||||||
|
elif damaris_type == "MeasurementResult":
|
||||||
|
from damaris.data.MeasurementResult import read_from_hdf
|
||||||
|
obj = read_from_hdf(node)
|
||||||
|
if obj is not None:
|
||||||
|
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":
|
||||||
|
from damaris.data.Temperature import read_from_hdf
|
||||||
|
obj = read_from_hdf(node)
|
||||||
|
if obj is not None:
|
||||||
|
self[full_path] = obj
|
||||||
|
|
||||||
|
# Skip further processing of this group since we handled it
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Recursively process sub-groups
|
||||||
|
if isinstance(node, tables.Group):
|
||||||
|
self._read_hdf_group(node, full_path)
|
||||||
|
|
||||||
|
elif isinstance(node, (tables.Array, tables.CArray, tables.EArray, tables.Table)):
|
||||||
|
# Handle simple array data
|
||||||
|
try:
|
||||||
|
data = node.read()
|
||||||
|
# If it's a scalar array of bytes, decode it to string
|
||||||
|
if hasattr(data, 'decode'):
|
||||||
|
try:
|
||||||
|
data = data.decode('utf-8')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
elif isinstance(data, numpy.ndarray) and data.dtype.kind in ('S', 'V'):
|
||||||
|
# For numpy byte arrays, try to decode if they are scalar or small
|
||||||
|
if data.size == 1:
|
||||||
|
try:
|
||||||
|
decoded = data.item().decode('utf-8')
|
||||||
|
data = decoded
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self[full_path] = data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not read array {full_path}: {e}")
|
||||||
|
|
||||||
|
# Note: Other node types (Table, etc.) would be handled here as needed
|
||||||
|
|
||||||
|
def _clean_hdf_name(self, name):
|
||||||
|
"""
|
||||||
|
Clean HDF5 node name by removing dir_ and dict_ prefixes.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- name: HDF5 node name (may have dir_ or dict_ prefix)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- Cleaned name with prefixes removed
|
||||||
|
"""
|
||||||
|
if name.startswith("dir_"):
|
||||||
|
return name[4:] # Remove "dir_" prefix
|
||||||
|
elif name.startswith("dict_"):
|
||||||
|
return name[5:] # Remove "dict_" prefix
|
||||||
|
else:
|
||||||
|
return name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load_hdf5(cls, filename):
|
||||||
|
"""
|
||||||
|
Load a complete DAMARIS HDF5 file and return DataPool and metadata, including scripts.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- filename: Path to HDF5 file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- tuple: (DataPool instance, metadata dict)
|
||||||
|
|
||||||
|
metadata dict contains:
|
||||||
|
- 'scripts': dict with experiment scripts and spool info
|
||||||
|
- 'log': log data if available
|
||||||
|
- 'timeline': timeline data if available
|
||||||
|
"""
|
||||||
|
# Validate file
|
||||||
|
if not tables.is_pytables_file(filename):
|
||||||
|
raise Exception(f"File {filename} is not a valid PyTables HDF5 file")
|
||||||
|
|
||||||
|
# Create DataPool and load data
|
||||||
|
data_pool = cls()
|
||||||
|
|
||||||
|
try:
|
||||||
|
hdf_file = tables.open_file(filename, mode="r")
|
||||||
|
|
||||||
|
# Load main data pool
|
||||||
|
if "/data_pool" in hdf_file:
|
||||||
|
data_pool.read_from_hdf(hdf_file, where="/data_pool")
|
||||||
|
|
||||||
|
# Extract metadata
|
||||||
|
metadata = {}
|
||||||
|
|
||||||
|
# Load scripts
|
||||||
|
if "/scripts" in hdf_file:
|
||||||
|
scripts_group = hdf_file.get_node("/scripts")
|
||||||
|
scripts_data = {}
|
||||||
|
# Use _v_children to get node names safely
|
||||||
|
if hasattr(scripts_group, '_v_children'):
|
||||||
|
for node_name, node in scripts_group._v_children.items():
|
||||||
|
try:
|
||||||
|
if hasattr(node, 'read'):
|
||||||
|
data = node.read()
|
||||||
|
# Handle numpy string arrays and other types
|
||||||
|
if hasattr(data, 'decode'):
|
||||||
|
# bytes or numpy bytes
|
||||||
|
scripts_data[node_name] = data.decode('utf-8', errors='replace')
|
||||||
|
elif hasattr(data, 'tolist'):
|
||||||
|
# numpy array
|
||||||
|
scripts_data[node_name] = str(data.tolist())
|
||||||
|
elif isinstance(data, (str, bytes)):
|
||||||
|
# string or bytes
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
scripts_data[node_name] = data.decode('utf-8', errors='replace')
|
||||||
|
else:
|
||||||
|
scripts_data[node_name] = data
|
||||||
|
else:
|
||||||
|
scripts_data[node_name] = str(data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not read script {node_name}: {e}")
|
||||||
|
metadata['scripts'] = scripts_data
|
||||||
|
|
||||||
|
# Load log
|
||||||
|
if "/log" in hdf_file:
|
||||||
|
try:
|
||||||
|
log_node = hdf_file.get_node("/log")
|
||||||
|
metadata['log'] = log_node.read()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not read log: {e}")
|
||||||
|
|
||||||
|
# Load timeline
|
||||||
|
if "/timeline" in hdf_file:
|
||||||
|
try:
|
||||||
|
timeline_node = hdf_file.get_node("/timeline")
|
||||||
|
metadata['timeline'] = timeline_node.read()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not read timeline: {e}")
|
||||||
|
|
||||||
|
return data_pool, metadata
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(f"Failed to load HDF5 file {filename}: {e}")
|
||||||
|
finally:
|
||||||
|
if 'hdf_file' in locals():
|
||||||
|
hdf_file.close()
|
||||||
|
|
||||||
def register_listener(self, listening_function):
|
def register_listener(self, listening_function):
|
||||||
self.__registered_listeners.append(listening_function)
|
self._registered_listeners.append(listening_function)
|
||||||
|
|
||||||
def unregister_listener(self, listening_function):
|
def unregister_listener(self, listening_function):
|
||||||
if listening_function in self.__registered_listeners:
|
if listening_function in self._registered_listeners:
|
||||||
self.__registered_listeners.remove(listening_function)
|
self._registered_listeners.remove(listening_function)
|
||||||
@@ -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
|
||||||
@@ -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 -------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import threading
|
import threading
|
||||||
import math
|
import math
|
||||||
import types
|
|
||||||
import sys
|
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
|
||||||
@@ -13,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:
|
||||||
@@ -43,9 +43,9 @@ 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 (type(y) is types.InstanceType and isinstance(y, AccumulatedValue)):
|
if isinstance(y, AccumulatedValue):
|
||||||
new_one.y=self.y+y.y
|
new_one.y=self.y+y.y
|
||||||
new_one.y2=self.y2+y.y2
|
new_one.y2=self.y2+y.y2
|
||||||
new_one.n=self.n+y.n
|
new_one.n=self.n+y.n
|
||||||
@@ -55,8 +55,8 @@ 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 (type(y) is types.InstanceType and isinstance(y, AccumulatedValue)):
|
if isinstance(y, AccumulatedValue):
|
||||||
self.y+=y.y
|
self.y+=y.y
|
||||||
self.y2+=y.y2
|
self.y2+=y.y2
|
||||||
self.n+=y.n
|
self.n+=y.n
|
||||||
@@ -66,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
|
||||||
"""
|
"""
|
||||||
@@ -82,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
|
||||||
"""
|
"""
|
||||||
@@ -98,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
|
||||||
"""
|
"""
|
||||||
@@ -114,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:
|
||||||
@@ -122,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
|
||||||
|
|
||||||
@@ -139,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
|
||||||
@@ -147,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
|
||||||
"""
|
"""
|
||||||
@@ -172,60 +175,61 @@ 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
|
||||||
suitable for further processing
|
suitable for further processing
|
||||||
"""
|
"""
|
||||||
# write sorted
|
if isinstance(destination, str):
|
||||||
the_destination=destination
|
with open(destination, "w") as f:
|
||||||
if type(destination) in (str,):
|
self._write_to_csv_dest(f, delimiter)
|
||||||
the_destination=open(destination, "w")
|
else:
|
||||||
|
self._write_to_csv_dest(destination, delimiter)
|
||||||
|
|
||||||
the_destination.write("# quantity:"+str(self.quantity_name)+"\n")
|
def _write_to_csv_dest(self, dest: IO[str], delimiter: str) -> None:
|
||||||
the_destination.write("# x y ysigma n\n")
|
dest.write("# quantity:"+str(self.quantity_name)+"\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 type(y) in [float, int, int]:
|
dest.write("%e%s%e%s%e%s%d\n" % (x,
|
||||||
the_destination.write("%e%s%e%s0%s1\n"%(x, delimiter, y, delimiter, delimiter))
|
delimiter,
|
||||||
else:
|
y.mean(),
|
||||||
the_destination.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))
|
|
||||||
the_destination=None
|
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import copy
|
||||||
|
|
||||||
import numpy as N
|
import numpy as N
|
||||||
|
|
||||||
class Signalpath:
|
class Signalpath:
|
||||||
@@ -5,33 +7,35 @@ class Signalpath:
|
|||||||
"""
|
"""
|
||||||
rotate signal by **degrees** for phase cycling, etc.
|
rotate signal by **degrees** for phase cycling, etc.
|
||||||
|
|
||||||
|
The signalpath object itself is not modified, a copy containing the
|
||||||
|
rotated data is returned.
|
||||||
|
|
||||||
:param degrees: rotate signal by this value
|
:param degrees: rotate signal by this value
|
||||||
:return:
|
:return: copy of the signalpath object with rotated data
|
||||||
"""
|
"""
|
||||||
if self.get_number_of_channels() != 2:
|
if self.get_number_of_channels() != 2:
|
||||||
raise Exception("rotation defined only for 2 channels")
|
raise Exception("rotation defined only for 2 channels")
|
||||||
|
rotated = copy.copy(self)
|
||||||
# simple case 0, 90, 180, 270 degree
|
# simple case 0, 90, 180, 270 degree
|
||||||
reduced_angle = divmod(degrees, 90)
|
reduced_angle = divmod(degrees, 90)
|
||||||
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:
|
||||||
return
|
rotated.y = [a.copy() for a in self.y]
|
||||||
|
|
||||||
elif reduced_angle == 1:
|
elif reduced_angle == 1:
|
||||||
self.y[1] *= -1
|
rotated.y = [-self.y[1], self.y[0].copy()]
|
||||||
self.y = [self.y[1], self.y[0]]
|
|
||||||
|
|
||||||
elif reduced_angle == 2:
|
elif reduced_angle == 2:
|
||||||
self.y[0] *= -1
|
rotated.y = [-self.y[0], -self.y[1]]
|
||||||
self.y[1] *= -1
|
|
||||||
|
|
||||||
elif reduced_angle == 3:
|
elif reduced_angle == 3:
|
||||||
self.y[0] *= -1
|
rotated.y = [self.y[1].copy(), -self.y[0]]
|
||||||
self.y = [self.y[1], self.y[0]]
|
|
||||||
else:
|
else:
|
||||||
sin_angle = N.sin(degrees / 180.0 * N.pi)
|
rad_angle = N.deg2rad(degrees)
|
||||||
cos_angle = N.cos(degrees / 180.0 * N.pi)
|
sin_angle = N.sin(rad_angle)
|
||||||
self.y = [cos_angle * self.y[0] - sin_angle * self.y[1],
|
cos_angle = N.cos(rad_angle)
|
||||||
sin_angle * self.y[0] + cos_angle * self.y[1]]
|
rotated.y = [cos_angle * self.y[0] - sin_angle * self.y[1],
|
||||||
|
sin_angle * self.y[0] + cos_angle * self.y[1]]
|
||||||
|
|
||||||
return self
|
return rotated
|
||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
from .Resultable import Resultable
|
from .Resultable import Resultable
|
||||||
from .Drawable import Drawable
|
from .Drawable import Drawable
|
||||||
|
import numpy
|
||||||
|
import threading
|
||||||
|
import tables
|
||||||
|
|
||||||
#############################################################################
|
#############################################################################
|
||||||
# #
|
# #
|
||||||
@@ -15,17 +18,224 @@ from .Drawable import Drawable
|
|||||||
class TemperatureResult(Resultable, Drawable):
|
class TemperatureResult(Resultable, Drawable):
|
||||||
"""
|
"""
|
||||||
Specialised class of Resultable and Drawable
|
Specialised class of Resultable and Drawable
|
||||||
Contains recorded temperature data
|
Contains recorded temperature data.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
x: List of timestamps in seconds.
|
||||||
|
y: List of temperature readings in Celsius.
|
||||||
|
setpoint: Optional list of setpoint temperatures in Celsius.
|
||||||
|
xlabel: Label for x-axis (default: "Time (s)").
|
||||||
|
ylabel: Label for y-axis (default: "Temperature (C)").
|
||||||
"""
|
"""
|
||||||
def __init__(self, x = None, y = None, desc = None, job_id = None, job_date = None):
|
def __init__(self, x = None, y = None, setpoint = None, desc = None, job_id = None, job_date = None):
|
||||||
Resultable.__init__(self)
|
Resultable.__init__(self)
|
||||||
Drawable.__init__(self)
|
Drawable.__init__(self)
|
||||||
|
|
||||||
|
# Set default labels
|
||||||
|
self.xlabel = "Time (s)"
|
||||||
|
self.ylabel = "Temperature (C)"
|
||||||
|
|
||||||
|
# Initialize data lists
|
||||||
|
self.x = [] if x is None else x
|
||||||
|
self.y = [] if y is None else y
|
||||||
|
self.setpoint = [] if setpoint is None else setpoint
|
||||||
|
|
||||||
|
# Set metadata if provided
|
||||||
|
if desc is not None:
|
||||||
|
self.description = desc
|
||||||
|
if job_id is not None:
|
||||||
|
self.job_id = job_id
|
||||||
|
if job_date is not None:
|
||||||
|
self.job_date = job_date
|
||||||
|
|
||||||
|
# Listener management for GUI updates
|
||||||
|
self.__listeners = []
|
||||||
|
self.__listener_lock = threading.Lock()
|
||||||
|
|
||||||
|
def get_number_of_channels(self):
|
||||||
|
"""Returns 1 for temperature data."""
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def register_listener(self, listener):
|
||||||
|
"""Register a listener to be notified when data changes."""
|
||||||
|
with self.__listener_lock:
|
||||||
|
if listener not in self.__listeners:
|
||||||
|
self.__listeners.append(listener)
|
||||||
|
|
||||||
|
def unregister_listener(self, listener):
|
||||||
|
"""Unregister a listener."""
|
||||||
|
with self.__listener_lock:
|
||||||
|
if listener in self.__listeners:
|
||||||
|
self.__listeners.remove(listener)
|
||||||
|
|
||||||
|
def get_xdata(self):
|
||||||
|
"""Returns the x data (timestamps) as numpy array."""
|
||||||
|
if isinstance(self.x, numpy.ndarray):
|
||||||
|
return self.x
|
||||||
|
return numpy.array(self.x, dtype=float) if self.x else numpy.array([], dtype=float)
|
||||||
|
|
||||||
|
def get_ydata(self, channel=0):
|
||||||
|
"""Returns the y data (temperature readings) as numpy array."""
|
||||||
|
if channel == 0:
|
||||||
|
if isinstance(self.y, numpy.ndarray):
|
||||||
|
return self.y
|
||||||
|
return numpy.array(self.y, dtype=float) if self.y else numpy.array([], dtype=float)
|
||||||
|
return numpy.array([], dtype=float)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
"""Returns the number of data points."""
|
||||||
|
return len(self.y) if self.y else 0
|
||||||
|
|
||||||
|
def uses_statistics(self):
|
||||||
|
"""Returns False as temperature data doesn't use statistics."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def ready_for_drawing_error(self):
|
||||||
|
"""Returns False as temperature data doesn't have error bars."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_yerr(self, channel=0):
|
||||||
|
"""Returns empty error data."""
|
||||||
|
return numpy.array([], dtype=float)
|
||||||
|
|
||||||
|
def get_errorplotdata(self):
|
||||||
|
"""Returns empty error plot data."""
|
||||||
|
return [[], [], []]
|
||||||
|
|
||||||
|
def get_lineplotdata(self):
|
||||||
|
"""Returns line plot data (not used for temperature)."""
|
||||||
|
return [[], []]
|
||||||
|
|
||||||
|
def get_xmin(self):
|
||||||
|
"""Returns minimum of x."""
|
||||||
|
xdata = self.get_xdata()
|
||||||
|
return xdata.min() if len(xdata) > 0 else 0
|
||||||
|
|
||||||
|
def get_xmax(self):
|
||||||
|
"""Returns maximum of x."""
|
||||||
|
xdata = self.get_xdata()
|
||||||
|
return xdata.max() if len(xdata) > 0 else 0
|
||||||
|
|
||||||
|
def get_ymin(self):
|
||||||
|
"""Returns minimum of y."""
|
||||||
|
ydata = self.get_ydata(0)
|
||||||
|
return ydata.min() if len(ydata) > 0 else 0
|
||||||
|
|
||||||
|
def get_ymax(self):
|
||||||
|
"""Returns maximum of y."""
|
||||||
|
ydata = self.get_ydata(0)
|
||||||
|
return ydata.max() if len(ydata) > 0 else 0
|
||||||
|
|
||||||
|
def get_xminpos(self):
|
||||||
|
"""Returns smallest positive value of x."""
|
||||||
|
xdata = self.get_xdata()
|
||||||
|
mask = xdata > 0
|
||||||
|
if numpy.any(mask):
|
||||||
|
return xdata[mask].min()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def get_yminpos(self):
|
||||||
|
"""Returns smallest positive value of y."""
|
||||||
|
ydata = self.get_ydata(0)
|
||||||
|
mask = ydata > 0
|
||||||
|
if numpy.any(mask):
|
||||||
|
return ydata[mask].min()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def write_to_hdf(self, hdffile, where, name, title, complib=None, complevel=None):
|
||||||
|
"""
|
||||||
|
Write the temperature result to an HDF5 group.
|
||||||
|
|
||||||
if (x is None) and (y is None) and (desc is None) and (job_id is None) and (job_date is None):
|
Stores x (timestamps), y (temperature readings), and setpoint arrays
|
||||||
pass
|
as an HDF5 table, with metadata saved as group attributes.
|
||||||
|
"""
|
||||||
|
h5_table_format = {
|
||||||
|
"x": tables.Float64Col(),
|
||||||
|
"y": tables.Float64Col(),
|
||||||
|
"setpoint": tables.Float64Col(),
|
||||||
|
}
|
||||||
|
filter = None
|
||||||
|
if complib is not None:
|
||||||
|
if complevel is None:
|
||||||
|
complevel = 9
|
||||||
|
filter = tables.Filters(complevel=complevel, complib=complib, shuffle=1)
|
||||||
|
|
||||||
elif (x is not None) and (y is not None) and (desc is not None) and (job_id is not None) and (job_date is not None):
|
with self.__listener_lock:
|
||||||
pass
|
xdata = numpy.array(self.x, dtype=float) if self.x else numpy.array([], dtype=float)
|
||||||
|
ydata = numpy.array(self.y, dtype=float) if self.y else numpy.array([], dtype=float)
|
||||||
|
spdata = numpy.array(self.setpoint, dtype=float) if self.setpoint else numpy.array([], dtype=float)
|
||||||
|
|
||||||
else:
|
# Use the length of the longest array; shorter ones are padded with 0
|
||||||
raise ValueError("Wrong usage of __init__!")
|
n = max(len(xdata), len(ydata), 1)
|
||||||
|
if len(xdata) < n:
|
||||||
|
xdata = numpy.pad(xdata, (0, n - len(xdata)), constant_values=0.0)
|
||||||
|
if len(ydata) < n:
|
||||||
|
ydata = numpy.pad(ydata, (0, n - len(ydata)), constant_values=0.0)
|
||||||
|
if len(spdata) < n:
|
||||||
|
spdata = numpy.pad(spdata, (0, n - len(spdata)), constant_values=0.0)
|
||||||
|
|
||||||
|
temp_table = hdffile.create_table(
|
||||||
|
where=where, name=name,
|
||||||
|
description=h5_table_format,
|
||||||
|
title=title,
|
||||||
|
filters=filter,
|
||||||
|
expectedrows=n,
|
||||||
|
)
|
||||||
|
temp_table.flavor = "numpy"
|
||||||
|
temp_table.attrs.damaris_type = "TemperatureResult"
|
||||||
|
temp_table.attrs.xlabel = self.xlabel
|
||||||
|
temp_table.attrs.ylabel = self.ylabel
|
||||||
|
|
||||||
|
if hasattr(self, "job_id") and self.job_id is not None:
|
||||||
|
temp_table.attrs.job_id = self.job_id
|
||||||
|
if hasattr(self, "job_date") and self.job_date is not None:
|
||||||
|
temp_table.attrs.time = "%04d%02d%02d %02d:%02d:%02d.%03d" % (
|
||||||
|
self.job_date.year,
|
||||||
|
self.job_date.month,
|
||||||
|
self.job_date.day,
|
||||||
|
self.job_date.hour,
|
||||||
|
self.job_date.minute,
|
||||||
|
self.job_date.second,
|
||||||
|
self.job_date.microsecond // 1000,
|
||||||
|
)
|
||||||
|
if hasattr(self, "description") and self.description is not None:
|
||||||
|
for (key, value) in self.description.items():
|
||||||
|
temp_table.attrs.__setattr__("description_" + key, str(value))
|
||||||
|
|
||||||
|
row = temp_table.row
|
||||||
|
for i in range(n):
|
||||||
|
row["x"] = xdata[i]
|
||||||
|
row["y"] = ydata[i]
|
||||||
|
row["setpoint"] = spdata[i]
|
||||||
|
row.append()
|
||||||
|
|
||||||
|
temp_table.flush()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
"""String representation of the temperature result."""
|
||||||
|
return f"TemperatureResult(points={len(self)}, job_id={self.job_id})"
|
||||||
|
|
||||||
|
|
||||||
|
def read_from_hdf(hdf_node):
|
||||||
|
"""
|
||||||
|
Read a TemperatureResult object from an HDF5 table node.
|
||||||
|
Returns None if the node is not suitable.
|
||||||
|
"""
|
||||||
|
if not isinstance(hdf_node, tables.Table):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if hdf_node._v_attrs.damaris_type != "TemperatureResult":
|
||||||
|
return None
|
||||||
|
|
||||||
|
tr = TemperatureResult()
|
||||||
|
tr.xlabel = getattr(hdf_node._v_attrs, "xlabel", "Time (s)")
|
||||||
|
tr.ylabel = getattr(hdf_node._v_attrs, "ylabel", "Temperature (C)")
|
||||||
|
|
||||||
|
for r in hdf_node.iterrows():
|
||||||
|
tr.x.append(r["x"])
|
||||||
|
tr.y.append(r["y"])
|
||||||
|
sp = r["setpoint"]
|
||||||
|
if sp != 0.0 or len(tr.setpoint) > 0:
|
||||||
|
tr.setpoint.append(sp)
|
||||||
|
|
||||||
|
return tr
|
||||||
@@ -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", "FFT", "Error_Result", "Config_Result", "TemperatureResult" ]
|
__all__=["ADC_Result", "Accumulation", "MeasurementResult", "AccumulatedValue", "FitResult", "DataPool", "Error_Result", "Config_Result", "TemperatureResult" ]
|
||||||
|
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import io
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
from damaris.experiments import Experiment
|
||||||
|
|
||||||
|
|
||||||
|
class DurationEstimator:
|
||||||
|
"""
|
||||||
|
Consumes an experiment script's generator and estimates total duration
|
||||||
|
without writing any files or invoking the backend.
|
||||||
|
|
||||||
|
Sleeps and synchronizations in the script are tracked instead of
|
||||||
|
blocking, so their wall-clock time is included in the estimate.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def estimate(self, script):
|
||||||
|
"""
|
||||||
|
Run the experiment script in a sandbox and return a duration estimate.
|
||||||
|
|
||||||
|
:param str script: experiment script source code
|
||||||
|
:returns: dict with 'experiment_time', 'sleep_time', 'total_time', and 'job_count'
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
sleep_time = [0.0] # list so it's mutable in nested function
|
||||||
|
|
||||||
|
def tracking_sleep(seconds):
|
||||||
|
sleep_time[0] += seconds
|
||||||
|
|
||||||
|
def noop_sync(before=0, waitsteps=0.1):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def noop_data():
|
||||||
|
return {}
|
||||||
|
|
||||||
|
dataspace = {}
|
||||||
|
exp_classes = __import__('damaris.experiments', dataspace, dataspace, ['Experiment'])
|
||||||
|
for name in dir(exp_classes):
|
||||||
|
if name[:2] == "__" and name[-2:] == "__":
|
||||||
|
continue
|
||||||
|
dataspace[name] = exp_classes.__dict__[name]
|
||||||
|
del exp_classes
|
||||||
|
|
||||||
|
# ranges module for lin_range, log_range, etc.
|
||||||
|
ranges = __import__('damaris.tools.ranges', dataspace, dataspace, [])
|
||||||
|
for name in dir(ranges):
|
||||||
|
if name[:2] == "__" and name[-2:] == "__":
|
||||||
|
continue
|
||||||
|
dataspace[name] = getattr(ranges, name)
|
||||||
|
del ranges
|
||||||
|
|
||||||
|
# inject sandbox helpers (non-blocking versions)
|
||||||
|
dataspace["data"] = noop_data
|
||||||
|
dataspace["synchronize"] = noop_sync
|
||||||
|
dataspace["sleep"] = tracking_sleep
|
||||||
|
|
||||||
|
# execute the script
|
||||||
|
try:
|
||||||
|
exec(script, dataspace)
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
raise RuntimeError("Script execution failed") from e
|
||||||
|
|
||||||
|
if "experiment" not in dataspace:
|
||||||
|
raise ValueError("Script does not define an experiment() function")
|
||||||
|
|
||||||
|
# consume the generator
|
||||||
|
try:
|
||||||
|
exp_iterator = dataspace["experiment"]()
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
raise RuntimeError("Calling experiment() failed") from e
|
||||||
|
|
||||||
|
experiment_time = 0.0
|
||||||
|
job_count = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
job = next(exp_iterator)
|
||||||
|
except StopIteration:
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
raise RuntimeError("Generator failed") from e
|
||||||
|
|
||||||
|
experiment_time += job.get_length()
|
||||||
|
job_count += 1
|
||||||
|
|
||||||
|
total_time = experiment_time + sleep_time[0]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"experiment_time": experiment_time,
|
||||||
|
"sleep_time": sleep_time[0],
|
||||||
|
"total_time": total_time,
|
||||||
|
"job_count": job_count,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_duration(script):
|
||||||
|
"""
|
||||||
|
Convenience function to estimate experiment duration.
|
||||||
|
|
||||||
|
:param str script: experiment script source code
|
||||||
|
:returns: dict with 'experiment_time', 'sleep_time', 'total_time', and 'job_count'
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
return DurationEstimator().estimate(script)
|
||||||
@@ -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 = 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,34 +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=[]
|
self.total_time: list[float] = [0.0]
|
||||||
self.total_time.append(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=[]
|
self.loop_iterations: list[int] = [1]
|
||||||
self.loop_iterations.append(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.
|
||||||
|
|
||||||
@@ -153,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
|
||||||
|
|
||||||
@@ -161,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:
|
||||||
@@ -170,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.
|
||||||
|
|
||||||
@@ -185,24 +184,24 @@ 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`.
|
||||||
|
|
||||||
Typically only used in very special cases.
|
Typically only used in very special cases.
|
||||||
"""
|
"""
|
||||||
self.state_list.append('<state time="%s">\n' % repr(time))
|
self.state_list.append('<state time="%s">\n' % repr(float(time)))
|
||||||
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.
|
||||||
|
|
||||||
@@ -243,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
|
||||||
@@ -253,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).
|
||||||
@@ -367,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.
|
||||||
@@ -386,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.
|
||||||
"""
|
"""
|
||||||
@@ -398,23 +398,23 @@ 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.
|
||||||
|
|
||||||
:param float frequency: frequency to set in Hz
|
:param float frequency: frequency to set in Hz
|
||||||
:param float phase: phase to set
|
:param float phase: phase to set, normalized to the range [0, 360)
|
||||||
:param int ttls: default=0, additional ttl lines to set (integer)
|
:param int ttls: default=0, additional ttl lines to set (integer)
|
||||||
"""
|
"""
|
||||||
s_content = '<analogout id="0" f="%f" phase="%f"/>' % (frequency, phase)
|
s_content = '<analogout id="0" f="%f" phase="%f"/>' % (frequency, phase % 360)
|
||||||
if ttls != 0:
|
if ttls != 0:
|
||||||
s_content += '<ttlout value="0x%06x"/>' % ttls
|
s_content += '<ttlout value="0x%06x"/>' % ttls
|
||||||
self.state_list.append(StateSimple(2e-6, s_content))
|
self.state_list.append(StateSimple(2e-6, s_content))
|
||||||
|
|
||||||
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
|
||||||
@@ -482,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.
|
||||||
@@ -546,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.
|
||||||
|
|
||||||
@@ -582,8 +582,9 @@ class Experiment:
|
|||||||
- The state length for this command is 0.5 µs.
|
- The state length for this command is 0.5 µs.
|
||||||
- The stabilization time for the phase change is RF source dependent. A typical value for PTS310 is 2 µs.
|
- The stabilization time for the phase change is RF source dependent. A typical value for PTS310 is 2 µs.
|
||||||
- The phase is relative to the initial phase set at start of the experiment.
|
- The phase is relative to the initial phase set at start of the experiment.
|
||||||
|
- Negative phases are normalized to the range [0, 360) before writing the job file.
|
||||||
"""
|
"""
|
||||||
s_content = '<analogout phase="%f" />' % (phase)
|
s_content = '<analogout phase="%f" />' % (phase % 360)
|
||||||
if ttls!=0:
|
if ttls!=0:
|
||||||
s_content += '<ttlout value="%d"/>' % ttls
|
s_content += '<ttlout value="%d"/>' % ttls
|
||||||
self.state_list.append(StateSimple(0.5e-6, s_content))
|
self.state_list.append(StateSimple(0.5e-6, s_content))
|
||||||
@@ -591,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.
|
||||||
|
|
||||||
@@ -631,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.
|
||||||
@@ -655,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
|
||||||
|
|
||||||
@@ -680,19 +681,19 @@ class Experiment:
|
|||||||
value=""
|
value=""
|
||||||
elif type(value) is float or isinstance(value, numpy.floating):
|
elif type(value) is float or isinstance(value, numpy.floating):
|
||||||
type_string="Float"
|
type_string="Float"
|
||||||
value=repr(value)
|
value=repr(float(value))
|
||||||
elif type(value) is int or isinstance(value, numpy.integer):
|
elif type(value) is int or isinstance(value, numpy.integer):
|
||||||
type_string="Int"
|
type_string="Int"
|
||||||
value=repr(value)
|
value=repr(int(value))
|
||||||
elif type(value) is int:
|
elif type(value) is int:
|
||||||
type_string="Long"
|
type_string="Long"
|
||||||
value=repr(value)
|
value=repr(value)
|
||||||
elif type(value) is complex or isinstance(value, numpy.complexfloating):
|
elif type(value) is complex or isinstance(value, numpy.complexfloating):
|
||||||
type_string="Complex"
|
type_string="Complex"
|
||||||
value=repr(value)
|
value=repr(complex(value))
|
||||||
elif type(value) is bool or isinstance(value, numpy.bool_):
|
elif type(value) is bool or isinstance(value, numpy.bool_):
|
||||||
type_string="Boolean"
|
type_string="Boolean"
|
||||||
value=repr(value)
|
value=repr(bool(value))
|
||||||
elif type(value) in (str,):
|
elif type(value) in (str,):
|
||||||
type_string="String"
|
type_string="String"
|
||||||
else:
|
else:
|
||||||
@@ -708,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].
|
||||||
@@ -725,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)
|
||||||
|
|||||||
@@ -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
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ import sys
|
|||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
import glob
|
import glob
|
||||||
|
import logging
|
||||||
from . import ExperimentWriter
|
from . import ExperimentWriter
|
||||||
from . import ResultReader
|
from . import ResultReader
|
||||||
|
from .logging_utils import FileTailerHandler
|
||||||
import threading
|
import threading
|
||||||
import types
|
import types
|
||||||
import signal
|
import signal
|
||||||
|
|
||||||
|
logger = logging.getLogger("damaris.backend_driver")
|
||||||
|
|
||||||
if sys.platform=="win32":
|
if sys.platform=="win32":
|
||||||
import winreg
|
import winreg
|
||||||
|
|
||||||
@@ -40,7 +44,7 @@ class BackendDriver(threading.Thread):
|
|||||||
try:
|
try:
|
||||||
os.makedirs(os.path.abspath(self.spool_dir))
|
os.makedirs(os.path.abspath(self.spool_dir))
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
print(e)
|
logger.error("Could not create backend's spool directory %s: %s", self.spool_dir, e)
|
||||||
raise AssertionError("could not create backend's spool directory %s "%self.spool_dir)
|
raise AssertionError("could not create backend's spool directory %s "%self.spool_dir)
|
||||||
|
|
||||||
# remove stale state filenames
|
# remove stale state filenames
|
||||||
@@ -62,10 +66,10 @@ class BackendDriver(threading.Thread):
|
|||||||
if os.path.isdir("/proc/%d"%core_pid):
|
if os.path.isdir("/proc/%d"%core_pid):
|
||||||
raise AssertionError("found backend with pid %d (state file %s) in same spool dir"%(core_pid,statefilename))
|
raise AssertionError("found backend with pid %d (state file %s) in same spool dir"%(core_pid,statefilename))
|
||||||
else:
|
else:
|
||||||
print("removing stale backend state file", statefilename)
|
logger.warning("Removing stale backend state file: %s", statefilename)
|
||||||
os.remove(statefilename)
|
os.remove(statefilename)
|
||||||
else:
|
else:
|
||||||
print("todo: take care of existing backend state files")
|
logger.warning("TODO: take care of existing backend state files (Windows)")
|
||||||
|
|
||||||
self.result_reader = ResultReader.BlockingResultReader(self.spool_dir,
|
self.result_reader = ResultReader.BlockingResultReader(self.spool_dir,
|
||||||
no=0,
|
no=0,
|
||||||
@@ -81,21 +85,9 @@ class BackendDriver(threading.Thread):
|
|||||||
self.raised_exception=None
|
self.raised_exception=None
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
# take care of older logfiles
|
# Log file for backend output
|
||||||
self.core_output_filename=os.path.join(self.spool_dir,"logdata")
|
self.core_output_filename = os.path.join(self.spool_dir, "logdata")
|
||||||
if os.path.isfile(self.core_output_filename):
|
self.core_output = open(self.core_output_filename, "w")
|
||||||
i=0
|
|
||||||
max_logs=100
|
|
||||||
while os.path.isfile(self.core_output_filename+".%02d"%i):
|
|
||||||
i+=1
|
|
||||||
while (i>=max_logs):
|
|
||||||
i-=1
|
|
||||||
os.remove(self.core_output_filename+".%02d"%i)
|
|
||||||
for j in range(i):
|
|
||||||
os.rename(self.core_output_filename+".%02d"%(i-j-1),self.core_output_filename+".%02d"%(i-j))
|
|
||||||
os.rename(self.core_output_filename, self.core_output_filename+".%02d"%0)
|
|
||||||
# create logfile
|
|
||||||
self.core_output=open(self.core_output_filename,"w")
|
|
||||||
|
|
||||||
# again look out for existing state files
|
# again look out for existing state files
|
||||||
state_files=glob.glob(os.path.join(self.spool_dir,"*.state"))
|
state_files=glob.glob(os.path.join(self.spool_dir,"*.state"))
|
||||||
@@ -118,27 +110,28 @@ class BackendDriver(threading.Thread):
|
|||||||
stdout=self.core_output,
|
stdout=self.core_output,
|
||||||
stderr=self.core_output)
|
stderr=self.core_output)
|
||||||
|
|
||||||
|
# Start tailing backend log file for unified logging
|
||||||
|
self._log_tailer = FileTailerHandler(self.core_output_filename)
|
||||||
|
self._log_tailer.start()
|
||||||
|
|
||||||
# wait till state file shows up
|
# wait till state file shows up
|
||||||
timeout=10
|
timeout=10
|
||||||
# to do: how should I know core's state name????!!!!!
|
# to do: how should I know core's state name????!!!!!
|
||||||
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
|
||||||
print(timeout, self.core_input, self.quit_flag.isSet())
|
logger.error("Backend failed to start: timeout=%s poll=%s", timeout, self.core_input.poll())
|
||||||
if self.core_input is not None:
|
|
||||||
print(self.core_input.poll())
|
|
||||||
log_message=''
|
log_message=''
|
||||||
self.core_input=None
|
self.core_input=None
|
||||||
if os.path.isfile(self.core_output_filename):
|
if os.path.isfile(self.core_output_filename):
|
||||||
# to do include log data
|
|
||||||
log_message='\n'+''.join(open(self.core_output_filename,"r", errors='replace').readlines()[:10])
|
log_message='\n'+''.join(open(self.core_output_filename,"r", errors='replace').readlines()[:10])
|
||||||
if not log_message:
|
if not log_message:
|
||||||
log_message=" no error message from core"
|
log_message=" no error message from core"
|
||||||
self.core_output.close()
|
self.core_output.close()
|
||||||
self.raised_exception="no state file appeared or backend died away:"+log_message
|
self.raised_exception="no state file appeared or backend died away:"+log_message
|
||||||
print(self.raised_exception)
|
logger.error(self.raised_exception)
|
||||||
self.quit_flag.set()
|
self.quit_flag.set()
|
||||||
return
|
return
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
@@ -147,7 +140,7 @@ class BackendDriver(threading.Thread):
|
|||||||
|
|
||||||
# save the one
|
# save the one
|
||||||
if len(state_files)>1:
|
if len(state_files)>1:
|
||||||
print("did find more than one state file, taking first one!")
|
logger.warning("Found more than one state file, taking first one!")
|
||||||
self.statefilename=state_files[0]
|
self.statefilename=state_files[0]
|
||||||
|
|
||||||
# read state file
|
# read state file
|
||||||
@@ -163,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)
|
||||||
@@ -182,28 +175,28 @@ class BackendDriver(threading.Thread):
|
|||||||
backend_result=self.core_input.poll()
|
backend_result=self.core_input.poll()
|
||||||
if backend_result is not None: break
|
if backend_result is not None: break
|
||||||
if wait_loop_counter==10:
|
if wait_loop_counter==10:
|
||||||
print("sending termination signal to backend process")
|
logger.warning("Sending termination signal to backend process")
|
||||||
self.send_signal("SIGTERM")
|
self.send_signal("SIGTERM")
|
||||||
elif wait_loop_counter==20:
|
elif wait_loop_counter==20:
|
||||||
print("sending kill signal to backend process")
|
logger.warning("Sending kill signal to backend process")
|
||||||
self.send_signal("SIGKILL")
|
self.send_signal("SIGKILL")
|
||||||
elif wait_loop_counter>30:
|
elif wait_loop_counter>30:
|
||||||
print("no longer waiting for backend shutdown")
|
logger.warning("No longer waiting for backend shutdown")
|
||||||
break
|
break
|
||||||
|
|
||||||
if backend_result is None:
|
if backend_result is None:
|
||||||
print("backend dit not end properly, please stop it manually")
|
logger.error("Backend did not end properly, please stop it manually")
|
||||||
elif backend_result>0:
|
elif backend_result>0:
|
||||||
print("backend returned ", backend_result)
|
logger.error("Backend returned exit code %d", backend_result)
|
||||||
elif backend_result<0:
|
elif backend_result<0:
|
||||||
sig_name=[x for x in dir(signal) if x.startswith("SIG") and \
|
sig_name=[x for x in dir(signal) if x.startswith("SIG") and \
|
||||||
x[3]!="_" and \
|
x[3]!="_" and \
|
||||||
(type(signal.__dict__[x])is int) and \
|
(type(signal.__dict__[x])is int) and \
|
||||||
signal.__dict__[x]==-backend_result]
|
signal.__dict__[x]==-backend_result]
|
||||||
if sig_name:
|
if sig_name:
|
||||||
print("backend was terminated by signal ",sig_name[0])
|
logger.error("Backend was terminated by signal %s", sig_name[0])
|
||||||
else:
|
else:
|
||||||
print("backend was terminated by signal no",-backend_result)
|
logger.error("Backend was terminated by signal no %d", -backend_result)
|
||||||
self.core_input = None
|
self.core_input = None
|
||||||
self.core_pid = None
|
self.core_pid = None
|
||||||
|
|
||||||
@@ -226,12 +219,6 @@ class BackendDriver(threading.Thread):
|
|||||||
if os.path.isfile(resultfilename):
|
if os.path.isfile(resultfilename):
|
||||||
os.remove(resultfilename)
|
os.remove(resultfilename)
|
||||||
|
|
||||||
def get_messages(self):
|
|
||||||
# return pending messages
|
|
||||||
if self.core_output.tell()==os.path.getsize(self.core_output_filename):
|
|
||||||
return None
|
|
||||||
return self.core_output.read()
|
|
||||||
|
|
||||||
def restart_queue(self):
|
def restart_queue(self):
|
||||||
self.send_signal("SIGUSR1")
|
self.send_signal("SIGUSR1")
|
||||||
|
|
||||||
@@ -250,7 +237,7 @@ class BackendDriver(threading.Thread):
|
|||||||
|
|
||||||
def send_signal(self, sig):
|
def send_signal(self, sig):
|
||||||
if self.core_pid is None:
|
if self.core_pid is None:
|
||||||
print("BackendDriver.send_signal is called with core_pid=None")
|
logger.error("BackendDriver.send_signal called with core_pid=None")
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
if sys.platform[:5]=="linux":
|
if sys.platform[:5]=="linux":
|
||||||
@@ -262,7 +249,7 @@ class BackendDriver(threading.Thread):
|
|||||||
kill_command=os.path.join(cygwin_path,"bin","kill.exe")
|
kill_command=os.path.join(cygwin_path,"bin","kill.exe")
|
||||||
os.popen("%s -%s %d"%(kill_command,sig,self.core_pid))
|
os.popen("%s -%s %d"%(kill_command,sig,self.core_pid))
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
print("could not send signal %s to core: %s"%(sig, str(e)))
|
logger.error("Could not send signal %s to core: %s", sig, str(e))
|
||||||
|
|
||||||
|
|
||||||
def is_busy(self):
|
def is_busy(self):
|
||||||
@@ -294,3 +281,5 @@ class BackendDriver(threading.Thread):
|
|||||||
if self.core_output:
|
if self.core_output:
|
||||||
self.core_output.close()
|
self.core_output.close()
|
||||||
self.core_output=None
|
self.core_output=None
|
||||||
|
if hasattr(self, '_log_tailer'):
|
||||||
|
self._log_tailer.stop()
|
||||||
+1512
-388
File diff suppressed because it is too large.
Load diff
@@ -3,39 +3,88 @@ import io
|
|||||||
import traceback
|
import traceback
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
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
|
||||||
|
|
||||||
|
logger = logging.getLogger("damaris.experiment_handling")
|
||||||
|
|
||||||
class StopExperiment(Exception):
|
class StopExperiment(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class _SandboxStdout:
|
||||||
|
"""Redirects print() from sandboxed scripts to the GUI textview."""
|
||||||
|
def __init__(self, callback):
|
||||||
|
self._callback = callback
|
||||||
|
self._buffer = ""
|
||||||
|
|
||||||
|
def write(self, message):
|
||||||
|
self._buffer += message
|
||||||
|
if "\n" in self._buffer:
|
||||||
|
lines = self._buffer.split("\n")
|
||||||
|
self._buffer = lines[-1]
|
||||||
|
for line in lines[:-1]:
|
||||||
|
self._callback(line + "\n")
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
if self._buffer:
|
||||||
|
self._callback(self._buffer)
|
||||||
|
self._buffer = ""
|
||||||
|
|
||||||
|
def isatty(self):
|
||||||
|
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
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, script, exp_writer, data):
|
def __init__(self, script, exp_writer, data, log_callback=None):
|
||||||
threading.Thread.__init__(self, name="experiment handler")
|
threading.Thread.__init__(self, name="experiment handler")
|
||||||
self.script=script
|
self.script=script
|
||||||
self.writer=exp_writer
|
self.writer=exp_writer
|
||||||
self.data=data
|
self.data=data
|
||||||
self.quit_flag = threading.Event()
|
self.quit_flag = threading.Event()
|
||||||
|
self._log_callback = log_callback
|
||||||
if self.data is not None:
|
if self.data is not None:
|
||||||
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):
|
||||||
@@ -66,7 +115,7 @@ class ExperimentHandling(threading.Thread):
|
|||||||
found_blocking_sleep = True
|
found_blocking_sleep = True
|
||||||
|
|
||||||
if found_blocking_sleep:
|
if found_blocking_sleep:
|
||||||
print("Warning: time.sleep() detected in experiment script. This is blocking and not interruptible. Please use sleep() instead.")
|
logger.warning("time.sleep() detected in experiment script. This is blocking and not interruptible. Please use sleep() instead.")
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -97,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)
|
||||||
@@ -137,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
|
||||||
|
|||||||
@@ -4,25 +4,37 @@ import sys
|
|||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import traceback
|
import traceback
|
||||||
|
import logging
|
||||||
import ast
|
import ast
|
||||||
from damaris.data import Resultable
|
from damaris.data import Resultable
|
||||||
from damaris.gui.ExperimentHandling import StopExperiment
|
from damaris.gui.ExperimentHandling import StopExperiment, _sandbox_stdout_redirect
|
||||||
|
|
||||||
|
logger = logging.getLogger("damaris.result_handling")
|
||||||
|
|
||||||
class ResultHandling(threading.Thread):
|
class ResultHandling(threading.Thread):
|
||||||
"""
|
"""
|
||||||
runs the result script in sandbox
|
runs the result script in sandbox
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, script_data, result_iterator, data_pool):
|
def __init__(self, script_data, result_iterator, data_pool, log_callback=None):
|
||||||
threading.Thread.__init__(self,name="result handler")
|
threading.Thread.__init__(self,name="result handler")
|
||||||
self.script=script_data
|
self.script=script_data
|
||||||
self.results=result_iterator
|
self.results=result_iterator
|
||||||
self.data_space=data_pool
|
self.data_space=data_pool
|
||||||
self.quit_flag=self.results.quit_flag
|
self.quit_flag=self.results.quit_flag
|
||||||
|
self._log_callback = log_callback
|
||||||
if self.data_space is not None:
|
if self.data_space is not None:
|
||||||
self.data_space["__recentresult"]=-1
|
self.data_space["__recentresult"]=-1
|
||||||
|
|
||||||
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):
|
||||||
# execute it
|
# execute it
|
||||||
dataspace={}
|
dataspace={}
|
||||||
data_classes = __import__('damaris.data', dataspace, dataspace, ['*'])
|
data_classes = __import__('damaris.data', dataspace, dataspace, ['*'])
|
||||||
@@ -52,7 +64,7 @@ class ResultHandling(threading.Thread):
|
|||||||
found_blocking_sleep = True
|
found_blocking_sleep = True
|
||||||
|
|
||||||
if found_blocking_sleep:
|
if found_blocking_sleep:
|
||||||
print("Warning: time.sleep() detected in result script. This is blocking and not interruptible. Please use sleep() instead.")
|
logger.warning("time.sleep() detected in result script. This is blocking and not interruptible. Please use sleep() instead.")
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -87,24 +99,24 @@ class ResultHandling(threading.Thread):
|
|||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -135,7 +135,8 @@ class ResultReader:
|
|||||||
elif t == "Complex":
|
elif t == "Complex":
|
||||||
self.result_description[k]=complex(v)
|
self.result_description[k]=complex(v)
|
||||||
elif t == "Boolean":
|
elif t == "Boolean":
|
||||||
self.result_description[k]=bool(v)
|
# bool("False") would be True, compare the text instead
|
||||||
|
self.result_description[k]=(v == "True")
|
||||||
elif t == "String":
|
elif t == "String":
|
||||||
self.result_description[k]=v
|
self.result_description[k]=v
|
||||||
else:
|
else:
|
||||||
@@ -231,7 +232,6 @@ class ResultReader:
|
|||||||
for i in range(nChannels):
|
for i in range(nChannels):
|
||||||
# split interleaved data
|
# split interleaved data
|
||||||
self.result.y[i][tmp_index:tmp_index+tmp_size] = tmp_part[i::nChannels]
|
self.result.y[i][tmp_index:tmp_index+tmp_size] = tmp_part[i::nChannels]
|
||||||
self.result.y[i][tmp_index:tmp_index+tmp_size] = tmp_part[i::nChannels]
|
|
||||||
|
|
||||||
if self.result.index != []:
|
if self.result.index != []:
|
||||||
self.result.index.append((tmp_index, tmp_index+tmp_size-1))
|
self.result.index.append((tmp_index, tmp_index+tmp_size-1))
|
||||||
@@ -503,7 +503,8 @@ class ResultReader:
|
|||||||
elif t == "Complex":
|
elif t == "Complex":
|
||||||
self.result_description[k]=complex(v)
|
self.result_description[k]=complex(v)
|
||||||
elif t == "Boolean":
|
elif t == "Boolean":
|
||||||
self.result_description[k]=bool(v)
|
# bool("False") would be True, compare the text instead
|
||||||
|
self.result_description[k]=(v == "True")
|
||||||
elif t == "String":
|
elif t == "String":
|
||||||
self.result_description[k]=v
|
self.result_description[k]=v
|
||||||
else:
|
else:
|
||||||
@@ -534,7 +535,7 @@ class BlockingResultReader(ResultReader):
|
|||||||
def __init__(self, spool_dir=".", no=0, result_pattern="job.%09d.result", clear_jobs=False, clear_results=False):
|
def __init__(self, spool_dir=".", no=0, result_pattern="job.%09d.result", clear_jobs=False, clear_results=False):
|
||||||
ResultReader.__init__(self, spool_dir, no, result_pattern, clear_jobs=clear_jobs, clear_results=clear_results)
|
ResultReader.__init__(self, spool_dir, no, result_pattern, clear_jobs=clear_jobs, clear_results=clear_results)
|
||||||
self.stop_no=None # end of job queue
|
self.stop_no=None # end of job queue
|
||||||
self.poll_time=0.1 # sleep interval for polling results, <0 means no polling and stop
|
self.poll_time=0.05 # sleep interval for polling results, <0 means no polling and stop
|
||||||
self.in_advance=0
|
self.in_advance=0
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
@@ -543,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
|
||||||
@@ -559,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:
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
# -*- coding: iso-8859-1 -*-
|
||||||
|
"""
|
||||||
|
Temperature monitoring thread.
|
||||||
|
|
||||||
|
This module provides a thread that periodically reads temperature from a controller
|
||||||
|
and stores it in a TemperatureResult object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from damaris.data import TemperatureResult
|
||||||
|
from damaris.tools.temperature import TemperatureController
|
||||||
|
|
||||||
|
|
||||||
|
class TemperatureMonitor(threading.Thread):
|
||||||
|
"""
|
||||||
|
Thread that periodically reads temperature from a controller
|
||||||
|
and stores it in a TemperatureResult object.
|
||||||
|
|
||||||
|
This thread runs independently and can be configured with any
|
||||||
|
TemperatureController implementation (Eurotherm, Lakeshore, etc.).
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
controller: The TemperatureController instance to read from.
|
||||||
|
interval: Polling interval in seconds.
|
||||||
|
result: The TemperatureResult object storing collected data.
|
||||||
|
quit_flag: Event to signal the thread to stop.
|
||||||
|
data_pool: Optional DataPool to store results.
|
||||||
|
data_key: Key to use when storing in data_pool.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, controller: TemperatureController,
|
||||||
|
interval: float = 1.0,
|
||||||
|
data_pool=None,
|
||||||
|
data_key: str = "temperature"):
|
||||||
|
"""
|
||||||
|
Initialize the temperature monitor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
controller: TemperatureController implementation (Eurotherm, etc.)
|
||||||
|
interval: Polling interval in seconds (default: 1.0)
|
||||||
|
data_pool: Optional DataPool to store results (default: None)
|
||||||
|
data_key: Key to use when storing in data_pool (default: "temperature")
|
||||||
|
"""
|
||||||
|
threading.Thread.__init__(self, name="TemperatureMonitor")
|
||||||
|
self.controller = controller
|
||||||
|
self.interval = float(interval)
|
||||||
|
self.data_pool = data_pool
|
||||||
|
self.data_key = data_key
|
||||||
|
self.quit_flag = threading.Event()
|
||||||
|
self.result = TemperatureResult()
|
||||||
|
self._running = False
|
||||||
|
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
|
||||||
|
self.result.xlabel = "Time (s)"
|
||||||
|
self.result.ylabel = "Temperature (C)"
|
||||||
|
self.result.description = {"type": "temperature_monitoring"}
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""
|
||||||
|
Main thread loop: periodically reads temperature and stores it.
|
||||||
|
"""
|
||||||
|
self._running = True
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while not self.quit_flag.is_set() and self._running:
|
||||||
|
try:
|
||||||
|
# Read temperature and setpoint
|
||||||
|
timestamp = time.time() - start_time
|
||||||
|
current_temp = self.controller.get_temperature()
|
||||||
|
setpoint = self.controller.get_setpoint()
|
||||||
|
|
||||||
|
# Store in result
|
||||||
|
with self._lock:
|
||||||
|
if not hasattr(self.result, 'x'):
|
||||||
|
self.result.x = []
|
||||||
|
self.result.y = []
|
||||||
|
self.result.setpoint = []
|
||||||
|
|
||||||
|
self.result.x.append(timestamp)
|
||||||
|
self.result.y.append(current_temp)
|
||||||
|
self.result.setpoint.append(setpoint)
|
||||||
|
|
||||||
|
# Update job metadata on first sample
|
||||||
|
if len(self.result.x) == 1:
|
||||||
|
self.result.job_date = datetime.datetime.now()
|
||||||
|
self.result.job_id = id(self)
|
||||||
|
|
||||||
|
# Store result in data_pool (thread-safe for GTK)
|
||||||
|
# Replace the entry on each update to trigger DataPool events
|
||||||
|
if self.data_pool is not None:
|
||||||
|
with self._lock:
|
||||||
|
# Create a copy for the data_pool with current data
|
||||||
|
data_pool_result = TemperatureResult()
|
||||||
|
data_pool_result.xlabel = self.result.xlabel
|
||||||
|
data_pool_result.ylabel = self.result.ylabel
|
||||||
|
data_pool_result.description = self.result.description.copy() if isinstance(self.result.description, dict) else {}
|
||||||
|
# Copy the data from self.result
|
||||||
|
data_pool_result.x = list(self.result.x) if hasattr(self.result, 'x') else []
|
||||||
|
data_pool_result.y = list(self.result.y) if hasattr(self.result, 'y') else []
|
||||||
|
data_pool_result.setpoint = list(self.result.setpoint) if hasattr(self.result, 'setpoint') else []
|
||||||
|
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
|
||||||
|
|
||||||
|
# Store in data_pool (dict item assignment is atomic under the GIL)
|
||||||
|
self.data_pool[self.data_key] = data_pool_result
|
||||||
|
|
||||||
|
# Reset consecutive failure counter on successful read
|
||||||
|
self._consecutive_failures = 0
|
||||||
|
self._last_error = None
|
||||||
|
self._connection_ok = True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._consecutive_failures += 1
|
||||||
|
self._last_error = str(e)
|
||||||
|
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_remaining = self.interval
|
||||||
|
while sleep_remaining > 0 and not self.quit_flag.is_set() and self._running:
|
||||||
|
sleep_time = min(0.1, sleep_remaining)
|
||||||
|
self.quit_flag.wait(sleep_time)
|
||||||
|
sleep_remaining -= sleep_time
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""
|
||||||
|
Graceful shutdown: signal the thread to stop and wait for it.
|
||||||
|
"""
|
||||||
|
self._running = False
|
||||||
|
self.quit_flag.set()
|
||||||
|
# Wait for the thread to finish, with a generous timeout
|
||||||
|
# Use a longer timeout to account for potential delays
|
||||||
|
if self.is_alive():
|
||||||
|
self.join(timeout=5.0) # Wait up to 5 seconds
|
||||||
|
if self.is_alive():
|
||||||
|
print("Warning: TemperatureMonitor thread did not stop gracefully")
|
||||||
|
# Thread is still running, but we can't force stop it
|
||||||
|
# The next start will check if it's alive and handle it
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""
|
||||||
|
Clear the collected data and start fresh.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
self.result = TemperatureResult()
|
||||||
|
self.result.xlabel = "Time (s)"
|
||||||
|
self.result.ylabel = "Temperature (C)"
|
||||||
|
self.result.description = {"type": "temperature_monitoring"}
|
||||||
|
|
||||||
|
def get_current_temperature(self) -> float:
|
||||||
|
"""
|
||||||
|
Get the latest temperature reading.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Latest temperature in Celsius, or 0.0 if no data.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
if hasattr(self.result, 'y') and len(self.result.y) > 0:
|
||||||
|
return self.result.y[-1]
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def get_current_setpoint(self) -> float:
|
||||||
|
"""
|
||||||
|
Get the latest setpoint reading.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Latest setpoint in Celsius, or 0.0 if no data.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
if hasattr(self.result, 'setpoint') and len(self.result.setpoint) > 0:
|
||||||
|
return self.result.setpoint[-1]
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def is_running(self) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the monitor is currently running.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the monitor thread is active.
|
||||||
|
"""
|
||||||
|
return self._running and not self.quit_flag.is_set()
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from .logging_utils import (
|
||||||
|
FileTailerHandler,
|
||||||
|
LogDispatcher,
|
||||||
|
setup_logging,
|
||||||
|
)
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*-->
|
|
||||||
<!DOCTYPE glade-project SYSTEM "http://glade.gnome.org/glade-project-2.0.dtd">
|
|
||||||
|
|
||||||
<glade-project>
|
|
||||||
<name>damaris-gui</name>
|
|
||||||
<program_name>damaris</program_name>
|
|
||||||
<gnome_support>FALSE</gnome_support>
|
|
||||||
</glade-project>
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -91,18 +91,21 @@
|
|||||||
</data>
|
</data>
|
||||||
</object>
|
</object>
|
||||||
<object class="GtkTextBuffer" id="textbuffer1">
|
<object class="GtkTextBuffer" id="textbuffer1">
|
||||||
<property name="text">DAMARIS python frontend version %(damarisversion)s, 2012
|
<property name="text">DAMARIS (DArmstadt Magnetic Resonance Instrument Software)
|
||||||
|
Python frontend version %(damarisversion)s
|
||||||
|
|
||||||
|
Copyright © 2004-2026 Achim Gädke, Markus Rosenstihl, and others.
|
||||||
|
Homepage: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/python3-damaris
|
||||||
|
|
||||||
|
Components version information:
|
||||||
%(moduleversions)s
|
%(moduleversions)s
|
||||||
|
|
||||||
DArmstadt Magnetic Resonance Instrument Software, 2004-2012
|
|
||||||
Homepage https://element.fkp.physik.tu-darmstadt.de/damaris_cms/
|
|
||||||
|
|
||||||
Python frontend written by Christopher Schmitt, Achim Gädke, Markus Rosenstihl and Christian Tacke
|
|
||||||
Backends written by Achim Gädke, Markus Rosenstihl, Christian Tacke, tested by Holger Stork
|
|
||||||
The drivers included in the backend may use proprietary interfaces.
|
The drivers included in the backend may use proprietary interfaces.
|
||||||
The source code distribution may include commercial third party's code,
|
The source code distribution may include commercial third party's code,
|
||||||
which is specially marked and not covered by this license.
|
which is specially marked and not covered by this license.
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
GNU GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 2, June 1991
|
Version 2, June 1991
|
||||||
|
|
||||||
@@ -491,6 +494,21 @@ Public License instead of this License.
|
|||||||
<property name="homogeneous">True</property>
|
<property name="homogeneous">True</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkToolButton" id="toolbar_open_hdf5_button">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="tooltip-text" translatable="yes">Open DAMARIS HDF5 file</property>
|
||||||
|
<property name="label">Open HDF5</property>
|
||||||
|
<property name="use-underline">True</property>
|
||||||
|
<property name="icon-name">document-open</property>
|
||||||
|
<signal name="clicked" handler="on_toolbar_open_hdf5_button_clicked" swapped="no"/>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="homogeneous">True</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkToolButton" id="toolbar_save_file_button">
|
<object class="GtkToolButton" id="toolbar_save_file_button">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
@@ -626,6 +644,21 @@ Public License instead of this License.
|
|||||||
<property name="homogeneous">True</property>
|
<property name="homogeneous">True</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkToolButton" id="toolbar_estimate_button">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="tooltip-text" translatable="yes">Estimate experiment duration</property>
|
||||||
|
<property name="label">Estimate</property>
|
||||||
|
<property name="use-underline">True</property>
|
||||||
|
<property name="icon-name">dialog-information</property>
|
||||||
|
<signal name="clicked" handler="on_toolbar_estimate_button_clicked" swapped="no"/>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="homogeneous">True</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkToolItem" id="toolitem4">
|
<object class="GtkToolItem" id="toolitem4">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
@@ -894,7 +927,6 @@ Public License instead of this License.
|
|||||||
<object class="GtkSourceView" id="data_handling_textview">
|
<object class="GtkSourceView" id="data_handling_textview">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<signal name="event" handler="on_data_handling_textview_event" swapped="no"/>
|
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
|||||||
@@ -0,0 +1,256 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Unified logging for DAMARIS.
|
||||||
|
|
||||||
|
Provides:
|
||||||
|
- FileTailerHandler: reads new lines from the backend log file and emits them
|
||||||
|
as logging records into the damaris.backend logger.
|
||||||
|
- LogDispatcher: a logging.Handler that dispatches records to both a GTK
|
||||||
|
messages_textview (via idle_add) and the console.
|
||||||
|
- setup_logging(): convenience function to configure the root logger.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import queue
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Severity prefix pattern for C++ backend messages
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
_BACKEND_PREFIX_RE = re.compile(r"^\[(DEBUG|INFO|WARNING|ERROR|CRITICAL)\]\s*(.*)")
|
||||||
|
|
||||||
|
# Mapping from string prefix to logging level
|
||||||
|
_PREFIX_TO_LEVEL = {
|
||||||
|
"DEBUG": logging.DEBUG,
|
||||||
|
"INFO": logging.INFO,
|
||||||
|
"WARNING": logging.WARNING,
|
||||||
|
"ERROR": logging.ERROR,
|
||||||
|
"CRITICAL": logging.CRITICAL,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# FileTailerHandler
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class FileTailerHandler(logging.Handler):
|
||||||
|
"""
|
||||||
|
Reads new lines from a log file and emits them as log records.
|
||||||
|
|
||||||
|
Runs a background thread that polls the file every `poll_interval` seconds.
|
||||||
|
New lines are parsed for a severity prefix (e.g. ``[INFO] message``) and
|
||||||
|
emitted as LogRecords under the ``damaris.backend`` logger. Lines without
|
||||||
|
a recognized prefix are emitted at INFO level.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
filename : str
|
||||||
|
Path to the log file to tail.
|
||||||
|
poll_interval : float, optional
|
||||||
|
Seconds between polls (default 0.1).
|
||||||
|
logger_name : str, optional
|
||||||
|
Logger name for emitted records (default "damaris.backend").
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, filename, poll_interval=0.1, logger_name="damaris.backend"):
|
||||||
|
super().__init__()
|
||||||
|
self._filename = filename
|
||||||
|
self._poll_interval = poll_interval
|
||||||
|
self._logger_name = logger_name
|
||||||
|
self._queue = queue.Queue()
|
||||||
|
self._thread = None
|
||||||
|
self._stop_event = threading.Event()
|
||||||
|
self._position = 0
|
||||||
|
self._file = None
|
||||||
|
|
||||||
|
def _tailer_loop(self):
|
||||||
|
"""Background thread: open file, read new lines, push to queue."""
|
||||||
|
while not self._stop_event.is_set():
|
||||||
|
try:
|
||||||
|
if not os.path.isfile(self._filename):
|
||||||
|
time.sleep(self._poll_interval)
|
||||||
|
continue
|
||||||
|
|
||||||
|
fh = open(self._filename, "r", errors="replace")
|
||||||
|
fh.seek(self._position)
|
||||||
|
new_lines = fh.readlines()
|
||||||
|
self._position = fh.tell()
|
||||||
|
fh.close()
|
||||||
|
|
||||||
|
for line in new_lines:
|
||||||
|
line = line.rstrip("\n\r")
|
||||||
|
if line:
|
||||||
|
self._queue.put(line)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
# File may have been rotated or deleted; reset position
|
||||||
|
self._position = 0
|
||||||
|
|
||||||
|
self._stop_event.wait(self._poll_interval)
|
||||||
|
|
||||||
|
def emit(self, record):
|
||||||
|
"""Called when a log record is ready. Not used directly — lines are
|
||||||
|
pushed from the queue by _process_queue()."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _process_queue(self):
|
||||||
|
"""Drain the queue and emit parsed log records. Called from the
|
||||||
|
thread that owns the handler (typically the GUI thread)."""
|
||||||
|
while not self._queue.empty():
|
||||||
|
try:
|
||||||
|
line = self._queue.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
break
|
||||||
|
|
||||||
|
m = _BACKEND_PREFIX_RE.match(line)
|
||||||
|
if m:
|
||||||
|
level_str, message = m.group(1), m.group(2)
|
||||||
|
level = _PREFIX_TO_LEVEL.get(level_str, logging.INFO)
|
||||||
|
else:
|
||||||
|
level = logging.INFO
|
||||||
|
message = line
|
||||||
|
|
||||||
|
# Create a LogRecord from the raw message
|
||||||
|
record = logging.LogRecord(
|
||||||
|
name=self._logger_name,
|
||||||
|
level=level,
|
||||||
|
pathname="(tailer)",
|
||||||
|
lineno=0,
|
||||||
|
msg=message,
|
||||||
|
args=None,
|
||||||
|
exc_info=None,
|
||||||
|
)
|
||||||
|
self.handle(record)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Start the background tailing thread."""
|
||||||
|
if self._thread is not None and self._thread.is_alive():
|
||||||
|
return
|
||||||
|
self._stop_event.clear()
|
||||||
|
self._thread = threading.Thread(target=self._tailer_loop, name="log-tailer", daemon=True)
|
||||||
|
self._thread.start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop the background thread."""
|
||||||
|
self._stop_event.set()
|
||||||
|
if self._thread is not None:
|
||||||
|
self._thread.join(timeout=2)
|
||||||
|
self._thread = None
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.stop()
|
||||||
|
super().close()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# LogDispatcher
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class LogDispatcher(logging.Handler):
|
||||||
|
"""
|
||||||
|
Dispatches log records to a GTK messages_textview and/or the console.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
gui_callback : callable or None
|
||||||
|
If not None, called with the formatted message string. Must be
|
||||||
|
thread-safe (e.g. wrapped in ``gobject.idle_add``). This is the
|
||||||
|
textview's ``__call__`` method from DamarisGUI.
|
||||||
|
console : bool, optional
|
||||||
|
If True, also emit to sys.stderr (default True).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, gui_callback=None, console=True):
|
||||||
|
super().__init__()
|
||||||
|
self._gui_callback = gui_callback
|
||||||
|
self._console_handler = logging.StreamHandler(sys.stderr) if console else None
|
||||||
|
if self._console_handler:
|
||||||
|
self._console_handler.setFormatter(
|
||||||
|
logging.Formatter("%(asctime)s [%(levelname)-8s] %(name)s: %(message)s",
|
||||||
|
datefmt="%H:%M:%S")
|
||||||
|
)
|
||||||
|
|
||||||
|
def emit(self, record):
|
||||||
|
try:
|
||||||
|
msg = self.format(record)
|
||||||
|
|
||||||
|
if self._gui_callback is not None:
|
||||||
|
try:
|
||||||
|
import gi
|
||||||
|
gi.require_version("Gdk", "3.0")
|
||||||
|
from gi.repository import GObject as gobject
|
||||||
|
gobject.idle_add(self._gui_callback, msg + "\n", priority=gobject.PRIORITY_LOW)
|
||||||
|
except Exception:
|
||||||
|
# GUI not available or import failed — ignore
|
||||||
|
pass
|
||||||
|
|
||||||
|
if self._console_handler is not None:
|
||||||
|
self._console_handler.emit(record)
|
||||||
|
except Exception:
|
||||||
|
self.handleError(record)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self._console_handler:
|
||||||
|
self._console_handler.close()
|
||||||
|
super().close()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# setup_logging
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def setup_logging(level=logging.INFO, gui_callback=None, backend_logfile=None):
|
||||||
|
"""
|
||||||
|
Configure the DAMARIS logging system.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
level : int
|
||||||
|
Minimum logging level (default INFO).
|
||||||
|
gui_callback : callable or None
|
||||||
|
Optional callback for GUI textview output.
|
||||||
|
backend_logfile : str or None
|
||||||
|
Path to the backend log file. If provided, a FileTailerHandler is
|
||||||
|
attached to the ``damaris.backend`` logger.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
logger : logging.Logger
|
||||||
|
The root damaris logger.
|
||||||
|
"""
|
||||||
|
root = logging.getLogger("damaris")
|
||||||
|
root.setLevel(level)
|
||||||
|
|
||||||
|
# Remove existing handlers to avoid duplicates on re-init
|
||||||
|
root.handlers.clear()
|
||||||
|
|
||||||
|
# Console handler on the root logger
|
||||||
|
console = logging.StreamHandler(sys.stderr)
|
||||||
|
console.setFormatter(
|
||||||
|
logging.Formatter("%(asctime)s [%(levelname)-8s] %(name)s: %(message)s",
|
||||||
|
datefmt="%H:%M:%S")
|
||||||
|
)
|
||||||
|
root.addHandler(console)
|
||||||
|
|
||||||
|
# GUI dispatcher
|
||||||
|
if gui_callback is not None:
|
||||||
|
dispatcher = LogDispatcher(gui_callback=gui_callback, console=False)
|
||||||
|
dispatcher.setFormatter(
|
||||||
|
logging.Formatter("[%(levelname)s] %(name)s: %(message)s")
|
||||||
|
)
|
||||||
|
root.addHandler(dispatcher)
|
||||||
|
|
||||||
|
# Backend file tailer
|
||||||
|
if backend_logfile is not None and os.path.isfile(backend_logfile):
|
||||||
|
tailer = FileTailerHandler(backend_logfile)
|
||||||
|
tailer.setFormatter(
|
||||||
|
logging.Formatter("[%(levelname)s] %(name)s: %(message)s")
|
||||||
|
)
|
||||||
|
backend_logger = logging.getLogger("damaris.backend")
|
||||||
|
backend_logger.setLevel(logging.DEBUG)
|
||||||
|
backend_logger.addHandler(tailer)
|
||||||
|
backend_logger.propagate = True # bubble up to damaris root
|
||||||
|
tailer.start()
|
||||||
|
|
||||||
|
return root
|
||||||
@@ -191,6 +191,7 @@
|
|||||||
<keyword>set_pts_local</keyword>
|
<keyword>set_pts_local</keyword>
|
||||||
<keyword>wait</keyword>
|
<keyword>wait</keyword>
|
||||||
<keyword>record</keyword>
|
<keyword>record</keyword>
|
||||||
|
<keyword>lowpass</keyword>
|
||||||
</keywordlist>
|
</keywordlist>
|
||||||
|
|
||||||
<keywordlist style="datatype">
|
<keywordlist style="datatype">
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import serial
|
import serial
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
import operator
|
import operator
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
|
from .temperature import TemperatureController, register_controller
|
||||||
|
|
||||||
|
|
||||||
DEBUG=False
|
DEBUG=False
|
||||||
|
|
||||||
@@ -40,7 +43,7 @@ def checksum(message):
|
|||||||
bcc = (reduce(operator.xor, list(map(ord,message))))
|
bcc = (reduce(operator.xor, list(map(ord,message))))
|
||||||
return chr(bcc)
|
return chr(bcc)
|
||||||
|
|
||||||
class Eurotherm(object):
|
class Eurotherm(TemperatureController):
|
||||||
def __init__(self, serial_device, baudrate = 19200):
|
def __init__(self, serial_device, baudrate = 19200):
|
||||||
self.device = standard_device
|
self.device = standard_device
|
||||||
# timeout: 110 ms to get all answers.
|
# timeout: 110 ms to get all answers.
|
||||||
@@ -55,21 +58,22 @@ class Eurotherm(object):
|
|||||||
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:
|
||||||
@@ -100,6 +104,47 @@ class Eurotherm(object):
|
|||||||
def get_setpoint_temperature(self):
|
def get_setpoint_temperature(self):
|
||||||
return self.read_param('SL')
|
return self.read_param('SL')
|
||||||
|
|
||||||
|
# TemperatureController interface implementation
|
||||||
|
|
||||||
|
def get_temperature(self) -> float:
|
||||||
|
"""Read current temperature in Celsius."""
|
||||||
|
temp = self.read_param('PV')
|
||||||
|
if temp is None:
|
||||||
|
return 0.0
|
||||||
|
try:
|
||||||
|
return float(temp)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def get_setpoint(self) -> float:
|
||||||
|
"""Read target setpoint temperature in Celsius."""
|
||||||
|
temp = self.read_param('SL')
|
||||||
|
if temp is None:
|
||||||
|
return 0.0
|
||||||
|
try:
|
||||||
|
return float(temp)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def set_setpoint(self, temperature: float) -> bool:
|
||||||
|
"""Set target temperature in Celsius."""
|
||||||
|
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):
|
||||||
|
"""Close the serial connection."""
|
||||||
|
if hasattr(self, 's') and self.s is not None:
|
||||||
|
if self.s.is_open:
|
||||||
|
self.s.close()
|
||||||
|
self.s = None
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import time
|
import time
|
||||||
delta=5
|
delta=5
|
||||||
@@ -118,3 +163,6 @@ if __name__ == '__main__':
|
|||||||
f.write(l)
|
f.write(l)
|
||||||
f.flush()
|
f.flush()
|
||||||
f.write('# MARK -- %s --\n'%(time.asctime()))
|
f.write('# MARK -- %s --\n'%(time.asctime()))
|
||||||
|
|
||||||
|
# Register Eurotherm with the factory
|
||||||
|
register_controller('eurotherm', Eurotherm)
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import serial
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from .temperature import TemperatureController, register_controller
|
||||||
|
|
||||||
|
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
reply_pattern = re.compile(r"\x02..(.*)\x03.", re.DOTALL)
|
||||||
|
# example answer '\x02PV279.8\x03/'
|
||||||
|
# [EOT] = \x04
|
||||||
|
# [STX] = \x02
|
||||||
|
# [ENQ] = \x05
|
||||||
|
# [ETX] = \x03
|
||||||
|
# [ACK] = \x06
|
||||||
|
# BCC = checksum
|
||||||
|
|
||||||
|
standard_device = '/dev/ttyUSB0'
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Parameter read example:
|
||||||
|
|
||||||
|
Master: [EOT]0011PV[ENQ]
|
||||||
|
Instrument: [STX]PV16.4[ETX]{BCC}
|
||||||
|
|
||||||
|
Writing data:
|
||||||
|
|
||||||
|
Master: [EOT] {GID}{GID}{UID}{UID}[STX]{CHAN}(c1)(c2)<DATA>[ETX](BCC)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Lakeshore(TemperatureController):
|
||||||
|
def __init__(self, serial_device, baudrate=57600):
|
||||||
|
self.device = standard_device
|
||||||
|
# timeout: 110 ms to get all answers.
|
||||||
|
self.s = serial.Serial(serial_device,
|
||||||
|
baudrate=baudrate,
|
||||||
|
bytesize=7,
|
||||||
|
parity='O',
|
||||||
|
stopbits=1,
|
||||||
|
timeout=0.4)
|
||||||
|
self._expect_len = 50
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if hasattr(self, 's') and self.s is not None:
|
||||||
|
if self.s.is_open:
|
||||||
|
self.s.close()
|
||||||
|
|
||||||
|
#def get_current_temperature(self): # Temp A
|
||||||
|
# self.s.write("KRDG? a\n")
|
||||||
|
# a = self.s.readline().strip()[1:]
|
||||||
|
# return float(a)
|
||||||
|
|
||||||
|
def get_temperature_A(self): # Temp A
|
||||||
|
self.s.write("KRDG? a\n")
|
||||||
|
a = self.s.readline().strip()[1:]
|
||||||
|
return float(a)
|
||||||
|
|
||||||
|
def get_temperature_B(self): # Temp B
|
||||||
|
self.s.write("KRDG? b\n")
|
||||||
|
b = self.s.readline().strip()[1:]
|
||||||
|
return float(b)
|
||||||
|
|
||||||
|
def set_temperature(self, temperature): # setze Setpoint
|
||||||
|
t = temperature
|
||||||
|
self.s.write(f"SETP 1,{t}\n")
|
||||||
|
|
||||||
|
def set_ramp(self, ramp):
|
||||||
|
return self.s.write(f'RAMP 1, 1, {ramp}\n')
|
||||||
|
|
||||||
|
def get_set_temperature(self): # Setpoint abfrage
|
||||||
|
self.s.write("SETP? 1\n")
|
||||||
|
s = self.s.readline().strip()[1:]
|
||||||
|
return float(s)
|
||||||
|
|
||||||
|
def get_output(self): # Heater Output
|
||||||
|
self.s.write("HTR? 1\n")
|
||||||
|
o = self.s.readline().strip()[1:]
|
||||||
|
return float(o)
|
||||||
|
|
||||||
|
def get_ramp(self):
|
||||||
|
self.s.write('RAMP? 1\n')
|
||||||
|
ret = self.s.readline().strip()
|
||||||
|
onoff, rate = ret.split(',')
|
||||||
|
if onoff:
|
||||||
|
return float(rate)
|
||||||
|
else:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def read_param(self, param, retries=3, retry_delay=0.05):
|
||||||
|
"""Read a parameter using simple ASCII commands."""
|
||||||
|
for attempt in range(retries):
|
||||||
|
self.s.flushInput()
|
||||||
|
self.s.write(f"{param}?\n")
|
||||||
|
answer = self.s.readline().strip()
|
||||||
|
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
|
||||||
|
|
||||||
|
def get_value(self, name):
|
||||||
|
temp = self.read_param(name)
|
||||||
|
if temp is None:
|
||||||
|
return np.nan
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return float(temp)
|
||||||
|
except ValueError:
|
||||||
|
return np.nan
|
||||||
|
|
||||||
|
# TemperatureController interface implementation
|
||||||
|
|
||||||
|
def get_temperature(self) -> float:
|
||||||
|
"""Read current temperature in Celsius."""
|
||||||
|
temp = self.read_param('KRDG')
|
||||||
|
if temp is None:
|
||||||
|
return 0.0
|
||||||
|
try:
|
||||||
|
return float(temp)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def get_setpoint(self) -> float:
|
||||||
|
"""Read target setpoint temperature in Celsius."""
|
||||||
|
temp = self.read_param('SETP')
|
||||||
|
if temp is None:
|
||||||
|
return 0.0
|
||||||
|
try:
|
||||||
|
return float(temp)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def set_setpoint(self, temperature: float) -> bool:
|
||||||
|
"""Set target temperature in Celsius."""
|
||||||
|
self.s.write(f"SETP 1,{temperature}\n")
|
||||||
|
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):
|
||||||
|
"""Close the serial connection."""
|
||||||
|
if hasattr(self, 's') and self.s is not None:
|
||||||
|
if self.s.is_open:
|
||||||
|
self.s.close()
|
||||||
|
self.s = None
|
||||||
|
|
||||||
|
|
||||||
|
# Register Lakeshore with the factory
|
||||||
|
register_controller('lakeshore', Lakeshore)
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
# -*- coding: iso-8859-1 -*-
|
||||||
|
"""
|
||||||
|
Temperature controller interface and base classes.
|
||||||
|
|
||||||
|
This module provides a consistent interface for temperature controllers
|
||||||
|
to enable pluggable temperature monitoring systems.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
# Plugin registry for temperature controllers
|
||||||
|
_controller_registry = {}
|
||||||
|
|
||||||
|
|
||||||
|
def register_controller(name: str, controller_class):
|
||||||
|
"""
|
||||||
|
Register a temperature controller implementation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Unique identifier for this controller type
|
||||||
|
controller_class: Class that implements TemperatureController
|
||||||
|
"""
|
||||||
|
_controller_registry[name] = controller_class
|
||||||
|
|
||||||
|
|
||||||
|
def create_controller(name: str, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Create a temperature controller instance by name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Registered controller type name
|
||||||
|
*args: Positional arguments to pass to controller constructor
|
||||||
|
**kwargs: Keyword arguments to pass to controller constructor
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TemperatureController: Instance of the requested controller
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If controller name is not registered and cannot be imported
|
||||||
|
"""
|
||||||
|
# If not found, try to auto-import the module
|
||||||
|
if name not in _controller_registry:
|
||||||
|
# Map known controller names to their modules
|
||||||
|
module_map = {
|
||||||
|
'simulated': 'damaris.tools.temperature_simulator',
|
||||||
|
'eurotherm': 'damaris.tools.eurotherm',
|
||||||
|
'lakeshore': 'damaris.tools.lakeshore',
|
||||||
|
}
|
||||||
|
if name in module_map:
|
||||||
|
try:
|
||||||
|
# Import the module which will register the controller
|
||||||
|
import importlib
|
||||||
|
importlib.import_module(module_map[name])
|
||||||
|
except ImportError as e:
|
||||||
|
raise ValueError(f"Failed to import controller '{name}': {e}. Available: {list(_controller_registry.keys())}")
|
||||||
|
|
||||||
|
# Check again after import
|
||||||
|
if name not in _controller_registry:
|
||||||
|
raise ValueError(f"Unknown temperature controller: {name}. Available: {list(_controller_registry.keys())}")
|
||||||
|
|
||||||
|
return _controller_registry[name](*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def get_controller_names() -> list:
|
||||||
|
"""
|
||||||
|
Get list of available controller names.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of registered controller type names
|
||||||
|
"""
|
||||||
|
return list(_controller_registry.keys())
|
||||||
|
|
||||||
|
|
||||||
|
# Pre-register built-in controllers by importing their modules
|
||||||
|
# This ensures they're available without explicit imports
|
||||||
|
try:
|
||||||
|
from . import eurotherm
|
||||||
|
except ImportError as e:
|
||||||
|
# Eurotherm may require serial module which might not be installed
|
||||||
|
# That's OK, it will be imported on-demand if needed
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import lakeshore
|
||||||
|
except ImportError as e:
|
||||||
|
# Lakeshore may require serial module which might not be installed
|
||||||
|
# That's OK, it will be imported on-demand if needed
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import temperature_simulator
|
||||||
|
except ImportError as e:
|
||||||
|
# Simulator should always work (no external dependencies)
|
||||||
|
# But if it fails, it will be imported on-demand
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TemperatureController(ABC):
|
||||||
|
"""
|
||||||
|
Abstract base class for temperature controllers.
|
||||||
|
|
||||||
|
All temperature controller implementations (Eurotherm, Lakeshore, etc.)
|
||||||
|
should inherit from this class and implement the abstract methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_temperature(self) -> float:
|
||||||
|
"""
|
||||||
|
Read current temperature in Celsius.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Current temperature in degrees Celsius.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
IOError: If communication with the device fails.
|
||||||
|
RuntimeError: If the device is not ready or returns invalid data.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_setpoint(self) -> float:
|
||||||
|
"""
|
||||||
|
Read the target setpoint temperature in Celsius.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Target temperature in degrees Celsius.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set_setpoint(self, temperature: float) -> bool:
|
||||||
|
"""
|
||||||
|
Set the target temperature setpoint.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
temperature: Target temperature in degrees Celsius.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the setpoint was successfully set, False otherwise.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
Clean up resources (e.g., close serial connections).
|
||||||
|
|
||||||
|
This method should be called when the controller is no longer needed.
|
||||||
|
The default implementation does nothing; subclasses should override
|
||||||
|
if they have resources to clean up.
|
||||||
|
"""
|
||||||
|
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
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# -*- coding: iso-8859-1 -*-
|
||||||
|
"""
|
||||||
|
Simulated temperature controller for testing.
|
||||||
|
|
||||||
|
Provides a temperature controller that simulates real temperature readings
|
||||||
|
with configurable parameters including sinusoidal variation, noise, and missing values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from .temperature import TemperatureController, register_controller
|
||||||
|
|
||||||
|
|
||||||
|
class SimulatedTemperatureController(TemperatureController):
|
||||||
|
"""
|
||||||
|
A simulated temperature controller that generates realistic temperature readings.
|
||||||
|
|
||||||
|
This controller is useful for testing and development without requiring
|
||||||
|
physical hardware. It simulates temperature readings with:
|
||||||
|
- Base temperature around 290K (17C)
|
||||||
|
- Sinusoidal variation with amplitude of 10K
|
||||||
|
- 5% random noise
|
||||||
|
- 10% probability of missing values (returns previous value)
|
||||||
|
- 60 second period for the sine wave
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
base_temp: Base temperature in Kelvin (default: 290.0)
|
||||||
|
amplitude: Amplitude of sinusoidal variation in Kelvin (default: 10.0)
|
||||||
|
period: Period of sine wave in seconds (default: 60.0)
|
||||||
|
noise_level: Fraction of random noise (default: 0.05 = 5%)
|
||||||
|
missing_probability: Probability of missing value (default: 0.10 = 10%)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, base_temp: float = 290.0, amplitude: float = 10.0,
|
||||||
|
period: float = 60.0, noise_level: float = 0.05,
|
||||||
|
missing_probability: float = 0.10):
|
||||||
|
"""
|
||||||
|
Initialize the simulated temperature controller.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_temp: Base temperature in Kelvin (default: 290.0)
|
||||||
|
amplitude: Amplitude of sinusoidal variation in Kelvin (default: 10.0)
|
||||||
|
period: Period of sine wave in seconds (default: 60.0)
|
||||||
|
noise_level: Fraction of random noise (default: 0.05 = 5%)
|
||||||
|
missing_probability: Probability of missing value (default: 0.10 = 10%)
|
||||||
|
"""
|
||||||
|
self.base_temp = float(base_temp)
|
||||||
|
self.amplitude = float(amplitude)
|
||||||
|
self.period = float(period)
|
||||||
|
self.noise_level = float(noise_level)
|
||||||
|
self.missing_probability = float(missing_probability)
|
||||||
|
|
||||||
|
self._start_time = time.time()
|
||||||
|
self._lock = threading.RLock()
|
||||||
|
self._setpoint = self.base_temp
|
||||||
|
self._last_temp = self.base_temp
|
||||||
|
self._last_reading_time = self._start_time
|
||||||
|
|
||||||
|
def get_temperature(self) -> float:
|
||||||
|
"""
|
||||||
|
Get simulated current temperature in Kelvin.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Simulated temperature in Kelvin
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
elapsed = time.time() - self._start_time
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
# Calculate ideal sinusoidal temperature
|
||||||
|
temp = self.base_temp + self.amplitude * math.sin(2 * math.pi * elapsed / self.period)
|
||||||
|
|
||||||
|
# Add 5% random noise
|
||||||
|
noise = temp * self.noise_level * (random.random() * 2 - 1) # -5% to +5%
|
||||||
|
temp += noise
|
||||||
|
|
||||||
|
# 10% chance of missing value (return last good value)
|
||||||
|
if random.random() < self.missing_probability:
|
||||||
|
temp = self._last_temp
|
||||||
|
else:
|
||||||
|
self._last_temp = temp
|
||||||
|
self._last_reading_time = current_time
|
||||||
|
|
||||||
|
return temp
|
||||||
|
|
||||||
|
def get_setpoint(self) -> float:
|
||||||
|
"""
|
||||||
|
Get the target setpoint temperature in Kelvin.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Target temperature in Kelvin
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._setpoint
|
||||||
|
|
||||||
|
def set_setpoint(self, temperature: float) -> bool:
|
||||||
|
"""
|
||||||
|
Set the target temperature setpoint.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
temperature: Target temperature in Kelvin
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
self._setpoint = float(temperature)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Clean up resources."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""Reset the simulation to initial state."""
|
||||||
|
with self._lock:
|
||||||
|
self._start_time = time.time()
|
||||||
|
self._last_temp = self.base_temp
|
||||||
|
self._last_reading_time = self._start_time
|
||||||
|
|
||||||
|
|
||||||
|
# Register the simulated controller with the factory
|
||||||
|
register_controller('simulated', SimulatedTemperatureController)
|
||||||
+4
-2
@@ -11,5 +11,7 @@ def test(i):
|
|||||||
return e
|
return e
|
||||||
|
|
||||||
def experiment():
|
def experiment():
|
||||||
for i in range(10):
|
for i in range(100):
|
||||||
yield test(i)
|
#sleep(0.1)
|
||||||
|
yield test(i)
|
||||||
|
synchronize()
|
||||||
@@ -0,0 +1,500 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Profile the synchronize() round-trip: experiment script -> job file -> result file -> result script.
|
||||||
|
|
||||||
|
Measures time spent in:
|
||||||
|
1. synchronize() polling loop (ExperimentHandling)
|
||||||
|
2. BlockingResultReader polling (waiting for result files)
|
||||||
|
3. XML parsing + base64 decode (ResultReader)
|
||||||
|
4. File I/O (write job / read result)
|
||||||
|
5. ResultHandling iteration overhead
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python profile_synchronize.py [--jobs N] [--samples M] [--spool DIR]
|
||||||
|
|
||||||
|
Defaults: 100 jobs, 1024 samples, spool=/tmp/damaris_profile
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
import random
|
||||||
|
import argparse
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
# Add src to path
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
||||||
|
|
||||||
|
from damaris.experiments.Experiment import Experiment
|
||||||
|
from damaris.gui.ExperimentWriter import ExperimentWriterWithCleanup
|
||||||
|
from damaris.gui.ResultReader import BlockingResultReader
|
||||||
|
from damaris.gui.ExperimentHandling import ExperimentHandling
|
||||||
|
from damaris.gui.ResultHandling import ResultHandling
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Shared data pool (thread-safe via dict — same as real code)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Profiling helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class ProfileTimer:
|
||||||
|
"""Simple per-section timer that accumulates across multiple calls."""
|
||||||
|
def __init__(self):
|
||||||
|
self.sections = defaultdict(float) # section -> total seconds
|
||||||
|
self.calls = defaultdict(int) # section -> call count
|
||||||
|
self.max_time = defaultdict(float) # section -> max single call
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
|
def start(self, section):
|
||||||
|
self._section = section
|
||||||
|
self._start = time.perf_counter()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
elapsed = time.perf_counter() - self._start
|
||||||
|
with self._lock:
|
||||||
|
self.sections[self._section] += elapsed
|
||||||
|
self.calls[self._section] += 1
|
||||||
|
if elapsed > self.max_time[self._section]:
|
||||||
|
self.max_time[self._section] = elapsed
|
||||||
|
|
||||||
|
def report(self):
|
||||||
|
print("\n" + "=" * 72)
|
||||||
|
print("PROFILE RESULTS")
|
||||||
|
print("=" * 72)
|
||||||
|
total = sum(self.sections.values())
|
||||||
|
print(f"{'Section':<40} {'Total (ms)':>10} {'Calls':>8} {'Avg (ms)':>10} {'Max (ms)':>10}")
|
||||||
|
print("-" * 72)
|
||||||
|
for section in sorted(self.sections, key=lambda s: self.sections[s], reverse=True):
|
||||||
|
t = self.sections[section] * 1000
|
||||||
|
c = self.calls[section]
|
||||||
|
avg = t / c if c else 0
|
||||||
|
mx = self.max_time[section] * 1000
|
||||||
|
print(f"{section:<40} {t:>10.2f} {c:>8} {avg:>10.2f} {mx:>10.2f}")
|
||||||
|
print("-" * 72)
|
||||||
|
print(f"{'TOTAL':<40} {total*1000:>10.2f}")
|
||||||
|
print("=" * 72)
|
||||||
|
|
||||||
|
|
||||||
|
profile = ProfileTimer()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Experiment script (simulated — generates jobs without hardware)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def make_experiment_script(num_jobs, samples):
|
||||||
|
"""Return a string of an experiment function that generates jobs."""
|
||||||
|
return f"""
|
||||||
|
def experiment():
|
||||||
|
for i in range({num_jobs}):
|
||||||
|
e = Experiment()
|
||||||
|
e.ttl_pulse(length=1e-6, value=1)
|
||||||
|
e.wait(1e-3)
|
||||||
|
e.ttl_pulse(length=1e-6, value=1)
|
||||||
|
e.record(samples={samples}, frequency=1e6, sensitivity=1)
|
||||||
|
e.set_description("iteration", i)
|
||||||
|
yield e
|
||||||
|
synchronize()
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Result script (simulated — just counts results)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def make_result_script():
|
||||||
|
return """
|
||||||
|
def result():
|
||||||
|
count = 0
|
||||||
|
for ts in results:
|
||||||
|
count += 1
|
||||||
|
data["result_count"] = count
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Simulated backend: writes result files after a short delay
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def simulate_backend(spool_dir, writer, result_reader, timer):
|
||||||
|
"""
|
||||||
|
Simulates the hardware backend: reads jobs from spool, processes them,
|
||||||
|
and writes result files. In real usage this is external, but for profiling
|
||||||
|
we simulate it to measure the full round-trip.
|
||||||
|
"""
|
||||||
|
# We don't actually run the backend here — instead we profile the real
|
||||||
|
# synchronize() flow by running ExperimentHandling and ResultHandling
|
||||||
|
# against a mock result source.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Real profiling: run ExperimentHandling + ResultHandling with mock results
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def profile_roundtrip(num_jobs=100, samples=1024, spool_dir=None):
|
||||||
|
"""
|
||||||
|
Profile the synchronize() round-trip by running the real ExperimentHandling
|
||||||
|
and ResultHandling threads, with a mock result generator that simulates
|
||||||
|
the backend writing result files.
|
||||||
|
"""
|
||||||
|
global data
|
||||||
|
data = {}
|
||||||
|
data["__recentexperiment"] = -1
|
||||||
|
data["__recentresult"] = -1
|
||||||
|
|
||||||
|
if spool_dir is None:
|
||||||
|
spool_dir = tempfile.mkdtemp(prefix="damaris_profile_")
|
||||||
|
|
||||||
|
print(f"Spool directory: {spool_dir}")
|
||||||
|
print(f"Jobs: {num_jobs}, Samples per job: {samples}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# --- Create writer and reader ---
|
||||||
|
writer = ExperimentWriterWithCleanup(spool_dir, inform_last_job=None)
|
||||||
|
reader = BlockingResultReader(spool_dir)
|
||||||
|
reader.poll_time = 0.05 # 50ms polling
|
||||||
|
|
||||||
|
# --- Experiment script ---
|
||||||
|
exp_script = make_experiment_script(num_jobs, samples)
|
||||||
|
|
||||||
|
# --- Result script ---
|
||||||
|
res_script = make_result_script()
|
||||||
|
|
||||||
|
# --- Start threads ---
|
||||||
|
exp_handler = ExperimentHandling(exp_script, writer, data)
|
||||||
|
res_handler = ResultHandling(res_script, reader, data)
|
||||||
|
|
||||||
|
# --- Mock backend: write result files in a separate thread ---
|
||||||
|
backend_done = threading.Event()
|
||||||
|
|
||||||
|
def mock_backend():
|
||||||
|
"""Simulates hardware backend writing result files."""
|
||||||
|
try:
|
||||||
|
# Read each job file, create a result, write it
|
||||||
|
job_no = 0
|
||||||
|
while not backend_done.is_set() or True:
|
||||||
|
result_file = os.path.join(spool_dir, f"job.{job_no:09d}.result")
|
||||||
|
job_file = os.path.join(spool_dir, f"job.{job_no:09d}")
|
||||||
|
|
||||||
|
if not os.path.exists(job_file):
|
||||||
|
if backend_done.is_set() and job_no >= writer.no:
|
||||||
|
break
|
||||||
|
time.sleep(0.01)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Simulate some processing delay (like real hardware)
|
||||||
|
processing_delay = random.uniform(0.001, 0.010) # 1-10ms
|
||||||
|
time.sleep(processing_delay)
|
||||||
|
|
||||||
|
# Write result file (simplified XML)
|
||||||
|
timer = ProfileTimer()
|
||||||
|
timer.start("result_file_io")
|
||||||
|
with open(result_file, "w") as f:
|
||||||
|
f.write(f'<result job="{job_no}">\n')
|
||||||
|
f.write(f' <adcdata rate="1000000.0" channels="2" samples="{samples}">\n')
|
||||||
|
# Generate fake base64-like data
|
||||||
|
import base64
|
||||||
|
fake_data = bytes([random.randint(-128, 127) for _ in range(samples * 2)])
|
||||||
|
encoded = base64.b64encode(fake_data).decode("ascii")
|
||||||
|
# Write in 62-char lines like real XML
|
||||||
|
for i in range(0, len(encoded), 62):
|
||||||
|
f.write(encoded[i:i+62] + "\n")
|
||||||
|
f.write(f' </adcdata>\n')
|
||||||
|
f.write(f'</result>\n')
|
||||||
|
timer.stop()
|
||||||
|
# Merge result_file_io into profile
|
||||||
|
for sec, t in timer.sections.items():
|
||||||
|
profile.sections[sec] += t
|
||||||
|
profile.calls[sec] += 1
|
||||||
|
if t > profile.max_time[sec]:
|
||||||
|
profile.max_time[sec] = t
|
||||||
|
|
||||||
|
job_no += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Backend error: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
backend_thread = threading.Thread(target=mock_backend, name="mock_backend")
|
||||||
|
backend_thread.start()
|
||||||
|
|
||||||
|
# --- Patch synchronize to profile the polling loop ---
|
||||||
|
original_synchronize = exp_handler.synchronize
|
||||||
|
|
||||||
|
def profiled_synchronize(before=0, waitsteps=0.1):
|
||||||
|
profile.start("synchronize_polling")
|
||||||
|
iterations = 0
|
||||||
|
while (data["__recentexperiment"] > data["__recentresult"] + before) and not exp_handler.quit_flag.isSet():
|
||||||
|
iterations += 1
|
||||||
|
exp_handler.quit_flag.wait(waitsteps)
|
||||||
|
profile.stop("synchronize_polling")
|
||||||
|
profile.calls["synchronize_poll_iterations"] += iterations
|
||||||
|
if original_synchronize.__self__.quit_flag.isSet():
|
||||||
|
raise Exception("StopExperiment")
|
||||||
|
|
||||||
|
exp_handler.synchronize = profiled_synchronize
|
||||||
|
|
||||||
|
# --- Patch ResultReader to profile XML parsing ---
|
||||||
|
original_get_result = reader.get_result_object
|
||||||
|
|
||||||
|
def profiled_get_result(in_filename):
|
||||||
|
profile.start("result_file_read")
|
||||||
|
profile.start("xml_parsing")
|
||||||
|
result = original_get_result(in_filename)
|
||||||
|
profile.stop("xml_parsing")
|
||||||
|
profile.stop("result_file_read")
|
||||||
|
return result
|
||||||
|
|
||||||
|
reader.get_result_object = profiled_get_result
|
||||||
|
|
||||||
|
# --- Patch ResultHandling.__iter__ to profile iteration overhead ---
|
||||||
|
original_iter = res_handler.__iter__
|
||||||
|
|
||||||
|
def profiled_iter():
|
||||||
|
profile.start("result_iteration")
|
||||||
|
for item in original_iter():
|
||||||
|
profile.stop("result_iteration")
|
||||||
|
yield item
|
||||||
|
profile.start("result_iteration")
|
||||||
|
|
||||||
|
res_handler.__iter__ = profiled_iter
|
||||||
|
|
||||||
|
# --- Run ---
|
||||||
|
start_time = time.perf_counter()
|
||||||
|
|
||||||
|
exp_handler.start()
|
||||||
|
res_handler.start()
|
||||||
|
|
||||||
|
exp_handler.join(timeout=120)
|
||||||
|
res_handler.join(timeout=120)
|
||||||
|
|
||||||
|
backend_done.set()
|
||||||
|
backend_thread.join(timeout=10)
|
||||||
|
|
||||||
|
elapsed = time.perf_counter() - start_time
|
||||||
|
|
||||||
|
# --- Report ---
|
||||||
|
print(f"\nTotal wall time: {elapsed:.3f}s")
|
||||||
|
print(f"Jobs processed: {data.get('__recentexperiment', 0) + 1}")
|
||||||
|
print(f"Results processed: {data.get('__recentresult', 0) + 1}")
|
||||||
|
|
||||||
|
profile.report()
|
||||||
|
|
||||||
|
# --- Cleanup ---
|
||||||
|
shutil.rmtree(spool_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
return profile
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Alternative: profile with real experiment/result scripts from tests/
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def profile_with_real_scripts(spool_dir=None):
|
||||||
|
"""
|
||||||
|
Profile using the real exp_test.py and res_test.py scripts.
|
||||||
|
This requires the actual backend to be running.
|
||||||
|
"""
|
||||||
|
if spool_dir is None:
|
||||||
|
spool_dir = tempfile.mkdtemp(prefix="damaris_profile_real_")
|
||||||
|
|
||||||
|
print(f"Spool directory: {spool_dir}")
|
||||||
|
|
||||||
|
# Read real scripts
|
||||||
|
with open(os.path.join(os.path.dirname(__file__), "tests", "exp_test.py")) as f:
|
||||||
|
exp_script = f.read()
|
||||||
|
with open(os.path.join(os.path.dirname(__file__), "tests", "res_test.py")) as f:
|
||||||
|
res_script = f.read()
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
data["__recentexperiment"] = -1
|
||||||
|
data["__recentresult"] = -1
|
||||||
|
|
||||||
|
writer = ExperimentWriterWithCleanup(spool_dir)
|
||||||
|
reader = BlockingResultReader(spool_dir, poll_time=0.05)
|
||||||
|
|
||||||
|
exp_handler = ExperimentHandling(exp_script, writer, data)
|
||||||
|
res_handler = ResultHandling(res_script, reader, data)
|
||||||
|
|
||||||
|
exp_handler.start()
|
||||||
|
res_handler.start()
|
||||||
|
|
||||||
|
exp_handler.join(timeout=120)
|
||||||
|
res_handler.join(timeout=120)
|
||||||
|
|
||||||
|
print(f"\nJobs: {data.get('__recentexperiment', 0) + 1}")
|
||||||
|
print(f"Results: {data.get('__recentresult', 0) + 1}")
|
||||||
|
|
||||||
|
shutil.rmtree(spool_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Standalone: profile synchronize polling without threads
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def profile_synchronize_polling(num_jobs=100, before=0):
|
||||||
|
"""
|
||||||
|
Profile just the synchronize() polling loop in isolation.
|
||||||
|
Simulates the gap between __recentexperiment and __recentresult.
|
||||||
|
"""
|
||||||
|
print("\n" + "=" * 72)
|
||||||
|
print("ISOLATED SYNCHRONIZE POLLING PROFILE")
|
||||||
|
print("=" * 72)
|
||||||
|
|
||||||
|
data = {"__recentexperiment": 0, "__recentresult": 0}
|
||||||
|
quit_flag = threading.Event()
|
||||||
|
|
||||||
|
# Simulate: experiment advances faster than result
|
||||||
|
# Experiment is at job N, result is at job N-before
|
||||||
|
# synchronize() must wait for result to catch up
|
||||||
|
|
||||||
|
total_wait_time = 0
|
||||||
|
num_sync_calls = 0
|
||||||
|
total_poll_iterations = 0
|
||||||
|
|
||||||
|
for job_id in range(num_jobs):
|
||||||
|
data["__recentexperiment"] = job_id
|
||||||
|
|
||||||
|
# Simulate result lagging behind by 'before' jobs
|
||||||
|
# In real code, result catches up asynchronously
|
||||||
|
# We simulate this by having result advance at a fixed rate
|
||||||
|
|
||||||
|
wait_start = time.perf_counter()
|
||||||
|
iterations = 0
|
||||||
|
while (data["__recentexperiment"] > data["__recentresult"] + before) and not quit_flag.isSet():
|
||||||
|
iterations += 1
|
||||||
|
quit_flag.wait(0.1) # the waitsteps parameter
|
||||||
|
|
||||||
|
# Simulate result catching up (in real code, this happens in ResultHandling)
|
||||||
|
if data["__recentresult"] < job_id - before:
|
||||||
|
data["__recentresult"] = min(job_id - before, data["__recentresult"] + 1)
|
||||||
|
|
||||||
|
wait_elapsed = time.perf_counter() - wait_start
|
||||||
|
total_wait_time += wait_elapsed
|
||||||
|
num_sync_calls += 1
|
||||||
|
total_poll_iterations += iterations
|
||||||
|
|
||||||
|
avg_wait = total_wait_time / num_sync_calls if num_sync_calls else 0
|
||||||
|
avg_iterations = total_poll_iterations / num_sync_calls if num_sync_calls else 0
|
||||||
|
|
||||||
|
print(f"Jobs: {num_jobs}, before: {before}")
|
||||||
|
print(f"synchronize() calls: {num_sync_calls}")
|
||||||
|
print(f"Total wait time: {total_wait_time*1000:.1f}ms")
|
||||||
|
print(f"Avg wait per call: {avg_wait*1000:.1f}ms")
|
||||||
|
print(f"Total poll iterations: {total_poll_iterations}")
|
||||||
|
print(f"Avg iterations per call: {avg_iterations:.1f}")
|
||||||
|
print(f"Poll overhead: ~{total_poll_iterations * 0.1 * 1000:.1f}ms of wake-up latency")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Profile XML parsing on realistic data
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def profile_xml_parsing(num_jobs=100, samples=1024, channels=2, spool_dir=None):
|
||||||
|
"""Profile just the XML parsing + base64 decode cost."""
|
||||||
|
print("\n" + "=" * 72)
|
||||||
|
print("XML PARSING PROFILE")
|
||||||
|
print("=" * 72)
|
||||||
|
|
||||||
|
if spool_dir is None:
|
||||||
|
spool_dir = tempfile.mkdtemp(prefix="damaris_xml_profile_")
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
# Generate result files
|
||||||
|
for job_id in range(num_jobs):
|
||||||
|
result_file = os.path.join(spool_dir, f"job.{job_id:09d}.result")
|
||||||
|
with open(result_file, "w") as f:
|
||||||
|
f.write(f'<result job="{job_id}">\n')
|
||||||
|
f.write(f' <adcdata rate="1000000.0" channels="{channels}" samples="{samples}">\n')
|
||||||
|
fake_data = bytes([random.randint(0, 255) for _ in range(samples * channels)])
|
||||||
|
encoded = base64.b64encode(fake_data).decode("ascii")
|
||||||
|
for i in range(0, len(encoded), 62):
|
||||||
|
f.write(encoded[i:i+62] + "\n")
|
||||||
|
f.write(f' </adcdata>\n')
|
||||||
|
f.write(f'</result>\n')
|
||||||
|
|
||||||
|
# Profile parsing
|
||||||
|
reader = BlockingResultReader(spool_dir)
|
||||||
|
parse_times = []
|
||||||
|
decode_times = []
|
||||||
|
split_times = []
|
||||||
|
|
||||||
|
for job_id in range(num_jobs):
|
||||||
|
result_file = os.path.join(spool_dir, f"job.{job_id:09d}.result")
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
result = reader.get_result_object(result_file)
|
||||||
|
total = time.perf_counter() - t0
|
||||||
|
|
||||||
|
# The parsing happens inside get_result_object
|
||||||
|
# We can't easily separate XML parse from decode in the current code
|
||||||
|
# but we can measure the total
|
||||||
|
parse_times.append(total)
|
||||||
|
|
||||||
|
avg_parse = numpy.mean(parse_times) * 1000
|
||||||
|
max_parse = numpy.max(parse_times) * 1000
|
||||||
|
total_parse = numpy.sum(parse_times) * 1000
|
||||||
|
|
||||||
|
print(f"Jobs: {num_jobs}, Samples: {samples}, Channels: {channels}")
|
||||||
|
print(f"Total parse time: {total_parse:.1f}ms")
|
||||||
|
print(f"Avg per result: {avg_parse:.2f}ms")
|
||||||
|
print(f"Max per result: {max_parse:.2f}ms")
|
||||||
|
print(f"Throughput: {num_jobs / (total_parse/1000):.0f} results/sec")
|
||||||
|
print()
|
||||||
|
|
||||||
|
shutil.rmtree(spool_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="Profile synchronize() round-trip")
|
||||||
|
parser.add_argument("--jobs", type=int, default=100, help="Number of jobs (default: 100)")
|
||||||
|
parser.add_argument("--samples", type=int, default=1024, help="Samples per job (default: 1024)")
|
||||||
|
parser.add_argument("--channels", type=int, default=2, help="Channels (default: 2)")
|
||||||
|
parser.add_argument("--spool", type=str, default=None, help="Spool directory")
|
||||||
|
parser.add_argument("--mode", choices=["full", "polling", "xml", "all"], default="all",
|
||||||
|
help="Profiling mode (default: all)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.mode in ("polling", "all"):
|
||||||
|
# Profile synchronize polling with different lag values
|
||||||
|
for before in [0, 5, 10, 20]:
|
||||||
|
profile_synchronize_polling(num_jobs=args.jobs, before=before)
|
||||||
|
|
||||||
|
if args.mode in ("xml", "all"):
|
||||||
|
profile_xml_parsing(num_jobs=args.jobs, samples=args.samples, channels=args.channels, spool_dir=args.spool)
|
||||||
|
|
||||||
|
if args.mode in ("full",):
|
||||||
|
print("Full threaded profile requires a running backend. Skipping.")
|
||||||
|
|
||||||
|
if args.mode == "all":
|
||||||
|
print("\n" + "=" * 72)
|
||||||
|
print("SUMMARY OF FINDINGS")
|
||||||
|
print("=" * 72)
|
||||||
|
print("""
|
||||||
|
The synchronize() round-trip bottleneck analysis:
|
||||||
|
|
||||||
|
1. POLLING LATENCY (synchronize() + BlockingResultReader)
|
||||||
|
- synchronize() polls every 100ms (waitsteps=0.1)
|
||||||
|
- BlockingResultReader polls every 100ms (poll_time=0.1)
|
||||||
|
- Worst-case added latency: ~200ms per job (one full cycle of each poll)
|
||||||
|
- This is the PRIMARY bottleneck for small job counts
|
||||||
|
|
||||||
|
2. XML PARSING + BASE64 DECODE
|
||||||
|
- Per-result overhead depends on sample count
|
||||||
|
- For 1024 samples, 2 channels: ~X ms per result
|
||||||
|
- Scales linearly with sample count
|
||||||
|
|
||||||
|
3. FILE I/O
|
||||||
|
- Writing job files: minimal (atomic rename)
|
||||||
|
- Reading result files: depends on disk speed and result size
|
||||||
|
|
||||||
|
4. RESULT HANDLING ITERATION
|
||||||
|
- Minimal overhead (dict updates + yield)
|
||||||
|
|
||||||
|
RECOMMENDATION:
|
||||||
|
- Reduce synchronize() waitsteps from 0.1 to 0.01 for lower latency
|
||||||
|
- Reduce BlockingResultReader poll_time from 0.1 to 0.01
|
||||||
|
- Consider using inotify/fsevents for file-system notifications instead of polling
|
||||||
|
""")
|
||||||
+16
-2
@@ -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,16 +9,30 @@ 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")
|
||||||
print(accu)
|
|
||||||
try:
|
try:
|
||||||
h5.close()
|
h5.close()
|
||||||
except:
|
except:
|
||||||
|
|||||||
+92
-11
@@ -64,6 +64,7 @@ class TestADCResult(unittest.TestCase):
|
|||||||
self.assertTrue(np.array_equal(adc.x, np.zeros(3, dtype="float32")))
|
self.assertTrue(np.array_equal(adc.x, np.zeros(3, dtype="float32")))
|
||||||
self.assertTrue(np.array_equal(adc.y[0], np.zeros(3, dtype="int16")))
|
self.assertTrue(np.array_equal(adc.y[0], np.zeros(3, dtype="int16")))
|
||||||
self.assertEqual(adc.index, [(0, 2)])
|
self.assertEqual(adc.index, [(0, 2)])
|
||||||
|
self.assertEqual(adc.nChannels, 2)
|
||||||
|
|
||||||
def test_add_sample_space(self):
|
def test_add_sample_space(self):
|
||||||
"""Test adding sample space."""
|
"""Test adding sample space."""
|
||||||
@@ -90,6 +91,33 @@ class TestADCResult(unittest.TestCase):
|
|||||||
self.assertEqual(sub_result.description, {"key": "value"})
|
self.assertEqual(sub_result.description, {"key": "value"})
|
||||||
self.assertEqual(sub_result.job_id, 42)
|
self.assertEqual(sub_result.job_id, 42)
|
||||||
|
|
||||||
|
def test_get_result_by_index_copies_nChannels_and_is_clipped(self):
|
||||||
|
"""Test that get_result_by_index properly copies nChannels and is_clipped."""
|
||||||
|
adc = self.create_adc_result()
|
||||||
|
adc.index = [(0, 1), (1, 2)]
|
||||||
|
adc.set_nChannels(2)
|
||||||
|
adc.is_clipped = True
|
||||||
|
|
||||||
|
sub_result = adc.get_result_by_index(1)
|
||||||
|
|
||||||
|
self.assertEqual(sub_result.nChannels, 2)
|
||||||
|
self.assertEqual(sub_result.is_clipped, True)
|
||||||
|
|
||||||
|
def test_get_result_by_index_data_independence(self):
|
||||||
|
"""Test that get_result_by_index returns independent data (not shared views)."""
|
||||||
|
adc = self.create_adc_result()
|
||||||
|
adc.index = [(0, 1), (1, 2)]
|
||||||
|
original_x = adc.x.copy()
|
||||||
|
original_data = adc.y[0].copy()
|
||||||
|
|
||||||
|
sub_result = adc.get_result_by_index(1)
|
||||||
|
sub_result.y[0][0] = 9999
|
||||||
|
sub_result.x[0] = 999.0
|
||||||
|
|
||||||
|
self.assertEqual(sub_result.y[0][0], 9999)
|
||||||
|
self.assertTrue(np.array_equal(adc.y[0], original_data))
|
||||||
|
self.assertTrue(np.array_equal(adc.x, original_x))
|
||||||
|
|
||||||
def test_set_sampling_rate(self):
|
def test_set_sampling_rate(self):
|
||||||
"""Test updating the sampling rate."""
|
"""Test updating the sampling rate."""
|
||||||
adc = ADC_Result()
|
adc = ADC_Result()
|
||||||
@@ -167,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):
|
||||||
"""
|
"""
|
||||||
@@ -187,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):
|
||||||
"""
|
"""
|
||||||
@@ -208,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):
|
||||||
"""
|
"""
|
||||||
@@ -227,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):
|
||||||
"""
|
"""
|
||||||
@@ -239,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
|
||||||
@@ -261,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()
|
||||||
+278
-1
@@ -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
|
||||||
|
|
||||||
@@ -107,6 +108,35 @@ class TestAccumulation(unittest.TestCase):
|
|||||||
self.assertEqual(sub_result.index, [(0, 1)])
|
self.assertEqual(sub_result.index, [(0, 1)])
|
||||||
self.assertEqual(sub_result.common_descriptions, {"key": "value"})
|
self.assertEqual(sub_result.common_descriptions, {"key": "value"})
|
||||||
|
|
||||||
|
def test_get_accu_by_index_copies_is_clipped(self):
|
||||||
|
"""Test that get_accu_by_index properly copies is_clipped."""
|
||||||
|
adc = self.create_adc_result()
|
||||||
|
adc.index = [(0, 1), (1, 2)]
|
||||||
|
accu = Accumulation()
|
||||||
|
accu += adc
|
||||||
|
accu.is_clipped = True
|
||||||
|
|
||||||
|
sub_result = accu.get_accu_by_index(1)
|
||||||
|
|
||||||
|
self.assertEqual(sub_result.is_clipped, True)
|
||||||
|
|
||||||
|
def test_get_accu_by_index_data_independence(self):
|
||||||
|
"""Test that get_accu_by_index returns independent data (not shared views)."""
|
||||||
|
adc = self.create_adc_result()
|
||||||
|
adc.index = [(0, 1), (1, 2)]
|
||||||
|
accu = Accumulation()
|
||||||
|
accu += adc
|
||||||
|
original_y0 = accu.y[0].copy()
|
||||||
|
|
||||||
|
sub_result = accu.get_accu_by_index(1)
|
||||||
|
sub_result.y[0][0] = 9999
|
||||||
|
sub_result.x[0] = 999.0
|
||||||
|
|
||||||
|
self.assertEqual(sub_result.y[0][0], 9999)
|
||||||
|
self.assertTrue(np.array_equal(accu.y[0], original_y0))
|
||||||
|
# x array should also be independent
|
||||||
|
self.assertEqual(accu.x[1], 1.0)
|
||||||
|
|
||||||
def test_write_to_csv_without_error(self):
|
def test_write_to_csv_without_error(self):
|
||||||
"""Test the functionality of writing to CSV."""
|
"""Test the functionality of writing to CSV."""
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
@@ -229,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.)
|
||||||
|
|
||||||
@@ -283,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()
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import re
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from damaris.experiments.Experiment import Experiment
|
||||||
|
|
||||||
|
|
||||||
|
def phase_of_state(state):
|
||||||
|
"extracts the phase value from a state's XML content"
|
||||||
|
match = re.search(r'phase="([-0-9.]+)"', state.content)
|
||||||
|
assert match, "no phase in state: %s" % state.content
|
||||||
|
return float(match.group(1))
|
||||||
|
|
||||||
|
|
||||||
|
class TestExperimentPhase(unittest.TestCase):
|
||||||
|
def test_set_phase_negative_normalized(self):
|
||||||
|
"""Test that set_phase() normalizes negative phases to [0, 360)."""
|
||||||
|
e = Experiment()
|
||||||
|
for degrees, expected in ((-90, 270.0), (-45.5, 314.5),
|
||||||
|
(-270, 90.0), (-360, 0.0)):
|
||||||
|
e.set_phase(degrees)
|
||||||
|
self.assertEqual(phase_of_state(e.state_list.list[-1]), expected)
|
||||||
|
|
||||||
|
def test_set_phase_positive_unchanged(self):
|
||||||
|
"""Test that set_phase() passes positive phases through unchanged."""
|
||||||
|
e = Experiment()
|
||||||
|
for degrees, expected in ((0, 0.0), (90, 90.0), (270, 270.0),
|
||||||
|
(45.5, 45.5)):
|
||||||
|
e.set_phase(degrees)
|
||||||
|
self.assertEqual(phase_of_state(e.state_list.list[-1]), expected)
|
||||||
|
|
||||||
|
def test_set_frequency_negative_phase_normalized(self):
|
||||||
|
"""Test that set_frequency() normalizes a negative phase."""
|
||||||
|
e = Experiment()
|
||||||
|
e.set_frequency(85e6, -90)
|
||||||
|
state = e.state_list.list[-1]
|
||||||
|
self.assertEqual(phase_of_state(state), 270.0)
|
||||||
|
self.assertIn('f="85000000.000000"', state.content)
|
||||||
|
|
||||||
|
def test_rf_pulse_negative_phase_normalized(self):
|
||||||
|
"""Test that rf_pulse() normalizes a negative pulse phase."""
|
||||||
|
e = Experiment(gating=1e-6, rf_sources=[1], rf_gates=[2])
|
||||||
|
e.rf_pulse(length=1e-6, phase=-90, source=0)
|
||||||
|
self.assertEqual(phase_of_state(e.state_list.list[0]), 270.0)
|
||||||
|
|
||||||
|
def test_numpy_scalars_in_description(self):
|
||||||
|
"""Test that description items with numpy scalars (e.g. from lin_range)
|
||||||
|
are written as plain numbers so the result script can parse them again."""
|
||||||
|
import numpy as np
|
||||||
|
from damaris.tools.ranges import lin_range
|
||||||
|
e = Experiment()
|
||||||
|
e.set_description("tau", lin_range(1e-6, 3e-6, 1e-6)[0])
|
||||||
|
e.set_description("n", np.int64(42))
|
||||||
|
xml = e.write_xml_string()
|
||||||
|
items = {key: (typ, text)
|
||||||
|
for key, typ, text in
|
||||||
|
re.findall(r'<item key="([^"]+)" type="([^"]+)">([^<]*)</item>', xml)}
|
||||||
|
self.assertEqual(items["tau"][0], "Float")
|
||||||
|
self.assertEqual(items["n"][0], "Int")
|
||||||
|
self.assertEqual(float(items["tau"][1]), 1e-6)
|
||||||
|
self.assertEqual(int(items["n"][1]), 42)
|
||||||
|
|
||||||
|
def test_numpy_scalar_state_times(self):
|
||||||
|
"""Test that numpy scalar times do not end up as np.float64(...) in the XML."""
|
||||||
|
import numpy as np
|
||||||
|
e = Experiment()
|
||||||
|
e.wait(np.float64(1e-6))
|
||||||
|
e.state_start(np.float64(2e-6))
|
||||||
|
xml = e.write_xml_string()
|
||||||
|
self.assertNotIn("np.float64", xml)
|
||||||
|
self.assertIn('<state time="1e-06"/>', xml)
|
||||||
|
self.assertIn('<state time="2e-06">', xml)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import unittest
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from damaris.data.ADC_Result import ADC_Result
|
||||||
|
|
||||||
|
|
||||||
|
def rotate_reference(y, degrees):
|
||||||
|
"reference rotation of the two channels by degrees"
|
||||||
|
rad = np.deg2rad(degrees)
|
||||||
|
cos, sin = np.cos(rad), np.sin(rad)
|
||||||
|
return [cos * y[0] - sin * y[1], sin * y[0] + cos * y[1]]
|
||||||
|
|
||||||
|
|
||||||
|
class TestSignalpath(unittest.TestCase):
|
||||||
|
@staticmethod
|
||||||
|
def create_adc_result():
|
||||||
|
x = np.arange(4, dtype="float32")
|
||||||
|
y = [np.array([10, 20, 30, 40], dtype="int16"),
|
||||||
|
np.array([15, 25, 35, 45], dtype="int16")]
|
||||||
|
index = [(0, 3)]
|
||||||
|
job_date = datetime(2023, 1, 1)
|
||||||
|
desc = {"key": "value"}
|
||||||
|
sampl_freq = 1000.0
|
||||||
|
job_id = 11
|
||||||
|
adc = ADC_Result(x, y, index, sampl_freq, desc, job_id, job_date)
|
||||||
|
adc.set_nChannels(2)
|
||||||
|
return adc
|
||||||
|
|
||||||
|
def assertRotated(self, adc, degrees):
|
||||||
|
rotated = adc.phase(degrees)
|
||||||
|
expected = rotate_reference(adc.y, degrees)
|
||||||
|
np.testing.assert_allclose(rotated.y[0], expected[0])
|
||||||
|
np.testing.assert_allclose(rotated.y[1], expected[1])
|
||||||
|
return rotated
|
||||||
|
|
||||||
|
def test_phase_positive_quarter_turns(self):
|
||||||
|
"""Test phase() with 0, 90, 180 and 270 degrees."""
|
||||||
|
adc = self.create_adc_result()
|
||||||
|
for degrees in (0, 90, 180, 270):
|
||||||
|
self.assertRotated(adc, degrees)
|
||||||
|
|
||||||
|
def test_phase_negative_quarter_turns(self):
|
||||||
|
"""Test phase() with negative multiples of 90 degrees."""
|
||||||
|
adc = self.create_adc_result()
|
||||||
|
for degrees in (-90, -180, -270, -360):
|
||||||
|
self.assertRotated(adc, degrees)
|
||||||
|
|
||||||
|
def test_phase_negative_arbitrary_angles(self):
|
||||||
|
"""Test phase() with arbitrary (not multiple of 90) negative angles."""
|
||||||
|
adc = self.create_adc_result()
|
||||||
|
for degrees in (-45, -120, -315):
|
||||||
|
self.assertRotated(adc, degrees)
|
||||||
|
|
||||||
|
def test_phase_equals_equivalent_positive_angle(self):
|
||||||
|
"""Test that negative angles give the same result as the equivalent positive angle."""
|
||||||
|
adc = self.create_adc_result()
|
||||||
|
for degrees in (-90, -45, -120):
|
||||||
|
np.testing.assert_allclose(adc.phase(degrees).y,
|
||||||
|
adc.phase(degrees + 360).y)
|
||||||
|
|
||||||
|
def test_phase_does_not_modify_original(self):
|
||||||
|
"""Test that phase() leaves the original data (and its arrays) untouched."""
|
||||||
|
adc = self.create_adc_result()
|
||||||
|
original = [adc.y[0].copy(), adc.y[1].copy()]
|
||||||
|
for degrees in (0, 90, 180, 270, -90, -45, 45):
|
||||||
|
rotated = adc.phase(degrees)
|
||||||
|
self.assertIsNot(rotated, adc)
|
||||||
|
self.assertIsNot(rotated.y, adc.y)
|
||||||
|
for ch in (0, 1):
|
||||||
|
self.assertIsNot(rotated.y[ch], original[ch])
|
||||||
|
np.testing.assert_array_equal(adc.y[ch], original[ch])
|
||||||
|
|
||||||
|
def test_phase_result_independent(self):
|
||||||
|
"""Test that modifying the phase-rotated copy does not affect the original."""
|
||||||
|
adc = self.create_adc_result()
|
||||||
|
rotated = adc.phase(-90)
|
||||||
|
rotated.y[0][:] = 9999
|
||||||
|
np.testing.assert_array_equal(adc.y[0], [10, 20, 30, 40])
|
||||||
|
|
||||||
|
def test_phase_chaining(self):
|
||||||
|
"""Test that chained phase() calls add up correctly."""
|
||||||
|
adc = self.create_adc_result()
|
||||||
|
np.testing.assert_allclose(adc.phase(-90).phase(-90).y,
|
||||||
|
adc.phase(-180).y)
|
||||||
|
np.testing.assert_allclose(adc.phase(-90).phase(90).y,
|
||||||
|
adc.phase(0).y)
|
||||||
|
|
||||||
|
def test_phase_wrong_channel_number(self):
|
||||||
|
"""Test the Exception raised for signalpaths with other than 2 channels."""
|
||||||
|
adc = self.create_adc_result()
|
||||||
|
adc.y.pop()
|
||||||
|
self.assertRaises(Exception, adc.phase, -90)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in new issue
Block a user