29 Commits

Author SHA1 Message Date
55e9407b67 update revision info
Some checks failed
Release / build-ubuntu-latest (push) Failing after 2s
Release / release (push) Has been skipped
Release / build-windows (push) Has been cancelled
2024-12-13 14:37:50 +01:00
5fc512632f Merge pull request #5 from jochenstahn/time
Handle timezone correctly
2024-12-13 14:35:16 +01:00
55a7b7270c minor cleanup 2024-12-13 14:33:04 +01:00
f81582554e remove unnecessary requirement for pytz and insert timezone into datetime instead of recreation 2024-12-13 14:17:51 +01:00
3e22a94ff6 install zoneinfo backports for python 3.8 2024-12-13 14:07:12 +01:00
Jochen Stahn
c1dc194548 different method to add timezone 2024-12-10 14:39:59 +01:00
Jochen Stahn
c5f5602a89 added pytz 2024-12-09 10:19:22 +01:00
cf9c0018a5 tried to handle summer time correctly.... 2024-12-09 10:03:56 +01:00
03ac04ab05 removed control output 2024-12-06 14:59:18 +01:00
b2df980a21 fix of handling of pulses for more than one file 2024-12-06 10:36:25 +01:00
eb54e52c66 changed format for a logging output about low monitor signal 2024-12-04 10:51:05 +01:00
9b4c384425 release to pypi after storing archive to artifact 2024-12-04 08:48:35 +01:00
ce44e04014 add windows build to release action
Some checks failed
Release / build-ubuntu-latest (push) Failing after 2s
Release / release (push) Has been skipped
Release / build-windows (push) Has been cancelled
2024-12-03 16:11:04 +01:00
ce90fc005a remove upterm from tests 2024-12-03 15:17:12 +01:00
29edbce5bc change iso time offset to include minutes for compatibility with older python 2024-12-03 15:02:25 +01:00
a0b0ff436c replace calculated offset by Iso string 2024-12-03 14:51:09 +01:00
2cae45659c correct for local timezone issues 2024-12-03 14:12:37 +01:00
9693951233 explicitly define utc as timezone for file 2024-12-03 13:52:29 +01:00
e2970a8a5f add upterm action to workflow for debugging 2024-12-03 13:13:28 +01:00
7dcede44b6 removed obsolete control output 2024-11-28 09:03:57 +01:00
f6f853e6e4 commented VERY time consuming control output: events per pulse 2024-11-27 08:29:33 +01:00
ad8245a9a0 relative theta range over absolute over default 2024-11-26 15:27:07 +01:00
33b8829a09 check for empty files introduced 2024-11-22 11:24:17 +01:00
f645082c20 Merge branch 'main' of https://github.com/jochenstahn/amor 2024-11-08 09:01:37 +01:00
1bd1100035 changes in the "pulse generation" - not yet finished 2024-11-08 08:51:56 +01:00
Artur Glavic
b94b6714a3 Update setup.cfg 2024-11-05 13:15:20 +01:00
d4462f450e change to rst README 2024-10-30 15:36:43 +01:00
80d1014750 update readme with installation instructions 2024-10-30 15:05:33 +01:00
a3eb6a173e bump version 2024-10-30 14:32:04 +01:00
11 changed files with 215 additions and 52 deletions

View File

