From 7f01f89f2b201880653c00a49af5ebb1f4673741 Mon Sep 17 00:00:00 2001 From: Artur Glavic Date: Wed, 27 Aug 2025 17:19:40 +0200 Subject: [PATCH] Start implementing new way to build command line arguments and defaults based on options classes directly --- eos.py | 4 +- libeos/command_line.py | 284 +++++++++++++++--------------------- libeos/file_reader.py | 26 ++-- libeos/logconfig.py | 6 +- libeos/options.py | 192 +++++++++++++++++++++--- libeos/reduction.py | 18 ++- tests/test_full_analysis.py | 2 + 7 files changed, 321 insertions(+), 211 deletions(-) diff --git a/eos.py b/eos.py index 61a2232..6c27520 100644 --- a/eos.py +++ b/eos.py @@ -20,7 +20,6 @@ import logging from libeos.command_line import command_line_options from libeos.logconfig import setup_logging -from libeos.reduction import AmorReduction #===================================================================================================== # TODO: @@ -35,6 +34,9 @@ def main(): # read command line arguments and generate classes holding configuration parameters config = command_line_options() + + # only import heavy module if sufficient command line parameters were provieded + from libeos.reduction import AmorReduction # Create reducer with these arguments reducer = AmorReduction(config) # Perform actual reduction diff --git a/libeos/command_line.py b/libeos/command_line.py index f77d6c6..036d95e 100644 --- a/libeos/command_line.py +++ b/libeos/command_line.py @@ -1,7 +1,7 @@ import argparse from .logconfig import update_loglevel -from .options import ReaderConfig, EOSConfig, ExperimentConfig, OutputConfig, ReductionConfig, Defaults +from .options import ReaderConfig, EOSConfig, ExperimentConfig, OutputConfig, ReductionConfig def commandLineArgs(): @@ -12,135 +12,126 @@ def commandLineArgs(): 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) + clas = argparse.ArgumentParser(description = msg, formatter_class=argparse.ArgumentDefaultsHelpFormatter) - input_data = clas.add_argument_group('input data') - input_data.add_argument("-f", "--fileIdentifier", - required = True, - nargs = '+', - help = "file number(s) or offset (if < 1)") - input_data.add_argument("-n", "--normalisationFileIdentifier", - default = Defaults.normalisationFileIdentifier, - nargs = '+', - help = "file number(s) of normalisation measurement") - input_data.add_argument("-rp", "--rawPath", - type = str, - default = Defaults.rawPath, - help = "ath to directory with .hdf files") - input_data.add_argument("-Y", "--year", - default = Defaults.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)") - input_data.add_argument("-nm", "--normalisationMethod", - default = Defaults.normalisationMethod, - help = "normalisation method: [o]verillumination, [u]nderillumination, [d]irect_beam") - input_data.add_argument("-mt", "--monitorType", - type = str, - default = Defaults.monitorType, - help = "one of [p]rotonCurrent, [t]ime or [n]eutronMonitor") + clas.add_argument('-v', '--verbose', action='count', default=0) - output = clas.add_argument_group('output') - output.add_argument("-o", "--outputName", - default = Defaults.outputName, - help = "output file name (withot suffix)") - output.add_argument("-op", "--outputPath", - type = str, - default = Defaults.outputPath, - help = "path for output") - output.add_argument("-of", "--outputFormat", - nargs = '+', - default = Defaults.outputFormat, - help = "one of [Rqz.ort, Rlt.ort]") - output.add_argument("-ai", "--incidentAngle", - type = str, - default = Defaults.incidentAngle, - help = "calulate alpha_i from [alphaF, mu, nu]", - ) - output.add_argument("-r", "--qResolution", - default = Defaults.qResolution, - type = float, - help = "q_z resolution") - output.add_argument("-ts", "--timeSlize", - nargs = '+', - type = float, - help = "time slizing ,[ [,stop]]") - output.add_argument("-s", "--scale", - nargs = '+', - default = Defaults.scale, - 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") + clas_groups = {} - masks = clas.add_argument_group('masks') - masks.add_argument("-l", "--lambdaRange", - default = Defaults.lambdaRange, - nargs = 2, - type = float, - help = "wavelength range") - masks.add_argument("-t", "--thetaRange", - default = Defaults.thetaRange, - nargs = 2, - type = float, - help = "absolute theta range") - masks.add_argument("-T", "--thetaRangeR", - default = Defaults.thetaRangeR, - nargs = 2, - type = float, - help = "relative theta range") - masks.add_argument("-y", "--yRange", - default = Defaults.yRange, - nargs = 2, - type = int, - help = "detector y range") - masks.add_argument("-q", "--qzRange", - default = Defaults.qzRange, - nargs = 2, - type = float, - help = "q_z range") - masks.add_argument("-ct", "--lowCurrentThreshold", - default = Defaults.lowCurrentThreshold, - type = float, - help = "proton current threshold for discarding neutron pulses") + all_arguments = [] + for cls in [ReaderConfig, ExperimentConfig, OutputConfig, ReductionConfig]: + all_arguments += cls.get_commandline_parameters() + all_arguments.sort() # parameters are sorted alphabetically, unless they have higher priority + for cpc in all_arguments: + if not cpc.group in clas_groups: + clas_groups[cpc.group] = clas.add_argument_group(cpc.group) + if cpc.short_form: + clas_groups[cpc.group].add_argument( + f'-{cpc.short_form}', f'--{cpc.argument}', **cpc.add_argument_args + ) + else: + clas_groups[cpc.group].add_argument( + f'--{cpc.argument}', **cpc.add_argument_args + ) - overwrite = clas.add_argument_group('overwrite') - overwrite.add_argument("-cs", "--chopperSpeed", - default = Defaults.chopperSpeed, - type = float, - help = "chopper speed in rpm") - overwrite.add_argument("-cp", "--chopperPhase", - default = Defaults.chopperPhase, - type = float, - help = "chopper phase") - overwrite.add_argument("-co", "--chopperPhaseOffset", - default = Defaults.chopperPhaseOffset, - type = float, - help = "phase offset between chopper opening and trigger pulse") - overwrite.add_argument("-m", "--muOffset", - default = Defaults.muOffset, - type = float, - help = "mu offset") - overwrite.add_argument("-mu", "--mu", - default = Defaults.mu, - type = float, - help ="value of mu") - overwrite.add_argument("-nu", "--nu", - default = Defaults.nu, - type = float, - help = "value of nu") - overwrite.add_argument("-sm", "--sampleModel", - default = Defaults.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') + # + # output = clas.add_argument_group('output') + # output.add_argument("-o", "--outputName", + # default = Defaults.outputName, + # help = "output file name (withot suffix)") + # output.add_argument("-op", "--outputPath", + # type = str, + # default = Defaults.outputPath, + # help = "path for output") + # output.add_argument("-of", "--outputFormat", + # nargs = '+', + # default = Defaults.outputFormat, + # help = "one of [Rqz.ort, Rlt.ort]") + # output.add_argument("-ai", "--incidentAngle", + # type = str, + # default = Defaults.incidentAngle, + # help = "calulate alpha_i from [alphaF, mu, nu]", + # ) + # output.add_argument("-r", "--qResolution", + # default = Defaults.qResolution, + # type = float, + # help = "q_z resolution") + # output.add_argument("-ts", "--timeSlize", + # nargs = '+', + # type = float, + # help = "time slizing ,[ [,stop]]") + # output.add_argument("-s", "--scale", + # nargs = '+', + # default = Defaults.scale, + # 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 = Defaults.lambdaRange, + # nargs = 2, + # type = float, + # help = "wavelength range") + # masks.add_argument("-t", "--thetaRange", + # default = Defaults.thetaRange, + # nargs = 2, + # type = float, + # help = "absolute theta range") + # masks.add_argument("-T", "--thetaRangeR", + # default = Defaults.thetaRangeR, + # nargs = 2, + # type = float, + # help = "relative theta range") + # masks.add_argument("-y", "--yRange", + # default = Defaults.yRange, + # nargs = 2, + # type = int, + # help = "detector y range") + # masks.add_argument("-q", "--qzRange", + # default = Defaults.qzRange, + # nargs = 2, + # type = float, + # help = "q_z range") + # masks.add_argument("-ct", "--lowCurrentThreshold", + # default = Defaults.lowCurrentThreshold, + # type = float, + # help = "proton current threshold for discarding neutron pulses") + # + # + # overwrite = clas.add_argument_group('overwrite') + # overwrite.add_argument("-cs", "--chopperSpeed", + # default = Defaults.chopperSpeed, + # type = float, + # help = "chopper speed in rpm") + # overwrite.add_argument("-cp", "--chopperPhase", + # default = Defaults.chopperPhase, + # type = float, + # help = "chopper phase") + # overwrite.add_argument("-co", "--chopperPhaseOffset", + # default = Defaults.chopperPhaseOffset, + # type = float, + # help = "phase offset between chopper opening and trigger pulse") + # overwrite.add_argument("-m", "--muOffset", + # default = Defaults.muOffset, + # type = float, + # help = "mu offset") + # overwrite.add_argument("-mu", "--mu", + # default = Defaults.mu, + # type = float, + # help ="value of mu") + # overwrite.add_argument("-nu", "--nu", + # default = Defaults.nu, + # type = float, + # help = "value of nu") + # overwrite.add_argument("-sm", "--sampleModel", + # default = Defaults.sampleModel, + # type = str, + # help = "1-line orso sample model description") return clas.parse_args() @@ -177,44 +168,11 @@ def output_format_list(outputFormat): def command_line_options(): clas = commandLineArgs() - update_loglevel(clas.verbose, clas.debug) + update_loglevel(clas.verbose) - reader_config = ReaderConfig( - year = clas.year, - rawPath = clas.rawPath, - ) - experiment_config = ExperimentConfig( - sampleModel = clas.sampleModel, - chopperSpeed = clas.chopperSpeed, - chopperPhase = clas.chopperPhase, - chopperPhaseOffset = clas.chopperPhaseOffset, - yRange = clas.yRange, - lambdaRange = clas.lambdaRange, - qzRange = clas.qzRange, - lowCurrentThreshold = clas.lowCurrentThreshold, - incidentAngle = clas.incidentAngle, - mu = clas.mu, - nu = clas.nu, - muOffset = clas.muOffset, - monitorType = clas.monitorType, - ) - reduction_config = ReductionConfig( - qResolution = clas.qResolution, - qzRange = clas.qzRange, - autoscale = clas.autoscale, - thetaRange = clas.thetaRange, - thetaRangeR = clas.thetaRangeR, - fileIdentifier = clas.fileIdentifier, - scale = clas.scale, - subtract = clas.subtract, - normalisationFileIdentifier = clas.normalisationFileIdentifier, - normalisationMethod = clas.normalisationMethod, - timeSlize = clas.timeSlize, - ) - output_config = OutputConfig( - outputFormats = output_format_list(clas.outputFormat), - outputName = clas.outputName, - outputPath = clas.outputPath, - ) + reader_config = ReaderConfig.from_args(clas) + experiment_config = ExperimentConfig.from_args(clas) + reduction_config = ReductionConfig.from_args(clas) + output_config = OutputConfig.from_args(clas) return EOSConfig(reader_config, experiment_config, reduction_config, output_config) diff --git a/libeos/file_reader.py b/libeos/file_reader.py index 5adfb66..433fd47 100644 --- a/libeos/file_reader.py +++ b/libeos/file_reader.py @@ -18,7 +18,7 @@ from orsopy.fileio.model_language import SampleModel from . import const from .header import Header from .instrument import Detector -from .options import ExperimentConfig, ReaderConfig +from .options import ExperimentConfig, IncidentAngle, MonitorType, ReaderConfig try: from . import nb_helpers @@ -162,7 +162,7 @@ class AmorData: 'deg'), wavelength = fileio.ValueRange(const.lamdaCut, self.config.lambdaRange[1], 'angstrom'), #polarization = fileio.Polarization.unpolarized, - polarization = self.polarizationConfig + polarization = fileio.Polarization(self.polarizationConfig) ) 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') @@ -189,7 +189,7 @@ class AmorData: self.associate_pulse_with_monitor() # following lines: debugging output to trace the time-offset of proton current and neutron pulses - if self.config.monitorType == 'x': + if self.config.monitorType == MonitorType.debug: cpp, t_bins = np.histogram(self.wallTime_e, self.pulseTimeS) np.savetxt('tme.hst', np.vstack((self.pulseTimeS[:-1], cpp, self.monitorPerPulse[:-1])).T) @@ -255,21 +255,21 @@ class AmorData: def read_proton_current_stream(self): self.currentTime = np.array(self.hdf['entry1/Amor/detector/proton_current/time'][:], dtype=np.int64) self.current = np.array(self.hdf['entry1/Amor/detector/proton_current/value'][:,0], dtype=float) - if self.config.monitorType == "auto": + if self.config.monitorType == MonitorType.auto: if self.current.sum() > 1: - self.monitorType = 'p' + self.monitorType = MonitorType.proton_charge logging.warn(' monitor type set to "proton current"') else: - self.monitorType = 't' + self.monitorType = MonitorType.time logging.warn(' monitor type set to "time"') def associate_pulse_with_monitor(self): - if self.config.monitorType == 'p': # protonCharge + if self.config.monitorType == MonitorType.proton_charge: self.currentTime -= np.int64(self.seriesStartTime) 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 + elif self.config.monitorType == MonitorType.time: self.monitorPerPulse = np.ones(np.shape(self.pulseTimeS)[0])*2*self.tau else: # pulses self.monitorPerPulse = np.ones(np.shape(self.pulseTimeS)[0]) @@ -288,13 +288,13 @@ class AmorData: return pulseCurrentS def average_events_per_pulse(self): - if self.config.monitorType == 'p': + if self.config.monitorType == MonitorType.proton_charge: for i, time in enumerate(self.pulseTimeS): events = np.shape(self.wallTime_e[self.wallTime_e == time])[0] 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 + #if self.config.monitorType == MonitorType.proton_charge: # fix to check for file compatibility self.totalNumber = np.shape(self.tof_e[self.tof_e<=self.stopTime])[0] if True: goodTimeS = self.pulseTimeS[self.monitorPerPulse!=0] @@ -337,7 +337,7 @@ class AmorData: def correct_for_chopper_opening(self): # correct tof for beam size effect at chopper: t_cor = (delta / 180 deg) * tau - if self.config.incidentAngle == 'alphaF': + if self.config.incidentAngle == IncidentAngle.alphaF: self.tof_e -= ( self.delta_e / 180. ) * self.tau else: # TODO: check sign of correction @@ -359,12 +359,12 @@ class AmorData: self.lamda_e<=self.config.lambdaRange[1])) # alpha_f # q_z - if self.config.incidentAngle == 'alphaF': + if self.config.incidentAngle == IncidentAngle.alphaF: alphaF_e = self.nu - self.mu + self.delta_e 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' - elif self.config.incidentAngle == 'nu': + elif self.config.incidentAngle == IncidentAngle.nu: alphaF_e = (self.nu + self.delta_e + self.kap + self.kad) / 2. self.qz_e = 4*np.pi*(np.sin(np.deg2rad(alphaF_e))/self.lamda_e) # qx_e = 0. diff --git a/libeos/logconfig.py b/libeos/logconfig.py index 6f80945..b93b104 100644 --- a/libeos/logconfig.py +++ b/libeos/logconfig.py @@ -33,10 +33,10 @@ def setup_logging(): logfile.setLevel(logging.DEBUG) logger.addHandler(logfile) -def update_loglevel(verbose=False, debug=False): - if verbose: +def update_loglevel(verbose=0): + if verbose==1: logging.getLogger().handlers[0].setLevel(logging.INFO) - if debug: + if verbose>1: console = logging.getLogger().handlers[0] console.setLevel(logging.DEBUG) formatter = logging.Formatter('%(levelname).1s %(message)s') diff --git a/libeos/options.py b/libeos/options.py index 2beedda..bb410b4 100644 --- a/libeos/options.py +++ b/libeos/options.py @@ -1,8 +1,10 @@ """ Classes for stroing various configurations needed for reduction. """ -from dataclasses import dataclass, field -from typing import Optional, Tuple +import argparse +from dataclasses import dataclass, field, Field, fields, MISSING +from enum import StrEnum +from typing import get_args, get_origin, List, Optional, Tuple, Union from datetime import datetime from os import path import numpy as np @@ -39,52 +41,196 @@ class Defaults: sampleModel = None lowCurrentThreshold = 50 # - - @dataclass -class ReaderConfig: - year: int - rawPath: Tuple[str] - startTime: Optional[float] = 0 +class CommandlineParameterConfig: + argument: str # default parameter for command line resutign ins "--argument" + add_argument_args: dict # all arguments that will be passed to add_argument method + short_form: Optional[str] = None + group: str = 'misc' + priority: int = 0 + + def __gt__(self, other): + """ + Sort required arguments first, then use priority, then name + """ + return (not self.add_argument_args.get('required', False), -self.priority, self.argument)>( + not other.add_argument_args.get('required', False), -other.priority, other.argument) + +class ArgParsable: + def __init_subclass__(cls): + # create a nice documentation string that takes help into account + cls.__doc__ = cls.__name__ + " Parameters:\n" + for key, typ in cls.__annotations__.items(): + if get_origin(typ) is Union and type(None) in get_args(typ): + optional = True + typ = get_args(typ)[0] + else: + optional = False + + value = getattr(cls, key, None) + cls.__doc__ += f" {key} ({typ.__name__})" + if isinstance(value, Field): + if value.default is not MISSING: + cls.__doc__ += f" = {value.default}" + if 'help' in value.metadata: + cls.__doc__ += f" - {value.metadata['help']}" + elif value is not None: + cls.__doc__ += f" = {value}" + if optional: + cls.__doc__ += " [Optional]" + cls.__doc__ += "\n" + return cls + + @classmethod + def get_commandline_parameters(cls) -> List[CommandlineParameterConfig]: + """ + Return a list of arguments used in building the command line parameters. + + Union types besides Optional are not supported. + """ + output = [] + for field in fields(cls): + args={} + if field.default is not MISSING: + args['default'] = field.default + args['required'] = False + elif field.default_factory is not MISSING: + args['default'] = field.default_factory() + args['required'] = False + else: + args['required'] = True + if get_origin(field.type) is Union and type(None) in get_args(field.type): + # optional argument + typ = get_args(field.type)[0] + del(args['default']) + else: + typ = field.type + if get_origin(typ) is list: + args['nargs'] = '+' + typ = get_args(typ)[0] + elif get_origin(typ) is tuple: + args['nargs'] = len(get_args(typ)) + typ = get_args(typ)[0] + + if issubclass(typ, StrEnum): + args['choices'] = [ci.value for ci in typ] + if field.default is not MISSING: + args['default'] = field.default.value + typ = str + + if typ is bool: + args['action'] = 'store_false' if field.default else 'store_true' + else: + args['type'] = typ + + if 'help' in field.metadata: + args['help'] = field.metadata['help'] + + output.append(CommandlineParameterConfig( + field.name, + add_argument_args=args, + group=field.metadata.get('group', 'misc'), + short_form=field.metadata.get('short', None), + priority=field.metadata.get('priority', 0), + )) + return output + + @classmethod + def from_args(cls, args: argparse.Namespace): + """ + Create the child class from the command line argument Namespace object. + All attributes that are not needed for this class are ignored. + """ + inpargs = {} + for field in fields(cls): + value = getattr(args, field.name) + typ = field.type + if get_origin(field.type) is Union and type(None) in get_args(field.type): + # optional argument + typ = get_args(field.type)[0] + + if issubclass(typ, StrEnum): + # convert str to enum + try: + value = typ(value) + except ValueError: + choices = [ci.value for ci in typ] + raise ValueError(f"Parameter --{field.name} has to be one of {choices}") + + inpargs[field.name] = value + return cls(**inpargs) @dataclass -class ExperimentConfig: - incidentAngle: str +class ReaderConfig(ArgParsable): + year: int = field(default=datetime.now().year, + metadata={'short': 'Y', 'group': 'input data', 'help': 'year the measurement was performed'}) + rawPath: List[str] = field(default_factory=lambda: ['.', path.join('.','raw'), path.join('..','raw'), path.join('..','..','raw')], + metadata={ + 'short': 'rp', + 'group': 'input data', + 'help': 'Search paths for hdf files'}) + startTime: Optional[float] = None + +class IncidentAngle(StrEnum): + alphaF = 'alphaF' + mu = 'mu' + nu = 'nu' + +class MonitorType(StrEnum): + auto = 'a' + proton_charge = 'p' + time = 't' + neutron_monitor = 'n' + debug = 'x' + +@dataclass +class ExperimentConfig(ArgParsable): chopperPhase: float chopperSpeed: float yRange: Tuple[float, float] lambdaRange: Tuple[float, float] - qzRange: Tuple[float, float] - monitorType: str lowCurrentThreshold: float + incidentAngle: IncidentAngle = IncidentAngle.alphaF sampleModel: Optional[str] = None chopperPhaseOffset: float = 0 mu: Optional[float] = None nu: Optional[float] = None muOffset: Optional[float] = None + monitorType: MonitorType = field(default=MonitorType.auto, metadata={'short': 'mt', + 'group': 'input data', 'help': 'one of [a]uto, [p]rotonCurrent, [t]ime or [n]eutronMonitor'}) + +class NormalisationMethod(StrEnum): + direct_beam = 'd' + over_illuminated = 'o' + under_illuminated = 'u' @dataclass -class ReductionConfig: - normalisationMethod: str +class ReductionConfig(ArgParsable): qResolution: float qzRange: Tuple[float, float] thetaRange: Tuple[float, float] #thetaRangeR: Tuple[float, float] - thetaRangeR: list + thetaRangeR: List[float] + fileIdentifier: List[str] = field(metadata={'short': 'f', 'priority': 100, + 'group': 'input data', 'help': 'file number(s) or offset (if < 1)'}) - 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 + normalisationMethod: NormalisationMethod = field(default=NormalisationMethod.over_illuminated, + metadata={'short': 'nm', 'priority': 90, 'group': 'input data', + 'help': 'normalisation method: [o]verillumination, [u]nderillumination, [d]irect_beam'}) + scale: List[float] = 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 + autoscale: bool = False # TODO: This made no sense, it is used as single bool. + subtract: Optional[str] = field(default=None, metadata={'short': 'sub', + 'group': 'input data', 'help': 'File with R(q_z) curve to be subtracted (in .Rqz.ort format)'}) + normalisationFileIdentifier: Optional[List[str]] = field(default=None, metadata={'short': 'n', 'priority': 90, + 'group': 'input data', 'help': 'file number(s) of normalisation measurement'}) + timeSlize: Optional[List[float]] = None @dataclass -class OutputConfig: - outputFormats: list +class OutputConfig(ArgParsable): + outputFormats: List[str] outputName: str outputPath: str diff --git a/libeos/reduction.py b/libeos/reduction.py index 3d35f55..0d0fc8e 100644 --- a/libeos/reduction.py +++ b/libeos/reduction.py @@ -8,7 +8,7 @@ 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 .options import EOSConfig, IncidentAngle, MonitorType, NormalisationMethod from .instrument import Grid class AmorReduction: @@ -22,7 +22,10 @@ class AmorReduction: self.header = Header() self.header.reduction.call = config.call_string() - self.monitorUnit = {'n': 'cnts', 'p': 'mC', 't': 's', 'auto': 'pulses'} + self.monitorUnit = {MonitorType.neutron_monitor: 'cnts', + MonitorType.proton_charge: 'mC', + MonitorType.time: 's', + MonitorType.auto: 'various'} def reduce(self): if not os.path.exists(f'{self.output_config.outputPath}'): @@ -353,8 +356,7 @@ class AmorReduction: self.normMonitor = np.sum(fromHDF.monitorPerPulse) norm_lz, bins_l, bins_z = np.histogram2d(lamda_e, detZ_e, bins = (self.grid.lamda(), self.grid.z())) norm_lz = np.where(norm_lz>2, norm_lz, np.nan) - if self.reduction_config.normalisationMethod == 'd': - # direct reference => invert map vertically + if self.reduction_config.normalisationMethod == NormalisationMethod.direct_beam: self.norm_lz = np.flip(norm_lz, 1) else: # correct for reference sm reflectivity @@ -426,7 +428,7 @@ class AmorReduction: #alphaF_lz += np.rad2deg( np.arctan( 3.07e-10 * (fromHDF.detectorDistance + detXdist_e) * lamda_lz**2 ) ) alphaF_lz += np.rad2deg( np.arctan( 3.07e-10 * fromHDF.detectorDistance * lamda_lz**2 ) ) - if self.experiment_config.incidentAngle == 'alphaF': + if self.experiment_config.incidentAngle == IncidentAngle.alphaF: #alphaI_lz = alphaF_lz qz_lz = 4.0*np.pi * np.sin(np.deg2rad(alphaF_lz)) / lamda_lz qx_lz = self.grid.lz() * 0. @@ -440,17 +442,17 @@ class AmorReduction: int_lz = np.where(mask_lz, int_lz, np.nan) thetaF_lz = np.where(mask_lz, alphaF_lz, np.nan) - if self.reduction_config.normalisationMethod == 'o': + if self.reduction_config.normalisationMethod == NormalisationMethod.over_illuminated: logging.debug(' assuming an overilluminated sample and correcting for the angle of incidence') 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.logical_and(mask_lz, np.where(np.absolute(thetaN_lz)>5e-3, True, False)) ref_lz = (int_lz * np.absolute(thetaN_lz)) / (norm_lz * np.absolute(thetaF_lz)) - elif self.reduction_config.normalisationMethod == 'u': + elif self.reduction_config.normalisationMethod == NormalisationMethod.under_illuminated: logging.debug(' assuming an underilluminated sample and ignoring the angle of incidence') ref_lz = (int_lz / norm_lz) - elif self.reduction_config.normalisationMethod == 'd': + elif self.reduction_config.normalisationMethod == NormalisationMethod.direct_beam: logging.debug(' assuming direct beam for normalisation and ignoring the angle of incidence') ref_lz = (int_lz / norm_lz) else: diff --git a/tests/test_full_analysis.py b/tests/test_full_analysis.py index a1fc875..b2d58e5 100644 --- a/tests/test_full_analysis.py +++ b/tests/test_full_analysis.py @@ -35,6 +35,7 @@ class FullAmorTest(TestCase): def test_time_slicing(self): experiment_config = options.ExperimentConfig( + chopperSpeed=options.Defaults.chopperSpeed, chopperPhase=-13.5, chopperPhaseOffset=-5, monitorType=options.Defaults.monitorType, @@ -75,6 +76,7 @@ class FullAmorTest(TestCase): def test_noslicing(self): experiment_config = options.ExperimentConfig( + chopperSpeed=options.Defaults.chopperSpeed, chopperPhase=-13.5, chopperPhaseOffset=-5, monitorType=options.Defaults.monitorType,