diff --git a/ewoksxrpd/gui/forms.py b/ewoksxrpd/gui/forms.py index ae2f948790419a7fe4bf0e2049253f2167ad1440..f0c22e640195934bf31ae1a6f374eb60b8c9ded9 100644 --- a/ewoksxrpd/gui/forms.py +++ b/ewoksxrpd/gui/forms.py @@ -1,278 +1,554 @@ -from typing import Dict, Optional, Sequence +from typing import Dict, Mapping, Optional, Sequence, Tuple from . import serialize from ewokscore import missing_data -def input_parameters_calibratesingle(fixed: Optional[Sequence] = None) -> dict: - if fixed is None: - fixed = list() +_GEOMETRY_KEYS = "dist", "poni1", "poni2", "rot1", "rot2", "rot3" +_ENERGY_GEOMETRY_KEYS = ("energy",) + _GEOMETRY_KEYS +_PARAMETRIZATION_KEYS = ( + "dist_expr", + "param_names", + "poni1_expr", + "poni2_expr", + "pos_names", + "rot1_expr", + "rot2_expr", + "rot3_expr", + "wavelength_expr", +) +_PARAMETERS_KEYS = ( + "arot1", + "arot2", + "arot3", + "dist_offset", + "dist_scale", + "energy", + "poni1_offset", + "poni1_scale", + "poni2_offset", + "poni2_scale", +) + + +def input_parameters_calibratesingle(values: Mapping) -> dict: parameters = dict() _add_data_singlecalib(parameters) - _add_energy_geometry(parameters, defaults=True, fixed=fixed) + _add_energy_geometry(parameters, values, calib=True) _add_detector(parameters) _add_calibrant(parameters) + _apply_values(parameters, values) return parameters -def input_parameters_calibratemulti(fixed: Optional[Sequence] = None) -> dict: - if fixed is None: - fixed = list() +def output_parameters_calibratesingle() -> dict: + parameters = dict() + _add_energy_geometry(parameters, dict()) + return parameters + + +def input_parameters_calibratemulti(values: Mapping) -> dict: parameters = dict() _add_data_multicalib(parameters) - _add_energy_geometry(parameters, defaults=True, fixed=fixed) + _add_energy_geometry(parameters, values, calib=True) _add_detector(parameters) _add_calibrant(parameters) + _apply_values(parameters, values) + return parameters + + +def output_parameters_calibratemulti() -> dict: + parameters = dict() + _add_parametrization(parameters, dict()) return parameters -def input_parameters_integrate1d() -> dict: +def input_parameters_integrate1d(values: Mapping) -> dict: parameters = dict() _add_data_integrate1d(parameters) - _add_energy_geometry(parameters) + _add_energy_geometry(parameters, values) _add_detector(parameters) parameters["integration_options"] = { "label": "Integrate Options", + "value_for_type": "", "serialize": serialize.json_dumps, "deserialize": serialize.json_loads, } parameters["worker_options"] = { "label": "Worker Options", + "value_for_type": "", "serialize": serialize.json_dumps, "deserialize": serialize.json_loads, } + _apply_values(parameters, values) return parameters -def input_parameters_ascii() -> dict: - return {"filename": {"label": "Output file", "default": "", "select": "newfile"}} +def input_parameters_ascii(values: Mapping) -> dict: + parameters = { + "filename": {"label": "Output file", "value_for_type": "", "select": "newfile"} + } + _apply_values(parameters, values) + return parameters -def input_parameters_nexus() -> dict: - return {"url": {"label": "Output file", "default": "", "select": "h5group"}} +def input_parameters_nexus(values: Mapping) -> dict: + parameters = { + "url": {"label": "Output file", "value_for_type": "", "select": "h5group"} + } + _apply_values(parameters, values) + return parameters -def input_parameters_background() -> dict: - return { - "image": {"label": "Image", "select": "h5dataset"}, - "monitor": {"label": "Monitor", "select": "h5dataset"}, - "background": {"label": "Background", "select": "h5dataset"}, +def input_parameters_background(values: Mapping) -> dict: + parameters = { + "image": {"label": "Image", "value_for_type": "", "select": "h5dataset"}, + "monitor": {"label": "Monitor", "value_for_type": "", "select": "h5dataset"}, + "background": { + "label": "Background", + "value_for_type": "", + "select": "h5dataset", + }, "background_monitor": { "label": "Background Monitor", + "value_for_type": "", "select": "h5dataset", }, } + _apply_values(parameters, values) + return parameters -def input_parameters_mask() -> dict: - return { - "image1": {"label": "Image 1", "select": "h5dataset"}, - "monitor1": {"label": "Monitor 1", "select": "h5dataset"}, - "image2": {"label": "Image 2", "select": "h5dataset"}, - "monitor2": {"label": "Monitor 2", "select": "h5dataset"}, +def input_parameters_mask(values: Mapping) -> dict: + parameters = { + "image1": {"label": "Image 1", "value_for_type": "", "select": "h5dataset"}, + "monitor1": {"label": "Monitor 1", "value_for_type": "", "select": "h5dataset"}, + "image2": {"label": "Image 2", "value_for_type": "", "select": "h5dataset"}, + "monitor2": {"label": "Monitor 2", "value_for_type": "", "select": "h5dataset"}, "smooth": { "label": "Smooth width", - "default": 0, + "value_for_type": 0, }, "monitor_ratio_margin": { "label": "Ratio margin", - "default": 0.1, + "value_for_type": "", "serialize": serialize.float_serialize, "deserialize": serialize.float_deserialize, }, } + _apply_values(parameters, values) + return parameters -def input_parameters_calculategeometry() -> dict: - return { - "position": {"label": "Position", "select": "h5dataset"}, +def input_parameters_calculategeometry(values: Mapping) -> dict: + parameters = { + "position": {"label": "Position", "value_for_type": "", "select": "h5dataset"}, } + _apply_values(parameters, values) + return parameters def output_parameters_calculategeometry() -> dict: parameters = dict() - _add_energy_geometry(parameters) + _add_energy_geometry(parameters, dict()) return parameters -def input_parameters_diagnose_integrate1d() -> dict: +def input_parameters_diagnose_integrate1d(values: Mapping) -> dict: parameters = dict() + _add_calibrant(parameters) _add_diagnostics_output(parameters) + _apply_values(parameters, values) return parameters -def input_parameters_diagnose_singlecalib() -> dict: +def input_parameters_diagnose_singlecalib(values: Mapping) -> dict: parameters = dict() _add_data_singlecalib(parameters) _add_calibrant(parameters) _add_diagnostics_output(parameters) + _apply_values(parameters, values) return parameters -def input_parameters_diagnose_multicalib() -> dict: +def input_parameters_diagnose_multicalib(values: Mapping) -> dict: parameters = dict() _add_diagnostics_output(parameters) parameters["positions"] = { "label": "Positions", "select": "h5datasets", + "value_for_type": "", "serialize": serialize.json_dumps, "deserialize": serialize.json_loads, } parameters["positionunits_in_meter"] = { "label": "Position Units (meter)", - "default": 1e-3, + "value_for_type": "", "serialize": serialize.float_serialize, "deserialize": serialize.float_deserialize, } + _apply_values(parameters, values) return parameters -def _add_diagnostics_output(parameters: Dict) -> None: +def input_parameters_pyfai_config(values: Mapping) -> dict: + parameters = dict() + parameters["filename"] = { + "label": "Poni file", + "value_for_type": "", + "select": "file", + } + _add_energy_geometry(parameters, dict()) + _add_calibrant(parameters) + _add_detector(parameters) + parameters["mask"] = { + "label": "Mask file", + "value_for_type": "", + "select": "file", + } + _apply_values(parameters, values) + return parameters + + +def output_parameters_pyfai_config() -> dict: + parameters = dict() + _add_energy_geometry(parameters, dict()) + _add_calibrant(parameters) + _add_detector(parameters) + return parameters + + +def _add_diagnostics_output(parameters: dict) -> None: parameters["filename"] = { "label": "Output file", - "default": "", + "value_for_type": "", "select": "newfile", } def _add_energy_geometry( - parameters: Dict, defaults: Optional[bool] = None, fixed: Optional[Sequence] = None + parameters: dict, values: Mapping, calib: Optional[bool] = None ) -> None: + values = _unpack(values, _GEOMETRY_KEYS, "geometry") + fixed = values.pop("fixed", None) + parameters["energy"] = { "label": "Energy (keV)", + "value_for_type": "", "serialize": serialize.float_serialize, "deserialize": serialize.float_deserialize, } parameters["dist"] = { "label": "Distance (cm)", + "value_for_type": "", "serialize": serialize.cm_serialize, "deserialize": serialize.cm_deserialize, } parameters["poni1"] = { "label": "Poni1 (cm)", + "value_for_type": "", "serialize": serialize.cm_serialize, "deserialize": serialize.cm_deserialize, } parameters["poni2"] = { "label": "Poni2 (cm)", + "value_for_type": "", "serialize": serialize.cm_serialize, "deserialize": serialize.cm_deserialize, } parameters["rot1"] = { "label": "Rot1 (deg)", + "value_for_type": "", "serialize": serialize.degrees_serialize, "deserialize": serialize.degrees_deserialize, } parameters["rot2"] = { "label": "Rot2 (deg)", + "value_for_type": "", "serialize": serialize.degrees_serialize, "deserialize": serialize.degrees_deserialize, } parameters["rot3"] = { "label": "Rot3 (deg)", + "value_for_type": "", "serialize": serialize.degrees_serialize, "deserialize": serialize.degrees_deserialize, } - if defaults: - defaults = { - "energy": 12.0, - "dist": 10e-2, - "poni1": 0.0, - "poni2": 0.0, - "rot1": 0.0, - "rot2": 0.0, - "rot3": 0.0, - } - for name, default in defaults.items(): - parameters[name]["default"] = default - - if fixed is not None: - for name, value in parameters.items(): - value.update( + if calib: + if not fixed: + fixed = list() + for name in _ENERGY_GEOMETRY_KEYS: + parameters[name].update( { - "onoff": name in fixed, - "onoff_label": "fixed", - "enable_when_on": False, + "checked": name not in fixed, + "checkbox_label": "refine", } ) + parameters["max_rings"] = { + "label": "# Rings", + "value_for_type": 0, + "serialize": serialize.posint_serialize, + "deserialize": serialize.posint_deserialize, + } + parameters["robust"] = { + "label": "Robust", + "value_for_type": True, + } + + +def _add_parametrization(parameters: dict, values: Mapping) -> None: + values = _unpack(values, _PARAMETRIZATION_KEYS, "parametrization") + values = _unpack(values, _PARAMETERS_KEYS, "parameters") + + parameters["energy"] = { + "label": "energy (keV)", + "value_for_type": "", + "serialize": serialize.float_serialize, + "deserialize": serialize.float_deserialize, + } + parameters["wavelength_expr"] = { + "label": "wavelength (m) =", + "value_for_type": "", + } + + parameters["dist_offset"] = { + "label": "dist_offset (cm)", + "value_for_type": "", + "serialize": serialize.float_serialize, + "deserialize": serialize.float_deserialize, + } + parameters["dist_scale"] = { + "label": "dist_scal", + "value_for_type": "", + "serialize": serialize.float_serialize, + "deserialize": serialize.float_deserialize, + } + parameters["dist_expr"] = { + "label": "dist (m) =", + "value_for_type": "", + } + + parameters["poni1_offset"] = { + "label": "poni1_offset (cm)", + "value_for_type": "", + "serialize": serialize.float_serialize, + "deserialize": serialize.float_deserialize, + } + parameters["poni1_scale"] = { + "label": "poni1_scale", + "value_for_type": "", + "serialize": serialize.float_serialize, + "deserialize": serialize.float_deserialize, + } + parameters["poni1_expr"] = { + "label": "poni1 (m) =", + "value_for_type": "", + } + + parameters["poni2_offset"] = { + "label": "poni2_offset (cm)", + "value_for_type": "", + "serialize": serialize.float_serialize, + "deserialize": serialize.float_deserialize, + } + parameters["poni2_scale"] = { + "label": "poni2_scale", + "value_for_type": "", + "serialize": serialize.float_serialize, + "deserialize": serialize.float_deserialize, + } + parameters["poni2_expr"] = { + "label": "poni2 (m) =", + "value_for_type": "", + } + parameters["arot1"] = { + "label": "arot1 (rad)", + "value_for_type": "", + "serialize": serialize.float_serialize, + "deserialize": serialize.float_deserialize, + } + parameters["rot1_expr"] = { + "label": "rot1 (rad) =", + "value_for_type": "", + } + + parameters["arot2"] = { + "label": "arot2 (rad)", + "value_for_type": "", + "serialize": serialize.float_serialize, + "deserialize": serialize.float_deserialize, + } + parameters["rot2_expr"] = { + "label": "rot2 (rad) =", + "value_for_type": "", + } -def _add_detector(parameters: Dict) -> None: - parameters["detector"] = {"label": "Detector", "default": "Pilatus1M"} + parameters["arot3"] = { + "label": "arot3 (drad)", + "value_for_type": "", + "serialize": serialize.float_serialize, + "deserialize": serialize.float_deserialize, + } + parameters["rot3_expr"] = { + "label": "rot3 (rad) =", + "value_for_type": "", + } -def _add_calibrant(parameters: Dict) -> None: - parameters["calibrant"] = {"label": "Calibrant", "default": "LaB6"} +def _add_detector(parameters: dict) -> None: + parameters["detector"] = {"label": "Detector", "value_for_type": ""} -def output_parameters_calib(parameters: Dict): - _add_energy_geometry(parameters, inputs=False) +def _add_calibrant(parameters: dict) -> None: + parameters["calibrant"] = {"label": "Calibrant", "value_for_type": ""} -def _add_data_singlecalib(parameters: Dict) -> None: - parameters["image"] = {"label": "Pattern", "select": "h5dataset"} +def output_parameters_calib(parameters: dict): + _add_energy_geometry(parameters, dict(), False) -def _add_data_multicalib(parameters: Dict) -> None: +def _add_data_singlecalib(parameters: dict) -> None: + parameters["image"] = { + "label": "Pattern", + "value_for_type": "", + "select": "h5dataset", + } + + +def _add_data_multicalib(parameters: dict) -> None: parameters["images"] = { "label": "Patterns", + "value_for_type": "", "select": "h5datasets", "serialize": serialize.json_dumps, "deserialize": serialize.json_loads, } parameters["positions"] = { "label": "Positions", + "value_for_type": "", "select": "h5datasets", "serialize": serialize.json_dumps, "deserialize": serialize.json_loads, } parameters["reference_position"] = { "label": "Reference Position", + "value_for_type": "", "select": "h5dataset", } parameters["sample_position"] = { "label": "Sample position", - "default": 0.0, + "value_for_type": "", "serialize": serialize.float_serialize, "deserialize": serialize.float_deserialize, } parameters["positionunits_in_meter"] = { "label": "Position Units (meter)", - "default": 1e-3, + "value_for_type": "", "serialize": serialize.float_serialize, "deserialize": serialize.float_deserialize, } -def _add_data_integrate1d(parameters: Dict) -> None: - parameters["image"] = {"label": "Pattern", "select": "h5dataset"} - parameters["monitor"] = {"label": "Monitor", "select": "h5dataset"} +def _add_data_integrate1d(parameters: dict) -> None: + parameters["image"] = { + "label": "Pattern", + "value_for_type": "", + "select": "h5dataset", + } + parameters["monitor"] = { + "label": "Monitor", + "value_for_type": "", + "select": "h5dataset", + } parameters["reference"] = { "label": "Reference", + "value_for_type": "", "serialize": serialize.float_serialize, "deserialize": serialize.float_deserialize, } - parameters["mask"] = {"label": "Mask", "select": "h5dataset"} - parameters["detector"] = {"label": "Detector", "default": "Pilatus1M"} + parameters["mask"] = {"label": "Mask", "value_for_type": "", "select": "h5dataset"} + parameters["detector"] = {"label": "Detector", "value_for_type": ""} + + +def _apply_values(parameters: dict, values: Mapping) -> None: + for k, v in values.items(): + if k in parameters: + parameters[k]["value"] = v + + +def unpack_geometry(values: Mapping) -> Tuple[Mapping, Dict[str, bool]]: + values = _unpack(values, _GEOMETRY_KEYS, "geometry") + fixed = values.pop("fixed", None) + if fixed: + checked = {name: name not in fixed for name in _ENERGY_GEOMETRY_KEYS} + else: + checked = dict() + return values, checked + + +def unpack_enabled_geometry(enabled: Dict[str, bool]) -> Dict[str, bool]: + values = _unpack_enabled(enabled, _GEOMETRY_KEYS, "geometry") + return values -def pack_geometry(parameters): - pack = "dist", "poni1", "poni2", "rot1", "rot2", "rot3" - result = {k: v for k, v in parameters.items() if k not in pack} - geometry = { +def pack_geometry(values: Mapping, checked: Dict[str, bool]) -> dict: + values = _pack(values, _GEOMETRY_KEYS, "geometry") + if checked: + values["fixed"] = [k for k, v in checked.items() if not v] + else: + values["fixed"] = missing_data.MISSING_DATA + return values + + +def unpack_parametrization(values: Mapping) -> dict: + values = _unpack(values, _PARAMETRIZATION_KEYS, "parametrization") + values = _unpack(values, _PARAMETERS_KEYS, "parameters") + return values + + +def unpack_enabled_parametrization(enabled: Dict[str, bool]) -> Dict[str, bool]: + values = _unpack_enabled(enabled, _PARAMETRIZATION_KEYS, "parametrization") + values = _unpack_enabled(enabled, _PARAMETERS_KEYS, "parameters") + return values + + +def pack_parametrization(values: Mapping) -> dict: + values = _pack(values, _PARAMETRIZATION_KEYS, "parametrization") + values = _pack(values, _PARAMETERS_KEYS, "parameters") + return values + + +def _pack(values: Mapping, keys: Sequence[str], pack_key: str) -> dict: + result = {k: v for k, v in values.items() if k not in keys} + packed = { k: v - for k, v in parameters.items() - if k in pack and not missing_data.is_missing_data(v) + for k, v in values.items() + if k in keys and not missing_data.is_missing_data(v) } - if len(geometry) == len(pack): - result["geometry"] = geometry + if len(packed) == len(keys): + result[pack_key] = packed + else: + result[pack_key] = missing_data.MISSING_DATA return result -def unpack_geometry(parameters): - parameters = dict(parameters) - geometry = parameters.pop("geometry", None) - if geometry: - parameters.update(geometry) - return parameters +def _unpack(values: Mapping, keys: Sequence[str], pack_key: str) -> dict: + result = dict(values) + packed = result.pop(pack_key, None) + if packed: + result.update(packed) + else: + result.update({k: missing_data.MISSING_DATA for k in keys}) + return result + + +def _unpack_enabled( + enabled: Dict[str, bool], keys: Sequence[str], pack_key: str +) -> Dict[str, bool]: + result = dict(enabled) + packed = result.pop(pack_key, None) + if packed is not None: + result.update({k: packed for k in keys}) + return result diff --git a/ewoksxrpd/gui/plots.py b/ewoksxrpd/gui/plots.py index e582d17f516165b77d58134cd8918b0c097f0bab..ba2dda603af8405b31f50ebf0eba101244a12c58 100644 --- a/ewoksxrpd/gui/plots.py +++ b/ewoksxrpd/gui/plots.py @@ -1,4 +1,5 @@ from numbers import Number +from typing import List import numpy from numpy.typing import ArrayLike @@ -6,22 +7,25 @@ from silx.gui.colors import Colormap from silx.gui.plot import PlotWidget from silx.image.marchingsquares import find_contours import pyFAI +import pyFAI.detectors from ewoksxrpd.tasks import utils -def plot_image(plot: PlotWidget, image: ArrayLike, **kwargs): - plot.addImage(image, **kwargs) +def plot_image(plot: PlotWidget, image: ArrayLike, **kwargs) -> str: + return plot.addImage(image, **kwargs) -def plot_rings( +def plot_theoretical_rings( plot: PlotWidget, detector: str, calibrant: str, energy: Number, geometry: dict, - rings: dict, -): + max_rings=None, + legend=None, + **kwargs, +) -> List[str]: """Plot theoretical and detected Debye rings""" detector = pyFAI.detectors.detector_factory(detector) mask = detector.mask @@ -32,30 +36,56 @@ def plot_rings( calibrant = pyFAI.calibrant.get_calibrant(calibrant) calibrant.set_wavelength(wavelength) - # Theoretical rings levels = calibrant.get_2th() + if max_rings: + if max_rings < 0: + max_rings = None + else: + max_rings = None + if max_rings: + levels = levels[:max_rings] image = ai.twoThetaArray() + legends = list() + if not legend: + legend = "theory" for i, level in enumerate(levels): polygons = find_contours(image, level, mask=mask) color = None for j, polygon in enumerate(polygons): x = polygon[:, 1] + 0.5 y = polygon[:, 0] + 0.5 - legend = plot.addCurve( + s = plot.addCurve( x=x, y=y, - legend=f"theory-{i}-{j}", + legend=f"{legend}-{i}-{j}", linestyle="-", resetzoom=False, color=color, + **kwargs, ) + legends.append(s) if j == 0: - color = plot.getCurve(legend).getColor() + color = plot.getCurve(s).getColor() + return legends - # Detected rings + +def plot_detected_rings( + plot: PlotWidget, rings: dict, legend=None, **kwargs +) -> List[str]: + legends = list() + if not legend: + legend = "detected" cm = Colormap(name="jet", normalization="linear", vmin=0, vmax=len(rings)) for value, (label, points) in enumerate(rings.items()): value = numpy.full_like(points["p1"], value * 100) - plot.addScatter( - points["p1"], points["p0"], value, legend=label, symbol=".", colormap=cm + legend = plot.addScatter( + points["p1"], + points["p0"], + value, + legend=f"{legend}_{label}", + symbol=".", + colormap=cm, + **kwargs, ) + legends.append(legend) + return legends diff --git a/ewoksxrpd/gui/serialize.py b/ewoksxrpd/gui/serialize.py index 6acbf913f7ba4e20c6b35f9b302c8f8ed8e81604..c79f05014cbae4f54e81971ffe2c9e305e2f486c 100644 --- a/ewoksxrpd/gui/serialize.py +++ b/ewoksxrpd/gui/serialize.py @@ -1,78 +1,52 @@ import json +from numbers import Integral from typing import Union import numpy -from ewokscore import missing_data - def json_dumps(value) -> str: - if missing_data.is_missing_data(value): - return "" - else: - return json.dumps(value) + return json.dumps(value) def json_loads(value: str): - if value: - return json.loads(value) - else: - return missing_data.MISSING_DATA + return json.loads(value) def float_serialize(value: float) -> str: - if missing_data.is_missing_data(value): - return "" - else: - return str(value) + return str(value) def float_deserialize(value: str) -> float: - if value: - return float(value) - else: - return missing_data.MISSING_DATA + return float(value) def strfloat_serialize(value: Union[float, str]) -> str: - if missing_data.is_missing_data(value): - return "" - else: - return str(value) + return str(value) -def strfloat_deserialize(value: str) -> Union[float, str]: - if value: - if value.isdigit(): - return float(value) - else: - return value - else: - return missing_data.MISSING_DATA +def strfloat_deserialize(value: str) -> float: + return float(value) def cm_serialize(value: float) -> str: - if missing_data.is_missing_data(value): - return "" - else: - return str(value * 1e2) + return str(value * 1e2) def cm_deserialize(value: str) -> float: - if value: - return float(value) * 1e-2 - else: - return missing_data.MISSING_DATA + return float(value) * 1e-2 def degrees_serialize(value: float) -> str: - if missing_data.is_missing_data(value): - return "" - else: - return str(numpy.degrees(value)) + return str(numpy.degrees(value)) def degrees_deserialize(value: str) -> float: - if value: - return numpy.radians(float(value)) - else: - return missing_data.MISSING_DATA + return numpy.radians(float(value)) + + +def posint_serialize(value: Integral) -> Integral: + return max(value, 0) + + +def posint_deserialize(value: Integral) -> Integral: + return max(value, 0) diff --git a/ewoksxrpd/gui/trigger_widget.py b/ewoksxrpd/gui/trigger_widget.py index f849b654c015776e9c01bd9b590506629d08d73f..0746a2c8101a3d79943878c0d8e12d7bdb59c27b 100644 --- a/ewoksxrpd/gui/trigger_widget.py +++ b/ewoksxrpd/gui/trigger_widget.py @@ -1,6 +1,6 @@ from contextlib import contextmanager import logging -from typing import Dict, Iterable +from typing import Dict, Mapping, Optional, Tuple from AnyQt import QtWidgets from ewoksorange.bindings import OWEwoksWidgetOneThread from ewoksorange.bindings import ow_build_opts @@ -12,145 +12,129 @@ logger = logging.getLogger(__name__) class OWTriggerWidget(OWEwoksWidgetOneThread, **ow_build_opts): def __init__(self, *args, **kwargs) -> None: - self._input_forms: Dict[str, ParameterForm] = dict() - self._output_forms: Dict[str, ParameterForm] = dict() + self._input_form: Optional[ParameterForm] = None + self._output_form: Optional[ParameterForm] = None super().__init__(*args, **kwargs) self._init_ui() def _init_ui(self): """Create widgets for input and output.""" - self._init_control_buttons() self._init_forms() self._init_control_area() + self._add_input_form_widget() self._init_main_area() + self._add_output_form_widget() + self._refresh_non_form_input_widgets() + self._refresh_non_form_output_widgets() - def _init_control_area(self): - """The control area is a collapsable area most for input parameter forms.""" - - def _init_main_area(self): - """The main area is used to display results.""" - - def _get_control_layout(self): - layout = self.controlArea.layout() - if layout is None: - layout = QtWidgets.QVBoxLayout() - self.controlArea.setLayout(layout) - return layout - - def _get_main_layout(self): - layout = self.mainArea.layout() - if layout is None: - layout = QtWidgets.QVBoxLayout() - self.mainArea.setLayout(layout) - return layout + def _init_forms(self) -> None: + pass - def _init_control_buttons(self): + def _init_control_area(self) -> None: """Buttons to trigger execution and refresh.""" - layout = self.controlArea.layout() - if layout is None: - layout = QtWidgets.QVBoxLayout() - self.controlArea.setLayout(layout) - - trigger = QtWidgets.QPushButton("Trigger") - layout.addWidget(trigger) - trigger.released.connect(self.executeEwoksTask) - trigger = QtWidgets.QPushButton("Execute") - layout.addWidget(trigger) - trigger.released.connect(self.executeEwoksTaskWithoutPropagation) + super()._init_control_area() + layout = self._get_control_layout() refresh = QtWidgets.QPushButton("Refresh") layout.addWidget(refresh) - refresh.released.connect(self._refresh) + refresh.released.connect(self._refresh_widgets) - def _init_forms(self): - """Forms for inputs and output parameters""" - pass + def _add_input_form_widget(self) -> None: + if self._input_form is not None: + layout = self._get_control_layout() + layout.addWidget(self._input_form) + + def _add_output_form_widget(self) -> None: + if self._output_form is not None: + layout = self._get_main_layout() + layout.addWidget(self._output_form) - def task_output_changed(self): + def task_output_changed(self) -> None: self._refresh_output_widgets() super().task_output_changed() - def handleNewSignals(self): + def handleNewSignals(self) -> None: self._refresh_input_widgets() super().handleNewSignals() - def _create_input_form( - self, name: str, parameter_info: dict, parent - ) -> ParameterForm: - assert name not in self._input_forms - form = ParameterForm(parent) + def _create_input_form(self, parameter_info: dict) -> None: + assert self._input_form is None + form = ParameterForm(self.controlArea) for name, info in parameter_info.items(): - form.addParameter(name, **info, changeCallback=self._input_form_edited) - form.set_parameter_used(name, False) - self._input_forms[name] = form - self.__update_input_forms(form) - return form - - def _create_output_form( - self, name: str, parameter_info: dict, parent - ) -> ParameterForm: - assert name not in self._output_forms - form = ParameterForm(parent) + form.addParameter( + name, **info, value_change_callback=self._input_form_edited + ) + self._input_form = form + self._refresh_input_form() + + def _create_output_form(self, parameter_info: dict) -> None: + assert self._output_form is None + form = ParameterForm(self.mainArea) for name, info in parameter_info.items(): form.addParameter(name, **info) - self._output_forms[name] = form - self.__update_output_forms(form) - return form + self._output_form = form + self._refresh_output_form() - def _refresh(self): + def _refresh_widgets(self) -> None: self._refresh_input_widgets() self._refresh_output_widgets() - def _refresh_input_widgets(self): - self._refresh_input_forms() + def _refresh_input_widgets(self) -> None: + self._refresh_input_form() self._refresh_non_form_input_widgets() - def _refresh_output_widgets(self): - self._refresh_output_forms() + def _refresh_output_widgets(self) -> None: + self._refresh_output_form() self._refresh_non_form_output_widgets() - def _refresh_non_form_input_widgets(self): + def _refresh_non_form_input_widgets(self) -> None: pass - def _refresh_non_form_output_widgets(self): + def _refresh_non_form_output_widgets(self) -> None: pass - def _refresh_input_forms(self): - self.__update_input_forms(*self._input_forms.values()) - - def _refresh_output_forms(self): - self.__update_output_forms(*self._output_forms.values()) - - def _input_form_edited(self): - """An input form has been edited by the user""" - for form in self._input_forms.values(): - parameters = self._parameters_from_form(form.get_parameter_values()) - self.update_default_inputs(parameters) + def _refresh_input_form(self) -> None: + """Set form values and disable rows with values from previous tasks""" + if self._input_form is None: + return - def __update_input_forms(self, *forms: Iterable[ParameterForm]) -> None: - if not forms: + # Set form value to default or dynamic inputs + values, checked = self._values_to_form(self.get_task_input_values()) + self._input_form.set_parameter_values(values) + self._input_form.set_parameters_checked(checked) + + # Disable form parameters with dynamic inputs + disabled_names = self.get_dynamic_input_names() + enabled = self._enabled_to_form( + {name: name not in disabled_names for name in self.get_input_names()} + ) + self._input_form.set_parameters_enabled(enabled) + + def _refresh_output_form(self) -> None: + """Set form values""" + if self._output_form is None: return - values = self._parameters_to_form(self.task_input_values) - dynamic_inputs = { - name: False for name in self._parameters_to_form(self.dynamic_input_values) - } - for form in forms: - used = { - name: dynamic_inputs.get(name, True) - for name in form.get_parameter_names() - } - form.set_parameters_used(used) - form.set_parameter_values(values) - - def __update_output_forms(self, *forms: Iterable[ParameterForm]) -> None: - values = self._parameters_to_form(self.task_output_values) - for form in forms: - form.set_parameter_values(values) - - def _parameters_from_form(self, parameters): - return parameters - - def _parameters_to_form(self, parameters): - return parameters + # Set form value to task outputs + values, checked = self._values_to_form(self.get_task_output_values()) + self._output_form.set_parameter_values(values) + self._output_form.set_parameters_checked(checked) + + def _input_form_edited(self) -> None: + """Store enabled form values as default inputs""" + values = self._input_form.get_parameter_values() + enabled = self._input_form.get_parameters_enabled() + checked = self._input_form.get_parameters_checked() + values = {k: v for k, v in values.items() if enabled[k]} + parameters = self._values_from_form(values, checked) + self.update_default_inputs(**parameters) + + def _values_from_form(self, values: Mapping, checked: Dict[str, bool]) -> Mapping: + return values + + def _values_to_form(self, values: Mapping) -> Tuple[Mapping, Dict[str, bool]]: + return values, dict() + + def _enabled_to_form(self, enabled: Dict[str, bool]) -> Dict[str, bool]: + return enabled @contextmanager def _capture_errors(self, msg="widget update failed"): diff --git a/ewoksxrpd/tasks/__init__.py b/ewoksxrpd/tasks/__init__.py index c089818504c15b3d32c36edd391c40a1ba0b6380..a3e5ecbb139eb8a6a1476d1c075ab0ab0d0f09d9 100644 --- a/ewoksxrpd/tasks/__init__.py +++ b/ewoksxrpd/tasks/__init__.py @@ -5,3 +5,4 @@ from .integrate import * # noqa F403 from .diagnostics import * # noqa F403 from .ascii import * # noqa F403 from .nexus import * # noqa F403 +from .pyfaiconfig import * # noqa F403 diff --git a/ewoksxrpd/tasks/ascii.py b/ewoksxrpd/tasks/ascii.py index f1943bcaef13f5643eecfe7ced57e66a6332b12f..248a50e13fcd52fd13f72ef90783db6da401410f 100644 --- a/ewoksxrpd/tasks/ascii.py +++ b/ewoksxrpd/tasks/ascii.py @@ -18,6 +18,7 @@ class SaveAsciiPattern1D( "xunits", ], optional_input_names=["header", "yerror", "metadata"], + output_names=["saved"], ): def run(self): if is_data(self.inputs.yerror): @@ -68,5 +69,8 @@ class SaveAsciiPattern1D( lst.extend(f"{k} = {v}" for k, v in metadata.items()) header = "\n".join(lst) - os.makedirs(os.path.dirname(self.inputs.filename), exist_ok=True) + dirname = os.path.dirname(self.inputs.filename) + if dirname: + os.makedirs(dirname, exist_ok=True) numpy.savetxt(self.inputs.filename, data, header=header) + self.outputs.saved = True diff --git a/ewoksxrpd/tasks/calibrate.py b/ewoksxrpd/tasks/calibrate.py index ba5be694d636a8f73f36489e24c2428b5d78f164..64ec97d734d4489a1d5c761e60df51fa3669ba32 100644 --- a/ewoksxrpd/tasks/calibrate.py +++ b/ewoksxrpd/tasks/calibrate.py @@ -32,15 +32,17 @@ def parse_fixed( class CalibrateSingle( TaskWithProgress, input_names=["image", "detector", "calibrant", "geometry", "energy"], - optional_input_names=["fixed"], + optional_input_names=["fixed", "max_rings", "robust"], output_names=["geometry", "energy", "detector", "rings", "chi2"], ): """Single distance and energy calibration.""" def run(self): - detector = pyFAI.detectors.detector_factory(self.inputs.detector) - calibrant = pyFAI.calibrant.get_calibrant(self.inputs.calibrant) - geometry0 = self.inputs.geometry + detector = utils.data_from_storage(self.inputs.detector) + detector = pyFAI.detectors.detector_factory(detector) + calibrant = utils.data_from_storage(self.inputs.calibrant) + calibrant = pyFAI.calibrant.get_calibrant(calibrant) + geometry0 = utils.data_from_storage(self.inputs.geometry) utils.validate_geometry(geometry0) wavelength0 = utils.energy_wavelength(self.inputs.energy) @@ -77,7 +79,16 @@ class CalibrateSingle( """Find diffraction rings and fit""" parametrization = {"energy": ["wavelength"]} force_fixed = parse_fixed(self.inputs.fixed, parametrization) - fixed = fixed_prev = set(utils.GEOMETRY_PARAMETERS) # all fixed + fixed = fixed_prev = set(utils.GEOMETRY_PARAMETERS) | { + "wavelength" + } # all fixed + + max_rings = self.inputs.max_rings + if max_rings: + if max_rings < 0: + max_rings = None + else: + max_rings = None def fit(*release): nonlocal fixed, fixed_prev @@ -85,41 +96,28 @@ class CalibrateSingle( if not set_free: return fixed -= set_free - setup.extract_cp() + setup.extract_cp(max_rings=max_rings) if setup.geometry_refinement.data.ndim != 2: logger.warning("No rings detected for calibration") return setup.geometry_refinement.refine3(fix=fixed) # method="slsqp" fixed_prev = fixed - n = 5 - i = 1 - - self.progress = i / n * 100 - i += 1 - - # only dist free (i.e. all other fixed) - fit("dist") - - self.progress = i / n * 100 - i += 1 - - # free poni1 and poni2 - fit("poni1", "poni2") - - self.progress = i / n * 100 - i += 1 - - # free rotations - fit("rot1", "rot2", "rot3") - - self.progress = i / n * 100 - i += 1 - - # free the energy - fit("wavelength") - - self.progress = i / n * 100 + if self.inputs.robust: + releases = [ + ["dist"], + ["poni1", "poni2"], + ["rot1", "rot2", "rot3"], + ["wavelength"], + ] + else: + releases = [ + ["dist", "poni1", "poni2", "rot1", "rot2", "rot3", "wavelength"] + ] + n = len(releases) + for i, release in enumerate(releases, 1): + fit(*release) + self.progress = i / n * 100 class CalibrateMulti( @@ -137,6 +135,8 @@ class CalibrateMulti( "sample_position", "positionunits_in_meter", "fixed", + "max_rings", + "robust", ], output_names=[ "geometry", @@ -171,10 +171,12 @@ class CalibrateMulti( def run(self): if len(self.inputs.images) != len(self.inputs.positions): raise ValueError("number of 'images' and 'positions' must be the same") - detector = pyFAI.detectors.detector_factory(self.inputs.detector) - calibrant = pyFAI.calibrant.get_calibrant(self.inputs.calibrant) + detector = utils.data_from_storage(self.inputs.detector) + detector = pyFAI.detectors.detector_factory(detector) + calibrant = utils.data_from_storage(self.inputs.calibrant) + calibrant = pyFAI.calibrant.get_calibrant(calibrant) wavelength0 = utils.energy_wavelength(self.inputs.energy) - geometry0 = self.inputs.geometry + geometry0 = utils.data_from_storage(self.inputs.geometry) utils.validate_geometry(geometry0) # Parameterize the detector geometry @@ -283,6 +285,13 @@ class CalibrateMulti( force_fixed = parse_fixed(self.inputs.fixed, parametrization) fixed = fixed_prev = set(setup.trans_function.param_names) # all fixed + max_rings = self.inputs.max_rings + if max_rings: + if max_rings < 0: + max_rings = None + else: + max_rings = None + def fit(*release): nonlocal fixed, fixed_prev set_free = set(release) - force_fixed @@ -290,44 +299,37 @@ class CalibrateMulti( return fixed -= set_free for setup_single_dist in setup.single_geometries.values(): - setup_single_dist.extract_cp() + setup_single_dist.extract_cp(max_rings=max_rings) setup.refine3(fix=fixed) # method="slsqp" fixed_prev = fixed - n = 6 - i = 1 - - self.progress = i / n * 100 - i += 1 - - # only dist_offset and dist_scale free (i.e. all other fixed) - fit("dist_offset", "dist_scale") - - self.progress = i / n * 100 - i += 1 - - # free poni1_offset and poni2_offset - fit("poni1_offset", "poni2_offset") - - self.progress = i / n * 100 - i += 1 - - # free rotations - fit("arot1", "arot2", "arot3") - - self.progress = i / n * 100 - i += 1 - - # free poni1_scale and poni2_scale - fit("poni1_scale", "poni2_scale") - - self.progress = i / n * 100 - i += 1 - - # free the energy - fit("energy") - - self.progress = i / n * 100 + if self.inputs.robust: + releases = [ + ["dist_offset", "dist_scale"], + ["poni1_offset", "poni2_offset"], + ["arot1", "arot2", "arot3"], + ["poni1_scale", "poni2_scale"], + ["energy"], + ] + else: + releases = [ + [ + "dist_offset", + "dist_scale", + "poni1_offset", + "poni2_offset", + "arot1", + "arot2", + "arot3", + "poni1_scale", + "poni2_scale", + "energy", + ] + ] + n = len(releases) + for i, release in enumerate(releases, 1): + fit(*release) + self.progress = i / n * 100 def _get_rings( self, diff --git a/ewoksxrpd/tasks/diagnostics.py b/ewoksxrpd/tasks/diagnostics.py index d56bf3c984f1b45c087b64a2c9005548e30e8a36..1e34f4e5589862f0f47542961ea93ef8e9a5bb61 100644 --- a/ewoksxrpd/tasks/diagnostics.py +++ b/ewoksxrpd/tasks/diagnostics.py @@ -54,7 +54,7 @@ class _DiagnoseCalibrateResults( ax1, ax2, image: ArrayLike, - rings: dict, + control_pts: dict, geometry: dict, energy: Number, title=None, @@ -71,39 +71,42 @@ class _DiagnoseCalibrateResults( title = "" ax1.imshow(image, origin="lower", cmap="inferno", norm=colornorm) - ax1.set_title(f"{title}{self}") - ax2.imshow(image, origin="lower", cmap="inferno", norm=colornorm) - ax2.set_title(f"{title}Control points") + ax1.set_title(f"{title}Rings") + if control_pts: + ax2.imshow(image, origin="lower", cmap="inferno", norm=colornorm) + ax2.set_title(f"{title}Control points") # Calibrant rings on 2D pattern - detector = pyFAI.detectors.detector_factory( - utils.data_from_storage(self.inputs.detector) - ) + detector = utils.data_from_storage(self.inputs.detector) + detector = pyFAI.detectors.detector_factory(detector) + calibrant = utils.data_from_storage(self.inputs.calibrant) + calibrant = pyFAI.calibrant.get_calibrant(calibrant) wavelength = utils.energy_wavelength(energy) ai = pyFAI.azimuthalIntegrator.AzimuthalIntegrator( detector=detector, **geometry, wavelength=wavelength ) - calibrant = pyFAI.calibrant.get_calibrant(self.inputs.calibrant) calibrant.set_wavelength(wavelength) tth = calibrant.get_2th() ttha = ai.twoThetaArray() ax1.contour( - ttha, levels=tth, cmap="autumn", linewidths=2 + ttha, levels=tth, cmap="autumn", linewidths=1 ) # linestyles="dashed" # Detected points on 2D pattern - for label, points in rings.items(): - ax2.scatter(points["p1"], points["p0"], label=label) + for label, points in control_pts.items(): + ax2.scatter(points["p1"], points["p0"], label=label, marker=".") def show(self): # Diagnose - plt.legend(loc="center left", bbox_to_anchor=(1, 0.5)) + # plt.legend(loc="center left", bbox_to_anchor=(1, 0.5)) + plt.tight_layout() super().show() class DiagnoseCalibrateSingleResults( _DiagnoseCalibrateResults, - input_names=["image", "rings", "geometry", "energy"], + input_names=["image", "geometry", "energy"], + optional_input_names=["rings"], ): def run(self): if self.inputs.filename: @@ -112,12 +115,20 @@ class DiagnoseCalibrateSingleResults( return if plt is None: raise RuntimeError("'matplotlib' is not installed") - _, (ax1, ax2) = plt.subplots(nrows=1, ncols=2) + + rings = self.inputs.rings + if rings: + _, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(20, 10)) + else: + rings = dict() + _, ax1 = plt.subplots(nrows=1, ncols=1, figsize=(10, 10)) + ax2 = None + self.plot_calibration( ax1, ax2, utils.get_image(self.inputs.image), - utils.data_from_storage(self.inputs.rings), + utils.data_from_storage(rings), utils.data_from_storage(self.inputs.geometry), self.inputs.energy, ) @@ -129,11 +140,10 @@ class DiagnoseCalibrateMultiResults( input_names=[ "images", "positions", - "rings", "parametrization", "parameters", ], - optional_input_names=["show", "pause"], + optional_input_names=["show", "pause", "rings"], ): def run(self): if self.inputs.filename: @@ -142,10 +152,18 @@ class DiagnoseCalibrateMultiResults( return if plt is None: raise RuntimeError("'matplotlib' is not installed") - nrows = len(self.inputs.images) - _, axes = plt.subplots(nrows=nrows, ncols=2) + nimages = len(self.inputs.images) + rings = self.inputs.rings + if rings: + rings = {int(k): v for k, v in rings.items()} + _, axes = plt.subplots(nrows=nimages, ncols=2, figsize=(20, 10 * nimages)) + else: + rings = {i: dict() for i in range(nimages)} + _, axes = plt.subplots(nrows=nimages, ncols=1, figsize=(10, 10 * nimages)) + axes = [(ax1, None) for ax1 in axes] + for image, position, ringsi, (ax1, ax2) in zip( - self.inputs.images, self.inputs.positions, sorted(self.inputs.rings), axes + self.inputs.images, self.inputs.positions, sorted(rings), axes ): image = utils.get_image(image) position = utils.get_data(position) @@ -154,8 +172,10 @@ class DiagnoseCalibrateMultiResults( geometry, energy = calculate_geometry( parametrization, self.inputs.parameters, position ) - rings = utils.data_from_storage(self.inputs.rings[ringsi]) - self.plot_calibration(ax1, ax2, image, rings, geometry, energy, title=title) + control_pts = utils.data_from_storage(rings[ringsi]) + self.plot_calibration( + ax1, ax2, image, control_pts, geometry, energy, title=title + ) self.show() @@ -175,8 +195,9 @@ class DiagnoseIntegrate1D( return if plt is None: raise RuntimeError("'matplotlib' is not installed") - plt.figure() + plt.figure(figsize=(20, 10)) plt.plot(self.inputs.x, self.inputs.y) + plt.yscale("symlog") plt.xlabel(self.inputs.xunits) if self.inputs.calibrant: assert self.inputs.energy, "'energy' task parameter is missing" @@ -184,7 +205,8 @@ class DiagnoseIntegrate1D( self.show() def plot_calibrant_lines(self): - calibrant = pyFAI.calibrant.get_calibrant(self.inputs.calibrant) + calibrant = utils.data_from_storage(self.inputs.calibrant) + calibrant = pyFAI.calibrant.get_calibrant(calibrant) wavelength = utils.energy_wavelength(self.inputs.energy) calibrant.set_wavelength(wavelength) @@ -192,9 +214,10 @@ class DiagnoseIntegrate1D( mask = (xvalues >= min(self.inputs.x)) & (xvalues <= max(self.inputs.x)) xvalues = xvalues[mask] if xvalues.size: + yvalues = numpy.interp(xvalues, self.inputs.x, self.inputs.y) labels = utils.calibrant_ring_labels(calibrant) labels = numpy.array(labels[: mask.size])[mask] - ymin, ymax = plt.gca().get_ylim() - for label, x in zip(labels, xvalues): + ymin = min(self.inputs.y) + for label, x, ymax in zip(labels, xvalues, yvalues): plt.plot([x, x], [ymin, ymax]) - plt.text(x, ymax, label) + # plt.text(x, ymax, label) diff --git a/ewoksxrpd/tasks/integrate.py b/ewoksxrpd/tasks/integrate.py index 8269d0eb56b598c5403d98f9b799266ea9edec7b..ec435e115ae300a3863b82497eab3f1d9d524943 100644 --- a/ewoksxrpd/tasks/integrate.py +++ b/ewoksxrpd/tasks/integrate.py @@ -64,7 +64,7 @@ class Integrate1D( config["detector"] = utils.data_from_storage(self.inputs.detector) config["wavelength"] = utils.energy_wavelength(self.inputs.energy) if not self.missing_inputs.mask: - config["mask"] = utils.get_image(self.inputs.mask) + config["mask"] = utils.get_image(utils.data_from_storage(self.inputs.mask)) return config def get_worker_options(self) -> dict: diff --git a/ewoksxrpd/tasks/nexus.py b/ewoksxrpd/tasks/nexus.py index 2f9b1ef354cd49346835881adb637a4b29b1aea2..e9478abe73f9b87e5cf07d99848f57d9d64e7bfa 100644 --- a/ewoksxrpd/tasks/nexus.py +++ b/ewoksxrpd/tasks/nexus.py @@ -66,6 +66,7 @@ class SaveNexusPattern1D( "xunits", ], optional_input_names=["header", "yerror", "metadata"], + output_names=["saved"], ): def run(self): url = create_url(self.inputs.url) @@ -74,6 +75,7 @@ class SaveNexusPattern1D( self.save_diffractogram(parent) self.save_metadata(parent) self.save_nxprocess(parent) + self.outputs.saved = True def save_diffractogram(self, parent): xunits = self.inputs.xunits diff --git a/ewoksxrpd/tasks/pyfaiconfig.py b/ewoksxrpd/tasks/pyfaiconfig.py new file mode 100644 index 0000000000000000000000000000000000000000..e6158ad872fdfda6720064a499f8c7d047fd6b03 --- /dev/null +++ b/ewoksxrpd/tasks/pyfaiconfig.py @@ -0,0 +1,46 @@ +import json +from ewokscore import Task +from .utils import energy_wavelength +from pyFAI.io.ponifile import PoniFile + +__all__ = ["PyFaiConfig"] + + +class PyFaiConfig( + Task, + optional_input_names=[ + "filename", + "energy", + "geometry", + "mask", + "detector", + "calibrant", + ], + output_names=["energy", "geometry", "detector", "calibrant", "mask"], +): + def run(self): + adict = self.from_file() + wavelength = adict.get("wavelength", None) + if wavelength is not None: + self.outputs.energy = energy_wavelength(wavelength) + geometry = { + k: adict[k] + for k in ["dist", "poni1", "poni2", "rot1", "rot2", "rot3"] + if k in adict + } + if len(geometry) != 6: + geometry = self.inputs.geometry + self.outputs.geometry = geometry + self.outputs.detector = adict.get("detector", self.inputs.detector) + self.outputs.calibrant = self.inputs.calibrant + self.outputs.mask = self.inputs.mask + + def from_file(self) -> dict: + filename = self.inputs.filename + if not filename: + return dict() + if filename.endswith(".json"): + with open(filename, "r") as fp: + return json.load(fp) + else: + return PoniFile(filename).as_dict() diff --git a/ewoksxrpd/tasks/utils.py b/ewoksxrpd/tasks/utils.py index 9c1a16f9f960ae00390426b8b125f068f7e58bf9..d9738bf1738ff16ea2ba1cfc79a7ead05291727e 100644 --- a/ewoksxrpd/tasks/utils.py +++ b/ewoksxrpd/tasks/utils.py @@ -1,4 +1,4 @@ -from typing import Dict, Iterable, Mapping, Tuple, List, Union +from typing import Dict, Iterable, Mapping, Sequence, Tuple, List, Union from numbers import Number from contextlib import contextmanager @@ -10,6 +10,7 @@ import pyFAI.calibrant from silx.io.url import DataUrl import silx.io.h5py_utils from silx.utils.retry import RetryError +from silx.io.utils import get_data as _get_data def energy_wavelength(x): @@ -113,15 +114,20 @@ def get_data( data: Union[str, ArrayLike, Number], gui: bool = False, **options ) -> Union[numpy.ndarray, Number]: if isinstance(data, str): - filename, h5path, idx = h5url_parse(data) - if gui: - return get_hdf5_data(filename, h5path, idx=idx, retry_timeout=0, **options) + if data.endswith(".h5") or data.endswith(".nx"): + filename, h5path, idx = h5url_parse(data) + if gui: + return get_hdf5_data( + filename, h5path, idx=idx, retry_timeout=0, **options + ) + else: + return get_hdf5_data(filename, h5path, idx=idx, **options) else: - return get_hdf5_data(filename, h5path, idx=idx, **options) - elif isinstance(data, (numpy.ndarray, Number)): + return _get_data(data) + elif isinstance(data, (Sequence, Number, numpy.ndarray)): return data else: - raise TypeError(data) + raise TypeError(type(data)) def get_image(*args, **kwargs) -> numpy.ndarray: diff --git a/ewoksxrpd/tasks/worker.py b/ewoksxrpd/tasks/worker.py index 581000d4820c0b0696ef5de869ec0eeab73c0921..04eb8d003d2c9be9808d5b3d23a32eabb0893d6b 100644 --- a/ewoksxrpd/tasks/worker.py +++ b/ewoksxrpd/tasks/worker.py @@ -1,6 +1,7 @@ +import logging from contextlib import contextmanager from collections import OrderedDict -from typing import Hashable, Iterable, Dict +from typing import Iterable, Dict, Mapping from ewokscore.hashing import uhash import pyFAI @@ -9,6 +10,9 @@ import pyFAI.worker _WORKER_POOL = None +logger = logging.getLogger(__name__) + + class WorkerPool: """Pool with one worker per configuration up to a maximum number of workers.""" @@ -24,19 +28,37 @@ class WorkerPool: @contextmanager def worker( - self, worker_options: Dict, integration_options: Dict[Hashable, Hashable] + self, worker_options: Mapping, integration_options: Mapping ) -> Iterable[pyFAI.worker.Worker]: # TODO: deal with threads and subprocesses worker_id = self._worker_id(worker_options, integration_options) worker = self._workers.pop(worker_id, None) if worker is None: - worker = pyFAI.worker.Worker(**worker_options) - worker.set_config(integration_options, consume_keys=False) + worker = self._create_worker(worker_options, integration_options) self._workers[worker_id] = worker while len(self._workers) > self.nworkers: self._workers.popitem(last=False) yield worker + @staticmethod + def _create_worker( + worker_options: Mapping, integration_options: Mapping + ) -> pyFAI.worker.Worker: + # Worker class has the following issues: + # - cannot provide a "mask" in memory through the configuration + # - the "error_model" parameter is not used + worker = pyFAI.worker.Worker(**worker_options) + integration_options = dict(integration_options) + mask = integration_options.pop("mask", None) + provided = set(integration_options) + worker.set_config(integration_options, consume_keys=True) + unused = {k: v for k, v in integration_options.items() if k in provided} + if unused: + logger.warning("Unused pyfai integration options: %s", unused) + if mask is not None: + worker.ai.set_mask(mask) + return worker + def _get_global_pool() -> WorkerPool: global _WORKER_POOL @@ -52,8 +74,8 @@ def maximum_persistent_workers(nworkers: int) -> None: @contextmanager def persistent_worker( - worker_options: Dict, - integration_options: Dict[Hashable, Hashable], + worker_options: Mapping, + integration_options: Mapping, ) -> Iterable[pyFAI.worker.Worker]: """Get a worker for a particular configuration that stays in memory.""" pool = _get_global_pool() diff --git a/ewoksxrpd/tests/conftest.py b/ewoksxrpd/tests/conftest.py index 07631c245747884a0b1d019d8b39b2a604efd15f..7c6a1d642654689bf2dc052761b3446c8c811808 100644 --- a/ewoksxrpd/tests/conftest.py +++ b/ewoksxrpd/tests/conftest.py @@ -1,8 +1,13 @@ +import os +import h5py import pytest import numpy +from silx.io.dictdump import dicttonx import pyFAI.azimuthalIntegrator import pyFAI.detectors +from ewoksorange.canvas.handler import OrangeCanvasHandler +from ewoksorange.tests.conftest import qtapp # noqa F401 from .utils import Calibration, Measurement, Setup, xPattern, yPattern from .utils import measurement, calibration @@ -138,3 +143,61 @@ def imageSetup2Calibrant1( setup2: Setup, ) -> Calibration: return calibration("LaB6", aiSetup2, setup2) + + +def next_scan_number(filename) -> int: + if not os.path.exists(filename): + return 1 + with h5py.File(filename, "r") as h5file: + return int(max(map(float, h5file.keys()))) + 1 + + +def singledistance_calibration_data( + tmpdir, imageSetup1Calibrant1, setup1, imageSetup2Calibrant1, setup2 +): + mcalib_images = list() + mcalib_positions = list() + images = [ + ( + imageSetup1Calibrant1.image, + setup1.geometry["dist"] * 100, + ), + ( + imageSetup2Calibrant1.image, + setup2.geometry["dist"] * 100, + ), + ] + data = {"@NX_class": "NXroot", "@default": "1.1"} + filename = str(tmpdir / "calib.h5") + for i, (image, detz) in enumerate(images, 1): + data[f"{i}.1"] = { + "@default": "plotselect", + "instrument": { + "@NX_class": "NXinstrument", + "pilatus1": { + "@NX_class": "NXdetector", + "data": image, + }, + "positioners": {"detz": detz, "detz@units": "cm"}, + }, + "title": "sct 1", + "measurement": {">pilatus1": "../instrument/pilatus1/data"}, + "plotselect": { + "@NX_class": "NXdata", + "@signal": "data", + ">data": "../instrument/pilatus1/data", + }, + } + mcalib_images.append(f"silx://{filename}?path=/{i}.1/measurement/pilatus1") + mcalib_positions.append( + f"silx://{filename}?path=/{i}.1/instrument/positioners/detz" + ) + dicttonx(data, filename, update_mode="add") + + return mcalib_images, mcalib_positions + + +@pytest.fixture(scope="session") +def ewoks_orange_canvas(qtapp): # noqa F811 + with OrangeCanvasHandler() as handler: + yield handler diff --git a/ewoksxrpd/tests/test_ascii.py b/ewoksxrpd/tests/test_ascii.py index 9f43120e39048741a9815c12a77461d94c07528d..4d6867109f5affed1b0d5bb15293a9ebf0985d26 100644 --- a/ewoksxrpd/tests/test_ascii.py +++ b/ewoksxrpd/tests/test_ascii.py @@ -1,9 +1,19 @@ import re import numpy from ewoksxrpd.tasks import SaveAsciiPattern1D +from orangecontrib.ewoksxrpd.ascii import OWSaveAsciiPattern1D +from .utils import execute_task -def test_save_ascii(tmpdir, setup1): +def test_save_ascii_task(tmpdir, setup1): + assert_save_ascii(tmpdir, setup1, None) + + +def test_save_ascii_widget(tmpdir, setup1, qtapp): + assert_save_ascii(tmpdir, setup1, qtapp) + + +def assert_save_ascii(tmpdir, setup1, qtapp): inputs = { "filename": str(tmpdir / "result.dat"), "x": numpy.linspace(1, 60, 60), @@ -16,8 +26,13 @@ def test_save_ascii(tmpdir, setup1): }, "metadata": {"name": "mysample"}, } - task = SaveAsciiPattern1D(inputs=inputs) - task.execute() + + execute_task( + SaveAsciiPattern1D, + OWSaveAsciiPattern1D, + inputs=inputs, + widget=qtapp is not None, + ) x, y = numpy.loadtxt(str(tmpdir / "result.dat")).T numpy.testing.assert_array_equal(x, inputs["x"]) diff --git a/ewoksxrpd/tests/test_background.py b/ewoksxrpd/tests/test_background.py index 35245c2bcb1dca681c489b895fb418a5b9ca0105..afac41c68a1415c754ec9b219263bd44c0a68ff5 100644 --- a/ewoksxrpd/tests/test_background.py +++ b/ewoksxrpd/tests/test_background.py @@ -1,12 +1,35 @@ import numpy from ewoksxrpd.tasks import SubtractBackground -from .utils import Measurement +from orangecontrib.ewoksxrpd.background import OWSubtractBackground +from .utils import Measurement, execute_task -def test_background_subtraction( +def test_background_subtraction_task( imageSetup1SampleA: Measurement, image1Setup1SampleB: Measurement, image2Setup1SampleB: Measurement, +): + assert_background_subtraction( + imageSetup1SampleA, image1Setup1SampleB, image2Setup1SampleB, None + ) + + +def test_background_subtraction_widget( + imageSetup1SampleA: Measurement, + image1Setup1SampleB: Measurement, + image2Setup1SampleB: Measurement, + qtapp, +): + assert_background_subtraction( + imageSetup1SampleA, image1Setup1SampleB, image2Setup1SampleB, qtapp + ) + + +def assert_background_subtraction( + imageSetup1SampleA: Measurement, + image1Setup1SampleB: Measurement, + image2Setup1SampleB: Measurement, + qtapp, ): image = imageSetup1SampleA.image + image1Setup1SampleB.image inputs = { @@ -15,8 +38,14 @@ def test_background_subtraction( "background": image2Setup1SampleB.image, "background_monitor": image2Setup1SampleB.monitor, } + results = execute_task( + SubtractBackground, + OWSubtractBackground, + inputs=inputs, + widget=qtapp is not None, + ) task = SubtractBackground(inputs=inputs) task.execute() numpy.testing.assert_allclose( - imageSetup1SampleA.image, task.outputs.image, atol=1e-10 + imageSetup1SampleA.image, results["image"], atol=1e-10 ) diff --git a/ewoksxrpd/tests/test_calibrate.py b/ewoksxrpd/tests/test_calibrate.py index 08857be27fe862dd66a13cb66869f5f4dde7808c..ef6efc2be0118b1c034ba2ba4755574ed473aad1 100644 --- a/ewoksxrpd/tests/test_calibrate.py +++ b/ewoksxrpd/tests/test_calibrate.py @@ -1,3 +1,4 @@ +from os import PathLike from typing import List import numpy import pytest @@ -7,15 +8,101 @@ from ewoksxrpd.tasks import CalibrateMulti from ewoksxrpd.tasks import CalculateGeometry from ewoksxrpd.tasks import DiagnoseCalibrateSingleResults from ewoksxrpd.tasks import DiagnoseCalibrateMultiResults -from .utils import Calibration, Setup +from .utils import Calibration, Setup, execute_task +from orangecontrib.ewoksxrpd.calibratesingle import OWCalibrateSingle +from orangecontrib.ewoksxrpd.calibratemulti import OWCalibrateMulti +from orangecontrib.ewoksxrpd.calculategeometry import OWCalculateGeometry +from orangecontrib.ewoksxrpd.diagnose_singlecalib import ( + OWDiagnoseCalibrateSingleResults, +) +from orangecontrib.ewoksxrpd.diagnose_multicalib import ( + OWDiagnoseCalibrateMultiResults, +) @pytest.mark.parametrize("fixed", [[], ["energy"], ["rot2", "dist"]]) -def test_calibrate_single_distance( +def test_calibrate_single_distance_task( fixed: List[str], imageSetup1Calibrant1: Calibration, setup1: Setup, aiSetup1: pyFAI.azimuthalIntegrator.AzimuthalIntegrator, + tmpdir: PathLike, +): + assert_calibrate_single_distance( + fixed, imageSetup1Calibrant1, setup1, aiSetup1, tmpdir, None + ) + + +@pytest.mark.parametrize("fixed", [[], ["energy"], ["rot2", "dist"]]) +def test_calibrate_single_distance_widget( + fixed: List[str], + imageSetup1Calibrant1: Calibration, + setup1: Setup, + aiSetup1: pyFAI.azimuthalIntegrator.AzimuthalIntegrator, + tmpdir: PathLike, + qtapp, +): + assert_calibrate_single_distance( + fixed, imageSetup1Calibrant1, setup1, aiSetup1, tmpdir, qtapp + ) + + +@pytest.mark.parametrize("fixed", [[], ["energy"], ["rot2", "dist"]]) +def test_calibrate_multi_distance_task( + fixed: List[str], + imageSetup1Calibrant1: Calibration, + setup1: Setup, + imageSetup2Calibrant1: Calibration, + setup2: Setup, + aiSetup1: pyFAI.azimuthalIntegrator.AzimuthalIntegrator, + aiSetup2: pyFAI.azimuthalIntegrator.AzimuthalIntegrator, + tmpdir: PathLike, +): + assert_calibrate_multi_distance( + fixed, + imageSetup1Calibrant1, + setup1, + imageSetup2Calibrant1, + setup2, + aiSetup1, + aiSetup2, + tmpdir, + None, + ) + + +@pytest.mark.parametrize("fixed", [[], ["energy"], ["rot2", "dist"]]) +def test_calibrate_multi_distance_widget( + fixed: List[str], + imageSetup1Calibrant1: Calibration, + setup1: Setup, + imageSetup2Calibrant1: Calibration, + setup2: Setup, + aiSetup1: pyFAI.azimuthalIntegrator.AzimuthalIntegrator, + aiSetup2: pyFAI.azimuthalIntegrator.AzimuthalIntegrator, + tmpdir: PathLike, + qtapp, +): + assert_calibrate_multi_distance( + fixed, + imageSetup1Calibrant1, + setup1, + imageSetup2Calibrant1, + setup2, + aiSetup1, + aiSetup2, + tmpdir, + qtapp, + ) + + +def assert_calibrate_single_distance( + fixed: List[str], + imageSetup1Calibrant1: Calibration, + setup1: Setup, + aiSetup1: pyFAI.azimuthalIntegrator.AzimuthalIntegrator, + tmpdir: PathLike, + qtapp: None, ): fixed = [] geometry0, energy0 = guess_fit_parameters( @@ -29,22 +116,28 @@ def test_calibrate_single_distance( "calibrant": imageSetup1Calibrant1.calibrant, "fixed": fixed, } - task = CalibrateSingle(inputs=inputs) - task.execute() + + outputs_values = execute_task( + CalibrateSingle, + OWCalibrateSingle, + inputs=inputs, + widget=qtapp is not None, + timeout=3, + ) assert_calibration( False, fixed, - task.outputs.geometry, - task.outputs.energy, - task.outputs.rings, + outputs_values["geometry"], + outputs_values["energy"], + outputs_values["rings"], setup1, imageSetup1Calibrant1, aiSetup1, + tmpdir, ) -@pytest.mark.parametrize("fixed", [[], ["energy"], ["rot2", "dist"]]) -def test_calibrate_multi_distance( +def assert_calibrate_multi_distance( fixed: List[str], imageSetup1Calibrant1: Calibration, setup1: Setup, @@ -52,6 +145,8 @@ def test_calibrate_multi_distance( setup2: Setup, aiSetup1: pyFAI.azimuthalIntegrator.AzimuthalIntegrator, aiSetup2: pyFAI.azimuthalIntegrator.AzimuthalIntegrator, + tmpdir: PathLike, + qtapp, ): images = [imageSetup1Calibrant1.image, imageSetup2Calibrant1.image] positionunits_in_meter = 1e-2 # cm @@ -75,74 +170,91 @@ def test_calibrate_multi_distance( "calibrant": imageSetup1Calibrant1.calibrant, "fixed": fixed, } - calibtask = CalibrateMulti(inputs=inputs) - calibtask.execute() + + calibresults = execute_task( + CalibrateMulti, OWCalibrateMulti, inputs=inputs, widget=qtapp is not None + ) + assert_calibration( True, fixed, - calibtask.outputs.geometry, - calibtask.outputs.energy, - calibtask.outputs.rings["0"], + calibresults["geometry"], + calibresults["energy"], + calibresults["rings"]["0"], setup1, imageSetup1Calibrant1, aiSetup1, + tmpdir, ) - geometry = dict(calibtask.outputs.geometry) + geometry = dict(calibresults["geometry"]) shift_in_meter = setup2.geometry["dist"] - setup1.geometry["dist"] geometry["dist"] += shift_in_meter assert_calibration( True, fixed, geometry, - calibtask.outputs.energy, - calibtask.outputs.rings["1"], + calibresults["energy"], + calibresults["rings"]["1"], setup2, imageSetup2Calibrant1, aiSetup2, + tmpdir, ) inputs = { - "parametrization": calibtask.outputs.parametrization, - "parameters": calibtask.outputs.parameters, + "parametrization": calibresults["parametrization"], + "parameters": calibresults["parameters"], "position": positions[0], } - calctask = CalculateGeometry(inputs) - calctask.execute() + + calcresults = execute_task( + CalculateGeometry, OWCalculateGeometry, inputs=inputs, widget=qtapp is not None + ) + assert_geometry( True, fixed, - calctask.outputs.geometry, - calctask.outputs.energy, + calcresults["geometry"], + calcresults["energy"], setup1, aiSetup1, ) inputs["position"] = positions[1] - calctask = CalculateGeometry(inputs) - calctask.execute() + calcresults = execute_task( + CalculateGeometry, OWCalculateGeometry, inputs=inputs, widget=qtapp is not None + ) + assert_geometry( True, fixed, - calctask.outputs.geometry, - calctask.outputs.energy, + calcresults["geometry"], + calcresults["energy"], setup2, aiSetup2, ) # Set show=True to visualize the calibration results + filename = tmpdir / "test.png" inputs = { "images": images, "positions": positions, "detector": setup1.detector, "calibrant": imageSetup1Calibrant1.calibrant, - "rings": calibtask.outputs.rings, - "parametrization": calibtask.outputs.parametrization, - "parameters": calibtask.outputs.parameters, + "rings": calibresults["rings"], + "parametrization": calibresults["parametrization"], + "parameters": calibresults["parameters"], "show": False, + "filename": str(filename), } - plottask = DiagnoseCalibrateMultiResults(inputs=inputs) - plottask.execute() + execute_task( + DiagnoseCalibrateMultiResults, + OWDiagnoseCalibrateMultiResults, + inputs=inputs, + widget=qtapp is not None, + ) + assert filename.exists() def assert_calibration( @@ -154,9 +266,12 @@ def assert_calibration( setup: Setup, calibration: Calibration, ai: pyFAI.azimuthalIntegrator.AzimuthalIntegrator, + tmpdir: PathLike, + qtapp=None, ): assert_geometry(multi_distance, fixed, geometry, energy, setup, ai) - # Set show=True to visualize the calibration results + + filename = tmpdir / "diagnose.png" inputs = { "image": calibration.image, "geometry": geometry, @@ -165,9 +280,17 @@ def assert_calibration( "calibrant": calibration.calibrant, "rings": rings, "show": False, + "filename": str(filename), } - task = DiagnoseCalibrateSingleResults(inputs=inputs) - task.execute() + + execute_task( + DiagnoseCalibrateSingleResults, + OWDiagnoseCalibrateSingleResults, + inputs=inputs, + widget=qtapp is not None, + timeout=10, + ) + assert filename.exists() def guess_fit_parameters( diff --git a/ewoksxrpd/tests/test_calint_workflow.py b/ewoksxrpd/tests/test_calint_workflow.py index f5423247b991cef287754625a3d170ce6f447cca..3d7252e44ee5638c2872d6c7d5728d1962596950 100644 --- a/ewoksxrpd/tests/test_calint_workflow.py +++ b/ewoksxrpd/tests/test_calint_workflow.py @@ -413,7 +413,6 @@ def test_calint_workflow( taskgraph = load_graph(calibintworkflow(), inputs=inputs) taskgraph.dump(str(tmpdir / "xrpd_workflow.json"), indent=2) - print(tmpdir) plt.show() diff --git a/ewoksxrpd/tests/test_integrate.py b/ewoksxrpd/tests/test_integrate.py index 294780e6a273febbf87e51cc11d7f8d5a17e5066..1a16c48ce3f1a70f21a468bf12179beeb7b56d4c 100644 --- a/ewoksxrpd/tests/test_integrate.py +++ b/ewoksxrpd/tests/test_integrate.py @@ -1,33 +1,50 @@ +from os import PathLike import numpy from ewoksxrpd.tasks import Integrate1D from ewoksxrpd.tasks import DiagnoseIntegrate1D -from .utils import Measurement, Setup, xPattern, yPattern +from orangecontrib.ewoksxrpd.integrate1d import OWIntegrate1D +from orangecontrib.ewoksxrpd.diagnose_integrate1d import OWDiagnoseIntegrate1D +from .utils import Measurement, Setup, xPattern, yPattern, execute_task -def test_integrate1d( +def test_integrate1d_task( + tmpdir: PathLike, imageSetup1SampleA: Measurement, setup1: Setup, xSampleA: xPattern, ySampleA: yPattern, ): - assert_integrate1d(imageSetup1SampleA, setup1, xSampleA, ySampleA) + assert_integrate1d(imageSetup1SampleA, setup1, xSampleA, ySampleA, tmpdir, None) -def test_sigma_clip( +def test_integrate1d_widget( + tmpdir: PathLike, imageSetup1SampleA: Measurement, setup1: Setup, xSampleA: xPattern, ySampleA: yPattern, + qtapp, ): - # from pyFAI.method_registry import IntegrationMethod - # for method in IntegrationMethod._registry: - # print(f"{method.split}_{method.algo}_{method.impl}") - # - # {split}_{algo}_{impl}{target} - # split: "no", "bbox", "pseudo", "full" - # algo: "histogram", "lut", "csr" - # impl: "python", "cython", "opencl" + assert_integrate1d(imageSetup1SampleA, setup1, xSampleA, ySampleA, tmpdir, qtapp) + +# from pyFAI.method_registry import IntegrationMethod +# for method in IntegrationMethod._registry: +# print(f"{method.split}_{method.algo}_{method.impl}") +# +# {split}_{algo}_{impl}{target} +# split: "no", "bbox", "pseudo", "full" +# algo: "histogram", "lut", "csr" +# impl: "python", "cython", "opencl" + + +def test_sigma_clip_task( + tmpdir: PathLike, + imageSetup1SampleA: Measurement, + setup1: Setup, + xSampleA: xPattern, + ySampleA: yPattern, +): integration_options = {"method": "no_csr_cython", "error_model": "azimuthal"} worker_options = { "integrator_name": "sigma_clip_ng", @@ -38,12 +55,40 @@ def test_sigma_clip( setup1, xSampleA, ySampleA, + tmpdir, + None, + integration_options=integration_options, + worker_options=worker_options, + ) + + +def test_sigma_clip_widget( + tmpdir: PathLike, + imageSetup1SampleA: Measurement, + setup1: Setup, + xSampleA: xPattern, + ySampleA: yPattern, + qtapp, +): + integration_options = {"method": "no_csr_cython", "error_model": "azimuthal"} + worker_options = { + "integrator_name": "sigma_clip_ng", + "extra_options": {"max_iter": 3, "thres": 0}, + } + assert_integrate1d( + imageSetup1SampleA, + setup1, + xSampleA, + ySampleA, + tmpdir, + qtapp, integration_options=integration_options, worker_options=worker_options, ) def test_integrate1d_reconfig( + tmpdir: PathLike, imageSetup1SampleA: Measurement, setup1: Setup, imageSetup2SampleA: Measurement, @@ -51,10 +96,10 @@ def test_integrate1d_reconfig( xSampleA: xPattern, ySampleA: yPattern, ): - assert_integrate1d(imageSetup1SampleA, setup1, xSampleA, ySampleA) - assert_integrate1d(imageSetup2SampleA, setup2, xSampleA, ySampleA) - assert_integrate1d(imageSetup1SampleA, setup1, xSampleA, ySampleA) - assert_integrate1d(imageSetup1SampleA, setup1, xSampleA, ySampleA) + assert_integrate1d(imageSetup1SampleA, setup1, xSampleA, ySampleA, tmpdir, None) + assert_integrate1d(imageSetup2SampleA, setup2, xSampleA, ySampleA, tmpdir, None) + assert_integrate1d(imageSetup1SampleA, setup1, xSampleA, ySampleA, tmpdir, None) + assert_integrate1d(imageSetup1SampleA, setup1, xSampleA, ySampleA, tmpdir, None) def assert_integrate1d( @@ -62,6 +107,8 @@ def assert_integrate1d( setup: Setup, xpattern: xPattern, ypattern: yPattern, + tmpdir: PathLike, + qtapp, integration_options=None, worker_options=None, ): @@ -79,22 +126,31 @@ def assert_integrate1d( } if worker_options: inputs["worker_options"] = worker_options - task = Integrate1D(inputs=inputs) - task.execute() - assert task.outputs.xunits == xpattern.units - numpy.testing.assert_allclose(xpattern.x, task.outputs.x, rtol=1e-6) + output_values = execute_task( + Integrate1D, OWIntegrate1D, inputs=inputs, widget=qtapp is not None + ) + + assert output_values["xunits"] == xpattern.units + numpy.testing.assert_allclose(xpattern.x, output_values["x"], rtol=1e-6) atol = ypattern.y.max() * 0.01 - numpy.testing.assert_allclose(ypattern.y, task.outputs.y, atol=atol) + numpy.testing.assert_allclose(ypattern.y, output_values["y"], atol=atol) # Set show=True to visualize the calibration results + filename = tmpdir / "diagnose.png" inputs = { - "x": task.outputs.x, - "y": task.outputs.y, - "xunits": task.outputs.xunits, + "x": output_values["x"], + "y": output_values["y"], + "xunits": output_values["xunits"], "show": False, + "filename": str(filename), # "energy": setup.energy, # "calibrant": "LaB6" } - plottask = DiagnoseIntegrate1D(inputs=inputs) - plottask.execute() + execute_task( + DiagnoseIntegrate1D, + OWDiagnoseIntegrate1D, + inputs=inputs, + widget=qtapp is not None, + ) + assert filename.exists() diff --git a/ewoksxrpd/tests/test_mask.py b/ewoksxrpd/tests/test_mask.py index 2b5b2fca2133bbaf7c9173da2a8adf56f5f14f6f..6ab1bb594cffcfde171dd69da3854ef302d0d573 100644 --- a/ewoksxrpd/tests/test_mask.py +++ b/ewoksxrpd/tests/test_mask.py @@ -1,13 +1,32 @@ import numpy import pyFAI.azimuthalIntegrator from ewoksxrpd.tasks import MaskDetection -from .utils import Measurement +from orangecontrib.ewoksxrpd.mask import OWMaskDetection +from .utils import Measurement, execute_task -def test_mask_detection( +def test_mask_detection_task( image1Setup1SampleB: Measurement, image2Setup1SampleB: Measurement, aiSetup1: pyFAI.azimuthalIntegrator.AzimuthalIntegrator, +): + assert_mask_detection(image1Setup1SampleB, image2Setup1SampleB, aiSetup1, None) + + +def test_mask_detection_widget( + image1Setup1SampleB: Measurement, + image2Setup1SampleB: Measurement, + aiSetup1: pyFAI.azimuthalIntegrator.AzimuthalIntegrator, + qtapp, +): + assert_mask_detection(image1Setup1SampleB, image2Setup1SampleB, aiSetup1, qtapp) + + +def assert_mask_detection( + image1Setup1SampleB: Measurement, + image2Setup1SampleB: Measurement, + aiSetup1: pyFAI.azimuthalIntegrator.AzimuthalIntegrator, + qtapp, ): inputs = { "image1": image1Setup1SampleB.image, @@ -15,15 +34,18 @@ def test_mask_detection( "image2": image2Setup1SampleB.image, "monitor2": image2Setup1SampleB.monitor, } - task = MaskDetection(inputs=inputs) - task.execute() + results = execute_task( + MaskDetection, OWMaskDetection, inputs=inputs, widget=qtapp is not None + ) + expected = aiSetup1.detector.get_mask() - numpy.testing.assert_array_equal(task.outputs.mask, expected) + numpy.testing.assert_array_equal(results["mask"], expected) inputs["smooth"] = 5 - task = MaskDetection(inputs=inputs) - task.execute() + results = execute_task( + MaskDetection, OWMaskDetection, inputs=inputs, widget=qtapp is not None + ) - borders = task.outputs.mask - expected + borders = results["mask"] - expected assert not (borders == 0).all(), "smoothing should add masked pixels" assert not (borders < 0).any(), "smoothing should never remove masked pixels" diff --git a/ewoksxrpd/tests/test_nexus.py b/ewoksxrpd/tests/test_nexus.py index 4b0ce0be02eab3058624aca49c6568d822fd1c2b..75785a9a4d32ee693790c4d92e3a7bab5f4f63d7 100644 --- a/ewoksxrpd/tests/test_nexus.py +++ b/ewoksxrpd/tests/test_nexus.py @@ -1,9 +1,19 @@ import numpy from silx.io.dictdump import nxtodict from ewoksxrpd.tasks import SaveNexusPattern1D +from orangecontrib.ewoksxrpd.nexus import OWSaveNexusPattern1D +from .utils import execute_task -def test_save_nexus(tmpdir, setup1): +def test_save_nexus_task(tmpdir, setup1): + assert_save_nexus(tmpdir, setup1, None) + + +def test_save_nexus_widget(tmpdir, setup1, qtapp): + assert_save_nexus(tmpdir, setup1, qtapp) + + +def assert_save_nexus(tmpdir, setup1, qtapp): inputs = { "url": str(tmpdir / "result.h5"), "x": numpy.linspace(1, 60, 60), @@ -16,8 +26,13 @@ def test_save_nexus(tmpdir, setup1): }, "metadata": {"sample": {"@NX_class": "NXSample", "name": "mysample"}}, } - task = SaveNexusPattern1D(inputs=inputs) - task.execute() + + execute_task( + SaveNexusPattern1D, + OWSaveNexusPattern1D, + inputs=inputs, + widget=qtapp is not None, + ) adict = nxtodict(str(tmpdir / "result.h5")) diff --git a/ewoksxrpd/tests/utils.py b/ewoksxrpd/tests/utils.py index 14224dd259ec0015b778146073c1eeddbd8a4d42..624a13a06bcd6198e7839934eeaf9ed19cc03d87 100644 --- a/ewoksxrpd/tests/utils.py +++ b/ewoksxrpd/tests/utils.py @@ -1,10 +1,11 @@ from dataclasses import dataclass, field -from typing import Any +from typing import Any, List, Mapping, Optional from numpy.typing import ArrayLike import numpy import pyFAI.units import pyFAI.azimuthalIntegrator import pyFAI.calibrant +from ewoksorange.bindings.taskwrapper import execute_ewoks_owwidget @dataclass(frozen=True) @@ -77,3 +78,19 @@ def measurement( monitor = mult * ypattern.monitor image = ai.calcfrom1d(x, y, dim1_unit=xpattern.units, mask=ai.mask) return Measurement(image=image, monitor=monitor) + + +def execute_task( + task_class, + widget_class, + inputs: Optional[List[Mapping]] = None, + widget: Optional[bool] = None, + timeout: int = 3, +) -> dict: + """Execute the task (use the orange widget or ewoks task class) and return the results""" + if widget: + return execute_ewoks_owwidget(widget_class, inputs=inputs, timeout=timeout) + else: + task = task_class(inputs=inputs) + task.execute() + return task.output_values diff --git a/orangecontrib/ewoksxrpd/ascii.py b/orangecontrib/ewoksxrpd/ascii.py index 893f07d7166c90ff3f7e146f62025b73b306d2db..8d9a75dcf2abee7431f887e1bbfad8be50b83c56 100644 --- a/orangecontrib/ewoksxrpd/ascii.py +++ b/orangecontrib/ewoksxrpd/ascii.py @@ -15,13 +15,8 @@ class OWSaveAsciiPattern1D(OWTriggerWidget, ewokstaskclass=SaveAsciiPattern1D): want_main_area = True def _init_forms(self): - super()._init_forms() - self._create_input_form("inputs", input_parameters_ascii(), self.controlArea) - - def _init_control_area(self): - layout = self._get_control_layout() - layout.addWidget(list(self._input_forms.values())[0]) - super()._init_control_area() + parameter_info = input_parameters_ascii(self.get_default_input_values()) + self._create_input_form(parameter_info) def _init_main_area(self): layout = self._get_main_layout() @@ -37,7 +32,7 @@ class OWSaveAsciiPattern1D(OWTriggerWidget, ewokstaskclass=SaveAsciiPattern1D): self._update_output_file() def _update_output_file(self): - inputs = self.task_input_values + inputs = self.get_task_input_values() filename = inputs.get("filename") if not filename or not os.path.isfile(filename): self._textedit.clear() diff --git a/orangecontrib/ewoksxrpd/background.py b/orangecontrib/ewoksxrpd/background.py index 6880ced9640c52189c449d074b2a82e74840d9ab..e9488feaf3339beb7d9c611fd9af90e6d0f0046e 100644 --- a/orangecontrib/ewoksxrpd/background.py +++ b/orangecontrib/ewoksxrpd/background.py @@ -15,17 +15,13 @@ class OWSubtractBackground(OWTriggerWidget, ewokstaskclass=SubtractBackground): icon = "icons/widget.png" want_main_area = True - def _init_forms(self): + def __init__(self, *args, **kwargs) -> None: self._tabs = QtWidgets.QTabWidget() - super()._init_forms() - self._create_input_form( - "inputs", input_parameters_background(), self.controlArea - ) + super().__init__(*args, **kwargs) - def _init_control_area(self): - layout = self._get_control_layout() - layout.addWidget(list(self._input_forms.values())[0]) - super()._init_control_area() + def _init_forms(self): + parameter_info = input_parameters_background(self.get_default_input_values()) + self._create_input_form(parameter_info) def _init_main_area(self): layout = self._get_main_layout() @@ -33,8 +29,6 @@ class OWSubtractBackground(OWTriggerWidget, ewokstaskclass=SubtractBackground): for name in ("Image", "Background", "Subtracted"): self._tabs.addTab(Plot2D(), name) super()._init_main_area() - self._refresh_non_form_input_widgets() - self._refresh_non_form_output_widgets() def _refresh_non_form_input_widgets(self): with self._capture_errors(): @@ -49,14 +43,14 @@ class OWSubtractBackground(OWTriggerWidget, ewokstaskclass=SubtractBackground): def _refresh_input_plots(self): if self._tabs.count() == 0: return - inputs = self.task_input_values + inputs = self.get_task_input_values() self._update_image(inputs) self._update_background(inputs) def _refresh_output_plots(self): if self._tabs.count() == 0: return - outputs = self.task_output_values + outputs = self.get_task_output_values() self._update_subtracted(outputs) def _update_image(self, inputs): diff --git a/orangecontrib/ewoksxrpd/calculategeometry.py b/orangecontrib/ewoksxrpd/calculategeometry.py index 28a57b4626682889d8167ddb1cbb5d3269bda64e..e4381c9867e6b9e9d30db4fed339255df629db55 100644 --- a/orangecontrib/ewoksxrpd/calculategeometry.py +++ b/orangecontrib/ewoksxrpd/calculategeometry.py @@ -1,10 +1,11 @@ +from typing import Dict, Mapping, Tuple from ewoksxrpd.tasks import CalculateGeometry from ewoksxrpd.gui.trigger_widget import OWTriggerWidget from ewoksxrpd.gui.forms import input_parameters_calculategeometry from ewoksxrpd.gui.forms import output_parameters_calculategeometry from ewoksxrpd.gui.forms import pack_geometry from ewoksxrpd.gui.forms import unpack_geometry - +from ewoksxrpd.gui.forms import unpack_enabled_geometry __all__ = ["OWCalculateGeometry"] @@ -16,26 +17,18 @@ class OWCalculateGeometry(OWTriggerWidget, ewokstaskclass=CalculateGeometry): want_main_area = True def _init_forms(self): - super()._init_forms() - self._create_input_form( - "inputs", input_parameters_calculategeometry(), self.controlArea - ) - self._create_output_form( - "results", output_parameters_calculategeometry(), self.mainArea + parameter_info = input_parameters_calculategeometry( + self.get_default_input_values() ) + self._create_input_form(parameter_info) + parameter_info = output_parameters_calculategeometry() + self._create_output_form(parameter_info) - def _init_control_area(self): - layout = self._get_control_layout() - layout.addWidget(list(self._input_forms.values())[0]) - super()._init_control_area() - - def _init_main_area(self): - layout = self._get_main_layout() - layout.addWidget(list(self._output_forms.values())[0]) - super()._init_main_area() + def _values_from_form(self, values: Mapping, checked: Dict[str, bool]) -> Mapping: + return pack_geometry(values, checked) - def _parameters_from_form(self, parameters): - return pack_geometry(parameters) + def _values_to_form(self, values: Mapping) -> Tuple[Mapping, Dict[str, bool]]: + return unpack_geometry(values) - def _parameters_to_form(self, parameters): - return unpack_geometry(parameters) + def _enabled_to_form(self, enabled: Dict[str, bool]) -> Dict[str, bool]: + return unpack_enabled_geometry(enabled) diff --git a/orangecontrib/ewoksxrpd/calibratemulti.py b/orangecontrib/ewoksxrpd/calibratemulti.py index 8e99c3e9cf5c73cbc540618038436a895cc3c2e2..a43aba0bdf1fdaa5f9dd1c2d15dfe201bb51526c 100644 --- a/orangecontrib/ewoksxrpd/calibratemulti.py +++ b/orangecontrib/ewoksxrpd/calibratemulti.py @@ -1,4 +1,4 @@ -from typing import Iterable +from typing import Dict, Iterable, List, Mapping, Tuple from AnyQt import QtWidgets from silx.gui.plot import ScatterView, PlotWidget from ewoksxrpd.tasks import CalibrateMulti @@ -7,9 +7,13 @@ from ewoksxrpd.gui import plots from ewoksxrpd.tasks import utils from ewoksxrpd.tasks.calibrate import calculate_geometry from ewoksxrpd.gui.forms import input_parameters_calibratemulti +from ewoksxrpd.gui.forms import output_parameters_calibratemulti from ewoksxrpd.gui.forms import pack_geometry from ewoksxrpd.gui.forms import unpack_geometry - +from ewoksxrpd.gui.forms import unpack_enabled_geometry +from ewoksxrpd.gui.forms import pack_parametrization +from ewoksxrpd.gui.forms import unpack_parametrization +from ewoksxrpd.gui.forms import unpack_enabled_parametrization __all__ = ["OWCalibrateMulti"] @@ -20,81 +24,226 @@ class OWCalibrateMulti(OWTriggerWidget, ewokstaskclass=CalibrateMulti): icon = "icons/widget.png" want_main_area = True - def _init_forms(self): + def __init__(self, *args, **kwargs) -> None: self._tabs = QtWidgets.QTabWidget() - super()._init_forms() - self._create_input_form( - "inputs", input_parameters_calibratemulti(), self.controlArea + self._show = { + "output_rings": True, + "detected_rings": False, + } + self._legends = list() + self._cache = list() + self._fixed_tabs = 0 + super().__init__(*args, **kwargs) + + def _init_forms(self): + parameter_info = input_parameters_calibratemulti( + self.get_default_input_values() ) + self._create_input_form(parameter_info) + parameter_info = output_parameters_calibratemulti() + self._create_output_form(parameter_info) def _init_control_area(self): - layout = self._get_control_layout() - layout.addWidget(list(self._input_forms.values())[0]) super()._init_control_area() - self._refresh_non_form_input_widgets() + layout = self._get_control_layout() + + w = QtWidgets.QPushButton("Accept refined") + layout.addWidget(w) + w.released.connect(self._accept_refined_parameters) + + self._show_output_rings_widget = w = QtWidgets.QCheckBox("Refined rings") + w.setChecked(self._show["output_rings"]) + layout.addWidget(w) + w.released.connect(self._set_show_output_rings) + + self._show_detected_rings_widget = w = QtWidgets.QCheckBox("Detected rings") + w.setChecked(self._show["detected_rings"]) + layout.addWidget(w) + w.released.connect(self._set_show_detected_rings) + + def _set_show_output_rings(self): + self._show["output_rings"] = self._show_output_rings_widget.isChecked() + self._refresh_non_form_output_widgets() + + def _set_show_detected_rings(self): + self._show["detected_rings"] = self._show_detected_rings_widget.isChecked() + self._refresh_non_form_output_widgets() def _init_main_area(self): layout = self._get_main_layout() layout.addWidget(self._tabs) + self._fixed_tabs = 1 + self._tabs.addTab(self._output_form, "Parametrization") super()._init_main_area() + self._refresh_non_form_input_widgets() self._refresh_non_form_output_widgets() - def _accept_refined_parameters(self): - try: - self.default_inputs["energy"] = self.get_task_output_value("energy") - except KeyError: - pass - try: - self.default_inputs["geometry"] = dict( - self.get_task_output_value("geometry") - ) - except KeyError: - pass - self._refresh_input_widgets() - - def _init_control_buttons(self): - super()._init_control_buttons() - accept = QtWidgets.QPushButton("Accept refined") - self.controlArea.layout().addWidget(accept) - accept.released.connect(self._accept_refined_parameters) + def _add_output_form_widget(self): + pass def _refresh_non_form_input_widgets(self): with self._capture_errors(): super()._refresh_non_form_input_widgets() self._refresh_input_plots() - self._refresh_mixed_plots() def _refresh_non_form_output_widgets(self): with self._capture_errors(): super()._refresh_non_form_output_widgets() - self._refresh_mixed_plots() + self._refresh_output_plots() + + def _accept_refined_parameters(self): + parametrization = self.get_task_output_value("parametrization") + parameters = self.get_task_output_value.get("parameters") + position = self.get_task_input_value.get("reference_position") + if not parametrization or not parameters or not position: + return + position = utils.get_data(position, gui=True) + parametrization = utils.data_from_storage(parametrization) + parameters = utils.data_from_storage(parameters) + geometry, energy = calculate_geometry(parametrization, parameters, position) + self.update_default_inputs(energy=energy, geometry=geometry) + self._refresh_input_widgets() + + def _input_form_edited(self): + super()._input_form_edited() + fixed = [ + k for k, v in self._input_form.get_parameters_checked().items() if not v + ] + self.update_default_inputs(fixed=fixed) + self._refresh_non_form_input_widgets() def _refresh_input_plots(self): - inputs = self.task_input_values - self._update_tabs(inputs) - self._update_images(inputs) + self._update_tabs() + nplots = self._get_nplots() + if not nplots: + return None + inputs = self.get_task_input_values() + for plot_index in range(nplots): + self._update_image(inputs, plot_index) + + def _refresh_output_plots(self): + nplots = self._get_nplots() + if not nplots: + return None + outputs = self.get_task_output_values() + inoutputs = {**self.get_task_input_values(), **outputs} + for plot_index in range(nplots): + self._update_detected_rings(outputs, plot_index) + self._update_output_rings(inoutputs, plot_index) - def _refresh_mixed_plots(self): - if self._tabs.count() == 0: + def _update_image(self, inputs, plot_index): + image = inputs["images"][plot_index] + if isinstance(image, str): + cache = self._cache[plot_index] + previous_image_url = cache.get("image") + if previous_image_url == image: + return + cache["image"] = image + self._remove_from_plot("image", plot_index) + if not utils.is_data(image): return - inputs = self.task_input_values - outputs = self.task_output_values - self._update_rings(inputs, outputs) + image = utils.get_image(image, gui=True) + self._legends[plot_index]["image"] = [ + plots.plot_image(self._get_plot(plot_index), image, legend="image") + ] - def _iter_plots(self) -> Iterable[PlotWidget]: - for i in range(self._tabs.count()): - yield self._tabs.widget(i).getPlotWidget() + def _update_output_rings(self, values, plot_index): + self._remove_from_plot("output_rings", plot_index) + if not self._show["output_rings"]: + return + self._legends[plot_index]["output_rings"] = self._update_theoretical_rings( + values, "output", plot_index + ) - def _update_tabs(self, inputs): - images = [ - utils.get_image(image, gui=True) for image in inputs.get("images", list()) - ] - positions = [ - utils.get_data(position, gui=True) - for position in inputs.get("positions", list()) - ] - positions += [float("nan")] * max(len(images) - len(positions), 0) - xunits_in_m = inputs.get("positionunits_in_meter") # xunits/m + def _update_theoretical_rings(self, values, legend, plot_index) -> List[str]: + energy = values.get("energy") + detector = values.get("detector") + calibrant = values.get("calibrant") + parametrization = values.get("parametrization") + parameters = values.get("parameters") + positions = values.get("positions") + if ( + not energy + or not detector + or not calibrant + or not parametrization + or not parameters + or not positions + ): + return + position = utils.get_data(positions[plot_index], gui=True) + parametrization = utils.data_from_storage(parametrization) + parameters = utils.data_from_storage(parameters) + geometry, energy = calculate_geometry(parametrization, parameters, position) + plot = self._get_plot(plot_index) + return plots.plot_theoretical_rings( + plot, + detector, + calibrant, + energy, + geometry, + max_rings=None, + legend=legend, + ) + + def _update_detected_rings(self, values, plot_index): + self._remove_from_plot("detected_rings", plot_index) + if not self._show["detected_rings"]: + return + rings = values.get("rings") + if not rings: + return + rings = utils.data_from_storage(rings) + plot = self._get_plot(plot_index) + self._legends[plot_index]["detected_rings"] = plots.plot_detected_rings( + plot, rings[str(plot_index)] + ) + + def _remove_from_plot(self, name: str, plot_index: int) -> None: + legends = self._legends[plot_index].pop(name, list()) + if legends: + plot = self._get_plot(plot_index) + for legend in legends: + plot.remove(legend=legend) + + def _values_from_form(self, values: Mapping, checked: Dict[str, bool]) -> Mapping: + values = pack_geometry(values, checked) + values = pack_parametrization(values) + return values + + def _values_to_form(self, values: Mapping) -> Tuple[Mapping, Dict[str, bool]]: + values, checked = unpack_geometry(values) + values = unpack_parametrization(values) + return values, checked + + def _enabled_to_form(self, enabled: Dict[str, bool]) -> Dict[str, bool]: + enabled = unpack_enabled_geometry(enabled) + enabled = unpack_enabled_parametrization(enabled) + return enabled + + def _get_nplots(self) -> Iterable[PlotWidget]: + return self._tabs.count() - self._fixed_tabs + + def _get_plot(self, plot_index) -> PlotWidget: + return self._tabs.widget(plot_index + self._fixed_tabs).getPlotWidget() + + def _update_tabs(self): + images = self.get_task_input_value("images") + if images: + images = [utils.get_image(image, gui=True) for image in images] + positions = self.get_task_input_value("positions") + if positions: + positions = [ + utils.get_data(position, gui=True) for position in positions + ] + else: + positions = list() + positions += [float("nan")] * max(len(images) - len(positions), 0) + else: + images = list() + positions = list() + + xunits_in_m = self.get_task_input_value("positionunits_in_meter") # xunits/m if not xunits_in_m: xunits_in_m = 1e-3 # xunits/m dunits_in_m = 1e-2 # dunits/m @@ -104,67 +253,25 @@ class OWCalibrateMulti(OWTriggerWidget, ewokstaskclass=CalibrateMulti): ] ntabs = self._tabs.count() - nadd = len(images) - ntabs + nfixed = self._fixed_tabs + nplots = ntabs - nfixed + nadd = len(images) - nplots + if nadd == 0: pass elif nadd > 0: - for i in range(ntabs + 1, ntabs + nadd + 1): - self._tabs.addTab(ScatterView(), positions[i - 1]) + for tab_index in range(ntabs, ntabs + nadd): + plot_index = tab_index - nfixed + self._tabs.addTab(ScatterView(), positions[plot_index]) + self._legends.append(dict()) + self._cache.append(dict()) else: - for i in range(ntabs - 1, ntabs + nadd - 1, -1): - self._tabs.addTab(i) - for i, position in enumerate(positions): - self._tabs.setTabText(i, position) - - def _update_images(self, inputs): - images = inputs.get("images", list()) - positions = inputs.get("positions", list()) - positions += [float("nan")] * max(len(images) - len(positions), 0) - - for plot, image, position in zip(self._iter_plots(), images, positions): - plot.remove(kind="image") - image = utils.get_image(image, gui=True) - position = utils.get_data(position, gui=True) - if utils.is_data(image) and utils.is_data(position): - plots.plot_image(plot, image, legend=f"Position = {position}") - - def _update_rings(self, inputs, outputs): - energy = inputs.get("energy") - detector = inputs.get("detector") - calibrant = inputs.get("calibrant") - parametrization = outputs.get("parametrization") - parameters = outputs.get("parameters") - positions = inputs.get("positions") - if ( - not energy - or not detector - or not calibrant - or not parametrization - or not parameters - or not positions - ): - return - rings = outputs.get("rings") - if not rings: - rings = dict() + for tab_index in range(ntabs - 1, ntabs + nadd - 1, -1): + plot_index = tab_index - nfixed + self._tabs.removeTab(tab_index) + del self._legends[plot_index] + del self._cache[plot_index] - parametrization = utils.data_from_storage(parametrization) - parameters = utils.data_from_storage(parameters) - rings = utils.data_from_storage(rings) - - for plot, ringname, position in zip( - self._iter_plots(), sorted(rings), positions - ): - position = utils.get_data(position, gui=True) - geometry, energy = calculate_geometry(parametrization, parameters, position) - plot.remove(kind="curve") - plot.remove(kind="scatter") - plots.plot_rings( - plot, detector, calibrant, energy, geometry, rings[ringname] - ) - - def _parameters_from_form(self, parameters): - return pack_geometry(parameters) - - def _parameters_to_form(self, parameters): - return unpack_geometry(parameters) + for plot_index, position in enumerate(positions): + tab_index = plot_index + nfixed + self._tabs.setTabText(tab_index, position) diff --git a/orangecontrib/ewoksxrpd/calibratesingle.py b/orangecontrib/ewoksxrpd/calibratesingle.py index ca227669a02316a73948b3cd51944feddcac4b10..040e0974845ebea582c3edb9ea38834e390ec979 100644 --- a/orangecontrib/ewoksxrpd/calibratesingle.py +++ b/orangecontrib/ewoksxrpd/calibratesingle.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Dict, List, Mapping, Optional, Tuple from AnyQt import QtWidgets from silx.gui.plot import ScatterView, PlotWidget from ewoksxrpd.tasks import CalibrateSingle @@ -6,8 +6,10 @@ from ewoksxrpd.tasks import utils from ewoksxrpd.gui.trigger_widget import OWTriggerWidget from ewoksxrpd.gui import plots from ewoksxrpd.gui.forms import input_parameters_calibratesingle +from ewoksxrpd.gui.forms import output_parameters_calibratesingle from ewoksxrpd.gui.forms import pack_geometry from ewoksxrpd.gui.forms import unpack_geometry +from ewoksxrpd.gui.forms import unpack_enabled_geometry __all__ = ["OWCalibrateSingle"] @@ -19,110 +21,185 @@ class OWCalibrateSingle(OWTriggerWidget, ewokstaskclass=CalibrateSingle): icon = "icons/widget.png" want_main_area = True + def __init__(self, *args, **kwargs) -> None: + self._tabs = QtWidgets.QTabWidget() + self._show = { + "input_rings": True, + "output_rings": False, + "detected_rings": False, + } + self._legends = dict() + self._cache = dict() + super().__init__(*args, **kwargs) + def _init_forms(self): - self._plot = None - super()._init_forms() - self._create_input_form( - "inputs", input_parameters_calibratesingle(), self.controlArea + parameter_info = input_parameters_calibratesingle( + self.get_default_input_values() ) + self._create_input_form(parameter_info) + parameter_info = output_parameters_calibratesingle() + self._create_output_form(parameter_info) def _init_control_area(self): - layout = self._get_control_layout() - layout.addWidget(list(self._input_forms.values())[0]) super()._init_control_area() + layout = self._get_control_layout() - def _init_main_area(self): - layout = self._get_main_layout() - self._plot = ScatterView() - layout.addWidget(self._plot) - super()._init_main_area() + w = QtWidgets.QPushButton("Accept refined") + layout.addWidget(w) + w.released.connect(self._accept_refined_parameters) + + self._show_input_rings_widget = w = QtWidgets.QCheckBox("Guess rings") + w.setChecked(self._show["input_rings"]) + layout.addWidget(w) + w.released.connect(self._set_show_input_rings) + + self._show_output_rings_widget = w = QtWidgets.QCheckBox("Refined rings") + w.setChecked(self._show["output_rings"]) + layout.addWidget(w) + w.released.connect(self._set_show_output_rings) + + self._show_detected_rings_widget = w = QtWidgets.QCheckBox("Detected rings") + w.setChecked(self._show["detected_rings"]) + layout.addWidget(w) + w.released.connect(self._set_show_detected_rings) + + def _set_show_input_rings(self): + self._show["input_rings"] = self._show_input_rings_widget.isChecked() self._refresh_non_form_input_widgets() - def _get_main_layout(self): - layout = self.mainArea.layout() - if layout is None: - layout = QtWidgets.QVBoxLayout() - self.mainArea.setLayout(layout) - return layout + def _set_show_output_rings(self): + self._show["output_rings"] = self._show_output_rings_widget.isChecked() + self._refresh_non_form_output_widgets() - def _accept_refined_parameters(self): - try: - self.default_inputs["energy"] = self.get_task_output_value("energy") - except KeyError: - pass - try: - self.default_inputs["geometry"] = dict( - self.get_task_output_value("geometry") - ) - except KeyError: - pass - self._refresh_input_widgets() + def _set_show_detected_rings(self): + self._show["detected_rings"] = self._show_detected_rings_widget.isChecked() + self._refresh_non_form_output_widgets() - @property - def plot(self) -> Optional[PlotWidget]: - if self._plot is None: - return None - return self._plot.getPlotWidget() + def _init_main_area(self): + layout = self._get_main_layout() + plot = ScatterView() + w = plot.getPlotWidget() + w.setGraphXLabel("Dim 2 (pixels)") + w.setGraphYLabel("Dim 1 (pixels)") + self._tabs.addTab(plot, "Image") + layout.addWidget(self._tabs) + super()._init_main_area() - def _init_control_buttons(self): - super()._init_control_buttons() - accept = QtWidgets.QPushButton("Accept refined") - self.controlArea.layout().addWidget(accept) - accept.released.connect(self._accept_refined_parameters) + def _add_output_form_widget(self): + self._tabs.addTab(self._output_form, "Refined Geometry") def _refresh_non_form_input_widgets(self): with self._capture_errors(): super()._refresh_non_form_input_widgets() self._refresh_input_plots() - self._refresh_mixed_plots() def _refresh_non_form_output_widgets(self): with self._capture_errors(): super()._refresh_non_form_output_widgets() - self._refresh_mixed_plots() + self._refresh_output_plots() + + def _accept_refined_parameters(self): + energy = self.get_task_output_value("energy") + if energy: + self.update_default_inputs(energy=energy) + geometry = self.get_task_output_value("geometry") + if geometry: + self.update_default_inputs(geometry=dict(geometry)) + self._refresh_input_widgets() def _input_form_edited(self): super()._input_form_edited() - self._refresh_mixed_plots() + fixed = [ + k for k, v in self._input_form.get_parameters_checked().items() if not v + ] + self.update_default_inputs(fixed=fixed) + self._refresh_non_form_input_widgets() def _refresh_input_plots(self): - if self._plot is None: + if self.plot is None: return None - inputs = self.task_input_values + inputs = self.get_task_input_values() self._update_image(inputs) + self._update_input_rings(inputs) - def _refresh_mixed_plots(self): - if self._plot is None: + def _refresh_output_plots(self): + if self.plot is None: return None - inputs = self.task_input_values - outputs = self.task_output_values - self._update_rings(inputs, outputs) + outputs = self.get_task_output_values() + self._update_detected_rings(outputs) + inoutputs = {**self.get_task_input_values(), **outputs} + self._update_output_rings(inoutputs) def _update_image(self, inputs): - self.plot.remove(kind="image") image = inputs.get("image") + if isinstance(image, str): + previous_image_url = self._cache.get("image") + if previous_image_url == image: + return + self._cache["image"] = image + self._remove_from_plot("image") if not utils.is_data(image): return - plots.plot_image(self.plot, utils.get_image(image, gui=True)) - - def _update_rings(self, inputs, outputs): - self.plot.remove(kind="curve") - self.plot.remove(kind="scatter") - energy = inputs.get("energy") - geometry = inputs.get("geometry") - detector = inputs.get("detector") - calibrant = inputs.get("calibrant") + image = utils.get_image(image, gui=True) + self._legends["image"] = [plots.plot_image(self.plot, image, legend="image")] + + def _update_input_rings(self, values): + self._remove_from_plot("input_rings") + if not self._show["input_rings"]: + return + self._legends["input_rings"] = self._update_theoretical_rings(values, "input") + + def _update_output_rings(self, values): + self._remove_from_plot("output_rings") + if not self._show["output_rings"]: + return + self._legends["output_rings"] = self._update_theoretical_rings(values, "output") + + def _update_theoretical_rings(self, values, legend) -> List[str]: + energy = values.get("energy") + geometry = values.get("geometry") + detector = values.get("detector") + calibrant = values.get("calibrant") + max_rings = values.get("max_rings") if not energy or not geometry or not detector or not calibrant: + return list() + geometry = utils.data_from_storage(geometry) + return plots.plot_theoretical_rings( + self.plot, + detector, + calibrant, + energy, + geometry, + max_rings=max_rings, + legend=legend, + ) + + def _update_detected_rings(self, values): + self._remove_from_plot("detected_rings") + if not self._show["detected_rings"]: return - rings = outputs.get("rings") + rings = values.get("rings") if not rings: - rings = dict() - geometry = utils.data_from_storage(geometry) + return rings = utils.data_from_storage(rings) - plots.plot_rings(self.plot, detector, calibrant, energy, geometry, rings) + self._legends["detected_rings"] = plots.plot_detected_rings(self.plot, rings) + + def _remove_from_plot(self, name: str) -> None: + legends = self._legends.pop(name, list()) + for legend in legends: + self.plot.remove(legend=legend) + + def _values_from_form(self, values: Mapping, checked: Dict[str, bool]) -> Mapping: + return pack_geometry(values, checked) - def _parameters_from_form(self, parameters): - return pack_geometry(parameters) + def _values_to_form(self, values: Mapping) -> Tuple[Mapping, Dict[str, bool]]: + return unpack_geometry(values) - def _parameters_to_form(self, parameters): - return unpack_geometry(parameters) + def _enabled_to_form(self, enabled: Dict[str, bool]) -> Dict[str, bool]: + return unpack_enabled_geometry(enabled) + + @property + def plot(self) -> Optional[PlotWidget]: + if self._tabs.count() == 0: + return None + return self._tabs.widget(0).getPlotWidget() diff --git a/orangecontrib/ewoksxrpd/diagnose_integrate1d.py b/orangecontrib/ewoksxrpd/diagnose_integrate1d.py index cf2861be8d1c388e18985866997a80105390c233..4e04343a35f5adb58f0545e3bb89e5c670bc9605 100644 --- a/orangecontrib/ewoksxrpd/diagnose_integrate1d.py +++ b/orangecontrib/ewoksxrpd/diagnose_integrate1d.py @@ -16,22 +16,16 @@ class OWDiagnoseIntegrate1D(OWTriggerWidget, ewokstaskclass=DiagnoseIntegrate1D) want_main_area = True def _init_forms(self): - super()._init_forms() - self._create_input_form( - "inputs", input_parameters_diagnose_integrate1d(), self.controlArea + parameter_info = input_parameters_diagnose_integrate1d( + self.get_default_input_values() ) - - def _init_control_area(self): - layout = self._get_control_layout() - layout.addWidget(list(self._input_forms.values())[0]) - super()._init_control_area() + self._create_input_form(parameter_info) def _init_main_area(self): layout = self._get_main_layout() self._label = QtWidgets.QLabel() layout.addWidget(self._label) super()._init_main_area() - self._refresh_non_form_output_widgets() def _refresh_non_form_output_widgets(self): with self._capture_errors(): @@ -39,7 +33,7 @@ class OWDiagnoseIntegrate1D(OWTriggerWidget, ewokstaskclass=DiagnoseIntegrate1D) self._update_output_file() def _update_output_file(self): - inputs = self.task_input_values + inputs = self.get_task_input_values() filename = inputs.get("filename") if not filename or not os.path.isfile(filename): self._label.clear() diff --git a/orangecontrib/ewoksxrpd/diagnose_multicalib.py b/orangecontrib/ewoksxrpd/diagnose_multicalib.py index b33452e74dbb06219ccf95ade315bd6d7b84f8e4..7d0692245d01f0deb203530bd1b04772f6b7d5f1 100644 --- a/orangecontrib/ewoksxrpd/diagnose_multicalib.py +++ b/orangecontrib/ewoksxrpd/diagnose_multicalib.py @@ -18,22 +18,16 @@ class OWDiagnoseCalibrateMultiResults( want_main_area = True def _init_forms(self): - super()._init_forms() - self._create_input_form( - "inputs", input_parameters_diagnose_multicalib(), self.controlArea + parameter_info = input_parameters_diagnose_multicalib( + self.get_default_input_values() ) - - def _init_control_area(self): - layout = self._get_control_layout() - layout.addWidget(list(self._input_forms.values())[0]) - super()._init_control_area() + self._create_input_form(parameter_info) def _init_main_area(self): layout = self._get_main_layout() self._label = QtWidgets.QLabel() layout.addWidget(self._label) super()._init_main_area() - self._refresh_non_form_output_widgets() def _refresh_non_form_output_widgets(self): with self._capture_errors(): @@ -41,7 +35,7 @@ class OWDiagnoseCalibrateMultiResults( self._update_output_file() def _update_output_file(self): - inputs = self.task_input_values + inputs = self.get_task_input_values() filename = inputs.get("filename") if not filename or not os.path.isfile(filename): self._label.clear() diff --git a/orangecontrib/ewoksxrpd/diagnose_singlecalib.py b/orangecontrib/ewoksxrpd/diagnose_singlecalib.py index 1d95d44b5d68ef1ff31decaa253a945943e04b55..9d4c2eb8b4664477063ef0dfe4e6d91519de7178 100644 --- a/orangecontrib/ewoksxrpd/diagnose_singlecalib.py +++ b/orangecontrib/ewoksxrpd/diagnose_singlecalib.py @@ -1,6 +1,8 @@ import os from AnyQt import QtWidgets from AnyQt import QtGui + +# from AnyQt.QtCore import Qt from ewoksxrpd.tasks import DiagnoseCalibrateSingleResults from ewoksxrpd.gui.trigger_widget import OWTriggerWidget from ewoksxrpd.gui.forms import input_parameters_diagnose_singlecalib @@ -18,22 +20,16 @@ class OWDiagnoseCalibrateSingleResults( want_main_area = True def _init_forms(self): - super()._init_forms() - self._create_input_form( - "inputs", input_parameters_diagnose_singlecalib(), self.controlArea + parameter_info = input_parameters_diagnose_singlecalib( + self.get_default_input_values() ) - - def _init_control_area(self): - layout = self._get_control_layout() - layout.addWidget(list(self._input_forms.values())[0]) - super()._init_control_area() + self._create_input_form(parameter_info) def _init_main_area(self): layout = self._get_main_layout() self._label = QtWidgets.QLabel() layout.addWidget(self._label) super()._init_main_area() - self._refresh_non_form_output_widgets() def _refresh_non_form_output_widgets(self): with self._capture_errors(): @@ -41,11 +37,12 @@ class OWDiagnoseCalibrateSingleResults( self._update_output_file() def _update_output_file(self): - inputs = self.task_input_values + inputs = self.get_task_input_values() filename = inputs.get("filename") if not filename or not os.path.isfile(filename): self._label.clear() return pixmap = QtGui.QPixmap(filename) + # pixmap = pixmap.scaled(10,5,Qt.KeepAspectRatio) self._label.setPixmap(pixmap) diff --git a/orangecontrib/ewoksxrpd/integrate1d.py b/orangecontrib/ewoksxrpd/integrate1d.py index 514524b71caab6e2446f03cde6a5309a5a4832cd..5bdaa1f9cac243453c2f520f75d3fe2978dbb341 100644 --- a/orangecontrib/ewoksxrpd/integrate1d.py +++ b/orangecontrib/ewoksxrpd/integrate1d.py @@ -1,3 +1,4 @@ +from typing import Dict, Mapping, Tuple from silx.gui.plot import Plot1D from ewoksxrpd.tasks import Integrate1D from ewoksxrpd.tasks import utils @@ -5,7 +6,7 @@ from ewoksxrpd.gui.trigger_widget import OWTriggerWidget from ewoksxrpd.gui.forms import input_parameters_integrate1d from ewoksxrpd.gui.forms import pack_geometry from ewoksxrpd.gui.forms import unpack_geometry - +from ewoksxrpd.gui.forms import unpack_enabled_geometry __all__ = ["OWIntegrate1D"] @@ -17,21 +18,14 @@ class OWIntegrate1D(OWTriggerWidget, ewokstaskclass=Integrate1D): want_main_area = True def _init_forms(self): - super()._init_forms() - self._create_input_form( - "inputs", input_parameters_integrate1d(), self.controlArea - ) - - def _init_control_area(self): - layout = self._get_control_layout() - layout.addWidget(list(self._input_forms.values())[0]) - super()._init_control_area() + parameter_info = input_parameters_integrate1d(self.get_default_input_values()) + self._create_input_form(parameter_info) def _init_main_area(self): + layout = self._get_main_layout() self._plot = Plot1D() - self.mainArea.layout().addWidget(self._plot) + layout.addWidget(self._plot) super()._init_main_area() - self._refresh_non_form_output_widgets() def _refresh_non_form_output_widgets(self): with self._capture_errors(): @@ -41,19 +35,22 @@ class OWIntegrate1D(OWTriggerWidget, ewokstaskclass=Integrate1D): def _update_plot(self): self._plot.remove(kind="curve") - outputs = self.task_output_values + outputs = self.get_task_output_values() x = outputs.get("x") y = outputs.get("y") xunits = outputs.get("xunits") if not xunits or not utils.is_data(x) or not utils.is_data(y): return - inputs = self.task_input_values + inputs = self.get_task_input_values() reference = inputs.get("reference") self._plot.addCurve(x, y, xlabel=xunits, ylabel=f"Normalized to {reference}") - def _parameters_from_form(self, parameters): - return pack_geometry(parameters) + def _values_from_form(self, values: Mapping, checked: Dict[str, bool]) -> Mapping: + return pack_geometry(values, checked) + + def _values_to_form(self, values: Mapping) -> Tuple[Mapping, Dict[str, bool]]: + return unpack_geometry(values) - def _parameters_to_form(self, parameters): - return unpack_geometry(parameters) + def _enabled_to_form(self, enabled: Dict[str, bool]) -> Dict[str, bool]: + return unpack_enabled_geometry(enabled) diff --git a/orangecontrib/ewoksxrpd/mask.py b/orangecontrib/ewoksxrpd/mask.py index e2b873b094b947bd6077aecd9e28286142c760c2..8f049766a99075a2e686cb1e2214655a033d9dcf 100644 --- a/orangecontrib/ewoksxrpd/mask.py +++ b/orangecontrib/ewoksxrpd/mask.py @@ -16,22 +16,20 @@ class OWMaskDetection(OWTriggerWidget, ewokstaskclass=MaskDetection): icon = "icons/widget.png" want_main_area = True - def _init_forms(self): + def __init__(self, *args, **kwargs) -> None: self._tabs = QtWidgets.QTabWidget() - super()._init_forms() - self._create_input_form("inputs", input_parameters_mask(), self.controlArea) + super().__init__(*args, **kwargs) - def _init_control_area(self): - layout = self._get_control_layout() - layout.addWidget(list(self._input_forms.values())[0]) - super()._init_control_area() + def _init_forms(self): + parameter_info = input_parameters_mask(self.get_default_input_values()) + self._create_input_form(parameter_info) def _init_main_area(self): - self.mainArea.layout().addWidget(self._tabs) + layout = self._get_main_layout() + layout.addWidget(self._tabs) for name in ("Image 1", "Image 2", "Ratio"): self._tabs.addTab(Plot2D(), name) super()._init_main_area() - self._refresh_non_form_output_widgets() def _refresh_non_form_input_widgets(self): with self._capture_errors(): @@ -46,8 +44,8 @@ class OWMaskDetection(OWTriggerWidget, ewokstaskclass=MaskDetection): def _refresh_mixed_plots(self): if self._tabs.count() == 0: return - inputs = self.task_input_values - outputs = self.task_output_values + inputs = self.get_task_input_values() + outputs = self.get_task_output_values() self._update_image(0, inputs, outputs) self._update_image(1, inputs, outputs) self._update_ratio(inputs, outputs) diff --git a/orangecontrib/ewoksxrpd/nexus.py b/orangecontrib/ewoksxrpd/nexus.py index bfa164cb0fc522a8c58fed7568841d283cde4622..67fbeac173615ee120707027c898e7802efae733 100644 --- a/orangecontrib/ewoksxrpd/nexus.py +++ b/orangecontrib/ewoksxrpd/nexus.py @@ -17,19 +17,13 @@ class OWSaveNexusPattern1D(OWTriggerWidget, ewokstaskclass=SaveNexusPattern1D): want_main_area = True def _init_forms(self): - super()._init_forms() - self._create_input_form("inputs", input_parameters_nexus(), self.controlArea) - - def _init_control_area(self): - layout = self._get_control_layout() - layout.addWidget(list(self._input_forms.values())[0]) - super()._init_control_area() + parameter_info = input_parameters_nexus(self.get_default_input_values()) + self._create_input_form(parameter_info) def _init_main_area(self): self._plot = Plot1D() self.mainArea.layout().addWidget(self._plot) super()._init_main_area() - self._refresh_non_form_output_widgets() def _refresh_non_form_output_widgets(self): with self._capture_errors(): @@ -39,7 +33,7 @@ class OWSaveNexusPattern1D(OWTriggerWidget, ewokstaskclass=SaveNexusPattern1D): def _update_plot(self): self._plot.remove(kind="curve") - url = self.task_input_values.get("url") + url = self.get_task_input_value("url") if not url: return url = DataUrl(url) diff --git a/orangecontrib/ewoksxrpd/pyfaiconfig.py b/orangecontrib/ewoksxrpd/pyfaiconfig.py new file mode 100644 index 0000000000000000000000000000000000000000..fa96179e063c5ea39c6c7a5129f0f3b570d8a9a6 --- /dev/null +++ b/orangecontrib/ewoksxrpd/pyfaiconfig.py @@ -0,0 +1,32 @@ +from typing import Dict, Mapping, Tuple +from ewoksxrpd.tasks import PyFaiConfig +from ewoksxrpd.gui.trigger_widget import OWTriggerWidget +from ewoksxrpd.gui.forms import input_parameters_pyfai_config +from ewoksxrpd.gui.forms import output_parameters_pyfai_config +from ewoksxrpd.gui.forms import pack_geometry +from ewoksxrpd.gui.forms import unpack_geometry +from ewoksxrpd.gui.forms import unpack_enabled_geometry + +__all__ = ["OWPyFaiConfig"] + + +class OWPyFaiConfig(OWTriggerWidget, ewokstaskclass=PyFaiConfig): + name = "PyFaiConfig" + description = "Configuration for pyfai" + icon = "icons/widget.png" + want_main_area = True + + def _init_forms(self): + parameter_info = input_parameters_pyfai_config(self.get_default_input_values()) + self._create_input_form(parameter_info) + parameter_info = output_parameters_pyfai_config() + self._create_output_form(parameter_info) + + def _values_from_form(self, values: Mapping, checked: Dict[str, bool]) -> Mapping: + return pack_geometry(values, checked) + + def _values_to_form(self, values: Mapping) -> Tuple[Mapping, Dict[str, bool]]: + return unpack_geometry(values) + + def _enabled_to_form(self, enabled: Dict[str, bool]) -> Dict[str, bool]: + return unpack_enabled_geometry(enabled) diff --git a/setup.cfg b/setup.cfg index 0eae7af8f37ce7250d5940adfc5adf3fc34c6048..571a3291292db0427942b74c0b0c94beb3f06aeb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,8 @@ classifiers = Intended Audience :: Science/Research License :: OSI Approved :: MIT License Programming Language :: Python :: 3 +keywords = + orange3 add-on [options] packages = find: