Merge pull request #2 from jochenstahn/input_artur

Refactor, first tests and speedup
This commit is contained in:
Jochen Stahn
2024-03-06 09:52:55 +01:00
committed by GitHub
15 changed files with 1401 additions and 894 deletions
+4 -2
View File
@@ -1,5 +1,7 @@
*~
*.bak
__py_cache__
*.log
__pycache__
raw
.idea
test_data
+6
View File
@@ -0,0 +1,6 @@
"""
Package to handle data redction at AMOR instrument to be used by eos.py script.
"""
__version__ = '2.0'
__date__ = '2024-03-04'
+193
View File
@@ -0,0 +1,193 @@
import argparse
from datetime import datetime
from .logconfig import update_loglevel
from .options import ReaderConfig, EOSConfig, ExperimentConfig, OutputConfig, ReductionConfig
def commandLineArgs():
"""
Process command line argument.
The type of the default values is used for conversion and validation.
"""
msg = "eos reads data from (one or several) raw file(s) of the .hdf format, \
performs various corrections, conversations and projections and exports\
the resulting reflectivity in an orso-compatible format."
clas = argparse.ArgumentParser(description = msg)
input_data = clas.add_argument_group('input data')
input_data.add_argument("-n", "--fileIdentifier",
default = ['0'],
nargs = '+',
help = "file number(s) or offset (if negative)")
input_data.add_argument("-r", "--normalisationFileIdentifier",
default = [],
nargs = '+',
help = "file number(s) of normalisation measurement")
input_data.add_argument("-d", "--dataPath",
type = str,
default = '.',
help = "relative path to directory with .hdf files")
input_data.add_argument("-Y", "--year",
default = datetime.year,
type = int,
help = "year the measurement was performed")
input_data.add_argument("-sub", "--subtract",
help = "R(q_z) curve to be subtracted (in .Rqz.ort format)")
output = clas.add_argument_group('output')
output.add_argument("-o", "--outputName",
default = "fromEOS",
help = "output file name (withot suffix)")
output.add_argument("-of", "--outputFormat",
nargs = '+',
default = ['Rqz.ort'])
output.add_argument("--offSpecular",
type = bool,
default = False,
)
output.add_argument("-a", "--qResolution",
default = 0.01,
type = float,
help = "q_z resolution")
output.add_argument("-ts", "--timeSlize",
nargs = '+',
type = float,
help = "time slizing <interval> ,[<start> [,stop]]")
output.add_argument("-s", "--scale",
nargs = '+',
default = [1],
type = float,
help = "scaling factor for R(q_z)")
output.add_argument("-S", "--autoscale",
nargs = 2,
type = float,
help = "scale to 1 in the given q_z range")
masks = clas.add_argument_group('masks')
masks.add_argument("-l", "--lambdaRange",
default = [2., 15.],
nargs = 2,
type = float,
help = "wavelength range")
masks.add_argument("-t", "--thetaRange",
default = [-12., 12.],
nargs = 2,
type = float,
help = "absolute theta range")
masks.add_argument("-T", "--thetaRangeR",
default = [-12., 12.],
nargs = 2,
type = float,
help = "relative theta range")
masks.add_argument("-y", "--yRange",
default = [11, 41],
nargs = 2,
type = int,
help = "detector y range")
masks.add_argument("-q", "--qzRange",
default = [0.005, 0.30],
nargs = 2,
type = float,
help = "q_z range")
overwrite = clas.add_argument_group('overwrite')
overwrite.add_argument("-cs", "--chopperSpeed",
type = float,
help = "chopper speed in rpm")
overwrite.add_argument("-cp", "--chopperPhase",
default = -13.5,
type = float,
help = "chopper phase")
overwrite.add_argument("-co", "--chopperPhaseOffset",
default = -5,
type = float,
help = "phase offset between chopper opening and trigger pulse")
overwrite.add_argument("-m", "--muOffset",
default = 0.,
type = float,
help = "mu offset")
overwrite.add_argument("-mu", "--mu",
default = 0,
type = float,
help ="value of mu")
overwrite.add_argument("-nu", "--nu",
default = 0,
type = float,
help = "value of nu")
overwrite.add_argument("-sm", "--sampleModel",
type = str,
help = "1-line orso sample model description")
misc = clas.add_argument_group('misc')
misc.add_argument('-v', '--verbose', action='store_true')
misc.add_argument('-vv', '--debug', action='store_true')
return clas.parse_args()
def expand_file_list(short_notation):
"""Evaluate string entry for file number lists"""
#log().debug('Executing get_flist')
file_list=[]
for i in short_notation.split(','):
if '-' in i:
if ':' in i:
step = i.split(':', 1)[1]
file_list += range(int(i.split('-', 1)[0]), int((i.rsplit('-', 1)[1]).split(':', 1)[0])+1, int(step))
else:
step = 1
file_list += range(int(i.split('-', 1)[0]), int(i.split('-', 1)[1])+1, int(step))
else:
file_list += [int(i)]
return sorted(file_list)
def output_format_list(outputFormat):
format_list = []
if 'ort' in outputFormat or 'Rqz.ort' in outputFormat or 'Rqz' in outputFormat:
format_list.append('Rqz.ort')
if 'ort' in outputFormat or 'Rlt.ort' in outputFormat or 'Rlt' in outputFormat:
format_list.append('Rlt.ort')
if 'orb' in outputFormat or 'Rqz.orb' in outputFormat or 'Rqz' in outputFormat:
format_list.append('Rqz.orb')
if 'orb' in outputFormat or 'Rlt.orb' in outputFormat or 'Rlt' in outputFormat:
format_list.append('Rlt.orb')
return sorted(format_list, reverse=True)
def command_line_options():
clas = commandLineArgs()
update_loglevel(clas.verbose, clas.debug)
reader_config = ReaderConfig(
year=clas.year,
dataPath=clas.dataPath)
experiment_config = ExperimentConfig(
sampleModel=clas.sampleModel,
chopperPhase=clas.chopperPhase,
chopperPhaseOffset=clas.chopperPhaseOffset,
yRange=clas.yRange,
lambdaRange=clas.lambdaRange,
qzRange=clas.qzRange,
offSpecular=clas.offSpecular,
mu=clas.mu,
nu=clas.nu,
muOffset=clas.muOffset
)
reduction_config = ReductionConfig(
qResolution=clas.qResolution,
autoscale=clas.autoscale,
thetaRange=clas.thetaRange,
thetaRangeR=clas.thetaRangeR,
fileIdentifier=clas.fileIdentifier,
scale=clas.scale,
subtract=clas.subtract,
normalisationFileIdentifier=clas.normalisationFileIdentifier,
timeSlize=clas.timeSlize
)
output_config = OutputConfig(
outputFormats=output_format_list(clas.outputFormat),
outputName=clas.outputName
)
return EOSConfig(reader_config, experiment_config, reduction_config, output_config)
+6
View File
@@ -0,0 +1,6 @@
"""
Constants used in data reduction.
"""
hdm = 6.626176e-34/1.674928e-27 # h / m
lamdaCut = 2.5 # Aa
+351
View File
@@ -0,0 +1,351 @@
import logging
import os
import subprocess
import sys
from datetime import datetime
from typing import List
import h5py
import numpy as np
from orsopy import fileio
from . import const
from .header import Header
from .instrument import Detector
from .options import ExperimentConfig, ReaderConfig
try:
from . import nb_helpers
except Exception:
nb_helpers = None
class AmorData:
"""read meta-data and event streams from .hdf file(s), apply filters and conversions"""
chopperDetectorDistance: float
chopperDistance: float
chopperPhase: float
chopperSpeed: float
ctime: float
div: float
data_file_numbers: List[int]
delta_z: np.ndarray
detZ_e: np.ndarray
lamda_e: np.ndarray
wallTime_e: np.ndarray
kad: float
kap: float
lambdaMax: float
lambda_e: np.ndarray
monitor1: float
monitor2: float
mu: float
nu: float
tau: float
tofCut: float
start_date: str
#-------------------------------------------------------------------------------------------------
def __init__(self, header: Header, reader_config: ReaderConfig, config: ExperimentConfig,
short_notation:str, norm=False):
self.startTime = reader_config.startTime
self.header = header
self.config = config
self.reader_config = reader_config
self.expand_file_list(short_notation)
self.read_data(norm=norm)
#-------------------------------------------------------------------------------------------------
def read_data(self, norm=False):
self.file_list = []
for number in self.data_file_numbers:
self.file_list.append(self.path_generator(number))
## read specific meta data and measurement from first file
if norm:
self.readHeaderInfo = False
else:
self.readHeaderInfo = True
_detZ_e = []
_lamda_e = []
_wallTime_e = []
for file in self.file_list:
self.read_individual_data(file, norm)
_detZ_e = np.append(_detZ_e, self.detZ_e)
_lamda_e = np.append(_lamda_e, self.lamda_e)
_wallTime_e = np.append(_wallTime_e, self.wallTime_e)
self.detZ_e = _detZ_e
self.lamda_e = _lamda_e
self.wallTime_e = _wallTime_e
#-------------------------------------------------------------------------------------------------
def path_generator(self, number):
fileName = f'amor{self.reader_config.year}n{number:06d}.hdf'
if os.path.exists(os.path.join(self.reader_config.dataPath,fileName)):
path = self.reader_config.dataPath
elif os.path.exists(fileName):
path = '.'
elif os.path.exists(os.path.join('.','raw', fileName)):
path = os.path.join('.','raw')
elif os.path.exists(os.path.join('..','raw', fileName)):
path = os.path.join('..','raw')
elif os.path.exists(f'/afs/psi.ch/project/sinqdata/{self.reader_config.year}/amor/{int(number/1000)}/{fileName}'):
path = f'/afs/psi.ch/project/sinqdata/{self.reader_config.year}/amor/{int(number/1000)}'
else:
sys.exit(f'# ERROR: the file {fileName} is nowhere to be found!')
return os.path.join(path, fileName)
#-------------------------------------------------------------------------------------------------
def expand_file_list(self, short_notation):
"""Evaluate string entry for file number lists"""
#log().debug('Executing get_flist')
file_list=[]
for i in short_notation.split(','):
if '-' in i:
if ':' in i:
step = i.split(':', 1)[1]
file_list += range(int(i.split('-', 1)[0]), int((i.rsplit('-', 1)[1]).split(':', 1)[0])+1, int(step))
else:
step = 1
file_list += range(int(i.split('-', 1)[0]), int(i.split('-', 1)[1])+1, int(step))
else:
file_list += [int(i)]
self.data_file_numbers=sorted(file_list)
#-------------------------------------------------------------------------------------------------
def resolve_pixels(self):
"""determine spatial coordinats and angles from pixel number"""
nPixel = Detector.nWires * Detector.nStripes * Detector.nBlades
pixelID = np.arange(nPixel)
(bladeNr, bPixel) = np.divmod(pixelID, Detector.nWires * Detector.nStripes)
(bZi, detYi) = np.divmod(bPixel, Detector.nStripes) # z index on blade, y index on detector
detZi = bladeNr * Detector.nWires + bZi # z index on detector
detX = bZi * Detector.dX # x position in detector
# detZ = Detector.zero - bladeNr * Detector.bladeZ - bZi * Detector.dZ # z position on detector
bladeAngle = np.rad2deg( 2. * np.arcsin(0.5*Detector.bladeZ / Detector.distance) )
delta = (Detector.nBlades/2. - bladeNr) * bladeAngle \
- np.rad2deg( np.arctan(bZi*Detector.dZ / ( Detector.distance + bZi * Detector.dX) ) )
self.delta_z = delta[detYi==1]
return np.vstack((detYi.T, detZi.T, detX.T, delta.T)).T
#return matr
#-------------------------------------------------------------------------------------------------
def read_individual_data(self, fileName, norm=False):
self.hdf = h5py.File(fileName, 'r', swmr=True)
if self.readHeaderInfo:
self.read_header_info()
logging.info(f'# data from file: {fileName}')
self.read_individual_header()
# add header content
if self.readHeaderInfo:
self.readHeaderInfo = False
self.header.measurement_instrument_settings = fileio.InstrumentSettings(
incident_angle = fileio.ValueRange(round(self.mu+self.kap+self.kad-0.5*self.div, 3),
round(self.mu+self.kap+self.kad+0.5*self.div, 3),
'deg'),
wavelength = fileio.ValueRange(const.lamdaCut, self.config.lambdaRange[1], 'angstrom'),
polarization = fileio.Polarization.unpolarized,
)
self.header.measurement_instrument_settings.mu = fileio.Value(round(self.mu, 3), 'deg', comment='sample angle to horizon')
self.header.measurement_instrument_settings.nu = fileio.Value(round(self.nu, 3), 'deg', comment='detector angle to horizon')
if norm:
self.header.measurement_additional_files.append(fileio.File(file=fileName.split('/')[-1], timestamp=self.fileDate))
else:
self.header.measurement_data_files.append(fileio.File(file=fileName.split('/')[-1], timestamp=self.fileDate))
logging.info(f'# mu = {self.mu:6.3f}, nu = {self.nu:6.3f}, kap = {self.kap:6.3f}, kad = {self.kap:6.3f}')
# TODO: should extract monitor from counts or beam current times time
self.monitor1 = self.ctime
self.monitor2 = self.monitor1
self.read_event_stream()
totalNumber = np.shape(self.tof_e)[0]
self.extract_walltime(norm)
if True:
self.filter_strange_times()
self.merge_frames()
self.filter_project_x()
# correct tof for beam size effect at chopper: t_cor = (delta / 180 deg) * tau
# TODO: check for correctness
if not self.config.offSpecular:
self.tof_e -= ( self.delta_e / 180. ) * self.tau
self.calculate_derived_properties()
self.filter_qz_range(norm)
logging.info(f'# number of events: total = {totalNumber:7d}, filtered = {np.shape(self.lamda_e)[0]:7d}')
def filter_qz_range(self, norm):
if self.config.qzRange[1]<0.3 and not norm:
self.mask_e = np.logical_and(self.mask_e,
(self.config.qzRange[0]<=self.qz_e) & (self.qz_e<=self.config.qzRange[1]))
self.detZ_e = self.detZ_e[self.mask_e]
self.lamda_e = self.lamda_e[self.mask_e]
self.wallTime_e = self.wallTime_e[self.mask_e]
def calculate_derived_properties(self):
self.lamdaMax = const.lamdaCut+1.e13*self.tau*const.hdm/(self.chopperDetectorDistance+124.)
if nb_helpers and not self.config.offSpecular:
self.lamda_e, self.qz_e, self.mask_e = nb_helpers.calculate_derived_properties_focussing(
self.tof_e, self.detXdist_e, self.delta_e, self.mask_e,
self.config.lambdaRange[0], self.config.lambdaRange[1], self.nu, self.mu,
self.chopperDetectorDistance, const.hdm
)
return
# lambda
self.lamda_e = (1.e13*const.hdm)*self.tof_e/(self.chopperDetectorDistance+self.detXdist_e)
self.mask_e = np.logical_and(self.mask_e, (self.config.lambdaRange[0]<=self.lamda_e) & (
self.lamda_e<=self.config.lambdaRange[1]))
# alpha_f
alphaF_e = self.nu-self.mu+self.delta_e
# q_z
if self.config.offSpecular:
alphaI = self.kap+self.kad+self.mu
self.qz_e = 2*np.pi*((np.sin(np.deg2rad(alphaF_e))+np.sin(np.deg2rad(alphaI)))/self.lamda_e)
self.qx_e = 2*np.pi*((np.cos(np.deg2rad(alphaF_e))-np.cos(np.deg2rad(alphaI)))/self.lamda_e)
self.header.measurement_scheme = 'energy-dispersive',
else:
self.qz_e = 4*np.pi*(np.sin(np.deg2rad(alphaF_e))/self.lamda_e)
# qx_e = 0.
self.header.measurement_scheme = 'angle- and energy-dispersive'
def filter_project_x(self):
pixelLookUp = self.resolve_pixels()
if nb_helpers:
(self.detZ_e, self.detXdist_e, self.delta_e, self.mask_e) = nb_helpers.filter_project_x(
pixelLookUp, self.pixelID_e.astype(np.int64), self.config.yRange[0], self.config.yRange[1]
)
else:
# resolve pixel ID into y and z indicees, x position and angle
(detY_e, self.detZ_e, self.detXdist_e, self.delta_e) = pixelLookUp[np.int_(self.pixelID_e)-1, :].T
# define mask and filter y range
self.mask_e = (self.config.yRange[0]<=detY_e) & (detY_e<=self.config.yRange[1])
def merge_frames(self):
total_offset = self.tofCut+self.tau*self.config.chopperPhaseOffset/180.
if nb_helpers:
self.tof_e = nb_helpers.merge_frames(self.tof_e, self.tofCut, self.tau, total_offset)
else:
self.tof_e = np.remainder(self.tof_e-(self.tofCut-self.tau), self.tau)+total_offset # tof shifted to 1 frame
def filter_strange_times(self):
# filter 'strange' tof times > 2 tau
filter_e = (self.tof_e<=2*self.tau)
self.tof_e = self.tof_e[filter_e]
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]}')
def extract_walltime(self, norm):
if nb_helpers:
self.wallTime_e = nb_helpers.extract_walltime(self.tof_e, self.dataPacket_p, self.dataPacketTime_p)
else:
totalNumber = np.shape(self.tof_e)[0]
self.wallTime_e = np.empty(totalNumber)
for i in range(len(self.dataPacket_p)-1):
self.wallTime_e[self.dataPacket_p[i]:self.dataPacket_p[i+1]] = self.dataPacketTime_p[i]
self.wallTime_e[self.dataPacket_p[-1]:] = self.dataPacketTime_p[-1]
if not self.startTime and not norm:
self.startTime = self.wallTime_e[0]
self.wallTime_e -= self.startTime
logging.debug(f'wall time from {self.wallTime_e[0]} to {self.wallTime_e[-1]}')
def read_event_stream(self):
self.tof_e = np.array(self.hdf['/entry1/Amor/detector/data/event_time_offset'][:])/1.e9
self.pixelID_e = np.array(self.hdf['/entry1/Amor/detector/data/event_id'][:], dtype=int)
self.dataPacket_p = np.array(self.hdf['/entry1/Amor/detector/data/event_index'][:], dtype=np.uint64)
self.dataPacketTime_p = np.array(self.hdf['/entry1/Amor/detector/data/event_time_zero'][:], dtype=np.uint64)/1e9
def read_individual_header(self):
self.chopperDistance = float(np.take(self.hdf['entry1/Amor/chopper/pair_separation'], 0))
self.detectorDistance = float(np.take(self.hdf['entry1/Amor/detector/transformation/distance'], 0))
self.chopperDetectorDistance = self.detectorDistance-float(np.take(self.hdf['entry1/Amor/chopper/distance'], 0))
self.tofCut = const.lamdaCut*self.chopperDetectorDistance/const.hdm*1.e-13
try:
self.mu = float(np.take(self.hdf['/entry1/Amor/master_parameters/mu/value'], 0))
self.nu = float(np.take(self.hdf['/entry1/Amor/master_parameters/nu/value'], 0))
self.kap = float(np.take(self.hdf['/entry1/Amor/master_parameters/kap/value'], 0))
self.kad = float(np.take(self.hdf['/entry1/Amor/master_parameters/kad/value'], 0))
self.div = float(np.take(self.hdf['/entry1/Amor/master_parameters/div/value'], 0))
self.chopperSpeed = float(np.take(self.hdf['/entry1/Amor/chopper/rotation_speed/value'], 0))
self.chopperPhase = float(np.take(self.hdf['/entry1/Amor/chopper/phase/value'], 0))
except(KeyError, IndexError):
logging.warning(" using parameters from nicos cache")
year_date = str(self.start_date).replace('-', '/', 1)
cachePath = '/home/amor/nicosdata/amor/cache/'
value = str(subprocess.getoutput(f'/usr/bin/grep "value" {cachePath}nicos-mu/{year_date}')).split('\t')[-1]
self.mu = float(value)
value = str(subprocess.getoutput(f'/usr/bin/grep "value" {cachePath}nicos-nu/{year_date}')).split('\t')[-1]
self.nu = float(value)
value = str(subprocess.getoutput(f'/usr/bin/grep "value" {cachePath}nicos-kap/{year_date}')).split('\t')[-1]
self.kap = float(value)
value = str(subprocess.getoutput(f'/usr/bin/grep "value" {cachePath}nicos-kad/{year_date}')).split('\t')[-1]
self.kad = float(value)
value = str(subprocess.getoutput(f'/usr/bin/grep "value" {cachePath}nicos-div/{year_date}')).split('\t')[-1]
self.div = float(value)
value = str(subprocess.getoutput(f'/usr/bin/grep "value" {cachePath}nicos-ch1_speed/{year_date}')).split('\t')[-1]
self.chopperSpeed = float(value)
self.chopperPhase = self.config.chopperPhase
self.tau = 30. / self.chopperSpeed
if self.config.muOffset:
self.mu += self.config.muOffset
if self.config.mu:
self.mu = self.config.mu
if self.config.nu:
self.nu = self.config.nu
# TODO: figure out real stop time....
self.ctime=(self.hdf['/entry1/Amor/detector/data/event_time_zero'][-1]
- self.hdf['/entry1/Amor/detector/data/event_time_zero'][0]) / 1.e9
self.fileDate = datetime.fromisoformat( self.hdf['/entry1/start_time'][0].decode('utf-8') )
def read_header_info(self):
# read general information and first data set
logging.info(f'# meta data from: {self.file_list[0]}')
self.hdf = h5py.File(self.file_list[0], 'r', swmr=True)
title = self.hdf['entry1/title'][0].decode('utf-8')
proposal_id = self.hdf['entry1/proposal_id'][0].decode('utf-8')
user_name = self.hdf['entry1/user/name'][0].decode('utf-8')
user_affiliation = 'unknown'
user_email = self.hdf['entry1/user/email'][0].decode('utf-8')
user_orcid = None
sampleName = self.hdf['entry1/sample/name'][0].decode('utf-8')
model = self.hdf['entry1/sample/model'][0].decode('utf-8')
instrumentName = 'Amor'
source = self.hdf['entry1/Amor/source/name'][0].decode('utf-8')
sourceProbe = 'neutron'
start_time = self.hdf['entry1/start_time'][0].decode('utf-8')
self.start_date = start_time.split(' ')[0]
if self.config.sampleModel:
model = self.config.sampleModel
# assembling orso header information
self.header.owner = fileio.Person(
name=user_name,
affiliation=user_affiliation,
contact=user_email,
)
if user_orcid:
self.header.owner.orcid = user_orcid
self.header.experiment = fileio.Experiment(
title=title,
instrument=instrumentName,
start_date=self.start_date,
probe=sourceProbe,
facility=source,
proposalID=proposal_id
)
self.header.sample = fileio.Sample(
name=sampleName,
model=model,
sample_parameters=None,
)
self.header.measurement_scheme = 'angle- and energy-dispersive'
+66
View File
@@ -0,0 +1,66 @@
"""
Class to handle Orso header information that changes gradually during the reduction process.
"""
import platform
import sys
from datetime import datetime
from orsopy import fileio
from . import __version__
class Header:
"""orso compatible output file header content"""
def __init__(self):
self.owner = None
self.experiment = None
self.sample = None
self.measurement_instrument_settings = None
self.measurement_scheme = None
self.measurement_data_files = []
self.measurement_additional_files = []
self.reduction = fileio.Reduction(
software = fileio.Software('eos', version=__version__),
call = ' '.join(sys.argv),
computer = platform.node(),
timestamp = datetime.now(),
creator = None,
corrections = ['histogramming in lambda and alpha_f',
'gravity'],
)
#-------------------------------------------------------------------------------------------------
def data_source(self):
return fileio.DataSource(
self.owner,
self.experiment,
self.sample,
fileio.Measurement(
instrument_settings = self.measurement_instrument_settings,
scheme = self.measurement_scheme,
data_files = self.measurement_data_files,
additional_files = self.measurement_additional_files,
),
)
#-------------------------------------------------------------------------------------------------
def columns(self):
cols = [
fileio.Column('Qz', '1/angstrom', 'normal momentum transfer'),
fileio.Column('R', '', 'specular reflectivity'),
fileio.ErrorColumn(error_of='R', error_type='uncertainty', distribution='gaussian', value_is='sigma'),
fileio.ErrorColumn(error_of='Qz', error_type='resolution', distribution='gaussian', value_is='sigma'),
]
return cols
def orso_header(self, columns=None, extra_columns=[]):
"""
Generate ORSO header from a copy of this class' data.
"""
ds = fileio.DataSource.from_dict(self.data_source().to_dict())
red = fileio.Reduction.from_dict(self.reduction.to_dict())
if columns is None:
columns = self.columns()
return fileio.Orso(ds, red, columns+extra_columns)
+68
View File
@@ -0,0 +1,68 @@
"""
Classes describing the AMOR instrument configuration used during reduction.
"""
import logging
import numpy as np
from . import const
class Detector:
nBlades = 14 # number of active blades in the detector
nWires = 32 # number of wires per blade
nStripes = 64 # number of stipes per blade
angle = np.deg2rad(5.1) # deg angle of incidence of the beam on the blades (def: 5.1)
dZ = 4.0*np.sin(angle) # mm height-distance of neighboring pixels on one blade
dX = 4.0*np.cos(angle) # mm depth-distance of neighboring pixels on one blace
bladeZ = 10.455 # mm distance between detector blades
zero = 0.5*nBlades*bladeZ # mm vertical center of the detector
distance = 4000. # mm distance from focal point to leading blade edge
class Grid:
def __init__(self, qResolution):
self.lamdaCut = const.lamdaCut
self.dldl = 0.005 # Delta lambda / lambda
self.qResolution = qResolution
def q(self):
resolutions = [0.005, 0.01, 0.02, 0.025, 0.04, 0.05, 0.1, 1]
a, b = np.histogram([self.qResolution], bins = resolutions)
dqdq = np.matmul(b[:-1],a)
if dqdq != self.qResolution:
logging.info(f'# changed resolution to {dqdq}')
qq = 0.01
# linear up to qq
q_grid = np.arange(0, qq, qq*dqdq)
# exponential from qq on
q_grid = np.append(q_grid, qq*(1.+dqdq)**np.arange(int(np.log(0.3/qq)/np.log(1+dqdq))))
return q_grid
def lamda(self):
lamdaMax = 16
lamdaMin = self.lamdaCut
lamda_grid = lamdaMin*(1+self.dldl)**np.arange(int(np.log(lamdaMax/lamdaMin)/np.log(1+self.dldl)+1))
return lamda_grid
def z(self):
return np.arange(Detector.nBlades*Detector.nWires+1)
def lz(self):
return np.ones(( np.shape(self.lamda()[:-1])[0], np.shape(self.z()[:-1])[0] ))
def delta(self, detectorDistance):
# unused for now
bladeAngle = np.rad2deg( 2. * np.arcsin(0.5*Detector.bladeZ / detectorDistance) )
blade_grid = np.arctan( np.arange(33) * Detector.dZ / ( detectorDistance + np.arange(33) * Detector.dX) )
blade_grid = np.rad2deg(blade_grid)
stepWidth = blade_grid[1] - blade_grid[0]
blade_grid = blade_grid - 0.2 * stepWidth
delta_grid = []
for b in np.arange(Detector.nBlades-1):
delta_grid = np.concatenate((delta_grid, blade_grid), axis=None)
blade_grid = blade_grid + bladeAngle
delta_grid = delta_grid[delta_grid<blade_grid[0]-0.5*stepWidth]
delta_grid = np.concatenate((delta_grid, blade_grid), axis=None)
return -np.flip(delta_grid) + 0.5*Detector.nBlades * bladeAngle
+43
View File
@@ -0,0 +1,43 @@
"""
Setup for the logging of eos.
"""
import sys
import logging
import logging.handlers
def setup_logging():
logger = logging.getLogger() # logging.getLogger('quicknxs')
logger.setLevel(logging.DEBUG)
# rename levels to make clear warning is can be a normal message
logging.addLevelName(logging.INFO, 'VERB')
logging.addLevelName(logging.WARNING, 'MESG')
# setting up a logger for console output
console = logging.StreamHandler(sys.__stdout__)
console.name = 'console'
formatter = logging.Formatter('# %(message)s')
console.setFormatter(formatter)
console.setLevel(logging.WARNING)
logger.addHandler(console)
# if os.path.exists('amor_eos.log'):
# rollover = True
# else:
# rollover = False
logfile = logging.handlers.RotatingFileHandler('amor_eos.log', encoding='utf8', mode='w',
maxBytes=200*1024**2, backupCount=20)
# if rollover: logfile.doRollover()
formatter = logging.Formatter(
'[%(levelname).4s] - %(asctime)s - %(filename)s:%(lineno)i:%(funcName)s %(message)s',
'')
logfile.setFormatter(formatter)
logfile.setLevel(logging.DEBUG)
logger.addHandler(logfile)
def update_loglevel(verbose=False, debug=False):
if verbose:
logging.getLogger().handlers[0].setLevel(logging.INFO)
if debug:
console = logging.getLogger().handlers[0]
console.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(levelname).1s %(message)s')
console.setFormatter(formatter)
+65
View File
@@ -0,0 +1,65 @@
import numba as nb
import numpy as np
@nb.jit(nb.float64[:](nb.float64[:], nb.float64, nb.float64, nb.float64),
nopython=True, parallel=True, cache=True)
def merge_frames(tof_e, tofCut, tau, total_offset):
# fast implementation of the merging function used in file_reader.AmorData.merge_frames
tof_e_out = np.empty(tof_e.shape, dtype=np.float64)
dt = (tofCut-tau)
for ti in nb.prange(tof_e.shape[0]):
tof_e_out[ti] = ((tof_e[ti]-dt)%tau)+total_offset # tof shifted to 1 frame
return tof_e
@nb.jit(nb.float64[:](nb.float64[:], nb.uint64[:], nb.float64[:]),
nopython=True, parallel=True, cache=True)
def extract_walltime(tof_e, dataPacket_p, dataPacketTime_p):
# assigning every event the wall time of the event packet (absolute time of pulse ?start?)
totalNumber = np.shape(tof_e)[0]
wallTime_e = np.empty(totalNumber, dtype=np.float64)
for i in nb.prange(len(dataPacket_p)-1):
for j in range(dataPacket_p[i], dataPacket_p[i+1]):
wallTime_e[j] = dataPacketTime_p[i]
for j in range(dataPacket_p[-1], totalNumber):
wallTime_e[j] = dataPacketTime_p[-1]
return wallTime_e
@nb.jit(nb.types.Tuple((nb.int64[:], nb.float64[:], nb.float64[:], nb.boolean[:]))
(nb.float64[:, :], nb.int64[:], nb.int64, nb.int64),
nopython=True, parallel=True, cache=True)
def filter_project_x(pixelLookUp, pixelID_e, ymin, ymax):
# project events on z-axis and create filter for events outside of y-range
events = pixelID_e.shape[0]
detY_e = np.empty(events, dtype=np.int64)
detZ_e = np.empty(events, dtype=np.int64)
detXdist_e = np.empty(events, dtype=np.float64)
delta_e = np.empty(events, dtype=np.float64)
mask_e = np.empty(events, dtype=nb.boolean)
for i in nb.prange(events):
# resolve pixel ID into y and z indicees, x position and angle
detY_e[i] = pixelLookUp[pixelID_e[i]-1, 0]
detZ_e[i] = pixelLookUp[pixelID_e[i]-1, 1]
detXdist_e[i] = pixelLookUp[pixelID_e[i]-1, 2]
delta_e[i] = pixelLookUp[pixelID_e[i]-1, 3]
# define mask and filter y range
mask_e[i] = (ymin<=detY_e[i]) & (detY_e[i]<=ymax)
return (detZ_e, detXdist_e, delta_e, mask_e)
@nb.jit(nb.types.Tuple((nb.float64[:], nb.float64[:], nb.boolean[:]))
(nb.float64[:], nb.float64[:], nb.float64[:], nb.boolean[:],
nb.float64, nb.float64, nb.float64, nb.float64, nb.float64, nb.float64),
nopython=True, parallel=True, cache=True)
def calculate_derived_properties_focussing(tof_e, detXdist_e, delta_e, mask_e,
lmin, lmax, nu, mu, chopperDetectorDistance, hdm):
events = tof_e.shape[0]
alphaF_e = np.empty(events, dtype=np.float64)
lamda_e = np.empty(events, dtype=np.float64)
qz_e = np.empty(events, dtype=np.float64)
mask_e_out = np.empty(events, dtype=nb.boolean)
denom_f1 = 1.e13*hdm
for i in nb.prange(events):
lamda_e[i] = denom_f1*tof_e[i]/(chopperDetectorDistance+detXdist_e[i])
mask_e_out[i] = mask_e[i] & ((lmin<=lamda_e[i]) & (lamda_e[i]<=lmax))
alphaF_e[i] = nu-mu+delta_e[i]
qz_e[i] = 4*np.pi*np.sin(np.deg2rad(alphaF_e[i]))/lamda_e[i]
return lamda_e, qz_e, mask_e
+51
View File
@@ -0,0 +1,51 @@
"""
Classes for stroing various configurations needed for reduction.
"""
from dataclasses import dataclass, field
from typing import Optional, Tuple
@dataclass
class ReaderConfig:
year: int
dataPath: str
startTime: Optional[float] = 0
@dataclass
class ExperimentConfig:
chopperPhase: float
yRange: Tuple[float, float]
lambdaRange: Tuple[float, float]
qzRange: Tuple[float, float]
sampleModel: Optional[str] = None
chopperPhaseOffset: float = 0.0
mu: Optional[float] = None
nu: Optional[float] = None
muOffset: Optional[float] = None
offSpecular: bool = False
@dataclass
class ReductionConfig:
qResolution: float
thetaRange: Tuple[float, float]
thetaRangeR: Tuple[float, float]
fileIdentifier: list = field(default_factory=lambda: ["0"])
scale: list = field(default_factory=lambda: [1]) #per file scaling; if less elements than files use the last one
autoscale: Optional[Tuple[bool, bool]] = None
subtract: Optional[str] = None
normalisationFileIdentifier: Optional[list] = None
timeSlize: Optional[list] = None
@dataclass
class OutputConfig:
outputFormats: list
outputName: str
@dataclass
class EOSConfig:
reader: ReaderConfig
experiment: ExperimentConfig
reductoin: ReductionConfig
output: OutputConfig
+430
View File
@@ -0,0 +1,430 @@
import logging
import os
import sys
import numpy as np
from orsopy import fileio
from .command_line import expand_file_list
from .file_reader import AmorData
from .header import Header
from .options import EOSConfig
from .instrument import Grid
class AmorReduction:
def __init__(self, config: EOSConfig):
self.experiment_config = config.experiment
self.reader_config = config.reader
self.reduction_config = config.reductoin
self.output_config = config.output
self.grid = Grid(config.reductoin.qResolution)
self.header = Header()
def reduce(self):
if not os.path.exists(f'{self.reader_config.dataPath}'):
logging.debug(f'Creating destination path {self.reader_config.dataPath}')
os.system(f'mkdir {self.reader_config.dataPath}')
# load or create normalisation matrix
if self.reduction_config.normalisationFileIdentifier:
self.create_normalisation_map(self.reduction_config.normalisationFileIdentifier[0])
else:
self.norm_lz = self.grid.lz()
self.normAngle = 1.
logging.warning('normalisation matrix: none requested')
# load R(q_z) curve to be subtracted:
if self.reduction_config.subtract:
self.sq_q, self.sR_q, self.sdR_q, self.sFileName = self.loadRqz(self.reduction_config.subtract)
logging.warning(f'loaded background file: {self.sFileName}')
self.header.reduction.corrections.append(f'background from \'{self.sFileName}\' subtracted')
self.subtract = True
else:
self.subtract = False
# load measurement data and do the reduction
self.datasetsRqz = []
self.datasetsRlt = []
for i, short_notation in enumerate(self.reduction_config.fileIdentifier):
self.read_file_block(i, short_notation)
# output
logging.warning('output:')
if 'Rqz.ort' in self.output_config.outputFormats:
self.save_Rqz()
if 'Rlt.ort' in self.output_config.outputFormats:
self.save_Rtl()
def read_file_block(self, i, short_notation):
logging.warning('reading input:')
self.header.measurement_data_files = []
self.file_reader = AmorData(header=self.header,
reader_config=self.reader_config,
config=self.experiment_config,
short_notation=short_notation)
if self.reduction_config.timeSlize:
self.read_timeslices(i)
else:
self.read_unsliced(i)
def read_unsliced(self, i):
lamda_e = self.file_reader.lamda_e
detZ_e = self.file_reader.detZ_e
qz_lz, ref_lz, err_lz, res_lz, lamda_lz, theta_lz, int_lz, self.mask_lz = self.project_on_lz(
self.file_reader, self.norm_lz, self.normAngle, lamda_e, detZ_e)
try:
ref_lz *= self.reduction_config.scale[i]
err_lz *= self.reduction_config.scale[i]
except IndexError:
ref_lz *= self.reduction_config.scale[-1]
err_lz *= self.reduction_config.scale[-1]
if 'Rqz.ort' in self.output_config.outputFormats:
headerRqz = self.header.orso_header()
headerRqz.data_set = f'Nr {i} : mu = {self.file_reader.mu:6.3f} deg'
# projection on q-grid
q_q, R_q, dR_q, dq_q = self.project_on_qz(qz_lz, ref_lz, err_lz, res_lz, self.norm_lz, self.mask_lz)
filter_q = np.where((self.experiment_config.qzRange[0]<q_q) & (q_q<self.experiment_config.qzRange[1]),
True, False)
q_q = q_q[filter_q]
R_q = R_q[filter_q]
dR_q = dR_q[filter_q]
dq_q = dq_q[filter_q]
if self.reduction_config.autoscale:
if i==0:
R_q, dR_q = self.autoscale(q_q, R_q, dR_q)
else:
pRq_z = self.datasetsRqz[i-1].data[:, 1]
pdRq_z = self.datasetsRqz[i-1].data[:, 2]
R_q, dR_q = self.autoscale(q_q, R_q, dR_q, pRq_z, pdRq_z)
if self.subtract:
if len(q_q)==len(self.sq_q):
R_q -= self.sR_q
dR_q = np.sqrt(dR_q**2+self.sdR_q**2)
else:
logging.warning(
f'backgroung file {self.sFileName} not compatible with q_z scale ({len(self.sq_q)} vs. {len(q_q)})')
data = np.array([q_q, R_q, dR_q, dq_q]).T
orso_data = fileio.OrsoDataset(headerRqz, data)
self.datasetsRqz.append(orso_data)
if 'Rlt.ort' in self.output_config.outputFormats:
columns = [
fileio.Column('Qz', '1/angstrom', 'normal momentum transfer'),
fileio.Column('R', '', 'specular reflectivity'),
fileio.ErrorColumn(error_of='R', error_type='uncertainty', value_is='sigma'),
fileio.ErrorColumn(error_of='Qz', error_type='resolution', value_is='sigma'),
fileio.Column('lambda', 'angstrom', 'wavelength'),
fileio.Column('alpha_f', 'deg', 'final angle'),
fileio.Column('l', '', 'index of lambda-bin'),
fileio.Column('t', '', 'index of theta bin'),
fileio.Column('intensity', '', 'filtered neutron events per pixel'),
fileio.Column('norm', '', 'normalisation matrix'),
fileio.Column('mask', '', 'pixels used for calculating R(q_z)'),
]
# data_source = file_reader.data_source
ts, zs = ref_lz.shape
lindex_lz = np.tile(np.arange(1, ts+1), (zs, 1)).T
tindex_lz = np.tile(np.arange(1, zs+1), (ts, 1))
j = 0
for item in zip(
qz_lz.T,
ref_lz.T,
err_lz.T,
res_lz.T,
lamda_lz.T,
theta_lz.T,
lindex_lz.T,
tindex_lz.T,
int_lz.T,
self.norm_lz.T,
np.where(self.mask_lz, 1, 0).T
):
data = np.array(list(item)).T
headerRlt = self.header.orso_header(columns=columns)
headerRlt.data_set = f'dataset_{i}_{j+1} : alpha_f = {theta_lz[0, j]:6.3f} deg'
orso_data = fileio.OrsoDataset(headerRlt, data)
self.datasetsRlt.append(orso_data)
j += 1
def read_timeslices(self, i):
wallTime_e = self.file_reader.wallTime_e
interval = self.reduction_config.timeSlize[0]
try:
start = self.reduction_config.timeSlize[1]
except:
start = 0
try:
stop = self.reduction_config.timeSlize[2]
except:
stop = wallTime_e[-1]
# make overwriting log lines possible by removing newline at the end
logging.StreamHandler.terminator = "\r"
for ti, time in enumerate(np.arange(start, stop, interval)):
logging.warning(f' time slize {ti:4d}')
filter_e = np.where((time<wallTime_e) & (wallTime_e<time+interval), True, False)
lamda_e = self.file_reader.lamda_e[filter_e]
detZ_e = self.file_reader.detZ_e[filter_e]
qz_lz, ref_lz, err_lz, res_lz, lamda_lz, theta_lz, int_lz, mask_lz = self.project_on_lz(
self.file_reader, self.norm_lz, self.normAngle, lamda_e, detZ_e)
q_q, R_q, dR_q, dq_q = self.project_on_qz(qz_lz, ref_lz, err_lz, res_lz, self.norm_lz, mask_lz)
filter_q = np.where((self.experiment_config.qzRange[0]<q_q) & (q_q<self.experiment_config.qzRange[1]),
True, False)
q_q = q_q[filter_q]
R_q = R_q[filter_q]
dR_q = dR_q[filter_q]
dq_q = dq_q[filter_q]
if self.reduction_config.autoscale:
R_q, dR_q = self.autoscale(q_q, R_q, dR_q)
if self.subtract:
if len(q_q)==len(self.sq_q):
R_q -= self.sR_q
dR_q = np.sqrt(dR_q**2+self.sdR_q**2)
else:
self.subtract = False
logging.warning(
f'background file {self.sFileName} not compatible with q_z scale ({len(self.sq_q)} vs. {len(q_q)})')
tme_q = np.ones(np.shape(q_q))*time
data = np.array([q_q, R_q, dR_q, dq_q, tme_q]).T
headerRqz = self.header.orso_header(
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'
orso_data = fileio.OrsoDataset(headerRqz, data)
self.datasetsRqz.append(orso_data)
# reset normal logging behavior
logging.StreamHandler.terminator = "\n"
logging.warning(f' time slize {ti:4d}, done')
def save_Rqz(self):
fname = os.path.join(self.reader_config.dataPath, f'{self.output_config.outputName}.Rqz.ort')
logging.warning(f' {fname}')
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):
fname = os.path.join(self.reader_config.dataPath, f'{self.output_config.outputName}.Rlt.ort')
logging.warning(f' {fname}')
theSecondLine = f' {self.header.experiment.title} | {self.header.experiment.start_date} | sample {self.header.sample.name} | R(lambda, theta)'
fileio.save_orso(self.datasetsRlt, fname, data_separator='\n', comment=theSecondLine)
def autoscale(self, q_q, R_q, dR_q, pR_q=[], pdR_q=[]):
autoscale = self.reduction_config.autoscale
if len(pR_q) == 0:
filter_q = np.where((autoscale[0]<=q_q)&(q_q<=autoscale[1]), True, False)
filter_q = np.where(dR_q>0, filter_q, False)
if len(filter_q[filter_q]) > 0:
scale = np.sum(R_q[filter_q]**2/dR_q[filter_q]) / np.sum(R_q[filter_q]/dR_q[filter_q])
else:
logging.warning(f'# automatic scaling not possible')
scale = 1.
else:
filter_q = np.where(np.isnan(pR_q*R_q), False, True)
filter_q = np.where(R_q>0, filter_q, False)
filter_q = np.where(pR_q>0, filter_q, False)
if len(filter_q[filter_q]) > 0:
scale = np.sum(R_q[filter_q]**3 * pR_q[filter_q] / (dR_q[filter_q]**2 * pdR_q[filter_q]**2)) \
/ np.sum(R_q[filter_q]**2 * pR_q[filter_q]**2 / (dR_q[filter_q]**2 * pdR_q[filter_q]**2))
else:
logging.warning(f'# automatic scaling not possible')
scale = 1.
R_q /= scale
dR_q /= scale
logging.debug(f'# scaling factor = {scale}')
return R_q, dR_q
def project_on_qz(self, q_lz, R_lz, dR_lz, dq_lz, norm_lz, mask_lz):
q_q = self.grid.q()
mask_lzf = mask_lz.flatten()
q_lzf = q_lz.flatten()[mask_lzf]
R_lzf = R_lz.flatten()[mask_lzf]
dR_lzf = dR_lz.flatten()[mask_lzf]
dq_lzf = dq_lz.flatten()[mask_lzf]
norm_lzf = norm_lz.flatten()[mask_lzf]
N_q = np.histogram(q_lzf, bins = q_q, weights = norm_lzf )[0]
N_q = np.where(N_q > 0, N_q, np.nan)
R_q = np.histogram(q_lzf, bins = q_q, weights = norm_lzf * R_lzf )[0]
R_q = R_q / N_q
dR_q = np.histogram(q_lzf, bins = q_q, weights = (norm_lzf * dR_lzf)**2 )[0]
dR_q = np.sqrt( dR_q ) / N_q
# TODO: different error propagations for dR and dq!
N_q = np.histogram(q_lzf, bins = q_q, weights = norm_lzf**2 )[0]
N_q = np.where(N_q > 0, N_q, np.nan)
dq_q = np.histogram(q_lzf, bins = q_q, weights = (norm_lzf * dq_lzf)**2 )[0]
dq_q = np.sqrt( dq_q / N_q )
q_q = 0.5 * (q_q + np.roll(q_q, 1))
return q_q[1:], R_q, dR_q, dq_q
def loadRqz(self, name):
fname = os.path.join(self.reader_config.dataPath, name)
if os.path.exists(fname):
fileName = fname
elif os.path.exists(f'{fname}.Rqz.ort'):
fileName = f'{fname}.Rqz.ort'
else:
sys.exit(f'### the background file \'{fname}\' does not exist! => stopping')
q_q, Sq_q, dS_q = np.loadtxt(fileName, usecols=(0, 1, 2), comments='#', unpack=True)
return q_q, Sq_q, dS_q, fileName
def create_normalisation_map(self, short_notation):
dataPath = self.reader_config.dataPath
normalisation_list = expand_file_list(short_notation)
name = str(normalisation_list[0])
for i in range(1, len(normalisation_list), 1):
name = f'{name}_{normalisation_list[i]}'
n_path = os.path.join(dataPath, f'{name}.norm')
if os.path.exists(n_path):
logging.info(f'# normalisation matrix: found and using {n_path}')
self.norm_lz = np.loadtxt(f'{dataPath}/{name}.norm')
with open(n_path, 'r') as fh:
fh.readline()
self.normFileList = fh.readline().split('[')[1].split(']')[0].replace('\'', '').split(', ')
self.normAngle = float(fh.readline().split('= ')[1])
for i, entry in enumerate(self.normFileList):
self.normFileList[i] = entry.split('/')[-1]
self.header.measurement_additional_files = self.normFileList
else:
logging.info(f'# normalisation matrix: using the files {normalisation_list}')
fromHDF = AmorData(header=self.header,
reader_config=self.reader_config,
config=self.experiment_config,
short_notation=short_notation, norm=True)
self.normAngle = fromHDF.nu - fromHDF.mu
lamda_e = fromHDF.lamda_e
detZ_e = fromHDF.detZ_e
self.norm_lz, bins_l, bins_z = np.histogram2d(lamda_e, detZ_e, bins = (self.grid.lamda(), self.grid.z()))
self.norm_lz = np.where(self.norm_lz>0, self.norm_lz, np.nan)
# correct for the SM reflectivity
lamda_l = self.grid.lamda()
theta_z = self.normAngle + fromHDF.delta_z
lamda_lz = (self.grid.lz().T*lamda_l[:-1]).T
theta_lz = self.grid.lz()*theta_z
qz_lz = 4.0*np.pi * np.sin(np.deg2rad(theta_lz)) / lamda_lz
Rsm_lz = np.ones(np.shape(qz_lz))
Rsm_lz = np.where(qz_lz>0.0217, 1-(qz_lz-0.0217)*(0.0625/0.0217), Rsm_lz)
Rsm_lz = np.where(qz_lz>0.0217*5, np.nan, Rsm_lz)
self.norm_lz = self.norm_lz / Rsm_lz
if len(lamda_e) > 1e6:
head = ('normalisation matrix based on the measurements\n'
f'{fromHDF.file_list}\n'
f'nu - mu = {self.normAngle}\n'
f'shape= {np.shape(self.norm_lz)} (lambda, z)\n'
f'measured at mu = {fromHDF.mu:6.3f} deg\n'
f'N(l_lambda, z) = theta(z) / sum_i=-1..1 I(l_lambda+i, z)')
head = head.replace('../', '')
head = head.replace('./', '')
head = head.replace('raw/', '')
np.savetxt(f'{dataPath}/{name}.norm', self.norm_lz, header = head)
self.normFileList = fromHDF.file_list
self.header.reduction.corrections.append('normalisation with \'additional files\'')
def project_on_lz(self, fromHDF, norm_lz, normAngle, lamda_e, detZ_e):
# projection on lambda-z-grid
lamda_l = self.grid.lamda()
theta_z = fromHDF.nu - fromHDF.mu + fromHDF.delta_z
lamda_lz = (self.grid.lz().T*lamda_l[:-1]).T
theta_lz = self.grid.lz()*theta_z
thetaN_z = fromHDF.delta_z + normAngle
thetaN_lz = np.ones(np.shape(norm_lz))*thetaN_z
thetaN_lz = np.where(np.absolute(thetaN_lz)>5e-3, thetaN_lz, np.nan)
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(theta_lz)>5e-3, True, False))
if self.reduction_config.thetaRange[1]<12:
mask_lz = np.logical_and(mask_lz, np.where(theta_lz >= self.reduction_config.thetaRange[0], True, False))
mask_lz = np.logical_and(mask_lz, np.where(theta_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(theta_lz-t0 >= self.reduction_config.thetaRangeR[0], True, False))
mask_lz = np.logical_and(mask_lz, np.where(theta_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))
# gravity correction
#theta_lz += np.rad2deg( np.arctan( 3.07e-10 * (fromHDF.detectorDistance + detXdist_e) * lamda_lz**2 ) )
theta_lz += np.rad2deg( np.arctan( 3.07e-10 * fromHDF.detectorDistance * lamda_lz**2 ) )
z_z = enumerate(theta_z)
qz_lz = 4.0*np.pi * np.sin(np.deg2rad(theta_lz)) / lamda_lz
int_lz, bins_l, bins_z = np.histogram2d(lamda_e, detZ_e, bins = (lamda_l, self.grid.z()))
# cut normalisation sample horizon
int_lz = np.where(mask_lz, int_lz, np.nan)
thetaF_lz = np.where(mask_lz, theta_lz, np.nan)
ref_lz = (int_lz * np.absolute(thetaN_lz)) / (norm_lz * np.absolute(thetaF_lz))
err_lz = ref_lz * np.sqrt( 1/(int_lz+.1) + 1/norm_lz )
res_lz = np.ones((np.shape(lamda_l[:-1])[0], np.shape(theta_z)[0])) * 0.022**2
res_lz = res_lz + (0.008/theta_lz)**2
res_lz = qz_lz * np.sqrt(res_lz)
return qz_lz, ref_lz, err_lz, res_lz, lamda_lz, theta_lz, int_lz, mask_lz
@staticmethod
def histogram2d_lz(lamda_e, detZ_e, bins):
"""
Perform binning operation equivalent to numpy bin2d for the sepcial case
of the second dimension using integer positions (pre-defined pixels).
Based on the devide_bin algorithm below.
"""
dimension = bins[1].shape[0]-1
if not (np.array(bins[1])==np.arange(dimension+1)).all():
raise ValueError("histogram2d_lz requires second bin dimension to be contigous integer range")
binning = AmorReduction.devide_bin(lamda_e, detZ_e.astype(np.int64), bins[0], dimension)
return np.array(binning), bins[0], bins[1]
@staticmethod
def devide_bin(lambda_e, position_e, lamda_edges, dimension):
'''
Use a divide and conquer strategy to bin the data. For the actual binning the
numpy bincount function is used, as it is much faster than histogram for
counting of integer values.
:param lambda_e: Array of wavelength for each event
:param position_e: Array of positional indices for each event
:param lamda_edges: The edges of bins to be used for the histogram
:param dimension: position number of buckets in output arrray
:return: 2D list of dimensions (lambda, x) of counts
'''
if len(lambda_e)==0:
# no more events in range, return empty bins
return [np.zeros(dimension, dtype=int).tolist()]*(len(lamda_edges)-1)
if len(lamda_edges)==2:
# deepest recursion reached, all items should be within the two ToF edges
return [np.bincount(position_e, minlength=dimension).tolist()]
# split all events into two time of flight regions
split_idx = len(lamda_edges)//2
left_region = lambda_e<lamda_edges[split_idx]
left_list = AmorReduction.devide_bin(lambda_e[left_region], position_e[left_region],
lamda_edges[:split_idx+1], dimension)
right_region = np.logical_not(left_region)
right_list = AmorReduction.devide_bin(lambda_e[right_region], position_e[right_region],
lamda_edges[split_idx:], dimension)
return left_list+right_list
+16 -892
View File
@@ -1,9 +1,9 @@
#!/usr/bin/env python
#-*- coding: utf-8 -*-
"""
eos reduces measurements performed on Amor@SINQ, PSI
Author: Jochen Stahn
Author: Jochen Stahn (algorithms, python draft),
Artur Glavic (structuring and optimisation of code)
conventions (not strictly followed, yet):
- array names end with the suffix '_x[y]' with the meaning
@@ -14,909 +14,33 @@ conventions (not strictly followed, yet):
_z = detector z
_lz = (lambda, detector z)
_q = q_z
- to come
"""
__version__ = '2.0'
__date__ = '2024-03-01'
import os
import sys
import subprocess
import logging
import argparse
import numpy as np
from datetime import datetime
import h5py
from orsopy import fileio
import platform
from libeos.command_line import command_line_options
from libeos.logconfig import setup_logging
from libeos.reduction import AmorReduction
#=====================================================================================================
# TODO:
# - calculate resolution using the chopperPhase
# - deal with background correction
# - format of 'call' + add '-Y' if not supplied
#=====================================================================================================
def commandLineArgs():
'''
Process command line argument.
The type of the default values is used for conversion and validation.
'''
msg = "eos reads data from (one or several) raw file(s) of the .hdf format, \
performs various corrections, conversations and projections and exports\
the resulting reflectivity in an orso-compatible format."
clas = argparse.ArgumentParser(description = msg)
input_data = clas.add_argument_group('input data')
input_data.add_argument("-n", "--fileIdentifier",
default = ['0'],
nargs = '+',
help = "file number(s) or offset (if negative)")
input_data.add_argument("-r", "--normalisationFileIdentifier",
default = [],
nargs = '+',
help = "file number(s) of normalisation measurement")
input_data.add_argument("-d", "--dataPath",
type = str,
default = '.',
help = "relative path to directory with .hdf files")
input_data.add_argument("-Y", "--year",
default = datetime.year,
type = int,
help = "year the measurement was performed")
input_data.add_argument("-sub", "--subtract",
help = "R(q_z) curve to be subtracted (in .Rqz.ort format)")
output = clas.add_argument_group('output')
output.add_argument("-o", "--outputName",
default = "fromEOS",
help = "output file name (withot suffix)")
output.add_argument("-of", "--outputFormat",
nargs = '+',
default = ['Rqz.ort'])
output.add_argument("--offSpecular",
type = bool,
default = False,
)
output.add_argument("-a", "--qResolution",
default = 0.01,
type = float,
help = "q_z resolution")
output.add_argument("-ts", "--timeSlize",
nargs = '+',
type = float,
help = "time slizing <interval> ,[<start> [,stop]]")
output.add_argument("-s", "--scale",
nargs = '+',
default = [1],
type = float,
help = "scaling factor for R(q_z)")
output.add_argument("-S", "--autoscale",
nargs = 2,
type = float,
help = "scale to 1 in the given q_z range")
masks = clas.add_argument_group('masks')
masks.add_argument("-l", "--lambdaRange",
default = [2., 15.],
nargs = 2,
type = float,
help = "wavelength range")
masks.add_argument("-t", "--thetaRange",
default = [-12., 12.],
nargs = 2,
type = float,
help = "absolute theta range")
masks.add_argument("-T", "--thetaRangeR",
default = [-12., 12.],
nargs = 2,
type = float,
help = "relative theta range")
masks.add_argument("-y", "--yRange",
default = [11, 41],
nargs = 2,
type = int,
help = "detector y range")
masks.add_argument("-q", "--qzRange",
default = [0.005, 0.30],
nargs = 2,
type = float,
help = "q_z range")
overwrite = clas.add_argument_group('overwrite')
overwrite.add_argument("-cs", "--chopperSpeed",
type = float,
help = "chopper speed in rpm")
overwrite.add_argument("-cp", "--chopperPhase",
default = -13.5,
type = float,
help = "chopper phase")
overwrite.add_argument("-co", "--chopperPhaseOffset",
default = -5,
type = float,
help = "phase offset between chopper opening and trigger pulse")
overwrite.add_argument("-m", "--muOffset",
default = 0.,
type = float,
help = "mu offset")
overwrite.add_argument("-mu", "--mu",
default = 0,
type = float,
help ="value of mu")
overwrite.add_argument("-nu", "--nu",
default = 0,
type = float,
help = "value of nu")
overwrite.add_argument("-sm", "--sampleModel",
type = str,
help = "1-line orso sample model description")
return clas.parse_args()
#=====================================================================================================
class Defs:
'''definition of a series of fixed parameters and constants'''
hdm = 6.626176e-34/1.674928e-27 # h / m
lamdaCut = 2.5 # Aa
#=====================================================================================================
class Header:
'''orso compatible output file header content'''
def __init__(self):
self.owner = None
self.experiment = None
self.sample = None
self.measurement_instrument_settings = None
self.measurement_scheme = None
self.measurement_data_files = []
self.measurement_additional_files = []
self.reduction = fileio.Reduction(
software = fileio.Software('eos', version=__version__),
call = ' '.join(sys.argv),
computer = platform.node(),
timestamp = datetime.now(),
creator = None,
corrections = ['histogramming in lambda and alpha_f',
'gravity'],
)
#-------------------------------------------------------------------------------------------------
def data_source(self):
return fileio.DataSource(
self.owner,
self.experiment,
self.sample,
fileio.Measurement(
instrument_settings = self.measurement_instrument_settings,
scheme = self.measurement_scheme,
data_files = self.measurement_data_files,
additional_files = self.measurement_additional_files,
),
)
#-------------------------------------------------------------------------------------------------
def columns(self):
cols = [
fileio.Column('Qz', '1/angstrom', 'normal momentum transfer'),
fileio.Column('R', '', 'specular reflectivity'),
fileio.ErrorColumn(error_of='R', error_type='uncertainty', distribution='gaussian', value_is='sigma'),
fileio.ErrorColumn(error_of='Qz', error_type='resolution', distribution='gaussian', value_is='sigma'),
]
return cols
#=====================================================================================================
class AmorData:
'''read meta-data and event streams from .hdf file(s), apply filters and conversions'''
#-------------------------------------------------------------------------------------------------
def __init__(self):
global startTime
self.startTime = startTime
#-------------------------------------------------------------------------------------------------
def read_data(self, short_notation, norm=False):
self.data_file_numbers = self.expand_file_list(short_notation)
#self.year = clas.year
self.file_list = []
for number in self.data_file_numbers:
self.file_list.append(self.path_generator(number))
## read specific meta data and measurement from first file
if norm:
self.readHeaderInfo = False
else:
self.readHeaderInfo = True
_detZ_e = []
_lamda_e = []
_wallTime_e = []
for file in self.file_list:
self.read_individual_data(file, norm)
_detZ_e = np.append(_detZ_e, self.detZ_e)
_lamda_e = np.append(_lamda_e, self.lamda_e)
_wallTime_e = np.append(_wallTime_e, self.wallTime_e)
self.detZ_e = _detZ_e
self.lamda_e = _lamda_e
self.wallTime_e = _wallTime_e
#-------------------------------------------------------------------------------------------------
def path_generator(self, number):
fileName = f'amor{clas.year}n{number:06d}.hdf'
if os.path.exists(f'{clas.dataPath}/{fileName}'):
path = clas.dataPath
elif os.path.exists(fileName):
path = '.'
elif os.path.exists(f'./raw/{fileName}'):
path = './raw'
elif os.path.exists(f'../raw/{fileName}'):
path = '../raw'
elif os.path.exists(f'/afs/psi.ch/project/sinqdata/{clas.year}/amor/{int(number/1000)}/{fileName}'):
path = '/afs/psi.ch/project/sinqdata/{clas.year}/amor/{int(number/1000)}'
else:
sys.exit(f'# ERROR: the file {fileName} is nowhere to be found!')
return f'{path}/{fileName}'
#-------------------------------------------------------------------------------------------------
def expand_file_list(self, short_notation):
'''Evaluate string entry for file number lists'''
#log().debug('Executing get_flist')
file_list=[]
for i in short_notation.split(','):
if '-' in i:
if ':' in i:
step = i.split(':', 1)[1]
file_list += range(int(i.split('-', 1)[0]), int((i.rsplit('-', 1)[1]).split(':', 1)[0])+1, int(step))
else:
step = 1
file_list += range(int(i.split('-', 1)[0]), int(i.split('-', 1)[1])+1, int(step))
else:
file_list += [int(i)]
return sorted(file_list)
#-------------------------------------------------------------------------------------------------
def resolve_pixels(self):
'''determine spatial coordinats and angles from pixel number'''
det = Detector()
nPixel = det.nWires * det.nStripes * det.nBlades
pixelID = np.arange(nPixel)
(bladeNr, bPixel) = np.divmod(pixelID, det.nWires * det.nStripes)
(bZi, detYi) = np.divmod(bPixel, det.nStripes) # z index on blade, y index on detector
detZi = bladeNr * det.nWires + bZi # z index on detector
detX = bZi * det.dX # x position in detector
# detZ = det.zero - bladeNr * det.bladeZ - bZi * det.dZ # z position on detector
bladeAngle = np.rad2deg( 2. * np.arcsin(0.5*det.bladeZ / det.distance) )
delta = (det.nBlades/2. - bladeNr) * bladeAngle \
- np.rad2deg( np.arctan(bZi*det.dZ / ( det.distance + bZi * det.dX) ) )
self.delta_z = delta[detYi==1]
return np.vstack((detYi.T, detZi.T, detX.T, delta.T)).T
#return matr
#-------------------------------------------------------------------------------------------------
def read_individual_data(self, fileName, norm=False):
defs = Defs()
pixelLookUp = self.resolve_pixels()
self.hdf = h5py.File(fileName, 'r', swmr=True)
if self.readHeaderInfo:
# read general information and first data set
print(f'# meta data from: {self.file_list[0]}')
self.hdf = h5py.File(self.file_list[0], 'r', swmr=True)
title = self.hdf['entry1/title'][0].decode('utf-8')
proposal_id = self.hdf['entry1/proposal_id'][0].decode('utf-8')
user_name = self.hdf['entry1/user/name'][0].decode('utf-8')
user_affiliation = 'unknown'
user_email = self.hdf['entry1/user/email'][0].decode('utf-8')
user_orcid = None
sampleName = self.hdf['entry1/sample/name'][0].decode('utf-8')
model = self.hdf['entry1/sample/model'][0].decode('utf-8')
instrumentName = 'Amor'
source = self.hdf['entry1/Amor/source/name'][0].decode('utf-8')
sourceProbe = 'neutron'
start_time = self.hdf['entry1/start_time'][0].decode('utf-8')
start_date = start_time.split(' ')[0]
if clas.sampleModel:
model = clas.sampleModel
else:
model = None
# assembling orso header information
header.owner = fileio.Person(
name = user_name,
affiliation = user_affiliation,
contact = user_email,
)
if user_orcid:
header.owner.orcid = user_orcid
header.experiment = fileio.Experiment(
title = title,
instrument = instrumentName,
start_date = start_date,
probe = sourceProbe,
facility = source,
proposalID = proposal_id
)
header.sample = fileio.Sample(
name = sampleName,
model = model,
sample_parameters = None,
)
header.measurement_scheme = 'angle- and energy-dispersive'
self.chopperDistance = float(np.take(self.hdf['entry1/Amor/chopper/pair_separation'], 0))
self.detectorDistance = float(np.take(self.hdf['entry1/Amor/detector/transformation/distance'], 0))
self.chopperDetectorDistance = self.detectorDistance - float(np.take(self.hdf['entry1/Amor/chopper/distance'], 0))
self.tofCut = defs.lamdaCut * self.chopperDetectorDistance / defs.hdm * 1.e-13
print(f'# data from file: {fileName}')
try:
self.mu = float(np.take(self.hdf['/entry1/Amor/master_parameters/mu/value'], 0))
self.nu = float(np.take(self.hdf['/entry1/Amor/master_parameters/nu/value'], 0))
self.kap = float(np.take(self.hdf['/entry1/Amor/master_parameters/kap/value'], 0))
self.kad = float(np.take(self.hdf['/entry1/Amor/master_parameters/kad/value'], 0))
self.div = float(np.take(self.hdf['/entry1/Amor/master_parameters/div/value'], 0))
self.chopperSpeed = float(np.take(self.hdf['/entry1/Amor/chopper/rotation_speed/value'], 0))
self.chopperPhase = float(np.take(self.hdf['/entry1/Amor/chopper/phase/value'], 0))
except(KeyError, IndexError):
logging.warning(" using parameters from nicos cache")
year_date = str(self.start_date).replace('-', '/', 1)
cachePath = '/home/amor/nicosdata/amor/cache/'
value = str(subprocess.getoutput(f'/usr/bin/grep "value" {cachePath}nicos-mu/{year_date}')).split('\t')[-1]
self.mu = float(value)
value = str(subprocess.getoutput(f'/usr/bin/grep "value" {cachePath}nicos-nu/{year_date}')).split('\t')[-1]
self.nu = float(value)
value = str(subprocess.getoutput(f'/usr/bin/grep "value" {cachePath}nicos-kap/{year_date}')).split('\t')[-1]
self.kap = float(value)
value = str(subprocess.getoutput(f'/usr/bin/grep "value" {cachePath}nicos-kad/{year_date}')).split('\t')[-1]
self.kad = float(value)
value = str(subprocess.getoutput(f'/usr/bin/grep "value" {cachePath}nicos-div/{year_date}')).split('\t')[-1]
self.div = float(value)
value = str(subprocess.getoutput(f'/usr/bin/grep "value" {cachePath}nicos-ch1_speed/{year_date}')).split('\t')[-1]
self.chopperSpeed = float(value)
self.chopperPhase = clas.chopperPhase
self.tau = 30. / self.chopperSpeed
if clas.muOffset:
self.mu += clas.muOffset
if clas.mu:
self.mu = clas.mu
if clas.nu:
self.mu = clas.nu
# TODO: figure out real stop time....
self.ctime=(self.hdf['/entry1/Amor/detector/data/event_time_zero'][-1]
- self.hdf['/entry1/Amor/detector/data/event_time_zero'][0]) / 1.e9
fileDate = datetime.fromisoformat( self.hdf['/entry1/start_time'][0].decode('utf-8') )
# add header content
if self.readHeaderInfo:
self.readHeaderInfo = False
header.measurement_instrument_settings = fileio.InstrumentSettings(
incident_angle = fileio.ValueRange(round(self.mu+self.kap+self.kad-0.5*self.div, 3),
round(self.mu+self.kap+self.kad+0.5*self.div, 3),
'deg'),
wavelength = fileio.ValueRange(defs.lamdaCut, clas.lambdaRange[1], 'angstrom'),
polarization = fileio.Polarization.unpolarized,
)
header.measurement_instrument_settings.mu = fileio.Value(round(self.mu, 3), 'deg', comment='sample angle to horizon')
header.measurement_instrument_settings.nu = fileio.Value(round(self.nu, 3), 'deg', comment='detector angle to horizon')
if norm:
header.measurement_additional_files.append(fileio.File(file=fileName.split('/')[-1], timestamp=fileDate))
else:
header.measurement_data_files.append(fileio.File(file=fileName.split('/')[-1], timestamp=fileDate))
print(f'# mu = {self.mu:6.3f}, nu = {self.nu:6.3f}, kap = {self.kap:6.3f}, kad = {self.kap:6.3f}')
# TODO: should extract monitor from counts or beam current times time
self.monitor1 = self.ctime
self.monitor2=self.monitor1
# read data event streams
tof_e = np.array(self.hdf['/entry1/Amor/detector/data/event_time_offset'][:])/1.e9
pixelID_e = np.array(self.hdf['/entry1/Amor/detector/data/event_id'][:], dtype=int)
totalNumber = np.shape(tof_e)[0]
wallTime_e = np.empty(totalNumber)
dataPacket_p = np.array(self.hdf['/entry1/Amor/detector/data/event_index'][:], dtype=np.uint64)
dataPacketTime_p = np.array(self.hdf['/entry1/Amor/detector/data/event_time_zero'][:], dtype=np.uint64)/1e9
#for i, index in enumerate(dataPacket_p):
# wallTime_e[index:] = dataPacketTime_p[i]
for i in range(len(dataPacket_p)-1):
wallTime_e[dataPacket_p[i]:dataPacket_p[i+1]] = dataPacketTime_p[i]
wallTime_e[dataPacket_p[-1]:] = dataPacketTime_p[-1]
if not self.startTime and not norm:
self.startTime = wallTime_e[0]
wallTime_e -= self.startTime
#print(f'wall time from {wallTime_e[0]} to {wallTime_e[-1]}')
# filter 'strange' tof times > 2 tau
if True:
filter_e = (tof_e <= 2*self.tau)
tof_e = tof_e[filter_e]
pixelID_e = pixelID_e[filter_e]
wallTime_e = wallTime_e[filter_e]
if np.shape(filter_e)[0]-np.shape(tof_e)[0] > 0.5 :
print(f'# strange times: {np.shape(filter_e)[0]-np.shape(tof_e)[0]}')
tof_e = np.remainder( tof_e - self.tofCut + self.tau, self.tau) + self.tofCut # tof shifted to 1 frame
tof_e = tof_e + self.tau * clas.chopperPhaseOffset / 180. # correction for time offset between chopper pulse and tof zero
# resolve pixel ID into y and z indicees, x position and angle
(detY_e, detZ_e, detXdist_e, delta_e) = pixelLookUp[np.int_(pixelID_e)-1,:].T
# define mask and filter y range
mask_e = (clas.yRange[0] <= detY_e) & (detY_e <= clas.yRange[1])
# correct tof for beam size effect at chopper: t_cor = (delta / 180 deg) * tau
# TODO: check for correctness
if not clas.offSpecular:
tof_e -= ( delta_e / 180. ) * self.tau
# lambda
lamda_e = 1.e13 * tof_e * defs.hdm / (self.chopperDetectorDistance + detXdist_e)
self.lamdaMax = defs.lamdaCut + 1.e13 * self.tau * defs.hdm / (self.chopperDetectorDistance + 124.)
mask_e = np.logical_and(mask_e, (clas.lambdaRange[0] <= lamda_e) & (lamda_e <= clas.lambdaRange[1]))
# alpha_f
alphaF_e = self.nu - self.mu + delta_e
# q_z
if clas.offSpecular:
alphaI = self.kap + self.kad + self.mu
qz_e = 2 * np.pi * ( np.sin( np.deg2rad(alphaF_e) ) + np.sin( np.deg2rad( alphaI ) ) ) / lamda_e
qx_e = 2 * np.pi * ( np.cos( np.deg2rad(alphaF_e) ) - np.cos( np.deg2rad( alphaI ) ) ) / lamda_e
header.measurement_scheme = 'energy-dispersive',
else:
qz_e = 4 * np.pi * np.sin( np.deg2rad(alphaF_e) ) / lamda_e
# qx_e = 0.
header.measurement_scheme = 'angle- and energy-dispersive'
# filter q_z range
if clas.qzRange[1] < 0.3 and not norm:
mask_e = np.logical_and(mask_e, (clas.qzRange[0] <= qz_e) & (qz_e <= clas.qzRange[1]))
self.detZ_e = detZ_e[mask_e]
self.lamda_e = lamda_e[mask_e]
self.wallTime_e = wallTime_e[mask_e]
print(f'# number of events: total = {totalNumber:7d}, filtered = {np.shape(self.lamda_e)[0]:7d}')
#=====================================================================================================
class Detector:
def __init__(self):
self.nBlades = 14 # number of active blades in the detector
self.nWires = 32 # number of wires per blade
self.nStripes = 64 # number of stipes per blade
self.angle = np.deg2rad(5.1) # deg angle of incidence of the beam on the blades (def: 5.1)
self.dZ = 4.0 * np.sin(self.angle) # mm height-distance of neighboring pixels on one blade
self.dX = 4.0 * np.cos(self.angle) # mm depth-distance of neighboring pixels on one blace
self.bladeZ = 10.455 # mm distance between detector blades
self.zero = 0.5 * self.nBlades * self.bladeZ # mm vertical center of the detector
self.distance = 4000. # mm distance from focal point to leading blade edge
#=====================================================================================================
def expand_file_list(short_notation):
'''Evaluate string entry for file number lists'''
#log().debug('Executing get_flist')
file_list=[]
for i in short_notation.split(','):
if '-' in i:
if ':' in i:
step = i.split(':', 1)[1]
file_list += range(int(i.split('-', 1)[0]), int((i.rsplit('-', 1)[1]).split(':', 1)[0])+1, int(step))
else:
step = 1
file_list += range(int(i.split('-', 1)[0]), int(i.split('-', 1)[1])+1, int(step))
else:
file_list += [int(i)]
return sorted(file_list)
#=====================================================================================================
def normalisation_map(short_notation):
fromHDF = AmorData()
normalisation_list = expand_file_list(short_notation)
name = str(normalisation_list[0])
for i in range(1, len(normalisation_list), 1):
name = f'{name}_{normalisation_list[i]}'
if os.path.exists(f'{clas.dataPath}/{name}.norm'):
print(f'# normalisation matrix: found and using {clas.dataPath}/{name}.norm')
norm_lz = np.loadtxt(f'{clas.dataPath}/{name}.norm')
fh = open(f'{clas.dataPath}/{name}.norm', 'r')
fh.readline()
normFileList = fh.readline().split('[')[1].split(']')[0].replace('\'', '').split(', ')
normAngle = float(fh.readline().split('= ')[1])
fh.close()
for i, entry in enumerate(normFileList):
normFileList[i] = entry.split('/')[-1]
header.measurement_additional_files = normFileList
else:
print(f'# normalisation matrix: using the files {normalisation_list}')
fromHDF.read_data(short_notation, norm=True)
normAngle = fromHDF.nu - fromHDF.mu
lamda_e = fromHDF.lamda_e
detZ_e = fromHDF.detZ_e
norm_lz, bins_l, bins_z = np.histogram2d(lamda_e, detZ_e, bins = (grid.lamda(), grid.z()))
norm_lz = np.where(norm_lz>0, norm_lz, np.nan)
# correct for the SM reflectivity
lamda_l = grid.lamda()
theta_z = normAngle + fromHDF.delta_z
lamda_lz = (grid.lz().T*lamda_l[:-1]).T
theta_lz = grid.lz()*theta_z
qz_lz = 4.0*np.pi * np.sin(np.deg2rad(theta_lz)) / lamda_lz
Rsm_lz = np.ones(np.shape(qz_lz))
Rsm_lz = np.where(qz_lz>0.0217, 1-(qz_lz-0.0217)*(0.0625/0.0217), Rsm_lz)
Rsm_lz = np.where(qz_lz>0.0217*5, np.nan, Rsm_lz)
norm_lz = norm_lz / Rsm_lz
if len(lamda_e) > 1e6:
head = ('normalisation matrix based on the measurements\n'
f'{fromHDF.file_list}\n'
f'nu - mu = {normAngle}\n'
f'shape= {np.shape(norm_lz)} (lambda, z)\n'
f'measured at mu = {fromHDF.mu:6.3f} deg\n'
f'N(l_lambda, z) = theta(z) / sum_i=-1..1 I(l_lambda+i, z)')
head = head.replace('../', '')
head = head.replace('./', '')
head = head.replace('raw/', '')
np.savetxt(f'{clas.dataPath}/{name}.norm', norm_lz, header = head)
normFileList = fromHDF.file_list
return norm_lz, normAngle, normFileList
#=====================================================================================================
def output_format_list(outputFormat):
format_list = []
if 'ort' in outputFormat or 'Rqz.ort' in outputFormat or 'Rqz' in outputFormat:
format_list.append('Rqz.ort')
if 'ort' in outputFormat or 'Rlt.ort' in outputFormat or 'Rlt' in outputFormat:
format_list.append('Rlt.ort')
if 'orb' in outputFormat or 'Rqz.orb' in outputFormat or 'Rqz' in outputFormat:
format_list.append('Rqz.orb')
if 'orb' in outputFormat or 'Rlt.orb' in outputFormat or 'Rlt' in outputFormat:
format_list.append('Rlt.orb')
return sorted(format_list, reverse=True)
#=====================================================================================================
def project_on_lz(fromHDF, norm_lz, normAngle, lamda_e, detZ_e):
# projection on lambda-z-grid
lamda_l = grid.lamda()
theta_z = fromHDF.nu - fromHDF.mu + fromHDF.delta_z
lamda_lz = (grid.lz().T*lamda_l[:-1]).T
theta_lz = grid.lz()*theta_z
thetaN_z = fromHDF.delta_z + normAngle
thetaN_lz = np.ones(np.shape(norm_lz))*thetaN_z
thetaN_lz = np.where(np.absolute(thetaN_lz)>5e-3, thetaN_lz, np.nan)
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(theta_lz)>5e-3, True, False))
if clas.thetaRange[1]<12:
mask_lz = np.logical_and(mask_lz, np.where(theta_lz >= clas.thetaRange[0], True, False))
mask_lz = np.logical_and(mask_lz, np.where(theta_lz <= clas.thetaRange[1], True, False))
if clas.thetaRangeR[1]<12:
t0 = fromHDF.nu - fromHDF.mu
mask_lz = np.logical_and(mask_lz, np.where(theta_lz-t0 >= clas.thetaRangeR[0], True, False))
mask_lz = np.logical_and(mask_lz, np.where(theta_lz-t0 <= clas.thetaRangeR[1], True, False))
if clas.lambdaRange[1]<15:
mask_lz = np.logical_and(mask_lz, np.where(lamda_lz >= clas.lambdaRange[0], True, False))
mask_lz = np.logical_and(mask_lz, np.where(lamda_lz <= clas.lambdaRange[1], True, False))
# gravity correction
#theta_lz += np.rad2deg( np.arctan( 3.07e-10 * (fromHDF.detectorDistance + detXdist_e) * lamda_lz**2 ) )
theta_lz += np.rad2deg( np.arctan( 3.07e-10 * fromHDF.detectorDistance * lamda_lz**2 ) )
z_z = enumerate(theta_z)
qz_lz = 4.0*np.pi * np.sin(np.deg2rad(theta_lz)) / lamda_lz
int_lz, bins_l, bins_z = np.histogram2d(lamda_e, detZ_e, bins = (lamda_l, grid.z()))
# cut normalisation sample horizon
int_lz = np.where(mask_lz, int_lz, np.nan)
thetaF_lz = np.where(mask_lz, theta_lz, np.nan)
ref_lz = (int_lz * np.absolute(thetaN_lz)) / (norm_lz * np.absolute(thetaF_lz))
err_lz = ref_lz * np.sqrt( 1/(int_lz+.1) + 1/norm_lz )
res_lz = np.ones((np.shape(lamda_l[:-1])[0], np.shape(theta_z)[0])) * 0.022**2
res_lz = res_lz + (0.008/theta_lz)**2
res_lz = qz_lz * np.sqrt(res_lz)
return qz_lz, ref_lz, err_lz, res_lz, lamda_lz, theta_lz, int_lz, mask_lz
#=====================================================================================================
def project_on_qz(q_lz, R_lz, dR_lz, dq_lz, norm_lz, mask_lz):
q_q = grid.q()
mask_lzf = mask_lz.flatten()
q_lzf = q_lz.flatten()[mask_lzf]
R_lzf = R_lz.flatten()[mask_lzf]
dR_lzf = dR_lz.flatten()[mask_lzf]
dq_lzf = dq_lz.flatten()[mask_lzf]
norm_lzf = norm_lz.flatten()[mask_lzf]
N_q = np.histogram(q_lzf, bins = q_q, weights = norm_lzf )[0]
N_q = np.where(N_q > 0, N_q, np.nan)
R_q = np.histogram(q_lzf, bins = q_q, weights = norm_lzf * R_lzf )[0]
R_q = R_q / N_q
dR_q = np.histogram(q_lzf, bins = q_q, weights = (norm_lzf * dR_lzf)**2 )[0]
dR_q = np.sqrt( dR_q ) / N_q
# TODO: different error propagations for dR and dq!
N_q = np.histogram(q_lzf, bins = q_q, weights = norm_lzf**2 )[0]
N_q = np.where(N_q > 0, N_q, np.nan)
dq_q = np.histogram(q_lzf, bins = q_q, weights = (norm_lzf * dq_lzf)**2 )[0]
dq_q = np.sqrt( dq_q / N_q )
q_q = 0.5 * (q_q + np.roll(q_q, 1))
return q_q[1:], R_q, dR_q, dq_q
#=====================================================================================================
def autoscale(q_q, R_q, dR_q, pR_q=[], pdR_q=[]):
if len(pR_q) == 0:
filter_q = np.where((clas.autoscale[0]<=q_q)&(q_q<=clas.autoscale[1]), True, False)
filter_q = np.where(dR_q>0, filter_q, False)
if len(filter_q[filter_q]) > 0:
scale = np.sum(R_q[filter_q]**2/dR_q[filter_q]) / np.sum(R_q[filter_q]/dR_q[filter_q])
else:
print(f'# automatic scaling not possible')
scale = 1.
else:
filter_q = np.where(np.isnan(pR_q*R_q), False, True)
filter_q = np.where(R_q>0, filter_q, False)
filter_q = np.where(pR_q>0, filter_q, False)
if len(filter_q[filter_q]) > 0:
scale = np.sum(R_q[filter_q]**3 * pR_q[filter_q] / (dR_q[filter_q]**2 * pdR_q[filter_q]**2)) \
/ np.sum(R_q[filter_q]**2 * pR_q[filter_q]**2 / (dR_q[filter_q]**2 * pdR_q[filter_q]**2))
else:
print(f'# automatic scaling not possible')
scale = 1.
R_q /= scale
dR_q /= scale
#print(f'# scaling factor = {scale}')
return R_q, dR_q
#=====================================================================================================
def loadRqz(name):
if os.path.exists(f'{clas.dataPath}/{name}'):
fileName = f'{clas.dataPath}/{name}'
elif os.path.exists(f'{clas.dataPath}/{name}.Rqz.ort'):
fileName = f'{clas.dataPath}/{name}.Rqz.ort'
else:
sys.exit(f'### the background file \'{clas.dataPath}/{name}\' does not exist! => stopping')
q_q, Sq_q, dS_q = np.loadtxt(fileName, usecols=(0, 1, 2), comments='#', unpack=True)
return q_q, Sq_q, dS_q, fileName
#=====================================================================================================
class Grid:
def __init__(self):
self.det = Detector()
self.lamdaCut = Defs.lamdaCut
self.dldl = 0.005 # Delta lambda / lambda
def q(self):
resolutions = [0.005, 0.01, 0.02, 0.025, 0.04, 0.05, 0.1, 1]
a, b = np.histogram([clas.qResolution], bins = resolutions)
dqdq = np.matmul(b[:-1],a)
if dqdq != clas.qResolution:
print(f'# changed resolution to {dqdq}')
qq = 0.01
# linear up to qq
q_grid = np.arange(0, qq, qq*dqdq)
# exponential from qq on
q_grid = np.append(q_grid, qq*(1.+dqdq)**np.arange(int(np.log(0.3/qq)/np.log(1+dqdq))))
return q_grid
def lamda(self):
lamdaMax = 16
lamdaMin = self.lamdaCut
lamda_grid = lamdaMin*(1+self.dldl)**np.arange(int(np.log(lamdaMax/lamdaMin)/np.log(1+self.dldl)+1))
return lamda_grid
def z(self):
return np.arange(self.det.nBlades*self.det.nWires+1)
def lz(self):
return np.ones(( np.shape(self.lamda()[:-1])[0], np.shape(self.z()[:-1])[0] ))
def delta(self, detectorDistance):
# unused for now
bladeAngle = np.rad2deg( 2. * np.arcsin(0.5*self.det.bladeZ / detectorDistance) )
blade_grid = np.arctan( np.arange(33) * self.det.dZ / ( detectorDistance + np.arange(33) * self.det.dX) )
blade_grid = np.rad2deg(blade_grid)
stepWidth = blade_grid[1] - blade_grid[0]
blade_grid = blade_grid - 0.2 * stepWidth
delta_grid = []
for b in np.arange(self.det.nBlades-1):
delta_grid = np.concatenate((delta_grid, blade_grid), axis=None)
blade_grid = blade_grid + bladeAngle
delta_grid = delta_grid[delta_grid<blade_grid[0]-0.5*stepWidth]
delta_grid = np.concatenate((delta_grid, blade_grid), axis=None)
return -np.flip(delta_grid) + 0.5*self.det.nBlades * bladeAngle
#=====================================================================================================
def main():
global startTime, grid, clas, header
clas = commandLineArgs()
grid = Grid()
header = Header()
startTime = 0
if not os.path.exists(f'{clas.dataPath}'):
os.system(f'mkdir {clas.dataPath}')
fromHDF = AmorData()
print('\n######## eos - data reduction for Amor ########')
# load or create normalisation matrix
if clas.normalisationFileIdentifier:
normalise = True
norm_lz, normAngle, normFileList = normalisation_map(clas.normalisationFileIdentifier[0])
header.reduction.corrections.append('normalisation with \'additional files\'')
else:
normalise = False
norm_lz = grid.lz()
normAngle = 1.
setup_logging()
logging.warning('######## eos - data reduction for Amor ########')
print('# normalisation matrix: none requested')
# read command line arguments and generate classes holding configuration parameters
config = command_line_options()
# Create reducer with these arguments
reducer = AmorReduction(config)
# Perform actual reduction
reducer.reduce()
# load R(q_z) curve to be subtracted:
if clas.subtract:
sq_q, sR_q, sdR_q, sFileName = loadRqz(clas.subtract)
subtract = True
print(f'# loaded background file: {sFileName}')
header.reduction.corrections.append(f'background from \'{sFileName}\' subtracted')
else:
subtract = False
# load measurement data and do the reduction
datasetsRqz = []
datasetsRlt = []
for i, short_notation in enumerate(clas.fileIdentifier):
print('# reading input:')
header.measurement_data_files = []
fromHDF.read_data(short_notation)
logging.info('######## eos - finished ########')
if clas.timeSlize:
wallTime_e = fromHDF.wallTime_e
columns = header.columns() + [fileio.Column('time', 's', 'time relative to start of measurement series')]
headerRqz = fileio.Orso(header.data_source(), header.reduction, columns)
interval = clas.timeSlize[0]
try:
start = clas.timeSlize[1]
except:
start = 0
try:
stop = clas.timeSlize[2]
except:
stop = wallTime_e[-1]
for i, time in enumerate(np.arange(start, stop, interval)):
print(f'# time slize {i:4d}', end='\r')
filter_e = np.where((time < wallTime_e) & (wallTime_e < time+interval), True, False)
lamda_e = fromHDF.lamda_e[filter_e]
detZ_e = fromHDF.detZ_e[filter_e]
qz_lz, ref_lz, err_lz, res_lz, lamda_lz, theta_lz, int_lz, mask_lz = project_on_lz(fromHDF, norm_lz, normAngle, lamda_e, detZ_e)
q_q, R_q, dR_q, dq_q = project_on_qz(qz_lz, ref_lz, err_lz, res_lz, norm_lz, mask_lz)
filter_q = np.where((clas.qzRange[0] < q_q) & (q_q < clas.qzRange[1]), True, False)
q_q = q_q[filter_q]
R_q = R_q[filter_q]
dR_q = dR_q[filter_q]
dq_q = dq_q[filter_q]
if clas.autoscale:
R_q, dR_q = autoscale(q_q, R_q, dR_q)
if subtract:
if len(q_q) == len(sq_q):
R_q -= sR_q
dR_q = np.sqrt( dR_q**2 + sdR_q**2 )
else:
subtract = False
print(f'# background file {sFileName} not compatible with q_z scale ({len(sq_q)} vs. {len(q_q)})')
tme_q = np.ones(np.shape(q_q))*time
data = np.array([q_q, R_q, dR_q, dq_q, tme_q]).T
headerRqz.data_set = f'{i}: time = {time:8.1f} s to {time+interval:8.1f} s'
orso_data = fileio.OrsoDataset(headerRqz, data)
# make a copy of the header for the next iteration
headerRqz = fileio.Orso.from_dict(headerRqz.to_dict())
datasetsRqz.append(orso_data)
print('')
else:
lamda_e = fromHDF.lamda_e
detZ_e = fromHDF.detZ_e
qz_lz, ref_lz, err_lz, res_lz, lamda_lz, theta_lz, int_lz, mask_lz = project_on_lz(fromHDF, norm_lz, normAngle, lamda_e, detZ_e)
try:
ref_lz *= clas.scale[i]
err_lz *= clas.scale[i]
except:
ref_lz *= clas.scale[-1]
err_lz *= clas.scale[-1]
if 'Rqz.ort' in output_format_list(clas.outputFormat):
headerRqz = fileio.Orso(header.data_source(), header.reduction, header.columns())
headerRqz.data_set = f'Nr {i} : mu = {fromHDF.mu:6.3f} deg'
# projection on q-grid
q_q, R_q, dR_q, dq_q = project_on_qz(qz_lz, ref_lz, err_lz, res_lz, norm_lz, mask_lz)
filter_q = np.where((clas.qzRange[0] < q_q) & (q_q < clas.qzRange[1]), True, False)
q_q = q_q[filter_q]
R_q = R_q[filter_q]
dR_q = dR_q[filter_q]
dq_q = dq_q[filter_q]
if clas.autoscale:
if i == 0:
R_q, dR_q = autoscale(q_q, R_q, dR_q)
else:
pRq_z = datasetsRqz[i-1].data[:,1]
pdRq_z = datasetsRqz[i-1].data[:,2]
R_q, dR_q = autoscale(q_q, R_q, dR_q, pRq_z, pdRq_z)
if subtract:
if len(q_q) == len(sq_q):
R_q -= sR_q
dR_q = np.sqrt( dR_q**2 + sdR_q**2 )
else:
print(f'# backgroung file {sFileName} not compatible with q_z scale ({len(sq_q)} vs. {len(q_q)})')
data = np.array([q_q, R_q, dR_q, dq_q]).T
orso_data = fileio.OrsoDataset(headerRqz, data)
headerRqz = fileio.Orso(**headerRqz.to_dict())
datasetsRqz.append(orso_data)
if 'Rlt.ort' in output_format_list(clas.outputFormat):
columns = [
fileio.Column('Qz', '1/angstrom', 'normal momentum transfer'),
fileio.Column('R', '', 'specular reflectivity'),
fileio.ErrorColumn(error_of='R', error_type='uncertainty', value_is='sigma'),
fileio.ErrorColumn(error_of='Qz', error_type='resolution', value_is='sigma'),
fileio.Column('lambda', 'angstrom', 'wavelength'),
fileio.Column('alpha_f', 'deg', 'final angle'),
fileio.Column('l', '', 'index of lambda-bin'),
fileio.Column('t', '', 'index of theta bin'),
fileio.Column('intensity', '', 'filtered neutron events per pixel'),
fileio.Column('norm', '', 'normalisation matrix'),
fileio.Column('mask', '', 'pixels used for calculating R(q_z)'),
]
#data_source = fromHDF.data_source
headerRlt = fileio.Orso(header.data_source, header.reduction, columns)
ts, zs=ref_lz.shape
lindex_lz=np.tile(np.arange(1, ts+1), (zs, 1)).T
tindex_lz=np.tile(np.arange(1, zs+1), (ts, 1))
j = 0
for item in zip(
qz_lz.T,
ref_lz.T,
err_lz.T,
res_lz.T,
lamda_lz.T,
theta_lz.T,
lindex_lz.T,
tindex_lz.T,
int_lz.T,
norm_lz.T,
np.where(mask_lz, 1, 0).T
):
data = np.array(list(item)).T
headerRlt = fileio.Orso(**headerRlt.to_dict())
headerRlt.data_set = f'dataset_{i}_{j+1} : alpha_f = {theta_lz[0,j]:6.3f} deg'
orso_data = fileio.OrsoDataset(headerRlt, data)
datasetsRlt.append(orso_data)
j += 1
# output
print('# output:')
if 'Rqz.ort' in output_format_list(clas.outputFormat):
print(f'# {clas.dataPath}/{clas.outputName}.Rqz.ort')
theSecondLine = f' {header.experiment.title} | {header.experiment.start_date} | sample {header.sample.name} | R(q_z)'
fileio.save_orso(datasetsRqz, f'{clas.dataPath}/{clas.outputName}.Rqz.ort', data_separator='\n', comment=theSecondLine)
if 'Rlt.ort' in output_format_list(clas.outputFormat):
print(f'# {clas.dataPath}/{clas.outputName}.Rlt.ort')
theSecondLine = f' {header.experiment.title} | {header.experiment.start_date} | sample {header.sample.name} | R(lambda, theta)'
fileio.save_orso(datasetsRlt, f'{clas.dataPath}/{clas.outputName}.Rlt.ort', data_separator='\n', comment=theSecondLine)
print('')
#=====================================================================================================
if __name__ == '__main__':
main()
+4
View File
@@ -0,0 +1,4 @@
numpy
h5py
orsopy
numba
View File
+98
View File
@@ -0,0 +1,98 @@
import os
import cProfile
from unittest import TestCase
from libeos import options, reduction, logconfig
logconfig.setup_logging()
logconfig.update_loglevel(True, False)
class FullAmorTest(TestCase):
@classmethod
def setUpClass(cls):
cls.pr = cProfile.Profile()
@classmethod
def tearDownClass(cls):
cls.pr.dump_stats("profile_test.prof")
def setUp(self):
self.pr.enable()
self.reader_config = options.ReaderConfig(
year=2023,
dataPath=os.path.join('..', "test_data"))
def tearDown(self):
self.pr.disable()
for fi in ['test.Rqz.ort', '614.norm']:
try:
os.unlink(os.path.join(self.reader_config.dataPath, fi))
except FileNotFoundError:
pass
def test_time_slicing(self):
experiment_config = options.ExperimentConfig(
chopperPhase=-13.5,
chopperPhaseOffset=-5,
yRange=(11., 41.),
lambdaRange=(2., 15.),
qzRange=(0.005, 0.30),
offSpecular=False,
mu=0,
nu=0,
muOffset=0.0,
sampleModel='air | 10 H2O | D2O'
)
reduction_config = options.ReductionConfig(
qResolution=0.01,
thetaRange=(-12., 12.),
thetaRangeR=(-12., 12.),
fileIdentifier=["610"],
scale=[1],
normalisationFileIdentifier=[],
timeSlize=[300.0]
)
output_config = options.OutputConfig(
outputFormats=["Rqz.ort"],
outputName='test'
)
config=options.EOSConfig(self.reader_config, experiment_config, reduction_config, output_config)
# run three times to get similar timing to noslicing runs
reducer = reduction.AmorReduction(config)
reducer.reduce()
reducer = reduction.AmorReduction(config)
reducer.reduce()
reducer = reduction.AmorReduction(config)
reducer.reduce()
def test_noslicing(self):
experiment_config = options.ExperimentConfig(
chopperPhase=-13.5,
chopperPhaseOffset=-5,
yRange=(11., 41.),
lambdaRange=(2., 15.),
qzRange=(0.005, 0.30),
offSpecular=False,
mu=0,
nu=0,
muOffset=0.0
)
reduction_config = options.ReductionConfig(
qResolution=0.01,
thetaRange=(-12., 12.),
thetaRangeR=(-12., 12.),
fileIdentifier=["610", "611", "608,612-613", "609"],
scale=[1],
normalisationFileIdentifier=["614"],
autoscale=(0.005, 0.008)
)
output_config = options.OutputConfig(
outputFormats=["Rqz.ort"],
outputName='test'
)
config=options.EOSConfig(self.reader_config, experiment_config, reduction_config, output_config)
reducer = reduction.AmorReduction(config)
reducer.reduce()
# run second time to reuse norm file
reducer = reduction.AmorReduction(config)
reducer.reduce()