Compare commits
45 Commits
v3.1.0
...
split_puls
| Author | SHA1 | Date | |
|---|---|---|---|
| 351fddb79c | |||
| f7b2e00ab2 | |||
| 745132495b | |||
| 0f7ec999f9 | |||
| b79f79b524 | |||
| 4f590f7761 | |||
| 21a1fd1be3 | |||
| 93baf06655 | |||
| 29d406a290 | |||
| 5d9c0879b4 | |||
| 7f0e6f1026 | |||
| 30574cdf7e | |||
| 6298487bf3 | |||
| 3a7f3cde53 | |||
| dafff07e68 | |||
| 9afd15bcb4 | |||
| 6a4f1c6205 | |||
| 6aacbd5f22 | |||
| dc7dd2a6f2 | |||
| a22c23658f | |||
| 246c179481 | |||
| 30f616d3ab | |||
| 20d3bf45c9 | |||
| f07a06370f | |||
| 7c951a2f14 | |||
| 493095331f | |||
| c78d5412d5 | |||
| 389d485476 | |||
| 9db1d399dc | |||
| 9facb0e04f | |||
| 198fc07421 | |||
| 80ec3da039 | |||
| 09ea513aad | |||
| 4ecf6252a9 | |||
| 0cb7f5ceb2 | |||
| 2df7fd9e7c | |||
| 42234d86fd | |||
| bc84b2e841 | |||
| 26b6057941 | |||
| 1d8fea7498 | |||
| d2fff51787 | |||
| 8347942c15 | |||
| f55d840b8e | |||
| 327d6a0ff8 | |||
| 22943bc513 |
17
.github/workflows/merge_PR.yml
vendored
Normal file
17
.github/workflows/merge_PR.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
name: Pull Request Merged
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
autoupdate_minor:
|
||||||
|
if: github.event.pull_request.merged == true
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.13'
|
||||||
|
- name: Perform PR auto-action
|
||||||
|
run: |
|
||||||
|
python PR_merged.py
|
||||||
152
.github/workflows/release.yml
vendored
152
.github/workflows/release.yml
vendored
@@ -22,7 +22,59 @@ on:
|
|||||||
- all_incl_release
|
- all_incl_release
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
test:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
|
||||||
|
fail-fast: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout LFS objects
|
||||||
|
run: git clone https://${{secrets.GITHUB_TOKEN}}@gitea.psi.ch/${{ github.repository }}.git .
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install pytest
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: Test with pytest
|
||||||
|
run: |
|
||||||
|
export PYTEST_DISABLE_PLUGIN_AUTOLOAD=1
|
||||||
|
python -m pytest tests
|
||||||
|
|
||||||
|
release:
|
||||||
|
if: ${{ (github.event_name != 'workflow_dispatch') || (contains(fromJson('["all_incl_release"]'), github.event.inputs.build-items)) }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [test]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: get latest version tag and release note
|
||||||
|
run: |
|
||||||
|
git fetch --depth=500
|
||||||
|
this_tag=$(python -c "import eos;print('v'+eos.__version__)")
|
||||||
|
prev_tag=$(git describe --abbrev=0 --tags `git rev-list --tags --skip=1 --max-count=1`)
|
||||||
|
echo "Relese tag $this_tag with changes since $prev_tag"
|
||||||
|
echo "Changes:
|
||||||
|
" > git_changes.log
|
||||||
|
git log $prev_tag..HEAD --pretty=format:"* %s" >> git_changes.log
|
||||||
|
cat git_changes.log
|
||||||
|
echo "RELEASE_TAG=$this_tag" >> $GITHUB_ENV
|
||||||
|
- name: Create Release
|
||||||
|
uses: actions/gitea-release-action@v1
|
||||||
|
with:
|
||||||
|
name: "Amor-Eos ${{ env.RELEASE_TAG }}"
|
||||||
|
tag_name: "${{ env.RELEASE_TAG }}"
|
||||||
|
body_path: git_changes.log
|
||||||
|
|
||||||
build-ubuntu-latest:
|
build-ubuntu-latest:
|
||||||
|
needs: [test]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ (github.event_name != 'workflow_dispatch') || (contains(fromJson('["all", "linux", "all_incl_release"]'), github.event.inputs.build-items)) }}
|
if: ${{ (github.event_name != 'workflow_dispatch') || (contains(fromJson('["all", "linux", "all_incl_release"]'), github.event.inputs.build-items)) }}
|
||||||
permissions:
|
permissions:
|
||||||
@@ -31,75 +83,55 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: '3.12'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install build
|
pip install build
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
pip install wheel build twine
|
||||||
- name: Build PyPI package
|
- name: Build PyPI package
|
||||||
run: |
|
run: |
|
||||||
python3 -m build
|
python3 -m build
|
||||||
- name: Archive distribution
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: linux-dist
|
|
||||||
path: |
|
|
||||||
dist/*.tar.gz
|
|
||||||
- name: Upload to PyPI
|
- name: Upload to PyPI
|
||||||
#if: github.event_name != 'workflow_dispatch'
|
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
|
||||||
with:
|
|
||||||
# user: __token__
|
|
||||||
# password: ${{ secrets.PYPI_TOKEN }}
|
|
||||||
skip-existing: true
|
|
||||||
|
|
||||||
build-windows:
|
|
||||||
runs-on: windows-latest
|
|
||||||
if: ${{ (github.event_name != 'workflow_dispatch') || (contains(fromJson('["all", "windows", "all_incl_release"]'), github.event.inputs.build-items)) }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: 3.12
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
run: |
|
||||||
C:\Miniconda\condabin\conda.bat env update --file conda_windows.yml --name base
|
twine upload dist/* -u __token__ -p ${{ secrets.PYPI_TOKEN }} --skip-existing
|
||||||
C:\Miniconda\condabin\conda.bat init powershell
|
this_tag=$(python -c "import eos;print('v'+eos.__version__)")
|
||||||
- name: Build with pyinstaller
|
echo "RELEASE_TAG=$this_tag" >> $GITHUB_ENV
|
||||||
run: |
|
- name: Update Release
|
||||||
pyinstaller windows_build.spec
|
if: ${{ (github.event_name != 'workflow_dispatch') || (contains(fromJson('["all_incl_release"]'), github.event.inputs.build-items)) }}
|
||||||
cd dist\eos
|
uses: actions/gitea-release-action@v1
|
||||||
Compress-Archive -Path .\* -Destination ..\..\eos.zip
|
|
||||||
- name: Archive distribution
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
name: windows-dist
|
name: "Amor-Eos ${{ env.RELEASE_TAG }}"
|
||||||
path: |
|
tag_name: "${{ env.RELEASE_TAG }}"
|
||||||
eos.zip
|
files: |-
|
||||||
|
dist/amor*.tar.gz
|
||||||
|
|
||||||
release:
|
# build-windows:
|
||||||
if: ${{ (github.event_name != 'workflow_dispatch') || (contains(fromJson('["all_incl_release"]'), github.event.inputs.build-items)) }}
|
# needs: [test]
|
||||||
runs-on: ubuntu-latest
|
# runs-on: windows-latest
|
||||||
needs: [build-ubuntu-latest, build-windows]
|
# if: ${{ (github.event_name != 'workflow_dispatch') || (contains(fromJson('["all", "windows", "all_incl_release"]'), github.event.inputs.build-items)) }}
|
||||||
steps:
|
#
|
||||||
- uses: actions/checkout@v4
|
# steps:
|
||||||
with:
|
# - uses: actions/checkout@v4
|
||||||
fetch-depth: 0
|
# - name: Set up Python
|
||||||
fetch-tags: true
|
# uses: actions/setup-python@v5
|
||||||
- uses: actions/download-artifact@v4
|
# with:
|
||||||
with:
|
# python-version: 3.12
|
||||||
name: linux-dist
|
# - name: Install dependencies
|
||||||
- uses: actions/download-artifact@v4
|
# run: |
|
||||||
with:
|
# C:\Miniconda\condabin\conda.bat env update --file conda_windows.yml --name base
|
||||||
name: windows-dist
|
# C:\Miniconda\condabin\conda.bat init powershell
|
||||||
- name: get latest version tag
|
# - name: Build with pyinstaller
|
||||||
run: echo "RELEASE_TAG=$(git describe --abbrev=0 --tags)" >> $GITHUB_ENV
|
# run: |
|
||||||
- uses: ncipollo/release-action@v1
|
# pyinstaller windows_build.spec
|
||||||
with:
|
# cd dist\eos
|
||||||
artifacts: "amor*.tar.gz,*.zip"
|
# Compress-Archive -Path .\* -Destination ..\..\eos.zip
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
# - name: Update Release
|
||||||
allowUpdates: true
|
# if: ${{ (github.event_name != 'workflow_dispatch') || (contains(fromJson('["all_incl_release"]'), github.event.inputs.build-items)) }}
|
||||||
tag: ${{ env.RELEASE_TAG }}
|
# uses: actions/gitea-release-action@v1
|
||||||
|
# with:
|
||||||
|
# name: "Amor-Eos ${{ env.RELEASE_TAG }}"
|
||||||
|
# tag_name: "${{ env.RELEASE_TAG }}"
|
||||||
|
# files: |-
|
||||||
|
# eos.zip
|
||||||
|
|||||||
11
.github/workflows/unit_tests.yml
vendored
11
.github/workflows/unit_tests.yml
vendored
@@ -10,18 +10,18 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
test:
|
||||||
|
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
|
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout LFS objects
|
||||||
with:
|
run: git clone https://${{secrets.GITHUB_TOKEN}}@gitea.psi.ch/${{ github.repository }}.git .
|
||||||
lfs: 'true'
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
@@ -35,4 +35,5 @@ jobs:
|
|||||||
|
|
||||||
- name: Test with pytest
|
- name: Test with pytest
|
||||||
run: |
|
run: |
|
||||||
|
export PYTEST_DISABLE_PLUGIN_AUTOLOAD=1
|
||||||
python -m pytest tests
|
python -m pytest tests
|
||||||
|
|||||||
30
PR_merged.py
Normal file
30
PR_merged.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
"""
|
||||||
|
Actions performed automatically on pull request merged. Update changed date and minor revision number.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from eos import __version__
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
INIT_HEADER = '''"""
|
||||||
|
Package to handle data redction at AMOR instrument to be used by __main__.py script.
|
||||||
|
"""
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("Running PR_merged.py")
|
||||||
|
print(f" Previous version: {__version__}")
|
||||||
|
version = __version__.split('.')
|
||||||
|
version[2] = str(int(version[2])+1)
|
||||||
|
version = '.'.join(version)
|
||||||
|
print(f" New version: {version}")
|
||||||
|
now=datetime.now()
|
||||||
|
with open(os.path.join('eos','__init__.py'), 'w') as f:
|
||||||
|
f.write(INIT_HEADER)
|
||||||
|
f.write(f"__version__ = '{version}'\n")
|
||||||
|
f.write(f"__date__ = '{now.strftime('%Y-%m-%d')}'\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
Package to handle data redction at AMOR instrument to be used by __main__.py script.
|
Package to handle data redction at AMOR instrument to be used by __main__.py script.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = '3.1.0'
|
__version__ = '3.2.4'
|
||||||
__date__ = '2026-02-20'
|
__date__ = '2026-03-02'
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from typing import List, Type
|
|||||||
from .options import ArgParsable
|
from .options import ArgParsable
|
||||||
|
|
||||||
|
|
||||||
def commandLineArgs(config_items: List[Type[ArgParsable]], program_name=None):
|
def commandLineArgs(config_items: List[Type[ArgParsable]], program_name=None, extra_args=[]):
|
||||||
"""
|
"""
|
||||||
Process command line argument.
|
Process command line argument.
|
||||||
The type of the default values is used for conversion and validation.
|
The type of the default values is used for conversion and validation.
|
||||||
@@ -36,4 +36,7 @@ def commandLineArgs(config_items: List[Type[ArgParsable]], program_name=None):
|
|||||||
f'--{cpc.argument}', **cpc.add_argument_args
|
f'--{cpc.argument}', **cpc.add_argument_args
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for ma in extra_args:
|
||||||
|
clas.add_argument(**ma)
|
||||||
|
|
||||||
return clas.parse_args()
|
return clas.parse_args()
|
||||||
|
|||||||
@@ -5,3 +5,7 @@ Constants used in data reduction.
|
|||||||
hdm = 6.626176e-34/1.674928e-27 # h / m
|
hdm = 6.626176e-34/1.674928e-27 # h / m
|
||||||
lamdaCut = 2.5 # Aa
|
lamdaCut = 2.5 # Aa
|
||||||
lamdaMax = 15.0 # Aa
|
lamdaMax = 15.0 # Aa
|
||||||
|
|
||||||
|
polarizationConfigs = ['unpolarized', 'unpolarized', 'po', 'mo', 'op', 'pp', 'mp', 'om', 'pm', 'mm']
|
||||||
|
polarizationLabels = ['undetermined', 'unpolarized', 'spin-up', 'spin-down', 'op',
|
||||||
|
'up-up', 'down-up', 'om', 'up-down', 'down-down']
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from typing import Tuple
|
|||||||
|
|
||||||
from . import const
|
from . import const
|
||||||
from .event_data_types import EventDataAction, EventDatasetProtocol, append_fields, EVENT_BITMASKS
|
from .event_data_types import EventDataAction, EventDatasetProtocol, append_fields, EVENT_BITMASKS
|
||||||
from .helpers import filter_project_x, merge_frames, extract_walltime
|
from .helpers import filter_project_x, merge_frames, extract_walltime, add_log_to_pulses, merge_frames_w_index
|
||||||
from .instrument import Detector
|
from .instrument import Detector
|
||||||
from .options import IncidentAngle
|
from .options import IncidentAngle
|
||||||
from .header import Header
|
from .header import Header
|
||||||
@@ -25,8 +25,9 @@ class ExtractWalltime(EventDataAction):
|
|||||||
dataset.data.events = new_events
|
dataset.data.events = new_events
|
||||||
|
|
||||||
class MergeFrames(EventDataAction):
|
class MergeFrames(EventDataAction):
|
||||||
def __init__(self, lamdaCut=None):
|
def __init__(self, lamdaCut=None, preciseTime=False):
|
||||||
self.lamdaCut=lamdaCut
|
self.lamdaCut=lamdaCut
|
||||||
|
self.preciseTime=preciseTime
|
||||||
|
|
||||||
def perform_action(self, dataset: EventDatasetProtocol)->None:
|
def perform_action(self, dataset: EventDatasetProtocol)->None:
|
||||||
if self.lamdaCut is None:
|
if self.lamdaCut is None:
|
||||||
@@ -36,7 +37,34 @@ class MergeFrames(EventDataAction):
|
|||||||
tofCut = lamdaCut*dataset.geometry.chopperDetectorDistance/const.hdm*1e-13
|
tofCut = lamdaCut*dataset.geometry.chopperDetectorDistance/const.hdm*1e-13
|
||||||
total_offset = (tofCut +
|
total_offset = (tofCut +
|
||||||
dataset.timing.tau * (dataset.timing.ch1TriggerPhase + dataset.timing.chopperPhase/2)/180)
|
dataset.timing.tau * (dataset.timing.ch1TriggerPhase + dataset.timing.chopperPhase/2)/180)
|
||||||
dataset.data.events.tof = merge_frames(dataset.data.events.tof, tofCut, dataset.timing.tau, total_offset)
|
if self.preciseTime and 'wallTime' in dataset.data.events.dtype.names:
|
||||||
|
d = dataset.data
|
||||||
|
# put events into precise sub-frame
|
||||||
|
d.events.tof, subframes = merge_frames_w_index(d.events.tof, tofCut, dataset.timing.tau, total_offset)
|
||||||
|
subframes = subframes.astype(int)
|
||||||
|
# add a sub-pulse time 1-tau before and after each existing time
|
||||||
|
utimes, uidxs = np.unique(d.events.wallTime, return_inverse=True)
|
||||||
|
inter_times = np.empty(2*utimes.shape[0]+1, dtype=d.events.wallTime.dtype)
|
||||||
|
tauns = dataset.timing.tau*1e9
|
||||||
|
inter_times[0] = utimes[0]-tauns
|
||||||
|
inter_times[1::2] = utimes
|
||||||
|
inter_times[2::2] = utimes+tauns
|
||||||
|
# use subframe indices to sort existing events into new times
|
||||||
|
d.events.wallTime = inter_times[2*uidxs+subframes+1]
|
||||||
|
# expand pulses array with additional sub-frames
|
||||||
|
new_pulses = np.recarray(2*d.pulses.shape[0]+1, dtype=d.pulses.dtype)
|
||||||
|
new_pulses[0] = d.pulses[0]
|
||||||
|
new_pulses[1::2] = d.pulses
|
||||||
|
new_pulses[2::2] = d.pulses
|
||||||
|
new_pulses.time[0] = d.pulses.time[0]-tauns
|
||||||
|
new_pulses.time[2::2] = d.pulses.time+tauns
|
||||||
|
new_pulses.monitor /= 2.0 # ~preserve total monitor counts
|
||||||
|
d.pulses = new_pulses
|
||||||
|
else:
|
||||||
|
if self.preciseTime:
|
||||||
|
logging.error(" Trying to use precise time frame separation while wallTime is not extracted, yet!")
|
||||||
|
dataset.data.events.tof = merge_frames(dataset.data.events.tof, tofCut, dataset.timing.tau, total_offset)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AnalyzePixelIDs(EventDataAction):
|
class AnalyzePixelIDs(EventDataAction):
|
||||||
@@ -124,5 +152,40 @@ class FilterQzRange(EventDataAction):
|
|||||||
if not 'qz' in dataset.data.events.dtype.names:
|
if not 'qz' in dataset.data.events.dtype.names:
|
||||||
raise ValueError("FilterQzRange requires dataset with qz values per events, perform WavelengthAndQ first")
|
raise ValueError("FilterQzRange requires dataset with qz values per events, perform WavelengthAndQ first")
|
||||||
|
|
||||||
if self.qzRange[1]<0.5:
|
d.events.mask += EVENT_BITMASKS["qRange"]*((self.qzRange[0]>d.events.qz) | (d.events.qz>self.qzRange[1]))
|
||||||
d.events.mask += EVENT_BITMASKS["qRange"]*((self.qzRange[0]>d.events.qz) | (d.events.qz>self.qzRange[1]))
|
|
||||||
|
class FilterByLog(EventDataAction):
|
||||||
|
|
||||||
|
def __init__(self, filter_string, remove_switchpulse=False):
|
||||||
|
if filter_string.startswith('!'):
|
||||||
|
filter_string = filter_string[1:]
|
||||||
|
remove_switchpulse = True
|
||||||
|
self.filter_string = filter_string
|
||||||
|
self.remove_switchpulse = remove_switchpulse
|
||||||
|
|
||||||
|
def perform_action(self, dataset: EventDatasetProtocol) -> None:
|
||||||
|
filter_variable = None
|
||||||
|
# go through existing keys in reverse order of their length to insure a name containing another is used
|
||||||
|
existing_keys = list(dataset.data.device_logs.keys())
|
||||||
|
existing_keys.sort(key=lambda x: -len(x))
|
||||||
|
for key in existing_keys:
|
||||||
|
if key in self.filter_string:
|
||||||
|
filter_variable = key
|
||||||
|
break
|
||||||
|
if filter_variable is None:
|
||||||
|
logging.warning(f' could not find suitable parameter to filter on in {self.filter_string}, '
|
||||||
|
f'available parameters are: {list(sorted(existing_keys))}')
|
||||||
|
return
|
||||||
|
logging.debug(f' using parameter {filter_variable} to apply filter {self.filter_string}')
|
||||||
|
if not filter_variable in EVENT_BITMASKS:
|
||||||
|
EVENT_BITMASKS[filter_variable] = max(EVENT_BITMASKS.values())*2
|
||||||
|
if not filter_variable in dataset.data.pulses.dtype.names:
|
||||||
|
# interpolate the parameter values for all existing pulses
|
||||||
|
add_log_to_pulses(filter_variable, dataset)
|
||||||
|
fltr_pulses = eval(self.filter_string, {filter_variable: dataset.data.pulses[filter_variable]})
|
||||||
|
if self.remove_switchpulse:
|
||||||
|
switched = fltr_pulses[:-1] & ~fltr_pulses[1:]
|
||||||
|
fltr_pulses[:-1] &= ~switched
|
||||||
|
goodTimeS = dataset.data.pulses.time[fltr_pulses]
|
||||||
|
filter_e = np.logical_not(np.isin(dataset.data.events.wallTime, goodTimeS))
|
||||||
|
dataset.data.events.mask += EVENT_BITMASKS[filter_variable]*filter_e
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
Specify the data type and protocol used for event datasets.
|
Specify the data type and protocol used for event datasets.
|
||||||
"""
|
"""
|
||||||
from typing import List, Optional, Protocol, Tuple
|
from typing import Dict, List, Optional, Protocol, Tuple
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from .header import Header
|
from .header import Header
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
@@ -34,6 +34,7 @@ EVENT_TYPE = np.dtype([('tof', np.float64), ('pixelID', np.uint32), ('mask', np.
|
|||||||
PACKET_TYPE = np.dtype([('start_index', np.uint32), ('time', np.int64)])
|
PACKET_TYPE = np.dtype([('start_index', np.uint32), ('time', np.int64)])
|
||||||
PULSE_TYPE = np.dtype([('time', np.int64), ('monitor', np.float32)])
|
PULSE_TYPE = np.dtype([('time', np.int64), ('monitor', np.float32)])
|
||||||
PC_TYPE = np.dtype([('current', np.float32), ('time', np.int64)])
|
PC_TYPE = np.dtype([('current', np.float32), ('time', np.int64)])
|
||||||
|
LOG_TYPE = np.dtype([('value', np.float32), ('time', np.int64)])
|
||||||
|
|
||||||
# define the bitmask for individual event filters
|
# define the bitmask for individual event filters
|
||||||
EVENT_BITMASKS = {
|
EVENT_BITMASKS = {
|
||||||
@@ -45,6 +46,8 @@ EVENT_BITMASKS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def append_fields(input: np.recarray, new_fields: List[Tuple[str, np.dtype]]):
|
def append_fields(input: np.recarray, new_fields: List[Tuple[str, np.dtype]]):
|
||||||
|
# TODO: This action is used often and time consuming as it runs len(flds) times over all indices.
|
||||||
|
# Could only be faster if array is allocated in the beginning with all fields, less flexible.
|
||||||
# add one ore more fields to a recarray, numpy functions seems to fail
|
# add one ore more fields to a recarray, numpy functions seems to fail
|
||||||
flds = [(name, dtypei[0]) for name, dtypei in input.dtype.fields.items()]
|
flds = [(name, dtypei[0]) for name, dtypei in input.dtype.fields.items()]
|
||||||
flds += new_fields
|
flds += new_fields
|
||||||
@@ -60,6 +63,7 @@ class AmorEventStream:
|
|||||||
packets: np.recarray # PACKET_TYPE
|
packets: np.recarray # PACKET_TYPE
|
||||||
pulses: np.recarray # PULSE_TYPE
|
pulses: np.recarray # PULSE_TYPE
|
||||||
proton_current: np.recarray # PC_TYPE
|
proton_current: np.recarray # PC_TYPE
|
||||||
|
device_logs: Dict[str, np.recarray] = field(default_factory=dict) # LOG_TYPE
|
||||||
|
|
||||||
class EventDatasetProtocol(Protocol):
|
class EventDatasetProtocol(Protocol):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class ApplyParameterOverwrites(EventDataAction):
|
|||||||
with open(self.config.sampleModel, 'r') as model_yml:
|
with open(self.config.sampleModel, 'r') as model_yml:
|
||||||
model = yaml.safe_load(model_yml)
|
model = yaml.safe_load(model_yml)
|
||||||
else:
|
else:
|
||||||
logging.warning(f' ! the file {self.config.sampleModel}.yml does not exist. Ignored!')
|
logging.warning(f' ! the file {self.config.sampleModel} does not exist. Ignored!')
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
model = dict(stack=self.config.sampleModel)
|
model = dict(stack=self.config.sampleModel)
|
||||||
@@ -71,6 +71,8 @@ class CorrectSeriesTime(EventDataAction):
|
|||||||
dataset.data.pulses.time -= self.seriesStartTime
|
dataset.data.pulses.time -= self.seriesStartTime
|
||||||
dataset.data.events.wallTime -= self.seriesStartTime
|
dataset.data.events.wallTime -= self.seriesStartTime
|
||||||
dataset.data.proton_current.time -= self.seriesStartTime
|
dataset.data.proton_current.time -= self.seriesStartTime
|
||||||
|
for value in dataset.data.device_logs.values():
|
||||||
|
value.time -= self.seriesStartTime
|
||||||
start, stop = dataset.data.events.wallTime[0], dataset.data.events.wallTime[-1]
|
start, stop = dataset.data.events.wallTime[0], dataset.data.events.wallTime[-1]
|
||||||
logging.debug(f' wall time from {start/1e9:6.1f} s to {stop/1e9:6.1f} s, '
|
logging.debug(f' wall time from {start/1e9:6.1f} s to {stop/1e9:6.1f} s, '
|
||||||
f'series time = {self.seriesStartTime/1e9:6.1f}')
|
f'series time = {self.seriesStartTime/1e9:6.1f}')
|
||||||
@@ -163,10 +165,12 @@ class ApplyMask(EventDataAction):
|
|||||||
self.bitmask_filter = bitmask_filter
|
self.bitmask_filter = bitmask_filter
|
||||||
|
|
||||||
def perform_action(self, dataset: EventDatasetProtocol) ->None:
|
def perform_action(self, dataset: EventDatasetProtocol) ->None:
|
||||||
# TODO: why is this action time consuming?
|
# TODO: Most time in test examples is spend here.
|
||||||
|
# While the actions here are very simple, they act on a large array,
|
||||||
|
# so even just comparison and indexing become time consuming.
|
||||||
d = dataset.data
|
d = dataset.data
|
||||||
pre_filter = d.events.shape[0]
|
pre_filter = d.events.shape[0]
|
||||||
if logging.getLogger().level == logging.DEBUG:
|
if logging.getLogger().level <= logging.DEBUG:
|
||||||
# only run this calculation if debug level is actually active
|
# only run this calculation if debug level is actually active
|
||||||
filtered_by_mask = {}
|
filtered_by_mask = {}
|
||||||
for key, value in EVENT_BITMASKS.items():
|
for key, value in EVENT_BITMASKS.items():
|
||||||
@@ -181,3 +185,20 @@ class ApplyMask(EventDataAction):
|
|||||||
d.events = d.events[fltr]
|
d.events = d.events[fltr]
|
||||||
post_filter = d.events.shape[0]
|
post_filter = d.events.shape[0]
|
||||||
logging.info(f' number of events: total = {pre_filter:7d}, filtered = {post_filter:7d}')
|
logging.info(f' number of events: total = {pre_filter:7d}, filtered = {post_filter:7d}')
|
||||||
|
if d.device_logs == {} or not hasattr(dataset, 'update_info_from_logs'):
|
||||||
|
return
|
||||||
|
# filter pulses and logs to allow update of header information
|
||||||
|
from .helpers import add_log_to_pulses
|
||||||
|
times = np.unique(d.events.wallTime)
|
||||||
|
# make sure all log variables are associated with pulses
|
||||||
|
for key, log in d.device_logs.items():
|
||||||
|
if not key in d.pulses.dtype.names:
|
||||||
|
# interpolate the parameter values for all existing pulses
|
||||||
|
add_log_to_pulses(key, dataset)
|
||||||
|
# remove all pulses that have no more events
|
||||||
|
d.pulses = d.pulses[np.isin(d.pulses.time, times)]
|
||||||
|
for key, log in d.device_logs.items():
|
||||||
|
d.device_logs[key] = np.recarray(d.pulses.shape, dtype = log.dtype)
|
||||||
|
d.device_logs[key].time = d.pulses.time
|
||||||
|
d.device_logs[key].value = d.pulses[key]
|
||||||
|
dataset.update_info_from_logs()
|
||||||
@@ -3,9 +3,9 @@ Reading of Amor NeXus data files to extract metadata and event stream.
|
|||||||
"""
|
"""
|
||||||
from typing import BinaryIO, List, Union
|
from typing import BinaryIO, List, Union
|
||||||
|
|
||||||
|
import sys
|
||||||
import h5py
|
import h5py
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import platform
|
|
||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
@@ -16,7 +16,8 @@ from orsopy.fileio.model_language import SampleModel
|
|||||||
|
|
||||||
from . import const
|
from . import const
|
||||||
from .header import Header
|
from .header import Header
|
||||||
from .event_data_types import AmorGeometry, AmorTiming, AmorEventStream, PACKET_TYPE, EVENT_TYPE, PULSE_TYPE, PC_TYPE
|
from .event_data_types import AmorGeometry, AmorTiming, AmorEventStream, LOG_TYPE, PACKET_TYPE, EVENT_TYPE, PULSE_TYPE, \
|
||||||
|
PC_TYPE
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import zoneinfo
|
import zoneinfo
|
||||||
@@ -27,17 +28,41 @@ except ImportError:
|
|||||||
|
|
||||||
# Time zone used to interpret time strings
|
# Time zone used to interpret time strings
|
||||||
AMOR_LOCAL_TIMEZONE = zoneinfo.ZoneInfo(key='Europe/Zurich')
|
AMOR_LOCAL_TIMEZONE = zoneinfo.ZoneInfo(key='Europe/Zurich')
|
||||||
|
UTC = zoneinfo.ZoneInfo(key='UTC')
|
||||||
if platform.node().startswith('amor'):
|
|
||||||
NICOS_CACHE_DIR = '/home/data/nicosdata/cache/'
|
|
||||||
GREP = '/usr/bin/grep "value"'
|
|
||||||
else:
|
|
||||||
NICOS_CACHE_DIR = None
|
|
||||||
|
|
||||||
class AmorHeader:
|
class AmorHeader:
|
||||||
"""
|
"""
|
||||||
Collects header information from Amor NeXus fiel without reading event data.
|
Collects header information from Amor NeXus fiel without reading event data.
|
||||||
"""
|
"""
|
||||||
|
# mapping of names to (hdf_path, dtype, nicos_key[, suffix])
|
||||||
|
hdf_paths = dict(
|
||||||
|
title=('entry1/title', str),
|
||||||
|
proposal_id=('entry1/proposal_id', str),
|
||||||
|
user_name=('entry1/user/name', str),
|
||||||
|
user_email=('entry1/user/email', str),
|
||||||
|
sample_name=('entry1/sample/name', str),
|
||||||
|
source_name=('entry1/Amor/source/name', str),
|
||||||
|
sample_model=('entry1/sample/model', str),
|
||||||
|
start_time=('entry1/start_time', str),
|
||||||
|
start_time_fallback=('entry1/Amor/instrument_control_parameters/start_time', str),
|
||||||
|
|
||||||
|
chopper_separation=('entry1/Amor/chopper/pair_separation', float),
|
||||||
|
detector_distance=('entry1/Amor/detector/transformation/distance', float),
|
||||||
|
chopper_distance=('entry1/Amor/chopper/distance', float),
|
||||||
|
sample_temperature=('entry1/sample/temperature', float),
|
||||||
|
sample_magnetic_field=('entry1/sample/magnetic_field', float),
|
||||||
|
|
||||||
|
mu=('entry1/Amor/instrument_control_parameters/mu', float, 'mu'),
|
||||||
|
nu=('entry1/Amor/instrument_control_parameters/nu', float, 'nu'),
|
||||||
|
kap=('entry1/Amor/instrument_control_parameters/kappa', float, 'kappa'),
|
||||||
|
kad=('entry1/Amor/instrument_control_parameters/kappa_offset', float, 'kappa_offset'),
|
||||||
|
div=('entry1/Amor/instrument_control_parameters/div', float, 'div'),
|
||||||
|
ch1_trigger_phase=('entry1/Amor/chopper/ch1_trigger_phase', float, 'ch1_trigger_phase'),
|
||||||
|
ch2_trigger_phase=('entry1/Amor/chopper/ch2_trigger_phase', float, 'ch2_trigger_phase'),
|
||||||
|
chopper_speed=('entry1/Amor/chopper/rotation_speed', float, 'chopper_phase'),
|
||||||
|
chopper_phase=('entry1/Amor/chopper/phase', float, 'chopper_phase'),
|
||||||
|
polarization_config_label=('entry1/Amor/polarization/configuration', int, 'polarization_config_label', '/*'),
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, fileName:Union[str, h5py.File, BinaryIO]):
|
def __init__(self, fileName:Union[str, h5py.File, BinaryIO]):
|
||||||
if type(fileName) is str:
|
if type(fileName) is str:
|
||||||
@@ -48,6 +73,8 @@ class AmorHeader:
|
|||||||
else:
|
else:
|
||||||
self.hdf = h5py.File(fileName, 'r')
|
self.hdf = h5py.File(fileName, 'r')
|
||||||
|
|
||||||
|
self._log_keys = []
|
||||||
|
|
||||||
self.read_header_info()
|
self.read_header_info()
|
||||||
self.read_instrument_configuration()
|
self.read_instrument_configuration()
|
||||||
|
|
||||||
@@ -57,52 +84,89 @@ class AmorHeader:
|
|||||||
del(self.hdf)
|
del(self.hdf)
|
||||||
|
|
||||||
def _replace_if_missing(self, key, nicos_key, dtype=float, suffix=''):
|
def _replace_if_missing(self, key, nicos_key, dtype=float, suffix=''):
|
||||||
try:
|
from .nicos_interface import lookup_nicos_value
|
||||||
return dtype(self.hdf[f'/entry1/Amor/{key}'][0])
|
year = self.fileDate.strftime('%Y')
|
||||||
except(KeyError, IndexError):
|
return lookup_nicos_value(key, nicos_key, dtype, suffix, year)
|
||||||
if NICOS_CACHE_DIR:
|
|
||||||
try:
|
|
||||||
logging.info(f" using parameter {nicos_key} from nicos cache")
|
|
||||||
year_date = self.fileDate.strftime('%Y')
|
|
||||||
call = f'{GREP} {NICOS_CACHE_DIR}nicos-{nicos_key}/{year_date}{suffix}'
|
|
||||||
value = str(subprocess.getoutput(call)).split('\t')[-1]
|
|
||||||
return dtype(value)
|
|
||||||
except Exception:
|
|
||||||
logging.error(f"Couldn't get value from nicos cache {nicos_key}, {call}")
|
|
||||||
return dtype(0)
|
|
||||||
else:
|
|
||||||
logging.warning(f" parameter {key} not found, relpace by zero")
|
|
||||||
return dtype(0)
|
|
||||||
|
|
||||||
def get_hdf_single_entry(self, path):
|
def rv(self, key):
|
||||||
if not np.shape(self.hdf['entry1/title']):
|
"""
|
||||||
|
Generic read value methos based on key in hdf_paths dictionary.
|
||||||
|
"""
|
||||||
|
hdf_path, dtype, *nicos = self.hdf_paths[key]
|
||||||
|
try:
|
||||||
|
hdfgrp = self.hdf[hdf_path]
|
||||||
|
if hdfgrp.attrs.get('NX_class', None) == 'NXlog':
|
||||||
|
# use the last value that was recoreded before the count started
|
||||||
|
time_column = hdfgrp['time'][:]
|
||||||
|
try:
|
||||||
|
start_index = np.where(time_column<=self._start_time_ns)[0][0]
|
||||||
|
except IndexError:
|
||||||
|
start_index = 0
|
||||||
|
if hdfgrp['value'].ndim==1:
|
||||||
|
output = dtype(hdfgrp['value'][start_index])
|
||||||
|
else:
|
||||||
|
output = dtype(hdfgrp['value'][start_index, 0])
|
||||||
|
# make sure key is only appended if no exception was raised
|
||||||
|
self._log_keys.append(key)
|
||||||
|
return output
|
||||||
|
elif dtype is str:
|
||||||
|
return self.read_string(hdf_path)
|
||||||
|
else:
|
||||||
|
if len(hdfgrp.shape)==1:
|
||||||
|
return dtype(hdfgrp[0])
|
||||||
|
else:
|
||||||
|
return dtype(hdfgrp[()])
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
if nicos:
|
||||||
|
nicos_key = nicos[0]
|
||||||
|
suffix = nicos[1] if len(nicos)>1 else ''
|
||||||
|
return self._replace_if_missing(key, nicos_key, dtype, suffix)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def read_string(self, path):
|
||||||
|
if not np.shape(self.hdf[path]):
|
||||||
return self.hdf[path][()].decode('utf-8')
|
return self.hdf[path][()].decode('utf-8')
|
||||||
else:
|
else:
|
||||||
# format until 2025
|
# format until 2025
|
||||||
return self.hdf[path][0].decode('utf-8')
|
return self.hdf[path][0].decode('utf-8')
|
||||||
|
|
||||||
def read_header_info(self):
|
def read_header_info(self):
|
||||||
|
self._start_time_ns = np.uint64(0)
|
||||||
|
try:
|
||||||
|
start_time = self.rv('start_time')
|
||||||
|
except KeyError:
|
||||||
|
start_time = self.rv('start_time_fallback')
|
||||||
|
|
||||||
|
# extract start time as unix time, adding UTC offset of 1h to time string
|
||||||
|
if start_time.endswith('Z') and sys.version_info.minor<11:
|
||||||
|
# older python versions did not support Z format
|
||||||
|
start_time = start_time[:-1]
|
||||||
|
TZ = UTC
|
||||||
|
else:
|
||||||
|
TZ = AMOR_LOCAL_TIMEZONE
|
||||||
|
start_date = datetime.fromisoformat(start_time)
|
||||||
|
self.fileDate = start_date.replace(tzinfo=TZ)
|
||||||
|
self._start_time_ns = np.uint64(self.fileDate.timestamp()*1e9)
|
||||||
|
|
||||||
# read general information and first data set
|
# read general information and first data set
|
||||||
title = self.get_hdf_single_entry('entry1/title')
|
title = self.rv('title')
|
||||||
proposal_id = self.get_hdf_single_entry('entry1/proposal_id')
|
proposal_id = self.rv('proposal_id')
|
||||||
user_name = self.get_hdf_single_entry('entry1/user/name')
|
user_name = self.rv('user_name')
|
||||||
user_affiliation = 'unknown'
|
user_affiliation = 'unknown'
|
||||||
user_email = self.get_hdf_single_entry('entry1/user/email')
|
user_email = self.rv('user_email')
|
||||||
user_orcid = None
|
user_orcid = None
|
||||||
sampleName = self.get_hdf_single_entry('entry1/sample/name')
|
sampleName = self.rv('sample_name')
|
||||||
instrumentName = 'Amor'
|
instrumentName = 'Amor'
|
||||||
source = self.get_hdf_single_entry('entry1/Amor/source/name')
|
source = self.rv('source_name')
|
||||||
sourceProbe = 'neutron'
|
sourceProbe = 'neutron'
|
||||||
model = self.get_hdf_single_entry('entry1/sample/model')
|
model = self.rv('sample_model')
|
||||||
if 'stack' in model:
|
if 'stack' in model:
|
||||||
import yaml
|
import yaml
|
||||||
model = yaml.safe_load(model)
|
model = yaml.safe_load(model)
|
||||||
else:
|
else:
|
||||||
model = dict(stack=model)
|
model = dict(stack=model)
|
||||||
start_time = self.get_hdf_single_entry('entry1/start_time')
|
|
||||||
# extract start time as unix time, adding UTC offset of 1h to time string
|
|
||||||
start_date = datetime.fromisoformat(start_time)
|
|
||||||
self.fileDate = start_date.replace(tzinfo=AMOR_LOCAL_TIMEZONE)
|
|
||||||
|
|
||||||
self.owner = fileio.Person(
|
self.owner = fileio.Person(
|
||||||
name=user_name,
|
name=user_name,
|
||||||
@@ -130,28 +194,32 @@ class AmorHeader:
|
|||||||
sample_parameters={},
|
sample_parameters={},
|
||||||
)
|
)
|
||||||
# while event times are not evaluated, use average_value reported in file for SEE
|
# while event times are not evaluated, use average_value reported in file for SEE
|
||||||
if self.hdf['entry1/sample'].get('temperature', None) is not None \
|
if self.hdf['entry1/sample'].get('temperature', None) is not None:
|
||||||
and float(self.hdf['entry1/sample/temperature/average_value'][0])>0:
|
try:
|
||||||
self.sample.sample_parameters['temperature'] = fileio.Value(
|
sample_temperature = self.rv('sample_temperature')
|
||||||
float(self.hdf['entry1/sample/temperature/average_value'][0]), unit='K')
|
except IndexError: pass
|
||||||
|
else:
|
||||||
|
self.sample.sample_parameters['temperature'] = fileio.Value(sample_temperature, unit='K')
|
||||||
if self.hdf['entry1/sample'].get('magnetic_field', None) is not None:
|
if self.hdf['entry1/sample'].get('magnetic_field', None) is not None:
|
||||||
self.sample.sample_parameters['magnetic_field'] = fileio.Value(
|
try:
|
||||||
float(self.hdf['entry1/sample/magnetic_field/average_value'][0]), unit='T')
|
sample_magnetic_field = self.rv('sample_magnetic_field')
|
||||||
|
except IndexError: pass
|
||||||
|
else:
|
||||||
|
self.sample.sample_parameters['magnetic_field'] = fileio.Value(sample_magnetic_field, unit='T')
|
||||||
|
|
||||||
def read_instrument_configuration(self):
|
def read_instrument_configuration(self):
|
||||||
chopperSeparation = float(np.take(self.hdf['entry1/Amor/chopper/pair_separation'], 0))
|
chopperSeparation = self.rv('chopper_separation')
|
||||||
detectorDistance = float(np.take(self.hdf['entry1/Amor/detector/transformation/distance'], 0))
|
detectorDistance = self.rv('detector_distance')
|
||||||
chopperDetectorDistance = detectorDistance-float(np.take(self.hdf['entry1/Amor/chopper/distance'], 0))
|
chopperDistance = self.rv('chopper_distance')
|
||||||
|
chopperDetectorDistance = detectorDistance - chopperDistance
|
||||||
|
|
||||||
polarizationConfigs = ['unpolarized', 'unpolarized', 'po', 'mo', 'op', 'pp', 'mp', 'om', 'pm', 'mm']
|
mu = self.rv('mu')
|
||||||
|
nu = self.rv('nu')
|
||||||
mu = self._replace_if_missing('instrument_control_parameters/mu', 'mu', float)
|
kap = self.rv('kap')
|
||||||
nu = self._replace_if_missing('instrument_control_parameters/nu', 'nu', float)
|
kad = self.rv('kad')
|
||||||
kap = self._replace_if_missing('instrument_control_parameters/kappa', 'kappa', float)
|
div = self.rv('div')
|
||||||
kad = self._replace_if_missing('instrument_control_parameters/kappa_offset', 'kappa_offset', float)
|
ch1TriggerPhase = self.rv('ch1_trigger_phase')
|
||||||
div = self._replace_if_missing('instrument_control_parameters/div', 'div', float)
|
ch2TriggerPhase = self.rv('ch2_trigger_phase')
|
||||||
ch1TriggerPhase = self._replace_if_missing('chopper/ch1_trigger_phase', 'ch1_trigger_phase', float)
|
|
||||||
ch2TriggerPhase = self._replace_if_missing('chopper/ch2_trigger_phase', 'ch2_trigger_phase', float)
|
|
||||||
try:
|
try:
|
||||||
chopperTriggerTime = (float(self.hdf['entry1/Amor/chopper/ch2_trigger/event_time_zero'][7]) \
|
chopperTriggerTime = (float(self.hdf['entry1/Amor/chopper/ch2_trigger/event_time_zero'][7]) \
|
||||||
-float(self.hdf['entry1/Amor/chopper/ch2_trigger/event_time_zero'][0])) \
|
-float(self.hdf['entry1/Amor/chopper/ch2_trigger/event_time_zero'][0])) \
|
||||||
@@ -159,8 +227,8 @@ class AmorHeader:
|
|||||||
chopperTriggerTimeDiff = float(self.hdf['entry1/Amor/chopper/ch2_trigger/event_time_offset'][2])
|
chopperTriggerTimeDiff = float(self.hdf['entry1/Amor/chopper/ch2_trigger/event_time_offset'][2])
|
||||||
except (KeyError, IndexError):
|
except (KeyError, IndexError):
|
||||||
logging.debug(' chopper speed and phase taken from .hdf file')
|
logging.debug(' chopper speed and phase taken from .hdf file')
|
||||||
chopperSpeed = self._replace_if_missing('chopper/rotation_speed', 'chopper_phase', float)
|
chopperSpeed = self.rv('chopper_speed')
|
||||||
chopperPhase = self._replace_if_missing('chopper/phase', 'chopper_phase', float)
|
chopperPhase = self.rv('chopper_phase')
|
||||||
tau = 30/chopperSpeed
|
tau = 30/chopperSpeed
|
||||||
else:
|
else:
|
||||||
tau = int(1e-6*chopperTriggerTime/2+0.5)*(1e-3)
|
tau = int(1e-6*chopperTriggerTime/2+0.5)*(1e-3)
|
||||||
@@ -172,8 +240,8 @@ class AmorHeader:
|
|||||||
chopperSeparation, detectorDistance, chopperDetectorDistance)
|
chopperSeparation, detectorDistance, chopperDetectorDistance)
|
||||||
self.timing = AmorTiming(ch1TriggerPhase, ch2TriggerPhase, chopperSpeed, chopperPhase, tau)
|
self.timing = AmorTiming(ch1TriggerPhase, ch2TriggerPhase, chopperSpeed, chopperPhase, tau)
|
||||||
|
|
||||||
polarizationConfigLabel = self._replace_if_missing('polarization/configuration/average_value', 'polarization_config_label', int, suffix='/*')
|
polarizationConfigLabel = self.rv('polarization_config_label')
|
||||||
polarizationConfig = fileio.Polarization(polarizationConfigs[polarizationConfigLabel])
|
polarizationConfig = fileio.Polarization(const.polarizationConfigs[polarizationConfigLabel])
|
||||||
logging.debug(f' polarization configuration: {polarizationConfig} (index {polarizationConfigLabel})')
|
logging.debug(f' polarization configuration: {polarizationConfig} (index {polarizationConfigLabel})')
|
||||||
|
|
||||||
|
|
||||||
@@ -258,7 +326,13 @@ class AmorEventData(AmorHeader):
|
|||||||
|
|
||||||
super().__init__(hdf)
|
super().__init__(hdf)
|
||||||
self.hdf = hdf
|
self.hdf = hdf
|
||||||
self.read_event_stream()
|
try:
|
||||||
|
self.read_event_stream()
|
||||||
|
except EOFError:
|
||||||
|
self.hdf.close()
|
||||||
|
del(self.hdf)
|
||||||
|
raise
|
||||||
|
self.read_log_streams()
|
||||||
|
|
||||||
if type(fileName) is str:
|
if type(fileName) is str:
|
||||||
# close the input file to free memory, only if the file was opened in this object
|
# close the input file to free memory, only if the file was opened in this object
|
||||||
@@ -281,15 +355,26 @@ class AmorEventData(AmorHeader):
|
|||||||
raise EOFError(f'No event packet found starting at event #{self.first_index}, '
|
raise EOFError(f'No event packet found starting at event #{self.first_index}, '
|
||||||
f'number of events is {self.hdf["/entry1/Amor/detector/data/event_time_offset"].shape[0]}')
|
f'number of events is {self.hdf["/entry1/Amor/detector/data/event_time_offset"].shape[0]}')
|
||||||
packets = packets[start_packet:]
|
packets = packets[start_packet:]
|
||||||
|
if packets.shape[0]==0:
|
||||||
|
raise EOFError(f'No more packets left after start_packet filter')
|
||||||
|
|
||||||
nevts = self.hdf['/entry1/Amor/detector/data/event_time_offset'].shape[0]
|
nevts = self.hdf['/entry1/Amor/detector/data/event_time_offset'].shape[0]
|
||||||
if (nevts-self.first_index)>self.max_events:
|
if (nevts-self.first_index)>self.max_events:
|
||||||
end_packet = np.where(packets.start_index<=(self.first_index+self.max_events))[0][-1]
|
end_packet = np.where(packets.start_index<=(self.first_index+self.max_events))[0][-1]
|
||||||
self.last_index = packets.start_index[end_packet]-1
|
end_packet = max(1, end_packet)
|
||||||
|
if len(packets)==1:
|
||||||
|
self.last_index = nevts-1
|
||||||
|
else:
|
||||||
|
self.last_index = packets.start_index[end_packet]-1
|
||||||
packets = packets[:end_packet]
|
packets = packets[:end_packet]
|
||||||
else:
|
else:
|
||||||
self.last_index = nevts-1
|
self.last_index = nevts-1
|
||||||
self.EOF = True
|
self.EOF = True
|
||||||
|
|
||||||
|
if packets.shape[0]==0:
|
||||||
|
raise EOFError(f'No more packets left after end_packet filter, first_index={self.first_index}, '
|
||||||
|
f'max_events={self.max_events}, nevts={nevts}')
|
||||||
|
|
||||||
nevts = self.last_index+1-self.first_index
|
nevts = self.last_index+1-self.first_index
|
||||||
|
|
||||||
# adapte packet to event index relation
|
# adapte packet to event index relation
|
||||||
@@ -308,6 +393,45 @@ class AmorEventData(AmorHeader):
|
|||||||
# label the file name if not all events were used
|
# label the file name if not all events were used
|
||||||
self.file_list[0] += f'[{self.first_index}:{self.last_index}]'
|
self.file_list[0] += f'[{self.first_index}:{self.last_index}]'
|
||||||
|
|
||||||
|
def read_log_streams(self):
|
||||||
|
"""
|
||||||
|
Read the individual NXlog datasets that can later be used for filtering etc.
|
||||||
|
"""
|
||||||
|
for key in self._log_keys:
|
||||||
|
hdf_path, dtype, *_ = self.hdf_paths[key]
|
||||||
|
hdfgroup = self.hdf[hdf_path]
|
||||||
|
shape = hdfgroup['time'].shape
|
||||||
|
data = np.recarray(shape, dtype=np.dtype([('value', self.hdf_paths[key][1]), ('time', np.int64)]))
|
||||||
|
data.time = hdfgroup['time'][:]
|
||||||
|
if len(hdfgroup['value'].shape)==1:
|
||||||
|
data.value = hdfgroup['value'][:]
|
||||||
|
else:
|
||||||
|
data.value = hdfgroup['value'][:, 0]
|
||||||
|
self.data.device_logs[key] = data
|
||||||
|
|
||||||
|
def update_info_from_logs(self):
|
||||||
|
RELEVANT_ITEMS = ['sample_temperature', 'sample_magnetic_field', 'polarization_config_label']
|
||||||
|
for key, log in self.data.device_logs.items():
|
||||||
|
if key not in RELEVANT_ITEMS:
|
||||||
|
continue
|
||||||
|
if log.value.dtype in [np.int8, np.int16, np.int32, np.int64]:
|
||||||
|
# for integer items (flags) report the most common one
|
||||||
|
value = np.bincount(log.value).argmax()
|
||||||
|
if logging.getLogger().getEffectiveLevel() <= logging.DEBUG \
|
||||||
|
and np.unique(log.value).shape[0]>1:
|
||||||
|
logging.debug(f' filtered values for {key} not unique, '
|
||||||
|
f'has {np.unique(log.value).shape[0]} values')
|
||||||
|
else:
|
||||||
|
value = log.value.mean()
|
||||||
|
if key == 'polarization_config_label':
|
||||||
|
self.instrument_settings.polarization = fileio.Polarization(const.polarizationConfigs[value])
|
||||||
|
elif key == 'sample_temperature':
|
||||||
|
self.sample.sample_parameters['temperature'].magnitue = value
|
||||||
|
elif key == 'sample_magnetic_field':
|
||||||
|
self.sample.sample_parameters['magnetic_field'].magnitue = value
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def read_chopper_trigger_stream(self, packets):
|
def read_chopper_trigger_stream(self, packets):
|
||||||
chopper1TriggerTime = np.array(self.hdf['entry1/Amor/chopper/ch2_trigger/event_time_zero'][:-2], dtype=np.int64)
|
chopper1TriggerTime = np.array(self.hdf['entry1/Amor/chopper/ch2_trigger/event_time_zero'][:-2], dtype=np.int64)
|
||||||
#self.chopper2TriggerTime = self.chopper1TriggerTime + np.array(self.hdf['entry1/Amor/chopper/ch2_trigger/event_time'][:-2], dtype=np.int64)
|
#self.chopper2TriggerTime = self.chopper1TriggerTime + np.array(self.hdf['entry1/Amor/chopper/ch2_trigger/event_time'][:-2], dtype=np.int64)
|
||||||
@@ -316,7 +440,7 @@ class AmorEventData(AmorHeader):
|
|||||||
startTime = chopper1TriggerTime[0]
|
startTime = chopper1TriggerTime[0]
|
||||||
pulseTimeS = chopper1TriggerTime
|
pulseTimeS = chopper1TriggerTime
|
||||||
else:
|
else:
|
||||||
logging.warn(' no chopper trigger data available, using event steram instead')
|
logging.critical(' No chopper trigger data available, using event steram instead, pulse filtering will fail!')
|
||||||
startTime = np.array(self.hdf['/entry1/Amor/detector/data/event_time_zero'][0], dtype=np.int64)
|
startTime = np.array(self.hdf['/entry1/Amor/detector/data/event_time_zero'][0], dtype=np.int64)
|
||||||
stopTime = np.array(self.hdf['/entry1/Amor/detector/data/event_time_zero'][-2], dtype=np.int64)
|
stopTime = np.array(self.hdf['/entry1/Amor/detector/data/event_time_zero'][-2], dtype=np.int64)
|
||||||
pulseTimeS = np.arange(startTime, stopTime, self.timing.tau*1e9, dtype=np.int64)
|
pulseTimeS = np.arange(startTime, stopTime, self.timing.tau*1e9, dtype=np.int64)
|
||||||
@@ -324,7 +448,7 @@ class AmorEventData(AmorHeader):
|
|||||||
pulses.time = pulseTimeS
|
pulses.time = pulseTimeS
|
||||||
pulses.monitor = 1. # default is monitor pulses as it requires no calculation
|
pulses.monitor = 1. # default is monitor pulses as it requires no calculation
|
||||||
# apply filter in case the events were filtered
|
# apply filter in case the events were filtered
|
||||||
if self.first_index>0 or not self.EOF:
|
if (self.first_index>0 or not self.EOF):
|
||||||
pulses = pulses[(pulses.time>=packets.time[0])&(pulses.time<=packets.time[-1])]
|
pulses = pulses[(pulses.time>=packets.time[0])&(pulses.time<=packets.time[-1])]
|
||||||
self.eventStartTime = startTime
|
self.eventStartTime = startTime
|
||||||
return pulses
|
return pulses
|
||||||
@@ -332,7 +456,11 @@ class AmorEventData(AmorHeader):
|
|||||||
def read_proton_current_stream(self, packets):
|
def read_proton_current_stream(self, packets):
|
||||||
proton_current = np.recarray(self.hdf['entry1/Amor/detector/proton_current/time'].shape, dtype=PC_TYPE)
|
proton_current = np.recarray(self.hdf['entry1/Amor/detector/proton_current/time'].shape, dtype=PC_TYPE)
|
||||||
proton_current.time = self.hdf['entry1/Amor/detector/proton_current/time'][:]
|
proton_current.time = self.hdf['entry1/Amor/detector/proton_current/time'][:]
|
||||||
proton_current.current = self.hdf['entry1/Amor/detector/proton_current/value'][:,0]
|
if self.hdf['entry1/Amor/detector/proton_current/value'].ndim==1:
|
||||||
|
proton_current.current = self.hdf['entry1/Amor/detector/proton_current/value'][:]
|
||||||
|
else:
|
||||||
|
proton_current.current = self.hdf['entry1/Amor/detector/proton_current/value'][:,0]
|
||||||
|
|
||||||
if self.first_index>0 or not self.EOF:
|
if self.first_index>0 or not self.EOF:
|
||||||
proton_current = proton_current[(proton_current.time>=packets.time[0])&
|
proton_current = proton_current[(proton_current.time>=packets.time[0])&
|
||||||
(proton_current.time<=packets.time[-1])]
|
(proton_current.time<=packets.time[-1])]
|
||||||
|
|||||||
@@ -2,9 +2,38 @@
|
|||||||
Helper functions used during calculations. Uses numba enhanced functions if available, otherwise numpy based
|
Helper functions used during calculations. Uses numba enhanced functions if available, otherwise numpy based
|
||||||
fallback is imported.
|
fallback is imported.
|
||||||
"""
|
"""
|
||||||
|
import numpy as np
|
||||||
|
from .event_data_types import EventDatasetProtocol, append_fields
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .helpers_numba import merge_frames, extract_walltime, filter_project_x, calculate_derived_properties_focussing
|
from .helpers_numba import merge_frames, extract_walltime, filter_project_x, \
|
||||||
|
calculate_derived_properties_focussing, merge_frames_w_index
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from .helpers_fallback import merge_frames, extract_walltime, filter_project_x, calculate_derived_properties_focussing
|
import logging
|
||||||
|
logging.warning('Cannot import numba enhanced functions, is it installed?')
|
||||||
|
from .helpers_fallback import merge_frames, extract_walltime, filter_project_x, \
|
||||||
|
calculate_derived_properties_focussing, merge_frames_w_index
|
||||||
|
|
||||||
|
def add_log_to_pulses(key, dataset: EventDatasetProtocol):
|
||||||
|
"""
|
||||||
|
Add a log value for each pulse to the pulses array.
|
||||||
|
"""
|
||||||
|
pulses = dataset.data.pulses
|
||||||
|
log_data = dataset.data.device_logs[key]
|
||||||
|
if log_data.time[0]>0:
|
||||||
|
logTimeS = np.hstack([[0], log_data.time, [pulses.time[-1]+1]])
|
||||||
|
logValues = np.hstack([[log_data.value[0]], log_data.value])
|
||||||
|
else:
|
||||||
|
logTimeS = np.hstack([log_data.time, [pulses.time[-1]+1]])
|
||||||
|
logValues = log_data.value
|
||||||
|
pulseLogS = np.zeros(pulses.time.shape[0], dtype=log_data.value.dtype)
|
||||||
|
j = 0
|
||||||
|
for i, ti in enumerate(pulses.time):
|
||||||
|
# find the last current item that was before this pulse
|
||||||
|
while ti>=logTimeS[j+1]:
|
||||||
|
j += 1
|
||||||
|
pulseLogS[i] = logValues[j]
|
||||||
|
pulses = append_fields(pulses, [(key, pulseLogS.dtype)])
|
||||||
|
pulses[key] = pulseLogS
|
||||||
|
dataset.data.pulses = pulses
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,17 @@ def merge_frames(tof_e, tofCut, tau, total_offset):
|
|||||||
# tof shifted to 1 frame
|
# tof shifted to 1 frame
|
||||||
return np.remainder(tof_e-(tofCut-tau), tau)+total_offset
|
return np.remainder(tof_e-(tofCut-tau), tau)+total_offset
|
||||||
|
|
||||||
|
def merge_frames_w_index(tof_e, tofCut, tau, total_offset):
|
||||||
|
"""
|
||||||
|
Version of merge frames that also returns a frame index for each pulse:
|
||||||
|
0 - belongs to the frame it was measured in
|
||||||
|
-1 - arrived in this frame but belongs to the previous neutron pulse
|
||||||
|
1 - belongs to the second neutron pulse of the original frame
|
||||||
|
"""
|
||||||
|
new_tof = merge_frames(tof_e, tofCut, tau, total_offset)
|
||||||
|
frame_idx = np.floor_divide(tof_e-tofCut, tau)
|
||||||
|
return new_tof, frame_idx
|
||||||
|
|
||||||
def extract_walltime(tof_e, dataPacket_p, dataPacketTime_p):
|
def extract_walltime(tof_e, dataPacket_p, dataPacketTime_p):
|
||||||
output = np.empty(np.shape(tof_e)[0], dtype=np.int64)
|
output = np.empty(np.shape(tof_e)[0], dtype=np.int64)
|
||||||
for i in range(len(dataPacket_p)-1):
|
for i in range(len(dataPacket_p)-1):
|
||||||
|
|||||||
@@ -11,6 +11,22 @@ def merge_frames(tof_e, tofCut, tau, total_offset):
|
|||||||
tof_e_out[ti] = ((tof_e[ti]-dt)%tau)+total_offset # tof shifted to 1 frame
|
tof_e_out[ti] = ((tof_e[ti]-dt)%tau)+total_offset # tof shifted to 1 frame
|
||||||
return tof_e_out
|
return tof_e_out
|
||||||
|
|
||||||
|
@nb.jit(nb.float64[:,:](nb.float64[:], nb.float64, nb.float64, nb.float64),
|
||||||
|
nopython=True, parallel=True, cache=True)
|
||||||
|
def merge_frames_w_index(tof_e, tofCut, tau, total_offset):
|
||||||
|
"""
|
||||||
|
Version of merge frames that also returns a frame index for each pulse:
|
||||||
|
0 - belongs to the frame it was measured in
|
||||||
|
-1 - arrived in this frame but belongs to the previous neutron pulse
|
||||||
|
1 - belongs to the second neutron pulse of the original frame
|
||||||
|
"""
|
||||||
|
tof_idx_out = np.empty((2, tof_e.shape[0]), dtype=np.float64)
|
||||||
|
dt = (tofCut-tau)
|
||||||
|
for ti in nb.prange(tof_e.shape[0]):
|
||||||
|
tof_idx_out[0, ti] = ((tof_e[ti]-dt)%tau)+total_offset # tof shifted to 1 frame
|
||||||
|
tof_idx_out[1, ti] = ((tof_e[ti]-tofCut) // tau) # tof shifted to 1 frame
|
||||||
|
return tof_idx_out
|
||||||
|
|
||||||
@nb.jit(nb.int64[:](nb.float64[:], nb.uint32[:], nb.int64[:]),
|
@nb.jit(nb.int64[:](nb.float64[:], nb.uint32[:], nb.int64[:]),
|
||||||
nopython=True, parallel=True, cache=True)
|
nopython=True, parallel=True, cache=True)
|
||||||
def extract_walltime(tof_e, dataPacket_p, dataPacketTime_p):
|
def extract_walltime(tof_e, dataPacket_p, dataPacketTime_p):
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ class ESSSerializer:
|
|||||||
)
|
)
|
||||||
self.producer.flush()
|
self.producer.flush()
|
||||||
if isinstance(command, Stop):
|
if isinstance(command, Stop):
|
||||||
if command.hist_id == self._active_histogram_yz:
|
if command.hist_id in [self._active_histogram_yz, self._active_histogram_tofz]:
|
||||||
self.count_stopped.set()
|
self.count_stopped.set()
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|||||||
54
eos/ls.py
Normal file
54
eos/ls.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"""
|
||||||
|
eosls executable script to list available datafiles in current folder with some metadata information.
|
||||||
|
|
||||||
|
Author: Jochen Stahn (algorithms, python draft),
|
||||||
|
Artur Glavic (structuring and optimisation of code)
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from eos.command_line import commandLineArgs
|
||||||
|
|
||||||
|
def main():
|
||||||
|
logging.getLogger().setLevel(logging.CRITICAL)
|
||||||
|
clas = commandLineArgs([], 'eosls', extra_args=[
|
||||||
|
dict(dest='path', nargs='*', default=['.'], help='paths to list file in')])
|
||||||
|
|
||||||
|
from glob import glob
|
||||||
|
import tabulate
|
||||||
|
from eos.file_reader import AmorHeader
|
||||||
|
|
||||||
|
files = []
|
||||||
|
for path in clas.path:
|
||||||
|
files+=glob(os.path.join(path, 'amor*.hdf'))
|
||||||
|
files.sort()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'File name': [],
|
||||||
|
'Start Time': [],
|
||||||
|
'mu': [],
|
||||||
|
'nu': [],
|
||||||
|
'div': [],
|
||||||
|
'Sample': [],
|
||||||
|
'T [K]': [],
|
||||||
|
'H [T]': [],
|
||||||
|
}
|
||||||
|
for fi in files:
|
||||||
|
data['File name'].append(os.path.basename(fi))
|
||||||
|
ah = AmorHeader(fi)
|
||||||
|
data['Sample'].append(ah.sample.name)
|
||||||
|
data['Start Time'].append(ah.fileDate.strftime('%y %m-%d %H:%M:%S'))
|
||||||
|
data['mu'].append('%.3f' % ah.geometry.mu)
|
||||||
|
data['nu'].append('%.3f' % ah.geometry.nu)
|
||||||
|
data['div'].append('%.3f' % ah.geometry.div)
|
||||||
|
|
||||||
|
T = ah.sample.sample_parameters.get('temperature', None)
|
||||||
|
data['T [K]'].append(T.magnitude if T is not None else '-')
|
||||||
|
|
||||||
|
H = ah.sample.sample_parameters.get('magnetic_field', None)
|
||||||
|
data['H [T]'].append(H.magnitude if H is not None else '-')
|
||||||
|
|
||||||
|
print(tabulate.tabulate(data, headers="keys"))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
events2histogram vizualising data from Amor@SINQ, PSI
|
amor-nicos vizualising data from Amor@SINQ, PSI
|
||||||
|
|
||||||
Author: Jochen Stahn (algorithms, python draft),
|
Author: Jochen Stahn (algorithms, python draft),
|
||||||
Artur Glavic (structuring and optimisation of code)
|
Artur Glavic (structuring and optimisation of code)
|
||||||
|
|||||||
60
eos/nicos_interface.py
Normal file
60
eos/nicos_interface.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"""
|
||||||
|
Functions used to directly access information from nicos.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import platform
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
ON_AMOR = platform.node().startswith('amor')
|
||||||
|
NICOS_CACHE_DIR = '/home/data/nicosdata/cache/'
|
||||||
|
GREP = '/usr/bin/grep "value"'
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_nicos_value(key, nicos_key, dtype=float, suffix='', year="2026"):
|
||||||
|
# TODO: Implement direct communication to nicos to read the cache
|
||||||
|
if nicos_key=='ignore':
|
||||||
|
return dtype(0)
|
||||||
|
try:
|
||||||
|
logging.debug(f' trying socket request for device {nicos_key}')
|
||||||
|
response = nicos_single_request(nicos_key)
|
||||||
|
logging.info(f" using parameter {nicos_key} from nicos cache via socket")
|
||||||
|
return dtype(response)
|
||||||
|
except Exception as e:
|
||||||
|
logging.debug(f' socket request failed with {e!r}')
|
||||||
|
if ON_AMOR:
|
||||||
|
logging.debug(f" trying to extract {nicos_key} from nicos cache files")
|
||||||
|
call = f'{GREP} {NICOS_CACHE_DIR}nicos-{nicos_key}/{year}{suffix}'
|
||||||
|
try:
|
||||||
|
value = str(subprocess.getoutput(call)).split('\t')[-1]
|
||||||
|
logging.info(f" using parameter {nicos_key} from nicos cache file")
|
||||||
|
return dtype(value)
|
||||||
|
except Exception:
|
||||||
|
logging.error(f" couldn't get value from nicos cache {nicos_key}, {call}")
|
||||||
|
return dtype(0)
|
||||||
|
else:
|
||||||
|
logging.warning(f" parameter {key} not found, relpace by zero")
|
||||||
|
return dtype(0)
|
||||||
|
|
||||||
|
def nicos_single_request(device):
|
||||||
|
sentinel = b'\n'
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
|
s.settimeout(1.0)
|
||||||
|
s.connect(('amor', 14869))
|
||||||
|
|
||||||
|
tosend = f'@nicos/{device}/value?\n'
|
||||||
|
|
||||||
|
# write request
|
||||||
|
# self.log.debug("get_explicit: sending %r", tosend)
|
||||||
|
s.sendall(tosend.encode())
|
||||||
|
|
||||||
|
# read response
|
||||||
|
data = b''
|
||||||
|
while not data.endswith(sentinel):
|
||||||
|
newdata = s.recv(8192) # blocking read
|
||||||
|
if not newdata:
|
||||||
|
raise IOError('cache closed connection')
|
||||||
|
data += newdata
|
||||||
|
s.shutdown(socket.SHUT_RDWR)
|
||||||
|
return data.decode('utf-8').split('=')[-1]
|
||||||
@@ -471,6 +471,25 @@ class ReflectivityReductionConfig(ArgParsable):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logfilter: List[str] = field(
|
||||||
|
default_factory=lambda: [],
|
||||||
|
metadata={
|
||||||
|
'short': 'lf',
|
||||||
|
'group': 'region of interest',
|
||||||
|
'help': 'filter using comparison to a log values, multpiple allowd (example "sample_temperature<150")',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
preciseTiming: bool = field(
|
||||||
|
default=False,
|
||||||
|
metadata={
|
||||||
|
'short': 'pt',
|
||||||
|
'group': 'data manicure',
|
||||||
|
'help': 're-assign events to actual neutron pulses => '
|
||||||
|
'2xbetter filter resolution but extra computation (experimental)',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class OutputFomatOption(StrEnum):
|
class OutputFomatOption(StrEnum):
|
||||||
Rqz_ort = "Rqz.ort"
|
Rqz_ort = "Rqz.ort"
|
||||||
@@ -534,6 +553,14 @@ class ReflectivityOutputConfig(ArgParsable):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
append: bool = field(
|
||||||
|
default=False,
|
||||||
|
metadata={
|
||||||
|
'group': 'output',
|
||||||
|
'help': 'if file already exists, append result as additional ORSO dataset (only Rqz.ort)',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def _output_format_list(self, outputFormat):
|
def _output_format_list(self, outputFormat):
|
||||||
format_list = []
|
format_list = []
|
||||||
if OutputFomatOption.ort in outputFormat\
|
if OutputFomatOption.ort in outputFormat\
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ class LZProjection(ProjectionInterface):
|
|||||||
# do not perform gravity correction for footprint, would require norm detector distance that is unknown here
|
# do not perform gravity correction for footprint, would require norm detector distance that is unknown here
|
||||||
fp_corr_lz = np.where(np.absolute(delta_lz+norm.angle)>5e-3,
|
fp_corr_lz = np.where(np.absolute(delta_lz+norm.angle)>5e-3,
|
||||||
(delta_lz+self.angle)/(delta_lz+norm.angle), np.nan)
|
(delta_lz+self.angle)/(delta_lz+norm.angle), np.nan)
|
||||||
|
fp_corr_lz[fp_corr_lz<0] = np.nan
|
||||||
self.data.mask &= np.logical_not(np.isnan(fp_corr_lz))
|
self.data.mask &= np.logical_not(np.isnan(fp_corr_lz))
|
||||||
self.data.norm = norm_lz*fp_corr_lz
|
self.data.norm = norm_lz*fp_corr_lz
|
||||||
self.norm_monitor = norm.monitor
|
self.norm_monitor = norm.monitor
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ class E2HReduction:
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
# check that events exist in the new file
|
# check that events exist in the new file
|
||||||
AmorEventData(new_files[-1], 0, max_events=1000)
|
AmorEventData(new_files[-1], 0, max_events=1_000)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.debug("Problem when trying to load new dataset", exc_info=True)
|
logging.debug("Problem when trying to load new dataset", exc_info=True)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import sys
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from orsopy import fileio
|
from orsopy import fileio
|
||||||
|
|
||||||
|
from .event_analysis import FilterByLog
|
||||||
|
from .event_handling import ApplyMask
|
||||||
from .file_reader import AmorEventData
|
from .file_reader import AmorEventData
|
||||||
from .header import Header
|
from .header import Header
|
||||||
from .path_handling import PathResolver
|
from .path_handling import PathResolver
|
||||||
@@ -63,12 +65,15 @@ class ReflectivityReduction:
|
|||||||
# the filtering only makes sense if using actual monitor data, not time
|
# the filtering only makes sense if using actual monitor data, not time
|
||||||
self.dataevent_actions |= eh.FilterMonitorThreshold(self.config.experiment.lowCurrentThreshold)
|
self.dataevent_actions |= eh.FilterMonitorThreshold(self.config.experiment.lowCurrentThreshold)
|
||||||
self.dataevent_actions |= eh.FilterStrangeTimes()
|
self.dataevent_actions |= eh.FilterStrangeTimes()
|
||||||
self.dataevent_actions |= ea.MergeFrames()
|
self.dataevent_actions |= ea.MergeFrames(preciseTime=self.config.reduction.preciseTiming)
|
||||||
self.dataevent_actions |= ea.AnalyzePixelIDs(self.config.experiment.yRange)
|
self.dataevent_actions |= ea.AnalyzePixelIDs(self.config.experiment.yRange)
|
||||||
self.dataevent_actions |= eh.TofTimeCorrection(self.config.experiment.incidentAngle==IncidentAngle.alphaF)
|
self.dataevent_actions |= eh.TofTimeCorrection(self.config.experiment.incidentAngle==IncidentAngle.alphaF)
|
||||||
self.dataevent_actions |= ea.CalculateWavelength(self.config.experiment.lambdaRange)
|
self.dataevent_actions |= ea.CalculateWavelength(self.config.experiment.lambdaRange)
|
||||||
self.dataevent_actions |= ea.CalculateQ(self.config.experiment.incidentAngle)
|
self.dataevent_actions |= ea.CalculateQ(self.config.experiment.incidentAngle)
|
||||||
self.dataevent_actions |= ea.FilterQzRange(self.config.reduction.qzRange)
|
if not self.config.reduction.is_default('qzRange'):
|
||||||
|
self.dataevent_actions |= ea.FilterQzRange(self.config.reduction.qzRange)
|
||||||
|
for lf in self.config.reduction.logfilter:
|
||||||
|
self.dataevent_actions |= ea.FilterByLog(lf)
|
||||||
self.dataevent_actions |= eh.ApplyMask()
|
self.dataevent_actions |= eh.ApplyMask()
|
||||||
|
|
||||||
self.grid = LZGrid(self.config.reduction.qResolution, self.config.reduction.qzRange)
|
self.grid = LZGrid(self.config.reduction.qResolution, self.config.reduction.qzRange)
|
||||||
@@ -106,7 +111,7 @@ class ReflectivityReduction:
|
|||||||
# output
|
# output
|
||||||
if self.config.output.is_default('outputName'):
|
if self.config.output.is_default('outputName'):
|
||||||
import datetime
|
import datetime
|
||||||
_date = datetime.datetime.now().replace(microsecond=0).isoformat()
|
_date = datetime.datetime.now().replace(microsecond=0).isoformat().replace(':', '-')
|
||||||
if self.header.sample.name:
|
if self.header.sample.name:
|
||||||
_sampleName = self.header.sample.name.replace(' ', '_')
|
_sampleName = self.header.sample.name.replace(' ', '_')
|
||||||
else:
|
else:
|
||||||
@@ -162,7 +167,31 @@ class ReflectivityReduction:
|
|||||||
self.header.measurement_data_files.append(fileio.File( file=os.path.basename(fileName),
|
self.header.measurement_data_files.append(fileio.File( file=os.path.basename(fileName),
|
||||||
timestamp=self.dataset.fileDate))
|
timestamp=self.dataset.fileDate))
|
||||||
|
|
||||||
|
if 'polarization_config_label' in self.dataset.data.device_logs:
|
||||||
|
pols = np.unique(self.dataset.data.device_logs['polarization_config_label'].value)
|
||||||
|
pols = pols[pols>0]
|
||||||
|
if len(pols)>1:
|
||||||
|
logging.warning(f' found {len(pols)} polarization configurations, splitting dataset accordingly')
|
||||||
|
from copy import deepcopy
|
||||||
|
from . import const
|
||||||
|
full_ds = deepcopy(self.dataset)
|
||||||
|
for pi in pols:
|
||||||
|
plabel = const.polarizationLabels[pi]
|
||||||
|
pol_filter = FilterByLog(f'polarization_config_label=={pi}',
|
||||||
|
remove_switchpulse=True) | ApplyMask()
|
||||||
|
logging.info(f' filter {plabel} using polarization_config_label=={pi}')
|
||||||
|
pol_filter(self.dataset)
|
||||||
|
self.dataset.update_header(self.header)
|
||||||
|
pol_filter.update_header(self.header)
|
||||||
|
if self.config.reduction.timeSlize:
|
||||||
|
if i>0:
|
||||||
|
logging.warning(
|
||||||
|
" time slizing should only be used for one set of datafiles, check parameters")
|
||||||
|
self.analyze_timeslices(i, polstr=f' : polarization = {plabel}')
|
||||||
|
else:
|
||||||
|
self.analyze_unsliced(i, polstr=f' : polarization = {plabel}')
|
||||||
|
self.dataset = deepcopy(full_ds)
|
||||||
|
return
|
||||||
if self.config.reduction.timeSlize:
|
if self.config.reduction.timeSlize:
|
||||||
if i>0:
|
if i>0:
|
||||||
logging.warning(" time slizing should only be used for one set of datafiles, check parameters")
|
logging.warning(" time slizing should only be used for one set of datafiles, check parameters")
|
||||||
@@ -170,7 +199,7 @@ class ReflectivityReduction:
|
|||||||
else:
|
else:
|
||||||
self.analyze_unsliced(i)
|
self.analyze_unsliced(i)
|
||||||
|
|
||||||
def analyze_unsliced(self, i):
|
def analyze_unsliced(self, i, polstr=''):
|
||||||
self.monitor = self.dataset.data.pulses.monitor.sum()
|
self.monitor = self.dataset.data.pulses.monitor.sum()
|
||||||
logging.info(f' monitor = {self.monitor:8.2f} {MONITOR_UNITS[self.config.experiment.monitorType]}')
|
logging.info(f' monitor = {self.monitor:8.2f} {MONITOR_UNITS[self.config.experiment.monitorType]}')
|
||||||
|
|
||||||
@@ -183,7 +212,7 @@ class ReflectivityReduction:
|
|||||||
|
|
||||||
if 'Rqz.ort' in self.config.output.outputFormats:
|
if 'Rqz.ort' in self.config.output.outputFormats:
|
||||||
headerRqz = self.header.orso_header()
|
headerRqz = self.header.orso_header()
|
||||||
headerRqz.data_set = f'Nr {i} : mu = {self.dataset.geometry.mu:6.3f} deg'
|
headerRqz.data_set = f'Nr {i} : mu = {self.dataset.geometry.mu:6.3f} deg{polstr}'
|
||||||
|
|
||||||
# projection on qz-grid
|
# projection on qz-grid
|
||||||
result = proj.project_on_qz()
|
result = proj.project_on_qz()
|
||||||
@@ -258,7 +287,7 @@ class ReflectivityReduction:
|
|||||||
proj.plot(colorbar=True, cmap=str(self.config.output.plot_colormap))
|
proj.plot(colorbar=True, cmap=str(self.config.output.plot_colormap))
|
||||||
plt.title(f'{self.config.reduction.fileIdentifier[i]}')
|
plt.title(f'{self.config.reduction.fileIdentifier[i]}')
|
||||||
|
|
||||||
def analyze_timeslices(self, i):
|
def analyze_timeslices(self, i, polstr=''):
|
||||||
wallTime_e = np.float64(self.dataset.data.events.wallTime)/1e9
|
wallTime_e = np.float64(self.dataset.data.events.wallTime)/1e9
|
||||||
pulseTimeS = np.float64(self.dataset.data.pulses.time)/1e9
|
pulseTimeS = np.float64(self.dataset.data.pulses.time)/1e9
|
||||||
interval = self.config.reduction.timeSlize[0]
|
interval = self.config.reduction.timeSlize[0]
|
||||||
@@ -308,7 +337,7 @@ class ReflectivityReduction:
|
|||||||
|
|
||||||
headerRqz = self.header.orso_header(
|
headerRqz = self.header.orso_header(
|
||||||
extra_columns=[fileio.Column('time', 's', 'time relative to start of measurement series')])
|
extra_columns=[fileio.Column('time', 's', 'time relative to start of measurement series')])
|
||||||
headerRqz.data_set = f'{i}_{ti}: time = {time:8.1f} s to {time+interval:8.1f} s'
|
headerRqz.data_set = f'{i}_{ti}: time = {time:8.1f} s to {time+interval:8.1f} s{polstr}'
|
||||||
orso_data = fileio.OrsoDataset(headerRqz, result.data_for_time(time))
|
orso_data = fileio.OrsoDataset(headerRqz, result.data_for_time(time))
|
||||||
self.datasetsRqz.append(orso_data)
|
self.datasetsRqz.append(orso_data)
|
||||||
|
|
||||||
@@ -326,8 +355,23 @@ class ReflectivityReduction:
|
|||||||
def save_Rqz(self):
|
def save_Rqz(self):
|
||||||
fname = os.path.join(self.config.output.outputPath, f'{self.config.output.outputName}.Rqz.ort')
|
fname = os.path.join(self.config.output.outputPath, f'{self.config.output.outputName}.Rqz.ort')
|
||||||
logging.warning(f' {fname}')
|
logging.warning(f' {fname}')
|
||||||
theSecondLine = f' {self.header.experiment.title} | {self.header.experiment.start_date} | sample {self.header.sample.name} | R(q_z)'
|
if os.path.exists(fname) and self.config.output.append:
|
||||||
fileio.save_orso(self.datasetsRqz, fname, data_separator='\n', comment=theSecondLine)
|
logging.info(' file already exists, append as new dataset')
|
||||||
|
with open(fname, 'r') as f:
|
||||||
|
f.readline()
|
||||||
|
theSecondLine = f.readline()[3:]
|
||||||
|
prev_data = fileio.load_orso(fname)
|
||||||
|
prev_names = [di.info.data_set for di in prev_data]
|
||||||
|
for i, di in enumerate(self.datasetsRqz):
|
||||||
|
while di.info.data_set in prev_names:
|
||||||
|
if di.info.data_set.startswith('Nr '):
|
||||||
|
di.info.data_set = f'Nr {i+len(prev_data)} :'+di.info.data_set.split(':', 1)[1]
|
||||||
|
break
|
||||||
|
di.info.data_set = di.info.data_set+'_'
|
||||||
|
fileio.save_orso(prev_data+self.datasetsRqz, fname, data_separator='\n', comment=theSecondLine)
|
||||||
|
else:
|
||||||
|
theSecondLine = f' {self.header.experiment.title} | {self.header.experiment.start_date} | sample {self.header.sample.name} | R(q_z)'
|
||||||
|
fileio.save_orso(self.datasetsRqz, fname, data_separator='\n', comment=theSecondLine)
|
||||||
|
|
||||||
def save_Rtl(self):
|
def save_Rtl(self):
|
||||||
fname = os.path.join(self.config.output.outputPath, f'{self.config.output.outputName}.Rlt.ort')
|
fname = os.path.join(self.config.output.outputPath, f'{self.config.output.outputName}.Rlt.ort')
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ h5py
|
|||||||
orsopy
|
orsopy
|
||||||
numba
|
numba
|
||||||
matplotlib
|
matplotlib
|
||||||
|
tabulate
|
||||||
backports.strenum; python_version<"3.11"
|
backports.strenum; python_version<"3.11"
|
||||||
backports.zoneinfo; python_version<"3.9"
|
backports.zoneinfo; python_version<"3.9"
|
||||||
|
|||||||
@@ -34,5 +34,6 @@ Homepage = "https://github.com/jochenstahn/amor"
|
|||||||
[options.entry_points]
|
[options.entry_points]
|
||||||
console_scripts =
|
console_scripts =
|
||||||
eos = eos.__main__:main
|
eos = eos.__main__:main
|
||||||
|
eosls = eos.ls:main
|
||||||
events2histogram = eos.e2h:main
|
events2histogram = eos.e2h:main
|
||||||
amor-nicos = eos.nicos:main
|
amor-nicos = eos.nicos:main
|
||||||
|
|||||||
BIN
test_data/amor2026n000826.hdf
LFS
Normal file
BIN
test_data/amor2026n000826.hdf
LFS
Normal file
Binary file not shown.
572
tests/test_event_handling.py
Normal file
572
tests/test_event_handling.py
Normal file
@@ -0,0 +1,572 @@
|
|||||||
|
import os
|
||||||
|
import numpy as np
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from unittest import TestCase
|
||||||
|
from datetime import datetime
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from orsopy.fileio import Person, Experiment, Sample, InstrumentSettings, Value, ValueRange, Polarization
|
||||||
|
|
||||||
|
from eos import const
|
||||||
|
from eos.header import Header
|
||||||
|
from eos.event_data_types import EVENT_BITMASKS, AmorGeometry, AmorTiming, AmorEventStream, \
|
||||||
|
EventDataAction, EventDatasetProtocol, PACKET_TYPE, PC_TYPE, PULSE_TYPE, EVENT_TYPE, append_fields
|
||||||
|
from eos.event_handling import ApplyPhaseOffset, ApplyParameterOverwrites, CorrectChopperPhase, CorrectSeriesTime, \
|
||||||
|
AssociatePulseWithMonitor, FilterMonitorThreshold, FilterStrangeTimes, TofTimeCorrection, ApplyMask
|
||||||
|
from eos.event_analysis import ExtractWalltime, MergeFrames, AnalyzePixelIDs, CalculateWavelength, CalculateQ, \
|
||||||
|
FilterQzRange, FilterByLog
|
||||||
|
from eos.options import MonitorType, IncidentAngle, ExperimentConfig
|
||||||
|
|
||||||
|
|
||||||
|
class MockEventData:
|
||||||
|
"""
|
||||||
|
Simulated dataset to be used with event handling unit tests
|
||||||
|
"""
|
||||||
|
geometry: AmorGeometry
|
||||||
|
timing: AmorTiming
|
||||||
|
data: AmorEventStream
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.geometry = AmorGeometry(mu=2.0, nu=1.0, kap=0.5, kad=0.0, div=1.5,
|
||||||
|
chopperSeparation=1000.0, detectorDistance=4000., chopperDetectorDistance=18842.)
|
||||||
|
self.timing = AmorTiming(
|
||||||
|
ch1TriggerPhase=-9.1, ch2TriggerPhase=6.75,
|
||||||
|
chopperPhase=0.17, chopperSpeed=500., tau=0.06
|
||||||
|
)
|
||||||
|
self.create_data()
|
||||||
|
|
||||||
|
def create_data(self):
|
||||||
|
# list of events, here with random time of fligh and pixel location
|
||||||
|
events = np.recarray((10000, ), dtype=EVENT_TYPE)
|
||||||
|
events.tof = np.random.uniform(low=0., high=0.12, size=events.shape)
|
||||||
|
events.pixelID = np.random.randint(0, 28671, size=events.shape)
|
||||||
|
events.mask = 0
|
||||||
|
|
||||||
|
# list of data packates containing previous events
|
||||||
|
packets = np.recarray((1000,), dtype=PACKET_TYPE)
|
||||||
|
packets.start_index = np.linspace(0, events.shape[0]-1, packets.shape[0], dtype=np.uint32)
|
||||||
|
packets.time = np.linspace(1700000000000000000, 1700000000000000000+3_600_000_000_000,
|
||||||
|
packets.shape[0], dtype=np.int64)
|
||||||
|
|
||||||
|
# chopper pulses within the measurement time
|
||||||
|
pulses = np.recarray((packets.shape[0],), dtype=PULSE_TYPE)
|
||||||
|
pulses.monitor = 1.0
|
||||||
|
pulses.time = packets.time
|
||||||
|
|
||||||
|
# proton current information with independent timing
|
||||||
|
proton_current = np.recarray((50,), dtype=PC_TYPE)
|
||||||
|
proton_current.current = 1500.0
|
||||||
|
proton_current[np.random.randint(0, proton_current.shape[0]-1, 10)] = 0. # random time with no current
|
||||||
|
proton_current.time = np.linspace(1700000000000000300, 1700000000000000000+3_600_000_000_000,
|
||||||
|
proton_current.shape[0], dtype=np.int64)
|
||||||
|
|
||||||
|
self.data = AmorEventStream(events, packets, pulses, proton_current)
|
||||||
|
self.orig_data = deepcopy(self.data)
|
||||||
|
|
||||||
|
|
||||||
|
def append(self, other):
|
||||||
|
raise NotImplementedError("Just for testing, no append")
|
||||||
|
|
||||||
|
def update_header(self, header:Header):
|
||||||
|
# update a header with the information read from file
|
||||||
|
header.owner = Person(name="test user", affiliation='PSI')
|
||||||
|
header.experiment = Experiment(title='test experiment', instrument='amor',
|
||||||
|
start_date=datetime.now(), probe="neutron")
|
||||||
|
header.sample = Sample(name='test sample')
|
||||||
|
header.measurement_instrument_settings = InstrumentSettings(incident_angle=Value(1.5, 'deg'),
|
||||||
|
wavelength = ValueRange(3.0, 12.5, 'angstrom'),
|
||||||
|
polarization = Polarization.unpolarized)
|
||||||
|
|
||||||
|
def update_info_from_logs(self):
|
||||||
|
RELEVANT_ITEMS = ['sample_temperature', 'sample_magnetic_field', 'polarization_config_label']
|
||||||
|
for key, log in self.data.device_logs.items():
|
||||||
|
if key not in RELEVANT_ITEMS:
|
||||||
|
continue
|
||||||
|
if log.value.dtype in [np.int8, np.int16, np.int32, np.int64]:
|
||||||
|
# for integer items (flags) report the most common one
|
||||||
|
value = np.bincount(log.value).argmax()
|
||||||
|
if logging.getLogger().getEffectiveLevel() <= logging.DEBUG \
|
||||||
|
and np.unique(log.value).shape[0]>1:
|
||||||
|
logging.debug(f' filtered values for {key} not unique, '
|
||||||
|
f'has {np.unique(log.value).shape[0]} values')
|
||||||
|
else:
|
||||||
|
value = log.value.mean()
|
||||||
|
if key == 'polarization_config_label':
|
||||||
|
self.instrument_settings.polarization = Polarization(const.polarizationConfigs[value])
|
||||||
|
elif key == 'sample_temperature':
|
||||||
|
self.sample.sample_parameters['temperature'].magnitue = value
|
||||||
|
elif key == 'sample_magnetic_field':
|
||||||
|
self.sample.sample_parameters['magnetic_field'].magnitue = value
|
||||||
|
|
||||||
|
|
||||||
|
class TestActionClass(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
"""
|
||||||
|
Create test classes to be used
|
||||||
|
"""
|
||||||
|
class T1(EventDataAction):
|
||||||
|
def perform_action(self, event: EventDatasetProtocol):
|
||||||
|
event.data.events.mask += 1
|
||||||
|
class T2(EventDataAction):
|
||||||
|
def perform_action(self, event: EventDatasetProtocol):
|
||||||
|
event.data.events.mask += 2
|
||||||
|
class T4(EventDataAction):
|
||||||
|
def perform_action(self, event: EventDatasetProtocol):
|
||||||
|
event.data.events.mask += 4
|
||||||
|
cls.T1=T1; cls.T2=T2; cls.T4=T4
|
||||||
|
|
||||||
|
class H1(EventDataAction):
|
||||||
|
def perform_action(self, event: EventDatasetProtocol):
|
||||||
|
...
|
||||||
|
def update_header(self, header:Header) ->None:
|
||||||
|
header.sample.name = 'h1'
|
||||||
|
class H2(EventDataAction):
|
||||||
|
def perform_action(self, event: EventDatasetProtocol):
|
||||||
|
...
|
||||||
|
def update_header(self, header: Header) -> None:
|
||||||
|
header.sample.name = 'h2'
|
||||||
|
class HN(EventDataAction):
|
||||||
|
def __init__(self, n):
|
||||||
|
self._n = n
|
||||||
|
def perform_action(self, event: EventDatasetProtocol):
|
||||||
|
...
|
||||||
|
def update_header(self, header: Header) -> None:
|
||||||
|
header.sample.name = self._n
|
||||||
|
cls.H1=H1; cls.H2=H2; cls.HN = HN
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.d = MockEventData()
|
||||||
|
self.header = Header()
|
||||||
|
self.d.update_header(self.header)
|
||||||
|
|
||||||
|
def test_individual(self):
|
||||||
|
t1 = self.T1()
|
||||||
|
t2 = self.T2()
|
||||||
|
t4 = self.T4()
|
||||||
|
|
||||||
|
np.testing.assert_array_equal(self.d.data.events.mask, 0)
|
||||||
|
t1.perform_action(self.d)
|
||||||
|
np.testing.assert_array_equal(self.d.data.events.mask, 1)
|
||||||
|
t2.perform_action(self.d)
|
||||||
|
np.testing.assert_array_equal(self.d.data.events.mask, 3)
|
||||||
|
t4.perform_action(self.d)
|
||||||
|
np.testing.assert_array_equal(self.d.data.events.mask, 7)
|
||||||
|
|
||||||
|
def test_header(self):
|
||||||
|
h1 = self.H1()
|
||||||
|
h2 = self.H2()
|
||||||
|
h3 = self.HN('h3')
|
||||||
|
h4 = self.HN('h4')
|
||||||
|
|
||||||
|
h1.update_header(self.header)
|
||||||
|
self.assertEqual(self.header.sample.name, 'h1')
|
||||||
|
h2.update_header(self.header)
|
||||||
|
self.assertEqual(self.header.sample.name, 'h2')
|
||||||
|
h3.update_header(self.header)
|
||||||
|
self.assertEqual(self.header.sample.name, 'h3')
|
||||||
|
h4.update_header(self.header)
|
||||||
|
self.assertEqual(self.header.sample.name, 'h4')
|
||||||
|
|
||||||
|
def test_combination(self):
|
||||||
|
t1 = self.T1()
|
||||||
|
t2 = self.T2()
|
||||||
|
t4 = self.T4()
|
||||||
|
t12 = t1 | t2
|
||||||
|
t24 = t2 | t4
|
||||||
|
t1224 = t1 | t2 | t2 | t4
|
||||||
|
t1224b = t12 | t24
|
||||||
|
|
||||||
|
np.testing.assert_array_equal(self.d.data.events.mask, 0)
|
||||||
|
t12.perform_action(self.d)
|
||||||
|
np.testing.assert_array_equal(self.d.data.events.mask, 3)
|
||||||
|
t24.perform_action(self.d)
|
||||||
|
np.testing.assert_array_equal(self.d.data.events.mask, 9)
|
||||||
|
|
||||||
|
t1224.perform_action(self.d)
|
||||||
|
np.testing.assert_array_equal(self.d.data.events.mask, 18)
|
||||||
|
t1224b.perform_action(self.d)
|
||||||
|
np.testing.assert_array_equal(self.d.data.events.mask, 27)
|
||||||
|
|
||||||
|
|
||||||
|
def test_combine_header(self):
|
||||||
|
h1 = self.H1()
|
||||||
|
h2 = self.H2()
|
||||||
|
h3 = self.HN('h3')
|
||||||
|
h4 = self.HN('h4')
|
||||||
|
|
||||||
|
(h1|h2).update_header(self.header)
|
||||||
|
self.assertEqual(self.header.sample.name, 'h2')
|
||||||
|
(h2|h1).update_header(self.header)
|
||||||
|
self.assertEqual(self.header.sample.name, 'h1')
|
||||||
|
(h3|h4).update_header(self.header)
|
||||||
|
self.assertEqual(self.header.sample.name, 'h4')
|
||||||
|
(h4|h3).update_header(self.header)
|
||||||
|
self.assertEqual(self.header.sample.name, 'h3')
|
||||||
|
|
||||||
|
def test_abstract_misssing(self):
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
class E(EventDataAction):
|
||||||
|
...
|
||||||
|
_ = E()
|
||||||
|
|
||||||
|
def test_hash(self):
|
||||||
|
"""
|
||||||
|
Check that hashes of different actions are different but
|
||||||
|
instances of same action have same hash
|
||||||
|
"""
|
||||||
|
t1 = self.T1()
|
||||||
|
t1b = self.T1()
|
||||||
|
t2 = self.T2()
|
||||||
|
t4 = self.T4()
|
||||||
|
h3 = self.HN('h3')
|
||||||
|
h3b = self.HN('h3')
|
||||||
|
h4 = self.HN('h4')
|
||||||
|
|
||||||
|
self.assertNotEqual(t1.action_hash(), t2.action_hash())
|
||||||
|
self.assertNotEqual(t2.action_hash(), t4.action_hash())
|
||||||
|
self.assertNotEqual(t1.action_hash(), t4.action_hash())
|
||||||
|
self.assertNotEqual(h3.action_hash(), h4.action_hash())
|
||||||
|
self.assertEqual(t1.action_hash(), t1b.action_hash())
|
||||||
|
self.assertEqual(h3.action_hash(), h3b.action_hash())
|
||||||
|
|
||||||
|
|
||||||
|
class TestSimpleActions(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.d = MockEventData()
|
||||||
|
self.header = Header()
|
||||||
|
self.d.update_header(self.header)
|
||||||
|
|
||||||
|
def test_chopper_phase(self):
|
||||||
|
cp = CorrectChopperPhase()
|
||||||
|
cp.perform_action(self.d)
|
||||||
|
np.testing.assert_array_equal(
|
||||||
|
self.d.data.events.tof,
|
||||||
|
self.d.orig_data.events.tof+
|
||||||
|
self.d.timing.tau*(self.d.timing.ch1TriggerPhase-self.d.timing.chopperPhase/2)/180
|
||||||
|
)
|
||||||
|
|
||||||
|
def _extract_walltime(self):
|
||||||
|
# Extract wall time for events and orig copy
|
||||||
|
wt = ExtractWalltime()
|
||||||
|
d = self.d.data
|
||||||
|
self.d.data = self.d.orig_data
|
||||||
|
wt.perform_action(self.d)
|
||||||
|
self.d.data = d
|
||||||
|
wt.perform_action(self.d)
|
||||||
|
|
||||||
|
def test_extract_walltime(self):
|
||||||
|
self._extract_walltime()
|
||||||
|
# wallTime should be always a time present in the packet times
|
||||||
|
np.testing.assert_array_equal(np.isin(self.d.data.events.wallTime, self.d.data.packets.time), True)
|
||||||
|
# make sure extraction works on both original and copy
|
||||||
|
np.testing.assert_array_equal(self.d.data.events.wallTime, self.d.orig_data.events.wallTime)
|
||||||
|
|
||||||
|
def test_series_time(self):
|
||||||
|
corr = 100
|
||||||
|
ct = CorrectSeriesTime(corr)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
ct.perform_action(self.d)
|
||||||
|
|
||||||
|
self._extract_walltime()
|
||||||
|
|
||||||
|
|
||||||
|
ct.perform_action(self.d)
|
||||||
|
np.testing.assert_array_equal(
|
||||||
|
self.d.data.pulses.time,
|
||||||
|
self.d.orig_data.pulses.time-corr
|
||||||
|
)
|
||||||
|
np.testing.assert_array_equal(
|
||||||
|
self.d.data.events.wallTime,
|
||||||
|
self.d.orig_data.events.wallTime-corr
|
||||||
|
)
|
||||||
|
np.testing.assert_array_equal(
|
||||||
|
self.d.data.proton_current.time,
|
||||||
|
self.d.orig_data.proton_current.time-corr
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_associate_monitor(self):
|
||||||
|
amPC = AssociatePulseWithMonitor(MonitorType.proton_charge)
|
||||||
|
amT = AssociatePulseWithMonitor(MonitorType.time)
|
||||||
|
amN = AssociatePulseWithMonitor(MonitorType.neutron_monitor)
|
||||||
|
|
||||||
|
self.d.data.pulses.monitor = 13
|
||||||
|
amN.perform_action(self.d)
|
||||||
|
np.testing.assert_array_equal(self.d.data.pulses.monitor, 1)
|
||||||
|
|
||||||
|
self.d.data.pulses.monitor = 13
|
||||||
|
amT.perform_action(self.d)
|
||||||
|
np.testing.assert_array_equal(self.d.data.pulses.monitor, np.float32(2*self.d.timing.tau))
|
||||||
|
|
||||||
|
self.d.data.pulses.monitor = 13
|
||||||
|
amPC.perform_action(self.d)
|
||||||
|
pcm = self.d.data.proton_current.current *2*self.d.timing.tau*1e-3
|
||||||
|
np.testing.assert_array_equal(np.isin(self.d.data.pulses.monitor, pcm), True)
|
||||||
|
|
||||||
|
def test_filter_monitor_threashold(self):
|
||||||
|
amPC = AssociatePulseWithMonitor(MonitorType.proton_charge)
|
||||||
|
fmt = amPC | FilterMonitorThreshold(1000.)
|
||||||
|
fma = amPC | FilterMonitorThreshold(2000.)
|
||||||
|
fm0 = amPC | FilterMonitorThreshold(-1.0)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
fmt.perform_action(self.d)
|
||||||
|
|
||||||
|
self._extract_walltime()
|
||||||
|
fm0.perform_action(self.d)
|
||||||
|
self.assertEqual(self.d.data.events.mask.sum(), 0)
|
||||||
|
fmt.perform_action(self.d)
|
||||||
|
# calculate, which events should have 0 monitor
|
||||||
|
zero_times = self.d.data.pulses.time[self.d.data.pulses.monitor==0]
|
||||||
|
zero_sum = np.isin(self.d.data.events.wallTime, zero_times).sum()
|
||||||
|
self.assertEqual(self.d.data.events.mask.sum(), zero_sum*EVENT_BITMASKS['MonitorThreshold'])
|
||||||
|
# filter all events
|
||||||
|
self.d.data.events.mask = 0
|
||||||
|
fma.perform_action(self.d)
|
||||||
|
self.assertEqual(self.d.data.events.mask.sum(), self.d.data.events.shape[0]*EVENT_BITMASKS['MonitorThreshold'])
|
||||||
|
|
||||||
|
def test_filter_strage_times(self):
|
||||||
|
st = FilterStrangeTimes()
|
||||||
|
|
||||||
|
st.perform_action(self.d)
|
||||||
|
self.assertEqual(self.d.data.events.mask.sum(), 0)
|
||||||
|
|
||||||
|
# half events should be strange times (outside of ToF frame)
|
||||||
|
self.d.data.events.tof += self.d.timing.tau
|
||||||
|
st.perform_action(self.d)
|
||||||
|
self.assertEqual(self.d.data.events.mask.sum(),
|
||||||
|
(self.d.data.events.tof>2*self.d.timing.tau).sum()*EVENT_BITMASKS['StrangeTimes'])
|
||||||
|
|
||||||
|
def test_apply_phase_offset(self):
|
||||||
|
action = ApplyPhaseOffset(12.5)
|
||||||
|
action.perform_action(self.d)
|
||||||
|
self.assertEqual(self.d.timing.ch1TriggerPhase, 12.5)
|
||||||
|
|
||||||
|
def test_apply_parameter_overwrites(self):
|
||||||
|
action = ApplyParameterOverwrites(ExperimentConfig(muOffset=0.25, mu=3.5, nu=4.5))
|
||||||
|
action.perform_action(self.d)
|
||||||
|
self.assertEqual(self.d.geometry.mu, 3.5)
|
||||||
|
self.assertEqual(self.d.geometry.nu, 4.5)
|
||||||
|
|
||||||
|
action = ApplyParameterOverwrites(ExperimentConfig(muOffset=0.25))
|
||||||
|
action.perform_action(self.d)
|
||||||
|
self.assertEqual(self.d.geometry.mu, 3.75)
|
||||||
|
|
||||||
|
action = ApplyParameterOverwrites(ExperimentConfig(sampleModel='air | Si | Fe'))
|
||||||
|
action.update_header(self.header)
|
||||||
|
self.assertIsNotNone(self.header.sample.model)
|
||||||
|
|
||||||
|
def test_apply_sample_model_file(self):
|
||||||
|
if os.path.isfile('test.yaml'):
|
||||||
|
os.remove('test.yaml')
|
||||||
|
action = ApplyParameterOverwrites(ExperimentConfig(sampleModel='test.yaml'))
|
||||||
|
action.update_header(self.header)
|
||||||
|
self.assertIsNone(self.header.sample.model)
|
||||||
|
|
||||||
|
with open('test.yaml', 'w') as fh:
|
||||||
|
fh.write("""stack: air | Si | Fe""")
|
||||||
|
|
||||||
|
try:
|
||||||
|
action = ApplyParameterOverwrites(ExperimentConfig(sampleModel='test.yaml'))
|
||||||
|
action.update_header(self.header)
|
||||||
|
self.assertEqual(self.header.sample.model.stack, 'air | Si | Fe')
|
||||||
|
finally:
|
||||||
|
os.remove('test.yaml')
|
||||||
|
|
||||||
|
def test_tof_time_correction(self):
|
||||||
|
action = TofTimeCorrection()
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
action.perform_action(self.d)
|
||||||
|
|
||||||
|
new_events = append_fields(self.d.data.events, [('delta', np.float64)])
|
||||||
|
new_events.delta = 10.0
|
||||||
|
self.d.data.events = new_events
|
||||||
|
tof_before = self.d.data.events.tof.copy()
|
||||||
|
action.perform_action(self.d)
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
self.d.data.events.tof,
|
||||||
|
tof_before - (10.0 / 180.0) * self.d.timing.tau
|
||||||
|
)
|
||||||
|
|
||||||
|
self.d.create_data()
|
||||||
|
new_events = append_fields(self.d.data.events, [('delta', np.float64)])
|
||||||
|
new_events.delta = 10.0
|
||||||
|
self.d.data.events = new_events
|
||||||
|
tof_before = self.d.data.events.tof.copy()
|
||||||
|
action = TofTimeCorrection(correct_chopper_opening=False)
|
||||||
|
action.perform_action(self.d)
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
self.d.data.events.tof,
|
||||||
|
tof_before - (self.d.geometry.kad / 180.0) * self.d.timing.tau
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_apply_mask(self):
|
||||||
|
self.d.data.events = self.d.data.events[:6].copy()
|
||||||
|
self.d.data.events.mask[:] = [0, 1, 2, 3, 4, 5]
|
||||||
|
|
||||||
|
action = ApplyMask()
|
||||||
|
action.perform_action(self.d)
|
||||||
|
self.assertEqual(self.d.data.events.shape[0], 1)
|
||||||
|
self.assertEqual(self.d.data.events.mask[0], 0)
|
||||||
|
|
||||||
|
self.d.create_data()
|
||||||
|
self.d.data.events = self.d.data.events[:6].copy()
|
||||||
|
self.d.data.events.mask[:] = [0, 1, 2, 3, 4, 5]
|
||||||
|
action = ApplyMask(bitmask_filter=EVENT_BITMASKS['MonitorThreshold'])
|
||||||
|
action.perform_action(self.d)
|
||||||
|
np.testing.assert_array_equal(self.d.data.events.mask, np.array([0, EVENT_BITMASKS['MonitorThreshold']],
|
||||||
|
dtype=np.int32))
|
||||||
|
|
||||||
|
def test_merge_frames(self):
|
||||||
|
action = MergeFrames(lamdaCut=0.0, preciseTime=False)
|
||||||
|
action.perform_action(self.d)
|
||||||
|
self.assertEqual(self.d.data.events.tof.shape, self.d.orig_data.events.tof.shape)
|
||||||
|
np.testing.assert_array_compare(lambda x,y: x<=y, self.d.data.events.tof, self.d.orig_data.events.tof)
|
||||||
|
self.assertTrue((-self.d.timing.tau<=self.d.data.events.tof).all())
|
||||||
|
np.testing.assert_array_less(self.d.data.events.tof, self.d.timing.tau)
|
||||||
|
|
||||||
|
action = MergeFrames(lamdaCut=2.0, preciseTime=False)
|
||||||
|
self.d.data.events.tof = self.d.orig_data.events.tof[:]
|
||||||
|
action.perform_action(self.d)
|
||||||
|
tofCut = 2.0*self.d.geometry.chopperDetectorDistance/const.hdm*1e-13
|
||||||
|
self.assertTrue((tofCut-self.d.timing.tau<=self.d.data.events.tof).all())
|
||||||
|
self.assertTrue((self.d.data.events.tof<=tofCut+self.d.timing.tau).all())
|
||||||
|
|
||||||
|
def test_merge_frames_splitting(self):
|
||||||
|
action = MergeFrames(lamdaCut=0.0, preciseTime=True)
|
||||||
|
self._extract_walltime()
|
||||||
|
action.perform_action(self.d)
|
||||||
|
self.assertEqual(self.d.data.events.tof.shape, self.d.orig_data.events.tof.shape)
|
||||||
|
np.testing.assert_array_compare(lambda x,y: x<=y, self.d.data.events.tof, self.d.orig_data.events.tof)
|
||||||
|
self.assertEqual(self.d.data.pulses.shape[0], self.d.orig_data.pulses.shape[0]*2+1)
|
||||||
|
np.testing.assert_array_less(self.d.orig_data.pulses.time[:-1], self.d.orig_data.pulses.time[1:])
|
||||||
|
np.testing.assert_array_less(self.d.data.pulses.time[:-1], self.d.data.pulses.time[1:])
|
||||||
|
|
||||||
|
def test_analyze_pixel_ids(self):
|
||||||
|
action = AnalyzePixelIDs((1000, 1001))
|
||||||
|
action.perform_action(self.d)
|
||||||
|
self.assertIn('detZ', self.d.data.events.dtype.names)
|
||||||
|
self.assertIn('detXdist', self.d.data.events.dtype.names)
|
||||||
|
self.assertIn('delta', self.d.data.events.dtype.names)
|
||||||
|
self.assertEqual(
|
||||||
|
np.bitwise_and(self.d.data.events.mask, EVENT_BITMASKS['yRange']).astype(bool).sum(),
|
||||||
|
self.d.data.events.shape[0]
|
||||||
|
)
|
||||||
|
# TODO: maybe add a test actually checking correct detector-id resolution
|
||||||
|
|
||||||
|
def test_calculate_wavelength(self):
|
||||||
|
action = CalculateWavelength((3.0, 5.0))
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
action.perform_action(self.d)
|
||||||
|
|
||||||
|
new_events = append_fields(self.d.data.events, [('detXdist', np.float64)])
|
||||||
|
new_events.detXdist = 0.0
|
||||||
|
self.d.data.events = new_events
|
||||||
|
action.perform_action(self.d)
|
||||||
|
self.assertIn('lamda', self.d.data.events.dtype.names)
|
||||||
|
flt = self.d.data.events.mask!=EVENT_BITMASKS['LamdaRange']
|
||||||
|
# check all wavelength in range not filtered
|
||||||
|
np.testing.assert_array_less(self.d.data.events.lamda[flt], 5.0)
|
||||||
|
np.testing.assert_array_less(3.0, self.d.data.events.lamda[flt])
|
||||||
|
# check all wavelength out of range filtered
|
||||||
|
flt = self.d.data.events.mask==EVENT_BITMASKS['LamdaRange']
|
||||||
|
self.assertTrue(((self.d.data.events.lamda[flt]<3.0)|(self.d.data.events.lamda[flt]>5.0)).all())
|
||||||
|
|
||||||
|
def test_calculate_q(self):
|
||||||
|
action = CalculateQ(IncidentAngle.alphaF)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
action.perform_action(self.d)
|
||||||
|
|
||||||
|
# TODO: add checks for actual resulting values
|
||||||
|
|
||||||
|
new_events = append_fields(self.d.data.events, [('lamda', np.float64), ('delta', np.float64)])
|
||||||
|
new_events.lamda = 5.0
|
||||||
|
new_events.delta = 0.0
|
||||||
|
self.d.data.events = new_events
|
||||||
|
action.perform_action(self.d)
|
||||||
|
self.assertIn('qz', self.d.data.events.dtype.names)
|
||||||
|
self.assertNotIn('qx', self.d.data.events.dtype.names)
|
||||||
|
action.update_header(self.header)
|
||||||
|
self.assertEqual(self.header.measurement_scheme, 'angle- and energy-dispersive')
|
||||||
|
|
||||||
|
self.d.create_data()
|
||||||
|
new_events = append_fields(self.d.data.events, [('lamda', np.float64), ('delta', np.float64)])
|
||||||
|
new_events.lamda = 5.0
|
||||||
|
new_events.delta = 0.0
|
||||||
|
self.d.data.events = new_events
|
||||||
|
action = CalculateQ(IncidentAngle.mu)
|
||||||
|
action.perform_action(self.d)
|
||||||
|
self.assertIn('qz', self.d.data.events.dtype.names)
|
||||||
|
self.assertIn('qx', self.d.data.events.dtype.names)
|
||||||
|
action.update_header(self.header)
|
||||||
|
self.assertEqual(self.header.measurement_scheme, 'energy-dispersive')
|
||||||
|
|
||||||
|
self.d.create_data()
|
||||||
|
new_events = append_fields(self.d.data.events, [('lamda', np.float64), ('delta', np.float64)])
|
||||||
|
new_events.lamda = 5.0
|
||||||
|
new_events.delta = 0.0
|
||||||
|
self.d.data.events = new_events
|
||||||
|
action = CalculateQ(IncidentAngle.nu)
|
||||||
|
action.perform_action(self.d)
|
||||||
|
self.assertIn('qz', self.d.data.events.dtype.names)
|
||||||
|
self.assertNotIn('qx', self.d.data.events.dtype.names)
|
||||||
|
action.update_header(self.header)
|
||||||
|
self.assertEqual(self.header.measurement_scheme, 'energy-dispersive')
|
||||||
|
|
||||||
|
def test_filter_qz_range(self):
|
||||||
|
action = FilterQzRange((0.1, 0.2))
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
action.perform_action(self.d)
|
||||||
|
|
||||||
|
self.d.data.events = self.d.data.events[:5].copy()
|
||||||
|
new_events = append_fields(self.d.data.events, [('qz', np.float64)])
|
||||||
|
new_events.qz = np.array([0.05, 0.1, 0.15, 0.2, 0.25])
|
||||||
|
self.d.data.events = new_events
|
||||||
|
action.perform_action(self.d)
|
||||||
|
np.testing.assert_array_equal(
|
||||||
|
self.d.data.events.mask,
|
||||||
|
np.array([1, 0, 0, 0, 1], dtype=np.int32) * EVENT_BITMASKS['qRange']
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_filter_by_log(self):
|
||||||
|
action = FilterByLog("test_log==0") | ApplyMask()
|
||||||
|
class LogWarnError(Exception):
|
||||||
|
...
|
||||||
|
def warn_raise(*args, **kwargs):
|
||||||
|
raise LogWarnError()
|
||||||
|
_orig_warn = logging.warning
|
||||||
|
try:
|
||||||
|
logging.warning = warn_raise
|
||||||
|
with self.assertRaises(LogWarnError):
|
||||||
|
action.perform_action(self.d)
|
||||||
|
finally:
|
||||||
|
logging.warning = _orig_warn
|
||||||
|
|
||||||
|
self._extract_walltime()
|
||||||
|
|
||||||
|
test_log = np.recarray(shape=(2,), dtype=np.dtype([('value', np.int32),
|
||||||
|
('time', np.int64)]))
|
||||||
|
test_log.time = [-5, self.d.data.pulses.time[100]+123]
|
||||||
|
test_log.value = [0, 1]
|
||||||
|
self.d.data.device_logs['test_log'] = test_log
|
||||||
|
action.perform_action(self.d)
|
||||||
|
self.assertEqual(self.d.data.pulses.shape[0], 101)
|
||||||
|
|
||||||
|
def test_filter_by_log_switchpulse(self):
|
||||||
|
action = FilterByLog("!test_log==0") | ApplyMask()
|
||||||
|
self._extract_walltime()
|
||||||
|
|
||||||
|
test_log = np.recarray(shape=(2,), dtype=np.dtype([('value', np.int32),
|
||||||
|
('time', np.int64)]))
|
||||||
|
test_log.time = [-5, self.d.data.pulses.time[100]+123]
|
||||||
|
test_log.value = [0, 1]
|
||||||
|
self.d.data.device_logs['test_log'] = test_log
|
||||||
|
self.d.data.device_logs['check_log'] = test_log.copy()
|
||||||
|
action.perform_action(self.d)
|
||||||
|
self.assertEqual(self.d.data.pulses.shape[0], 100)
|
||||||
|
np.testing.assert_array_equal(
|
||||||
|
self.d.data.device_logs['test_log'],
|
||||||
|
self.d.data.device_logs['check_log'],
|
||||||
|
)
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import os
|
import os
|
||||||
import cProfile
|
import cProfile
|
||||||
|
import numpy as np
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from dataclasses import fields, MISSING
|
from dataclasses import fields, MISSING
|
||||||
from eos import options, reduction_reflectivity, logconfig
|
from eos import options, reduction_reflectivity, logconfig
|
||||||
|
from orsopy import fileio
|
||||||
|
|
||||||
logconfig.setup_logging()
|
logconfig.setup_logging()
|
||||||
logconfig.update_loglevel(1)
|
logconfig.update_loglevel(1)
|
||||||
@@ -38,30 +40,25 @@ class FullAmorTest(TestCase):
|
|||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.pr.disable()
|
self.pr.disable()
|
||||||
for fi in ['test_results/test.Rqz.ort', 'test_results/5952.norm']:
|
for fi in ['test_results/test.Rqz.ort', 'test_results/5952.norm']:
|
||||||
try:
|
try:
|
||||||
os.unlink(fi)
|
os.unlink(fi)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_time_slicing(self):
|
def test_time_slicing(self):
|
||||||
experiment_config = options.ExperimentConfig(
|
experiment_config = options.ExperimentConfig(
|
||||||
chopperSpeed=self._field_defaults['ExperimentConfig']['chopperSpeed'],
|
|
||||||
chopperPhase=-13.5,
|
chopperPhase=-13.5,
|
||||||
chopperPhaseOffset=-5,
|
chopperPhaseOffset=-5,
|
||||||
monitorType=self._field_defaults['ExperimentConfig']['monitorType'],
|
|
||||||
lowCurrentThreshold=self._field_defaults['ExperimentConfig']['lowCurrentThreshold'],
|
|
||||||
yRange=(18, 48),
|
yRange=(18, 48),
|
||||||
lambdaRange=(3., 11.5),
|
lambdaRange=(3., 11.5),
|
||||||
incidentAngle=self._field_defaults['ExperimentConfig']['incidentAngle'],
|
|
||||||
mu=0,
|
mu=0,
|
||||||
nu=0,
|
nu=0,
|
||||||
muOffset=0.0,
|
muOffset=0.0,
|
||||||
sampleModel='air | 10 H2O | D2O'
|
sampleModel='air | 10 H2O | D2O'
|
||||||
)
|
)
|
||||||
reduction_config = options.ReflectivityReductionConfig(
|
reduction_config = options.ReflectivityReductionConfig(
|
||||||
normalisationMethod=self._field_defaults['ReflectivityReductionConfig']['normalisationMethod'],
|
|
||||||
qResolution=0.01,
|
qResolution=0.01,
|
||||||
qzRange=self._field_defaults['ReflectivityReductionConfig']['qzRange'],
|
qzRange=(0.01, 0.15),
|
||||||
thetaRange=(-0.75, 0.75),
|
thetaRange=(-0.75, 0.75),
|
||||||
fileIdentifier=["6003-6005"],
|
fileIdentifier=["6003-6005"],
|
||||||
scale=[1],
|
scale=[1],
|
||||||
@@ -84,22 +81,16 @@ class FullAmorTest(TestCase):
|
|||||||
|
|
||||||
def test_noslicing(self):
|
def test_noslicing(self):
|
||||||
experiment_config = options.ExperimentConfig(
|
experiment_config = options.ExperimentConfig(
|
||||||
chopperSpeed=self._field_defaults['ExperimentConfig']['chopperSpeed'],
|
|
||||||
chopperPhase=-13.5,
|
chopperPhase=-13.5,
|
||||||
chopperPhaseOffset=-5,
|
chopperPhaseOffset=-5,
|
||||||
monitorType=self._field_defaults['ExperimentConfig']['monitorType'],
|
|
||||||
lowCurrentThreshold=self._field_defaults['ExperimentConfig']['lowCurrentThreshold'],
|
|
||||||
yRange=(18, 48),
|
yRange=(18, 48),
|
||||||
lambdaRange=(3., 11.5),
|
lambdaRange=(3., 11.5),
|
||||||
incidentAngle=self._field_defaults['ExperimentConfig']['incidentAngle'],
|
|
||||||
mu=0,
|
mu=0,
|
||||||
nu=0,
|
nu=0,
|
||||||
muOffset=0.0,
|
muOffset=0.0,
|
||||||
)
|
)
|
||||||
reduction_config = options.ReflectivityReductionConfig(
|
reduction_config = options.ReflectivityReductionConfig(
|
||||||
normalisationMethod=self._field_defaults['ReflectivityReductionConfig']['normalisationMethod'],
|
|
||||||
qResolution=0.01,
|
qResolution=0.01,
|
||||||
qzRange=self._field_defaults['ReflectivityReductionConfig']['qzRange'],
|
|
||||||
thetaRange=(-0.75, 0.75),
|
thetaRange=(-0.75, 0.75),
|
||||||
fileIdentifier=["6003", "6004", "6005"],
|
fileIdentifier=["6003", "6004", "6005"],
|
||||||
scale=[1],
|
scale=[1],
|
||||||
@@ -117,3 +108,57 @@ class FullAmorTest(TestCase):
|
|||||||
# run second time to reuse norm file
|
# run second time to reuse norm file
|
||||||
reducer = reduction_reflectivity.ReflectivityReduction(config)
|
reducer = reduction_reflectivity.ReflectivityReduction(config)
|
||||||
reducer.reduce()
|
reducer.reduce()
|
||||||
|
|
||||||
|
def test_eventfilter(self):
|
||||||
|
self.reader_config.year = 2026
|
||||||
|
experiment_config = options.ExperimentConfig()
|
||||||
|
reduction_config = options.ReflectivityReductionConfig(fileIdentifier=["826"],
|
||||||
|
logfilter=['polarization_config_label==2'])
|
||||||
|
output_config = options.ReflectivityOutputConfig(
|
||||||
|
outputFormats=[options.OutputFomatOption.Rqz_ort],
|
||||||
|
outputName='test',
|
||||||
|
outputPath='test_results',
|
||||||
|
)
|
||||||
|
config=options.ReflectivityConfig(self.reader_config, experiment_config, reduction_config, output_config)
|
||||||
|
reducer = reduction_reflectivity.ReflectivityReduction(config)
|
||||||
|
reducer.reduce()
|
||||||
|
espin_up = reducer.dataset.data.events.shape[0]
|
||||||
|
|
||||||
|
reduction_config.logfilter = ['polarization_config_label==3']
|
||||||
|
output_config.append = True
|
||||||
|
reducer = reduction_reflectivity.ReflectivityReduction(config)
|
||||||
|
reducer.reduce()
|
||||||
|
espin_down = reducer.dataset.data.events.shape[0]
|
||||||
|
# measurement should have about 2x as many counts in spin_down
|
||||||
|
self.assertAlmostEqual(espin_down/espin_up, 2., 2)
|
||||||
|
|
||||||
|
# perform the same filter but remove pulses during which the switch occured
|
||||||
|
reduction_config.logfilter = ['!polarization_config_label==3']
|
||||||
|
output_config.append = True
|
||||||
|
reducer = reduction_reflectivity.ReflectivityReduction(config)
|
||||||
|
reducer.reduce()
|
||||||
|
espin_down2 = reducer.dataset.data.events.shape[0]
|
||||||
|
# measurement should have about 2x as many counts in spin_down
|
||||||
|
self.assertLess(espin_down2, espin_down)
|
||||||
|
|
||||||
|
def test_polsplitting(self):
|
||||||
|
self.reader_config.year = 2026
|
||||||
|
experiment_config = options.ExperimentConfig()
|
||||||
|
reduction_config = options.ReflectivityReductionConfig(fileIdentifier=["826"])
|
||||||
|
output_config = options.ReflectivityOutputConfig(
|
||||||
|
outputFormats=[options.OutputFomatOption.Rqz_ort],
|
||||||
|
outputName='test',
|
||||||
|
outputPath='test_results',
|
||||||
|
)
|
||||||
|
config=options.ReflectivityConfig(self.reader_config, experiment_config, reduction_config, output_config)
|
||||||
|
reducer = reduction_reflectivity.ReflectivityReduction(config)
|
||||||
|
reducer.reduce()
|
||||||
|
|
||||||
|
results = fileio.load_orso(os.path.join(output_config.outputPath, output_config.outputName+'.Rqz.ort'))
|
||||||
|
self.assertEqual(len(results), 2)
|
||||||
|
self.assertEqual(results[0].info.data_source.measurement.instrument_settings.polarization, 'po')
|
||||||
|
self.assertEqual(results[1].info.data_source.measurement.instrument_settings.polarization, 'mo')
|
||||||
|
espin_up = np.nansum(results[0].data[:,1])
|
||||||
|
espin_down = np.nansum(results[1].data[:,1])
|
||||||
|
# the total intensity should be around equal as events are doubled and monitor counts are doubled
|
||||||
|
self.assertAlmostEqual(espin_down/espin_up, 1., 2)
|
||||||
|
|||||||
12
update.md
12
update.md
@@ -3,14 +3,14 @@ Make new release
|
|||||||
|
|
||||||
- Update revision in `eos/__init__.py`
|
- Update revision in `eos/__init__.py`
|
||||||
- Commit changes `git commit -a -m "your message here"`
|
- Commit changes `git commit -a -m "your message here"`
|
||||||
- Tag version `git tag v3.x.y`
|
- Push changes `git push`
|
||||||
- Push changes `git push` and `git push --tags`
|
- Use the workflow dispatch for [**Release**](https://gitea.psi.ch/sinq-reflectometry/eos/actions?workflow=release.yml&actor=0&status=0) action
|
||||||
- This should trigger the **Release** action on GitHub that builds a new version and uploads it to PyPI.
|
on Gitea with option "all_incl_release", that builds a new version, releases it and uploads it to PyPI.
|
||||||
|
|
||||||
|
|
||||||
Update on AMOR
|
Update on AMOR-DR
|
||||||
==============
|
=================
|
||||||
|
|
||||||
- Login via SSH using the **amor** user.
|
- Login via SSH using the **amor-dr** user.
|
||||||
- Activate eos virtual environment `source /home/software/virtualenv/eosenv/bin/activate`
|
- Activate eos virtual environment `source /home/software/virtualenv/eosenv/bin/activate`
|
||||||
- Update eos packge `pip install --upgrade amor-eos`
|
- Update eos packge `pip install --upgrade amor-eos`
|
||||||
|
|||||||
Reference in New Issue
Block a user