@@ -38,6 +38,12 @@ jobs:
- name: Build PyPI package
run: |
python3 -m build
- name: Archive distribution
uses: actions/upload-artifact@v4
with:
name: linux-dist
path: |
dist/*.tar.gz
- name: Upload to PyPI
if: github.event_name != 'workflow_dispatch'
uses: pypa/gh-action-pypi-publish@release/v1
@@ -45,12 +51,32 @@ jobs:
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"]'), 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: |
C:\Miniconda\condabin\conda.bat env update --file conda_windows.yml --name base
C:\Miniconda\condabin\conda.bat init powershell
- name: Build with pyinstaller
run: |
pyinstaller windows_folder.spec
cd dist\eos
Compress-Archive -Path .\* -Destination ..\..\eos.zip
- name: Archive distribution
uses: actions/upload-artifact@v4
with:
name: linux-dist
name: windows-dist
path: |
dist/*.tar.gz
eos.zip
release:
if: github.event_name != 'workflow_dispatch'
@@ -61,8 +87,11 @@ jobs:
- uses: actions/download-artifact@v4
with:
name: linux-dist
- uses: actions/download-artifact@v4
with:
name: windows-dist
- uses: ncipollo/release-action@v1
with:
artifacts: "amor*.tar.gz"
artifacts: "amor*.tar.gz,*.zip"
token: ${{ secrets.GITHUB_TOKEN }}
allowUpdates: true

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
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
steps:
@@ -31,6 +31,11 @@ jobs:
pip install pytest
pip install -r requirements.txt
- name: Backport to 3.8
if: matrix.python-version == '3.8'
run: |
pip install backports.zoneinfo
- name: Test with pytest
run: |
cd tests

View File

@@ -1,12 +0,0 @@
Software repository for the neutron reflectometer Amor at the Paul Scherrer Institut, Switzerland
reduction of the raw files (.hdf) to reflectivity files in one of the representations of the **ORSO reflectivity file format**:
> `eos.py`
> `neos.py` : version for playing and testing
visualisation of the content of a raw file (.hdf):
> `events2histogram.py`
#TODO: real readme for final system needed.

27
README.rst Normal file
View File

@@ -0,0 +1,27 @@
EOS - The AMOR focusing reflectometry data reduction software
-------------------------------------------------------------
.. image:: https://img.shields.io/pypi/v/amor-eos.svg
:target: https://pypi.python.org/pypi/amor-eos/
Software repository for the neutron reflectometer Amor at the Paul Scherrer Institut, Switzerland
Reduction of the raw files (.hdf) to reflectivity files in one of the representations of the **ORSO reflectivity file format**:
eos.py --help
visualisation of the content of a raw file (.hdf):
events2histogram.py
:TODO: real readme for final system needed.
Installation
------------
Create a virtual python environment (>3.8) and install the PyPI package:
pip install amor-eos
On Windows you can also use the binary eos.exe that you find in the
[GitHub Releases]([https://github.com/jochenstahn/amor/releases/latest) section

43
conda_windows.yml Normal file
View File

@@ -0,0 +1,43 @@
name: eos_build
channels:
- defaults
dependencies:
- altgraph=0.17.3=py312haa95532_0
- blas=1.0=mkl
- bzip2=1.0.8=h2bbff1b_6
- ca-certificates=2024.11.26=haa95532_0
- expat=2.6.3=h5da7b33_0
- h5py=3.12.1=py312h3b2c811_0
- hdf5=1.12.1=h51c971a_3
- icc_rt=2022.1.0=h6049295_2
- intel-openmp=2023.1.0=h59b6b97_46320
- libffi=3.4.4=hd77b12b_1
- llvmlite=0.43.0=py312hf2fb9eb_0
- mkl=2023.1.0=h6b88ed4_46358
- mkl-service=2.4.0=py312h2bbff1b_1
- mkl_fft=1.3.11=py312h827c3e9_0
- mkl_random=1.2.8=py312h0158946_0
- numba=0.60.0=py312h0158946_0
- numpy=1.26.4=py312hfd52020_0
- numpy-base=1.26.4=py312h4dde369_0
- openssl=3.0.15=h827c3e9_0
- packaging=24.1=py312haa95532_0
- pefile=2023.2.7=py312haa95532_0
- pip=24.2=py312haa95532_0
- pyinstaller=6.9.0=py312h0416ee5_0
- pyinstaller-hooks-contrib=2024.7=py312haa95532_0
- python=3.12.7=h14ffc60_0
- pywin32-ctypes=0.2.2=py312haa95532_0
- setuptools=75.1.0=py312haa95532_0
- sqlite=3.45.3=h2bbff1b_0
- tbb=2021.8.0=h59b6b97_0
- tk=8.6.14=h0416ee5_0
- tzdata=2024b=h04d1e81_0
- vc=14.40=h2eaa2aa_1
- vs2015_runtime=14.40.33807=h98bb1dd_1
- wheel=0.44.0=py312haa95532_0
- xz=5.4.6=h8cc25b3_1
- zlib=1.2.13=h8cc25b3_1
- pip:
- orsopy==1.2.1
- pyyaml==6.0.2

View File

@@ -2,5 +2,5 @@
Package to handle data redction at AMOR instrument to be used by eos.py script.
"""
__version__ = '2.1.1'
__date__ = '2024-10-30'
__version__ = '2.1.3'
__date__ = '2024-12-13'

View File

@@ -2,7 +2,12 @@ import logging
import os
import subprocess
import sys
from datetime import datetime
from datetime import datetime, timezone
try:
import zoneinfo
except ImportError:
# for python versions < 3.9 try to use the backports version
from backports import zoneinfo
from typing import List
import h5py
@@ -20,18 +25,8 @@ try:
except Exception:
nb_helpers = None
def get_current_per_pulse(pulseTimeS, currentTimeS, currents):
# add currents for early pulses and current time value after last pulse (j+1)
currentTimeS = np.hstack([[0], currentTimeS, [pulseTimeS[-1]+1]])
currents = np.hstack([[0], currents])
pulseCurrentS = np.zeros(pulseTimeS.shape[0], dtype=float)
j = 0
for i, ti in enumerate(pulseTimeS):
if ti >= currentTimeS[j+1]:
j += 1
pulseCurrentS[i] = currents[j]
#print(f' {i} {pulseTimeS[i]} {pulseCurrentS[i]}')
return pulseCurrentS
# Time zone used to interpret time strings
AMOR_LOCAL_TIMEZONE = zoneinfo.ZoneInfo(key='Europe/Zurich')
class AmorData:
"""read meta-data and event streams from .hdf file(s), apply filters and conversions"""
@@ -196,6 +191,10 @@ class AmorData:
self.read_event_stream()
totalNumber = np.shape(self.tof_e)[0]
# check for empty event stream
if totalNumber == 0:
logging.error('empty event stream: can not determine end time')
sys.exit()
self.sort_pulses()
@@ -204,11 +203,11 @@ class AmorData:
self.extract_walltime(norm)
# following lines: debugging output to trace the time-offset of proton current and neutron pulses
if self.config.monitorType == 'p':
if self.config.monitorType == 'x':
cpp, t_bins = np.histogram(self.wallTime_e, self.pulseTimeS)
np.savetxt('tme.hst', np.vstack((self.pulseTimeS[:-1], cpp, self.monitorPerPulse[:-1])).T)
self.average_events_per_pulse()
#self.average_events_per_pulse() # for debugging only. VERY time consuming!!!
self.monitor_threshold()
@@ -233,25 +232,46 @@ class AmorData:
pulseTime -= np.int64(self.seriesStartTime)
self.stopTime = pulseTime[-1]
pulseTime = pulseTime[pulseTime>=0]
# fill in missing pulse times
# TODO: check for real end time
pulseTime = pulseTime[pulseTime>=0]
firstPulse = pulseTime[0] % np.int64(self.tau*2e9)
self.pulseTimeS = np.array([], dtype=np.int64)
nxt = firstPulse
try:
# further files
# TODO: use the first pulse of the respective measurement
#nextPulseTime = startTime % np.int64(self.tau*2e9)
#nextPulseTime = self.pulseTimeS[-1] + chopperPeriod
nextPulseTime = pulseTime[0]
except AttributeError:
# first file
nextPulseTime = pulseTime[0] % np.int64(self.tau*2e9)
for tt in pulseTime:
while tt - nxt > self.tau*1e9:
self.pulseTimeS = np.append(self.pulseTimeS, nxt)
nxt += chopperPeriod
while tt - nextPulseTime > self.tau*1e9:
self.pulseTimeS = np.append(self.pulseTimeS, nextPulseTime)
nextPulseTime += chopperPeriod
self.pulseTimeS = np.append(self.pulseTimeS, tt)
nxt = self.pulseTimeS[-1] + chopperPeriod
nextPulseTime = self.pulseTimeS[-1] + chopperPeriod
def get_current_per_pulse(self, pulseTimeS, currentTimeS, currents):
# add currents for early pulses and current time value after last pulse (j+1)
currentTimeS = np.hstack([[0], currentTimeS, [pulseTimeS[-1]+1]])
currents = np.hstack([[0], currents])
pulseCurrentS = np.zeros(pulseTimeS.shape[0], dtype=float)
j = 0
for i, ti in enumerate(pulseTimeS):
if ti >= currentTimeS[j+1]:
j += 1
pulseCurrentS[i] = currents[j]
#print(f' {i} {pulseTimeS[i]} {pulseCurrentS[i]}')
return pulseCurrentS
def associate_pulse_with_monitor(self):
if self.config.monitorType == 'p': # protonCharge
self.currentTime -= np.int64(self.seriesStartTime)
self.currentTime -= np.int64(16e9) # time offset of proton current signal
self.monitorPerPulse = get_current_per_pulse(self.pulseTimeS, self.currentTime, self.current) * 2*self.tau * 1e-3
self.monitorPerPulse = self.get_current_per_pulse(self.pulseTimeS, self.currentTime, self.current) * 2*self.tau * 1e-3
# filter low-current pulses
self.monitorPerPulse = np.where(self.monitorPerPulse > 2*self.tau * self.config.lowCurrentThreshold * 1e-3, self.monitorPerPulse, 0)
elif self.config.monitorType == 't': # countingTime
@@ -274,7 +294,7 @@ class AmorData:
if self.config.monitorType == 'p':
for i, time in enumerate(self.pulseTimeS):
events = np.shape(self.wallTime_e[self.wallTime_e == time])[0]
#print(f' {i:6.0f} {events:6.0f} {self.monitorPerPulse[i]:6.2f}')
logging.info(f'pulse: {i:6.0f}, events: {events:6.0f}, monitor: {self.monitorPerPulse[i]:6.2f}')
def monitor_threshold(self):
if self.config.monitorType == 'p': # fix to check for file compatibility
@@ -361,7 +381,7 @@ class AmorData:
self.pixelID_e = self.pixelID_e[filter_e]
self.wallTime_e = self.wallTime_e[filter_e]
if np.shape(filter_e)[0]-np.shape(self.tof_e)[0]>0.5:
logging.warning(f'# strange times: {np.shape(filter_e)[0]-np.shape(self.tof_e)[0]}')
logging.warning(f' strange times: {np.shape(filter_e)[0]-np.shape(self.tof_e)[0]}')
def read_event_stream(self):
self.tof_e = np.array(self.hdf['/entry1/Amor/detector/data/event_time_offset'][:])/1.e9
@@ -424,8 +444,10 @@ class AmorData:
if self.config.nu:
self.nu = self.config.nu
self.fileDate = datetime.fromisoformat( self.hdf['/entry1/start_time'][0].decode('utf-8') )
self.startTime = np.int64( self.fileDate.timestamp() * 1e9 )
# extract start time as unix time, adding UTC offset of 1h to time string
dz = datetime.fromisoformat(self.hdf['/entry1/start_time'][0].decode('utf-8'))
self.fileDate=dz.replace(tzinfo=AMOR_LOCAL_TIMEZONE)
self.startTime = np.int64( (self.fileDate.timestamp() ) * 1e9 )
if self.seriesStartTime is None:
self.seriesStartTime = self.startTime

View File

@@ -372,11 +372,15 @@ class AmorReduction:
# projection on lambda-z-grid
lamda_l = self.grid.lamda()
alphaF_z = fromHDF.nu - fromHDF.mu + fromHDF.delta_z
# TODO: implement various methods to obtain alpha_i.
#if self.experiment_config.incidentAngle == 'alphaF':
# # for specular reflectometry with a highly divergent beam
# alphaF_z = fromHDF.nu - fromHDF.mu + fromHDF.delta_z
#elif self.experiment_config.incidentAngle == 'nu':
# # for specular reflectometry, using kappa nad nu but ignoring mu
# alphaF_z = (fromHDF.nu + fromHDF.delta_z + fromHDF.kap + fromHDF.kad) / 2.
#else:
# # using kappa, for a collimated incoming beam
# pass
lamda_lz = (self.grid.lz().T*lamda_l[:-1]).T
alphaF_lz = self.grid.lz()*alphaF_z
@@ -388,7 +392,11 @@ class AmorReduction:
mask_lz = np.where(np.isnan(norm_lz), False, True)
mask_lz = np.logical_and(mask_lz, np.where(np.absolute(thetaN_lz)>5e-3, True, False))
mask_lz = np.logical_and(mask_lz, np.where(np.absolute(alphaF_lz)>5e-3, True, False))
if self.reduction_config.thetaRange[1]<12:
if self.reduction_config.thetaRangeR[1]<12:
t0 = fromHDF.nu - fromHDF.mu
mask_lz = np.logical_and(mask_lz, np.where(alphaF_lz-t0 >= self.reduction_config.thetaRangeR[0], True, False))
mask_lz = np.logical_and(mask_lz, np.where(alphaF_lz-t0 <= self.reduction_config.thetaRangeR[1], True, False))
elif self.reduction_config.thetaRange[1]<12:
mask_lz = np.logical_and(mask_lz, np.where(alphaF_lz >= self.reduction_config.thetaRange[0], True, False))
mask_lz = np.logical_and(mask_lz, np.where(alphaF_lz <= self.reduction_config.thetaRange[1], True, False))
else:
@@ -396,10 +404,6 @@ class AmorReduction:
fromHDF.nu - fromHDF.mu + fromHDF.div/2]
mask_lz = np.logical_and(mask_lz, np.where(alphaF_lz >= self.reduction_config.thetaRange[0], True, False))
mask_lz = np.logical_and(mask_lz, np.where(alphaF_lz <= self.reduction_config.thetaRange[1], True, False))
if self.reduction_config.thetaRangeR[1]<12:
t0 = fromHDF.nu - fromHDF.mu
mask_lz = np.logical_and(mask_lz, np.where(alphaF_lz-t0 >= self.reduction_config.thetaRangeR[0], True, False))
mask_lz = np.logical_and(mask_lz, np.where(alphaF_lz-t0 <= self.reduction_config.thetaRangeR[1], True, False))
if self.experiment_config.lambdaRange[1]<15:
mask_lz = np.logical_and(mask_lz, np.where(lamda_lz >= self.experiment_config.lambdaRange[0], True, False))
mask_lz = np.logical_and(mask_lz, np.where(lamda_lz <= self.experiment_config.lambdaRange[1], True, False))
@@ -438,7 +442,7 @@ class AmorReduction:
if self.monitor > 1e-6 :
ref_lz *= self.normMonitor / self.monitor
else:
logging.warning(' too small monitor value for normalisation -> ignoring monitors')
logging.info(' too small monitor value for normalisation -> ignoring monitors')
err_lz = ref_lz * np.sqrt( 1/(int_lz+.1) + 1/norm_lz )
# TODO: allow for non-ideal Delta lambda / lambda (rather than 2.2%)

View File

@@ -7,6 +7,7 @@ version = attr: libeos.__version__
author = Jochen Stahn - Paul Scherrer Institut
author_email = jochen.stahn@psi.ch
description = EOS reflectometry reduction for AMOR instrument
long_description = Reduces data obtained by focusing time of flight neutron reflectivity to full reflectivity curve.
license = MIT
classifiers =
Programming Language :: Python :: 3

View File

@@ -12,7 +12,7 @@ a = Analysis(
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
optimize=1,
)
pyz = PYZ(a.pure)

44
windows_folder.spec Normal file
View File

@@ -0,0 +1,44 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['eos.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=1,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='eos',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='eos',
)