diff --git a/src/pyFAI/diffmap.py b/src/pyFAI/diffmap.py index fdd296592..4ba28562b 100644 --- a/src/pyFAI/diffmap.py +++ b/src/pyFAI/diffmap.py @@ -227,7 +227,7 @@ def parse(self, sysargv=None, with_config=False): self.inputfiles = [i[0] for i in config.get("input_data", [])] if "ai" in config: ai = config["ai"] - elif config.get("application", None) == "pyfai-integrate": + elif config.get("application", None) in ("pyfai-integrate", "worker"): ai = config.copy() else: ai = {} diff --git a/src/pyFAI/gui/pilx/utils.py b/src/pyFAI/gui/pilx/utils.py index b51008d23..9f382090f 100644 --- a/src/pyFAI/gui/pilx/utils.py +++ b/src/pyFAI/gui/pilx/utils.py @@ -47,7 +47,7 @@ def compute_radial_values(pyFAI_config_as_str: str) -> numpy.ndarray: pyFAI_config: dict = json.loads(pyFAI_config_as_str) ai = AzimuthalIntegrator.sload(pyFAI_config) - if "detector" not in pyFAI_config: + if "detector" not in pyFAI_config and "detector" not in pyFAI_config.get("poni", {}): ai.detector = Detector.factory("detector", { "pixel1": pyFAI_config.get("pixel1"), "pixel2": pyFAI_config.get("pixel2"), diff --git a/src/pyFAI/gui/widgets/WorkerConfigurator.py b/src/pyFAI/gui/widgets/WorkerConfigurator.py index 18194c8d9..fbdefa8e6 100644 --- a/src/pyFAI/gui/widgets/WorkerConfigurator.py +++ b/src/pyFAI/gui/widgets/WorkerConfigurator.py @@ -33,7 +33,7 @@ __contact__ = "Jerome.Kieffer@ESRF.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -__date__ = "05/09/2023" +__date__ = "22/07/2024" __status__ = "development" import logging @@ -209,6 +209,20 @@ def __getAzimuthalNbpt(self): return None return int(value) + def getPoniDict(self): + poni = {} + poni["wavelength"] = self.__geometryModel.wavelength().value() + poni["dist"] = self.__geometryModel.distance().value() + poni["poni1"] = self.__geometryModel.poni1().value() + poni["poni2"] = self.__geometryModel.poni2().value() + poni["rot1"] = self.__geometryModel.rotation1().value() + poni["rot2"] = self.__geometryModel.rotation2().value() + poni["rot3"] = self.__geometryModel.rotation3().value() + if self.__detector is not None: + poni["detector"] = self.__detector.__class__.__name__ + poni["detector_config"] = self.__detector.get_config() + return poni + def getConfig(self): """Read the configuration of the plugin and returns it as a dictionary @@ -230,21 +244,10 @@ def splitFiles(filenames): # file-version config["application"] = "pyfai-integrate" - config["version"] = 3 + config["version"] = 4 # geometry - config["wavelength"] = self.__geometryModel.wavelength().value() - config["dist"] = self.__geometryModel.distance().value() - config["poni1"] = self.__geometryModel.poni1().value() - config["poni2"] = self.__geometryModel.poni2().value() - config["rot1"] = self.__geometryModel.rotation1().value() - config["rot2"] = self.__geometryModel.rotation2().value() - config["rot3"] = self.__geometryModel.rotation3().value() - - # detector - if self.__detector is not None: - config["detector"] = self.__detector.__class__.__name__ - config["detector_config"] = self.__detector.get_config() + config["poni"] = self.getPoniDict() # pre-processing config["do_mask"] = bool(self.do_mask.isChecked()) @@ -319,7 +322,7 @@ def setConfig(self, dico): application = dico.pop("application", None) if application != "pyfai-integrate": logger.error("It is not a configuration file from pyFAI-integrate.") - if version > 3: + if version > 4: logger.error("Configuration file %d too recent. This version of pyFAI maybe too old to read the configuration", version) # Clean up the GUI @@ -332,7 +335,32 @@ def setConfig(self, dico): self.__geometryModel.rotation2().setValue(None) self.__geometryModel.rotation3().setValue(None) - # geometry + # patch for version 4 + poni_dict = dico.get("poni", None) + if isinstance(poni_dict, dict) and version > 3: + if "wavelength" in poni_dict: + value = poni_dict.pop("wavelength") + self.__geometryModel.wavelength().setValue(value) + if "dist" in poni_dict: + value = poni_dict.pop("dist") + self.__geometryModel.distance().setValue(value) + if "poni1" in poni_dict: + value = poni_dict.pop("poni1") + self.__geometryModel.poni1().setValue(value) + if "poni2" in poni_dict: + value = poni_dict.pop("poni2") + self.__geometryModel.poni2().setValue(value) + if "rot1" in poni_dict: + value = poni_dict.pop("rot1") + self.__geometryModel.rotation1().setValue(value) + if "rot2" in poni_dict: + value = poni_dict.pop("rot2") + self.__geometryModel.rotation2().setValue(value) + if "rot3" in poni_dict: + value = poni_dict.pop("rot3") + self.__geometryModel.rotation3().setValue(value) + + # For older versions (<4) if "wavelength" in dico: value = dico.pop("wavelength") self.__geometryModel.wavelength().setValue(value) @@ -399,6 +427,8 @@ def normalizeFiles(filenames): for key, value in setup_data.items(): if key in dico and (value is not None): + if key == "polarization_factor" and not isinstance(dico[key], (float, int)): + continue value(dico.pop(key)) normalizationFactor = dico.pop("normalization_factor", None) @@ -550,15 +580,9 @@ def __savePoni(self): return filename = dialog.selectedFiles()[0] - config = self.getConfig() - if config.get("wavelength") is None: - config.pop("wavelength") - from ...detectors import detector_factory - detector = detector_factory(config.get("detector"), config.get("detector_config")) - from ...geometry import Geometry - ai = Geometry(detector=detector) - ai.set_config(config) - ai.save(filename) + poni = PoniFile(data=self.getPoniDict()) + with open(filename, 'w') as fd: + poni.write(fd) def __maskFileChanged(self): model = self.__model.maskFileModel diff --git a/src/pyFAI/io/integration_config.py b/src/pyFAI/io/integration_config.py index f4f998d03..ef0ba7aaf 100644 --- a/src/pyFAI/io/integration_config.py +++ b/src/pyFAI/io/integration_config.py @@ -190,6 +190,35 @@ def _patch_v2_to_v3(config): config["version"] = 3 +def _patch_v3_to_v4(config): + """Rework the config dictionary from version 3 to version 4 + + The geometric, detector and beam parameters (contained in the .poni file) now they are gathered in a dictionary in the "poni" key + This will ease the methods that handle only the PONI parameters defined during the calibration step + that now they can be retrieved just by getting the value of the key "poni" from the config. The rest of the parameters are + characteristic of the integration protocol. + + :param dict config: Dictionary reworked inplace. + """ + poni_dict = {} + poni_parameters = ["dist", + "poni1", + "poni2", + "rot1", + "rot2", + "rot3", + "detector", + "detector_config", + "wavelength", + "poni_version", + ] + for poni_param in poni_parameters: + if config.get(poni_param, None) is not None: + poni_dict[poni_param] = config.pop(poni_param) + + config["poni"] = poni_dict + config["version"] = 4 + def normalize(config, inplace=False, do_raise=False): """Normalize the configuration file to the one supported internally\ @@ -212,14 +241,17 @@ def normalize(config, inplace=False, do_raise=False): _patch_v2_to_v3(config) application = config.get("application", None) - if application != "pyfai-integrate": + if application not in ("pyfai-integrate", "worker"): txt = f"Configuration application do not match. Found '{application}'" if do_raise: raise ValueError(txt) else: _logger.error(txt) - if version > 3: + if version == 3: + _patch_v3_to_v4(config) + + if version > 4: _logger.error("Configuration file %d too recent. This version of pyFAI maybe too old to read this configuration", version) return config @@ -233,6 +265,9 @@ def __init__(self, config): def pop_ponifile(self): """Returns the geometry subpart of the configuration""" + if isinstance(self._config.get("poni", None), dict): + return ponifile.PoniFile(self._config["poni"]) + dico = {"poni_version":2} mapping = { i:i for i in ('wavelength', 'poni1', 'poni2', 'rot1', 'rot2', 'rot3', 'detector', 'detector_config')} @@ -250,12 +285,20 @@ def pop_detector(self): :rtype: pyFAI.detectors.Detector """ - detector_class = self._config.pop("detector", None) - if detector_class is not None: - # NOTE: Default way to describe a detector since pyFAI 0.17 - detector_config = self._config.pop("detector_config", None) - detector = detectors.detector_factory(detector_class, config=detector_config) - return detector + if isinstance(self._config.get("poni", None), dict): + poni_dict = self._config["poni"].copy() + detector_class = poni_dict.pop("detector", None) + if detector_class is not None: + detector_config = poni_dict.pop("detector_config", None) + detector = detectors.detector_factory(detector_class, config=detector_config) + return detector + else: + detector_class = self._config.pop("detector", None) + if detector_class is not None: + # NOTE: Default way to describe a detector since pyFAI 0.17 + detector_config = self._config.pop("detector_config", None) + detector = detectors.detector_factory(detector_class, config=detector_config) + return detector return None diff --git a/src/pyFAI/io/ponifile.py b/src/pyFAI/io/ponifile.py index 2ffb026ff..2eb44a691 100644 --- a/src/pyFAI/io/ponifile.py +++ b/src/pyFAI/io/ponifile.py @@ -110,6 +110,10 @@ def read_from_dict(self, config): * 2: store detector and detector_config instead of pixelsize1, pixelsize2 and splinefile * 2.1: manage orientation of detector in detector_config """ + # Patch for worker version 4 + if "poni" in config and config.get("version", 0) > 3: + config = config.get("poni", {}) + version = float(config.get("poni_version", 1)) if "detector_config" in config: if "orientation" in config["detector_config"]: diff --git a/src/pyFAI/test/test_worker.py b/src/pyFAI/test/test_worker.py index a37f61fe2..09d85e54b 100644 --- a/src/pyFAI/test/test_worker.py +++ b/src/pyFAI/test/test_worker.py @@ -46,6 +46,9 @@ from ..azimuthalIntegrator import AzimuthalIntegrator from ..containers import Integrate1dResult from ..containers import Integrate2dResult +from ..io.integration_config import ConfigurationReader +from ..io.ponifile import PoniFile +from .. import detector_factory from . import utilstest logger = logging.getLogger(__name__) @@ -461,6 +464,102 @@ def test_regression_2227(self): self.assertEqual(worker_generic.ai.detector.shape, [576, 748], "Shape matches") self.assertEqual(worker_generic.ai.detector.orientation, 3, "Orientation matches") + def test_regression_v4(self): + """loading poni dictionary as a separate key in configuration""" + detector = detector_factory(name='Eiger2_4M', config={"orientation" : 3}) + ai = AzimuthalIntegrator(dist=0.1, + poni1=0.1, + poni2=0.1, + wavelength=1e-10, + detector=detector, + ) + worker = Worker(azimuthalIntegrator=ai) + + self.assertEqual(ai.dist, worker.ai.dist, "Distance matches") + self.assertEqual(ai.poni1, worker.ai.poni1, "PONI1 matches") + self.assertEqual(ai.poni2, worker.ai.poni2, "PONI2 matches") + self.assertEqual(ai.rot1, worker.ai.rot1, "Rot1 matches") + self.assertEqual(ai.rot2, worker.ai.rot2, "Rot2 matches") + self.assertEqual(ai.rot3, worker.ai.rot3, "Rot3 matches") + self.assertEqual(ai.wavelength, worker.ai.wavelength, "Wavelength matches") + self.assertEqual(ai.detector, worker.ai.detector, "Detector matches") + + config = worker.get_config() + config_reader = ConfigurationReader(config) + + detector_from_reader = config_reader.pop_detector() + self.assertEqual(detector, detector_from_reader, "Detector from reader matches") + + config = worker.get_config() + config_reader = ConfigurationReader(config) + poni = config_reader.pop_ponifile() + + self.assertEqual(ai.dist, poni.dist, "Distance matches") + self.assertEqual(ai.poni1, poni.poni1, "PONI1 matches") + self.assertEqual(ai.poni2, poni.poni2, "PONI2 matches") + self.assertEqual(ai.rot1, poni.rot1, "Rot1 matches") + self.assertEqual(ai.rot2, poni.rot2, "Rot2 matches") + self.assertEqual(ai.rot3, poni.rot3, "Rot3 matches") + self.assertEqual(ai.wavelength, poni.wavelength, "Wavelength matches") + self.assertEqual(ai.detector, poni.detector, "Detector matches") + + def test_v3_equal_to_v4(self): + """checking equivalence between v3 and v4""" + config_v3 = { + "application": "pyfai-integrate", + "version": 3, + "wavelength": 1e-10, + "dist": 0.1, + "poni1": 0.1, + "poni2": 0.2, + "rot1": 1, + "rot2": 2, + "rot3": 3, + "detector": "Eiger2_4M", + "detector_config": { + "orientation": 3 + }, + } + + config_v4 = { + "application": "pyfai-integrate", + "version": 4, + "poni": { + "wavelength": 1e-10, + "dist": 0.1, + "poni1": 0.1, + "poni2": 0.2, + "rot1": 1, + "rot2": 2, + "rot3": 3, + "detector": "Eiger2_4M", + "detector_config": { + "orientation": 3 + } + }, + } + + worker_v3 = Worker() + worker_v3.set_config(config=config_v3) + worker_v4 = Worker() + worker_v4.set_config(config=config_v4) + self.assertEqual(worker_v3.get_config(), worker_v4.get_config(), "Worker configs match") + + ai_config_v3 = worker_v3.ai.get_config() + ai_config_v4 = worker_v4.ai.get_config() + self.assertEqual(ai_config_v3, ai_config_v4, "AI configs match") + + poni_v3 = PoniFile(data=ai_config_v3) + poni_v4 = PoniFile(data=ai_config_v4) + self.assertEqual(poni_v3.as_dict(), poni_v4.as_dict(), "PONI dictionaries match") + + poni_v3_from_config = PoniFile(data=config_v3) + poni_v4_from_config = PoniFile(data=config_v4) + self.assertEqual(poni_v3_from_config.as_dict(), poni_v4_from_config.as_dict(), "PONI dictionaries from config match") + + + + def suite(): loader = unittest.defaultTestLoader.loadTestsFromTestCase diff --git a/src/pyFAI/worker.py b/src/pyFAI/worker.py index 809270cd8..a4f342aaa 100644 --- a/src/pyFAI/worker.py +++ b/src/pyFAI/worker.py @@ -591,11 +591,12 @@ def get_config(self): :return: dict with the config to be de-serialized with set_config/loaded with pyFAI.load """ config = { - "version": 3, + "version": 4, + "application" : "worker", "unit": str(self.unit), } - config.update(self.ai.get_config()) + config["poni"] = dict(self.ai.get_config()) for key in ["nbpt_azim", "nbpt_rad", "polarization_factor", "dummy", "delta_dummy", "correct_solid_angle", "dark_current_image", "flat_field_image", @@ -696,6 +697,10 @@ def validate_config(config, raise_exception=RuntimeError): :return: None or reason as a string when raise_exception is None, else raise the given exception """ reason = None + + config = config.copy() + if "poni" in config and config.get("version", 0) > 3: + config.update(config.pop("poni")) if not config.get("dist"): reason = "Detector distance is undefined" elif config.get("poni1") is None: