diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml index 737d72bf0e..5e7956485f 100644 --- a/.github/workflows/create-tag.yml +++ b/.github/workflows/create-tag.yml @@ -8,6 +8,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + with: + token: ${{ secrets.CI_TOKEN }} - name: Set up Python 3.10 uses: actions/setup-python@v4 with: @@ -23,7 +25,7 @@ jobs: git config --global user.name "Marcus Oskarsson" git add -A git commit -m "[skip ci] Bumped minor version" - git push + git push -f poetry build - name: Publish package to PyPI id: publish-pacakge diff --git a/deprecated/HardwareObjects/mockup/MultiCollectMockup.py b/deprecated/HardwareObjects/mockup/MultiCollectMockup.py index ffac7646ac..b84185c768 100644 --- a/deprecated/HardwareObjects/mockup/MultiCollectMockup.py +++ b/deprecated/HardwareObjects/mockup/MultiCollectMockup.py @@ -157,10 +157,15 @@ def set_detector_filenames( ): return - def prepare_oscillation(self, start, osc_range, exptime, npass): + def prepare_oscillation( + self, start, osc_range, exptime, number_of_images, shutterless, first_frame + ): return (start, start + osc_range) - def do_oscillation(self, start, end, exptime, npass): + + def do_oscillation( + self, start, end, exptime, number_of_images, shutterless, first_frame + ): gevent.sleep(exptime) def start_acquisition(self, exptime, npass, first_frame): diff --git a/docs/source/dev/configuration_files.md b/docs/source/dev/configuration_files.md new file mode 100644 index 0000000000..10c5b1f193 --- /dev/null +++ b/docs/source/dev/configuration_files.md @@ -0,0 +1,136 @@ +# Configuration files + +The MXCuBE core is organised as a tree of *HardwareObjects*, +and configuration is organised with a file per HardwareObject. +The (mandatory) topmost object in the tree is the *Beamline* object, +which is configured in the `beamline_config.yml` file. +This in turn contains the names of other configuration files, +recursively, which may be a mixture of YAML and XML configuration files. +The Beamline object can be accessed as an attribute of the HardwareRepository module +(`HardwareRepository.beamline`). +As of 2024-03-14 MXCuBE is in the middle of a change-over to a new system for configuration +that uses YAML files instead of XML files, +which has different ways of accessing the configuration data from inside the code. +HardwareObjects can be configured to contain both other hardware objects and properties. +The former are identified relative to the container by a role. + +## Finding the files +Configuration files are searched for by name in a series of directories given as a lookup path. + +In mxcubeqt this is specified either with the `--coreConfigPath` command line parameter to MXCuBE, +or through the `MXCUBE_CORE_CONFIG_PATH` environment variable. + +In mxcubeweb the configuration lookup path is specified with the `--repository` option to `mxcube-server`, +the default being `mxcubeweb/test/HardwareObjectsMockup.xml/`, +where the mxcubeweb mock configuration resides. + +There is a set of configuration data under the `mxcubecore/mxcubecore/configuration` directory. +This includes the `mockup/` directory +with `qt/` and `gphl/` subdirectories for mock mxcubeqt operation. +There is also a directory with configuration for each beamline, +but these are mostly not up to date. +The actual beamline configuration files are mostly held in a separate (and non-comitted) directory at the beamlines. + +## Yaml-configured objects +### Code and file structure +Each YAML-configured object has a `name` attribute, +which is equal to the role that identifies the object within the containing object +(the name of the Beamline object is `beamline`). + +YAML-configured objects must be subclasses of the `BaseHardwareObjects.ConfiguredObject` class. +HardwareObjects proper (which excludes e.g. `Beamline` and procedures) +are subclasses of `BaseHardwareObjects.HadwareObjectYaml`. +The most complete example is the Beamline object and the comments in `beamline_config.yml` +are the best guide to the syntax of YAML configuration files. +It is a key principle of YAML-configured classes that **all** attributes +added in the configuration must match a pre-defined attribute coded in the class. +This means that you can look in the class code to see which attributes are available. +The only exception is the `_initialise_class` attribute at the start of the file. +This dictionary contains the import name of the class that is to be created, +and optionally parameters to be passed to the `init()` method of that class. +The `_objects` attribute in the file gives the HardwareObjects that are contained in +(i.e. children of) the object. +The dictionary key is the role name, and the value is the name of the configuration file. +Each `role_name` must match a read-only property coded in the body of the class, +and must be added to the `__content_roles` list of the class by the class code. +Note that classes are loaded and initialised in the order given by this list, +so that there is a reproducible loading order. +Contained objects can be defined as procedures, so that they are added to the list of procedures. +Each YAML-configured class has an `_init()` method that is executed immediately after the object is created, +and an `init()` function that is executed after configured parameters and contained objects have been loaded. + +### Accessing configuration data +The Beamline object (`HardwareRepository.beamline`) is a YAML-configured object, +and is the starting point for finding other hardware objects. +These may in turn contain other objects, so you can do e.g. +`HardwareRepository.beamline.detector.distance` to get the detector distance motor object. +Configured properties are similarly accessed as simple attributes, e,g, `beamline.default_acquisition_parameters`. +Each `ConfiguredObject` has three special properties and one function to deal with the objects contained within it. +These are: + +- `all_roles`: a list of the roles (attribute names) of contained HardwareObjects, in loading order; +- `all_objects_by_role`: an ordered dictionary of contained HardwareObjects; +- `procedures` an ordered dictionary of HardwareObjects for procedures; +- `replace_object()`: a method to replace an existing configured object at runtime with a new object. + +## XML-configured objects +### Code and file structure +XML-configured objects have a `name()` method, +that returns the name of the configuration file used to specify it (without the `.xml` suffix). +It is this name that is used in internal data structures and a number of access functions. + +XML-configured objects must be subclasses of the `BaseHardwareObjects.HardwareObject` class. +A good example is `mxcubecore/configuration/mockup/detector-mockup.xml` +(note that `hwrid` is an alias for what is normally written as `href`). +In XML configuration contained objects are given using the "object" element, +with the `href` attribute giving the configuration file name to pick up +(you can use a similar syntax to redirect the topmost element to another file) +and the `role` attribute giving the role name. +Simple properties are given as contained XML elements, +and complex properties (dictionaries) are given as elements of type 'object' without a 'href' attribute. + + +The configuration data are kept in complex internal data structures, +with links back to the original XML. + +The important methods can be found in the `BaseHardwareObjects,HardwareObjectNode` class. +XML-configured files have no limits on the attributes or objects they can contain. +This leads to greater flexibility, since you can add a new attribute when needed without modifying the class code; +it also means that there is no way to check which attributes are supported without looking into the configuration files, +and gives more scope for local and potentially conflicting implementations. +The functions have quite complex behaviour that amounts to overloading. + +### Accessing configuration data + +The recommended way to access contained objects is through the `get_object_by_role` function, +since it works on role names rather than the less predictable file names. +As implemented the function will look recursively in contained objects for a given role name +if the topmost object does not contain it. +The `get_roles` method returns a list of roles that are defined on the object itself. + +You can get and set the values of simple properties by normal `obj.attr` syntax, +which will also get you normal, non-property attributes. +The `get_properties` method returns a dictionary of all properties and their values, +and the `get_property` method behaves as `get_properties().get`. +Direct setting of properties internally calls the `set_property` function, +and this function automatically converts strings to `int`, `float` or `bool` if possible. + +There are additional ways of accessing contained objects. +`get_objects` and `has_object` take as input the object name +As currently coded (was it always thus?) the name is equal to the role name used to add the object. +An XML-configured object is also coded to mimic a Python list and dictionary of contained objects, +so that `anObject[ii]` +(`ii` being an integer) returns the `ii`'th contained object, +whereas `anObject[key]` (key being a string) returns the contained object defined by the name (i.e. the role name). + +For XML-configured HardwareObjects (but not for YAML:-configured ones) +there are two additional ways of getting hold of HardwareObjects. + +Beamline.get_hardware_object lets you get a HO from a dotted list of rolenames (e.g. 'detector.distance') +This is essentially a convenience function to avoid repeated get_object_by_role calls. +For YAML-configured objects the same could be done by direct attribute access. + +HardwareReposotory.get_hardware_object, on the other hand, +lets you access hardware objects by the name of the configuration file, +loading the file if it has not been loaded already. +Use of this function requires you to hardwire the names of configuration files in the code. diff --git a/docs/source/dev/json-schema-generated-user-interface.md b/docs/source/dev/json-schema-generated-user-interface.md index efa9edf674..39afe0872b 100644 --- a/docs/source/dev/json-schema-generated-user-interface.md +++ b/docs/source/dev/json-schema-generated-user-interface.md @@ -10,7 +10,7 @@ We have a system for code-generated interfaces that can be plugged in to both a The system is used to generate parameter queries from mxcubecore (for now from the GΦL workflow). It includes a wide range of widgets; multiple nested layout fields; support for custom update functions triggered when values are edited, that can reset values of other fields, change widget colouring and pulldown enums; field value validation; and colouring and disabling of action for invalid values. There are several examples of pulldown menu contents being modified depending on the values of other fields. The system is started with a `PARAMETERS_NEEDED` on signals being sent from the mxcubecore side with the JSON schemas as parameters. The UI side responds by sending a `PARAMETER_RETURN_SIGNAL` with the complete dictionary of parameter values. There is also a control parameter, that determines whether the response matches a Continue or Cancel click (closing the UI popup), or whether it is a request for a UI update. In the latter case the update is sent from the mxcubecore side with a `PARAMETER_UPDATE_SIGNAL`. -The form of the JSON schemas used has been agreed between Rasmus Foghy, Jean-Baptiste FLorial, an MArcus Oscarsson. Hopefully it fits with standard practices. +The form of the JSON schemas used has been agreed between Rasmus Fogh, Jean-Baptiste Florial, and Marcus Oscarsson. Hopefully it fits with standard practices. The only really detailed documentation available is the Qt implementation, which serves as a worked example. The attached files show screenshots of the generated interfaces (with some examples of e.g. widget colouring); the JSON schemas that generate them can be found in the mxcubecore repository, in `mxcubecore/doc/json_schema_gui` as can the parameters returned, and some examples of the response to gui update requests. diff --git a/mxcubecore/BaseHardwareObjects.py b/mxcubecore/BaseHardwareObjects.py index dcd0d97656..4e6012961a 100644 --- a/mxcubecore/BaseHardwareObjects.py +++ b/mxcubecore/BaseHardwareObjects.py @@ -123,7 +123,7 @@ def replace_object(self, role: str, new_object: object) -> None: # NB this function must be re-implemented in nested subclasses @property def all_roles(self) -> Tuple[str]: - """Tuple of all content object roles, indefinition and loading order + """Tuple of all content object roles, in definition and loading order Returns: Tuple[str]: Content object roles diff --git a/mxcubecore/Command/exporter/ExporterClient.py b/mxcubecore/Command/exporter/ExporterClient.py index c72508ffb5..6cb98d175d 100644 --- a/mxcubecore/Command/exporter/ExporterClient.py +++ b/mxcubecore/Command/exporter/ExporterClient.py @@ -107,12 +107,22 @@ def execute(self, method, pars=None, timeout=-1): pars(str): parameters timeout(float): Timeout [s] """ + + # runScript returns results in a different way than any other + # comand, fix on MD side ? + if method == "runScript" and pars is not None: + pars = pars[0].split(",") + cmd = "{} {} ".format(CMD_SYNC_CALL, method) + if pars is not None: - for par in pars: - if isinstance(par, (list, tuple)): - par = self.create_array_parameter(par) - cmd += str(par) + PARAMETER_SEPARATOR + if isinstance(pars, (list, tuple)): + for par in pars: + if isinstance(par, (list, tuple)): + par = self.create_array_parameter(par) + cmd += str(par) + PARAMETER_SEPARATOR + else: + cmd += str(pars) ret = self.send_receive(cmd, timeout) return self.__process_return(ret) diff --git a/mxcubecore/HardwareObjects/BeamlineActions.py b/mxcubecore/HardwareObjects/BeamlineActions.py index 43fa0c87d1..5bb7610526 100644 --- a/mxcubecore/HardwareObjects/BeamlineActions.py +++ b/mxcubecore/HardwareObjects/BeamlineActions.py @@ -87,6 +87,7 @@ def __init__(self, name, hwobj): self.type = TWO_STATE_COMMAND_T self.argument_type = ARGUMENT_TYPE_LIST self._hwobj.connect("valueChanged", self._cmd_done) + self._running = False def _get_action(self): """Return which action has to be executed. @@ -105,6 +106,7 @@ def __call__(self, *args, **kwargs): Args: None Kwargs: None """ + self._running = True self.emit("commandBeginWaitReply", (str(self.name()),)) value = self._get_action() self._hwobj.set_value(value, timeout=60) @@ -122,9 +124,11 @@ def _cmd_done(self, state): else: if isinstance(res, gevent.GreenletExit): self.emit("commandFailed", (str(self.name()),)) - else: + elif self._running: self.emit("commandReplyArrived", (str(self.name()), res)) + self.running = False + def value(self): """Return the current command vaue. Return: diff --git a/mxcubecore/HardwareObjects/EDNACharacterisation.py b/mxcubecore/HardwareObjects/EDNACharacterisation.py index a812bd7237..c419fcc7e2 100644 --- a/mxcubecore/HardwareObjects/EDNACharacterisation.py +++ b/mxcubecore/HardwareObjects/EDNACharacterisation.py @@ -91,9 +91,6 @@ def _run_edna(self, input_file, results_file, process_directory): logging.getLogger("queue_exec").info( "Received characterisation results via XMLRPC" ) - # logging.getLogger("queue_exec").info([self.characterisationResult]) - # with open("/tmp/EDNA_results.xml", "w") as f: - # f.write(self.characterisationResult) self.result = XSDataResultMXCuBE.parseString( self.characterisationResult ) @@ -265,8 +262,9 @@ def input_from_params(self, data_collection, char_params): for img_num in range(int(acquisition_parameters.num_images)): image_file = XSDataImage() path = XSDataString() - path.setValue(path_str % (img_num + 1)) - image_file.setPath(path) + path.value = path_str % (img_num + 1) + image_file.path = path + image_file.number = XSDataInteger(img_num + 1) data_set.addImageFile(image_file) edna_input.addDataSet(data_set) @@ -372,7 +370,11 @@ def dc_from_output(self, edna_result, reference_image_collection): # and update the members the needs to be changed. Keeping # the directories of the reference collection. ref_pt = reference_image_collection.acquisitions[0].path_template + acq.path_template = copy.deepcopy(ref_pt) + acq.path_template.directory = "/".join( + ref_pt.directory.split("/")[0:-2] + ) acq.path_template.wedge_prefix = "w" + str(i + 1) acq.path_template.reference_image_prefix = str() diff --git a/mxcubecore/HardwareObjects/EMBLFlexHCD.py b/mxcubecore/HardwareObjects/EMBLFlexHCD.py index 95f7e1a9df..d06d57bed4 100644 --- a/mxcubecore/HardwareObjects/EMBLFlexHCD.py +++ b/mxcubecore/HardwareObjects/EMBLFlexHCD.py @@ -587,8 +587,6 @@ def _do_load(self, sample=None): return self._set_loaded_sample_and_prepare(loaded_sample, previous_sample) def _do_unload(self, sample=None): - HWR.beamline.diffractometer.set_phase("Transfer") - self._execute_cmd_exporter( "unloadSample", sample.get_cell_no(), diff --git a/mxcubecore/HardwareObjects/ESRF/ESRFBeam.py b/mxcubecore/HardwareObjects/ESRF/ESRFBeam.py index 8378b35221..9abba378fa 100644 --- a/mxcubecore/HardwareObjects/ESRF/ESRFBeam.py +++ b/mxcubecore/HardwareObjects/ESRF/ESRFBeam.py @@ -100,6 +100,18 @@ def _get_aperture_size(self): return _size / 1000.0, _label def _get_complex_size(self): + """Get the size and the name of the definer in place. + Returns: + (float, str): Size [mm], label. + """ + try: + value = self._complex.get_value() + return value.value[1], value.name + except AttributeError: + logging.getLogger("HWR").info("Could not read beam size") + return (-1, -1), "UNKNOWN" + + def _get_complex_size_old(self): """Get the size and the name of the definer in place. Returns: (float, str): Size [mm], label. @@ -167,7 +179,7 @@ def _beam_size_compare(size): self.beam_height = _val[1] else: self.beam_height = _val[0] - except ValueError: + except (ValueError, TypeError): return None, None, _shape, "none" return self.beam_width, self.beam_height, _shape, _name @@ -201,7 +213,7 @@ def get_available_size(self): "values": [_low_w, _high_w, _low_h, _high_h], } - return None + return [] def _set_slits_size(self, size=None): """Move the slits to the desired position. diff --git a/mxcubecore/HardwareObjects/ESRF/ESRFBeamlineActions.py b/mxcubecore/HardwareObjects/ESRF/ESRFBeamlineActions.py new file mode 100644 index 0000000000..ef4d154eca --- /dev/null +++ b/mxcubecore/HardwareObjects/ESRF/ESRFBeamlineActions.py @@ -0,0 +1,97 @@ +# Project: MXCuBE +# https://github.com/mxcube. +# +# This file is part of MXCuBE software. +# +# MXCuBE is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# MXCuBE is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Lesser Public License +# along with MXCuBE. If not, see . +""" Execute commands and toggle two state actions +Example xml file: + + + + + + + + + Centre beam + Quick realign + Anneal + + + ["hutchtrigger", "scintillator", "detector_cover", "aperture", "cryostream"] + + +""" +import ast +import gevent + +from mxcubecore.TaskUtils import task +from mxcubecore.HardwareObjects.BeamlineActions import ( + BeamlineActions, + ControllerCommand, + HWObjActuatorCommand, +) + +__copyright__ = """ Copyright © 2010-2023 by the MXCuBE collaboration """ +__license__ = "LGPLv3+" + + +class ESRFBeamlineActions(BeamlineActions): + """Beam action commands""" + + def __init__(self, *args): + super().__init__(*args) + self.ctrl_list = [] + self.hwobj_list = [] + + def init(self): + """Initialise the controller commands and the actuator object + to be used. + """ + try: + ctrl_cmds = self["controller_commands"].get_properties().items() + + if ctrl_cmds: + controller = self.get_object_by_role("controller") + for key, name in ctrl_cmds: + # name = self.get_property(cmd) + action = getattr(controller, key) + self.ctrl_list.append(ControllerCommand(name, action)) + except KeyError: + pass + + try: + hwobj_cmd_roles = ast.literal_eval( + self.get_property("hwobj_command_roles").strip() + ) + + if hwobj_cmd_roles: + for role in hwobj_cmd_roles: + try: + hwobj_cmd = self.get_object_by_role(role) + self.hwobj_list.append( + HWObjActuatorCommand(hwobj_cmd.username, hwobj_cmd) + ) + except: + pass + except AttributeError: + pass + + def get_commands(self): + """Get which objects to be used in the GUI + Returns: + (list): List of object + """ + return self.ctrl_list + self.hwobj_list diff --git a/mxcubecore/HardwareObjects/ESRF/ESRFEnergyScan.py b/mxcubecore/HardwareObjects/ESRF/ESRFEnergyScan.py index 1c4a66a96e..17277567be 100644 --- a/mxcubecore/HardwareObjects/ESRF/ESRFEnergyScan.py +++ b/mxcubecore/HardwareObjects/ESRF/ESRFEnergyScan.py @@ -202,7 +202,6 @@ def storeEnergyScan(self): StoreEnergyScanThread, HWR.beamline.lims, self.energy_scan_parameters ) - # def do_chooch(self, elt, edge, directory, archive_directory, prefix): def do_chooch(self, elt, edge, directory, archive_directory, prefix): self.energy_scan_parameters["endTime"] = time.strftime("%Y-%m-%d %H:%M:%S") diff --git a/mxcubecore/HardwareObjects/ESRF/ESRFMultiCollect.py b/mxcubecore/HardwareObjects/ESRF/ESRFMultiCollect.py index 8292f0ac7c..990210d619 100644 --- a/mxcubecore/HardwareObjects/ESRF/ESRFMultiCollect.py +++ b/mxcubecore/HardwareObjects/ESRF/ESRFMultiCollect.py @@ -121,7 +121,6 @@ def prepare_oscillation( exptime, number_of_images, shutterless, - npass, first_frame, ): if shutterless: @@ -129,14 +128,14 @@ def prepare_oscillation( exptime = (exptime + self._detector.get_deadtime()) * number_of_images if first_frame: - self.do_prepare_oscillation(start, end, exptime, npass) + self.do_prepare_oscillation(start, end, exptime) else: if osc_range < 1e-4: # still image end = start else: end = start + osc_range - self.do_prepare_oscillation(start, end, exptime, npass) + self.do_prepare_oscillation(start, end, exptime) return start, end @@ -146,26 +145,31 @@ def no_oscillation(self, exptime): time.sleep(exptime) self.close_fast_shutter() + @task - def do_oscillation(self, start, end, exptime, shutterless, npass, first_frame): + def do_oscillation( + self, start, end, exptime, number_of_images, shutterless, first_frame, + ): if shutterless: if first_frame: exptime = (exptime + self._detector.get_deadtime()) * number_of_images - self.oscillation_task = self.oscil(start, end, exptime, npass wait=False) + self.oscillation_task = self.oscil( + start, end, exptime, number_of_images, wait=False + ) if self.oscillation_task.ready(): self.oscillation_task.get() else: - self.oscil(start, end, exptime, npass) + self.oscil(start, end, exptime, number_of_images) @task - def oscil(self, start, end, exptime, npass, wait=False): + def oscil(self, start, end, exptime, number_of_images, wait=False): if math.fabs(end - start) < 1e-4: self.open_fast_shutter() time.sleep(exptime) self.close_fast_shutter() else: - return self.execute_command("do_oscillation", start, end, exptime, npass) + return self.execute_command("do_oscillation", start, end, exptime, number_of_images) def set_wavelength(self, wavelength): if HWR.beamline.tunable_wavelength: diff --git a/mxcubecore/HardwareObjects/ESRF/ESRFPhotonFlux.py b/mxcubecore/HardwareObjects/ESRF/ESRFPhotonFlux.py index 23a9d996fc..a39992b947 100644 --- a/mxcubecore/HardwareObjects/ESRF/ESRFPhotonFlux.py +++ b/mxcubecore/HardwareObjects/ESRF/ESRFPhotonFlux.py @@ -84,7 +84,7 @@ def _poll_flux(self): """Poll the flux every 2 seconds""" while True: self.re_emit_values() - gevent.sleep(0.5) + gevent.sleep(3) def get_value(self): """Calculate the flux value as function of a reading""" @@ -96,8 +96,14 @@ def get_value(self): if counts == -9999: counts = 0.0 - egy = HWR.beamline.energy.get_value() - calib = self._flux_calc.calc_flux_factor(egy * 1000.0)[self._counter.diode.name] + try: + egy = HWR.beamline.energy.get_value() + calib = self._flux_calc.calc_flux_factor(egy * 1000.0)[ + self._counter.diode.name + ] + except AttributeError: + egy = 0 + calib = 0 try: label = self._aperture.get_value().name @@ -130,4 +136,7 @@ def wait_for_beam(self, timeout=None): (default); if timeout is None: wait forever. """ - return self.beam_check_obj.wait_for_beam(timeout) + try: + return self.beam_check_obj.wait_for_beam(timeout) + except AttributeError: + return True diff --git a/mxcubecore/HardwareObjects/ESRF/ESRFSession.py b/mxcubecore/HardwareObjects/ESRF/ESRFSession.py index f149b3bbc8..0211108144 100644 --- a/mxcubecore/HardwareObjects/ESRF/ESRFSession.py +++ b/mxcubecore/HardwareObjects/ESRF/ESRFSession.py @@ -44,12 +44,14 @@ def get_full_path(self, subdir: str, tag: str) -> Tuple[str, str]: os.path.join(self.get_base_image_directory(), subdir) + "/run*" ) - runs = [1] + runs = [0] for folder in folders: runs.append(int(folder.split("/")[-1].strip("run_").split("_")[0])) run_num = max(runs) + 1 + # Use the same sequnce numbering for all tags/types, (We are not + # creating individual run number per tag) full_path = os.path.join( self.get_base_image_directory(), subdir, f"run_{run_num:02d}/" ) diff --git a/mxcubecore/HardwareObjects/ESRF/ESRFXRFSpectrum.py b/mxcubecore/HardwareObjects/ESRF/ESRFXRFSpectrum.py new file mode 100644 index 0000000000..3169a6ee5f --- /dev/null +++ b/mxcubecore/HardwareObjects/ESRF/ESRFXRFSpectrum.py @@ -0,0 +1,254 @@ +# encoding: utf-8 +# +# Project: MXCuBE +# https://github.com/mxcube. +# +# This file is part of MXCuBE software. +# +# MXCuBE is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# MXCuBE is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Lesser Public License +# along with MXCuBE. If not, see . + +"""ESRF XRF scan procedure""" +from unittest.mock import MagicMock +import logging +import os.path +import numpy +from ast import literal_eval + +from mxcubecore.HardwareObjects.XRFSpectrum import XRFSpectrum + +try: + from PyMca import ConfigDict + from PyMca import ClassMcaTheory + from PyMca import QtMcaAdvancedFitReport +except ImportError: + from PyMca5.PyMca import ConfigDict + from PyMca5.PyMca import ClassMcaTheory + from PyMca5.PyMca import QtMcaAdvancedFitReport + +# Next line is a trick to avoid core dump in QtMcaAdvancedFitReport +QtMcaAdvancedFitReport.qt = MagicMock() + + +class ESRFXRFSpectrum(XRFSpectrum): + """ESRF implementation of the XRF spectrum procedure""" + + def init(self): + super().init() + self.ctrl_hwobj = self.get_object_by_role("controller") + self.cfgfile = self.get_property( + "cfgfile", "/users/blissadm/local/beamline_configuration/misc/15keV.cfg" + ) + self.config = ConfigDict.ConfigDict() + self.mcafit = ClassMcaTheory.McaTheory(self.cfgfile) + self.default_integration_time = self.get_property( + "default_integration_time", 3.5 + ) + self.default_erange = literal_eval( + self.get_property("default_energy_range", "[2.0, 15.0]") + ) + self.cfg_energies = literal_eval( + self.get_property("cfg_energies", "[7, 9, 12, 15]") + ) + + def _doSpectrum(self, ctime, filename, wait=None): + return self.choose_attenuation(ctime, filename) + + def choose_attenuation(self, ctime=None, fname=None): + """Choose appropriate maximum attenuation. + Keyword Args: + ctime (float): integration time [s] + fname (str): Filename to save the MCA data (full path) + Returns: + (bool): Procedure executed correcly (True) or error (False) + """ + fname = fname or self.spectrumInfo["filename"] + ctime = ctime or self.default_integration_time + + # protect the detector + self.ctrl_hwobj.detcover.set_in(20) + try: + current_transm = self.ctrl_hwobj.find_max_attenuation( + ctime=float(ctime), datafile=fname, roi=self.default_erange + ) + self.spectrumInfo["beamTransmission"] = current_transm + except Exception as exp: + logging.getLogger("user_level_log").exception(str(exp)) + self.spectrumCommandFailed() + return False + + # put away the fluo detector + self.ctrl_hwobj.diffractometer.fldet_out() + return True + + def _findAttenuation(self, ctime=None): + return self.choose_attenuation(ctime) + + # Next methods are for fitting the data with pymca + + def mcafit_configuration(self, config=None): + """Configure the fitting parameters. The procedure is time consuming. + It is only executed if the last configuration file is not the same. + Args: + config(dict): Configuration dictionary, containing among others the + configuration file name. + """ + change = False + if not config or "file" not in config: + cfgfile = self._get_cfgfile(self.spectrumInfo["energy"]) + else: + cfgfile = config["file"] + + if self.cfgfile != cfgfile: + self.cfgfile = cfgfile + change = True + self.config.read(self.cfgfile) + if "concentrations" not in self.config: + self.config["concentrations"] = {} + change = True + if "attenuators" not in self.config: + self.config["attenuators"] = {"Matrix": [1, "Water", 1.0, 0.01, 45.0, 45.0]} + change = True + if "flux" in config: + self.config["concentrations"]["flux"] = float(config["flux"]) + change = True + if "time" in config: + self.config["concentrations"]["time"] = float(config["time"]) + change = True + + if change: + self.mcafit.configure(self.config) + + def set_data(self, data, calib=None, config=None): + """Execute the fitting. Write the fitted data files to pyarch. + Args: + data (list): The raw data. + calib (list): The mca calibration. + config (dict): The configuration dictionary. + """ + if config: + self.mcafit_configuration(config) + try: + if data[0].size == 2: + xdata = numpy.array(data[:, 0]) * 1.0 + ydata = numpy.array(data[:, 1]) + else: + xdata = data[0] * 1.0 + ydata = data[1] + + xmin = int(config["min"]) + xmax = int(config["max"]) + self.mcafit.setData(xdata, ydata, xmin=xmin, xmax=xmax, calibration=calib) + + self.mcafit.estimate() + # fitresult = self._fit() + + fitresult = self.mcafit.startfit(digest=1) + if fitresult: + fitresult = {"fitresult": fitresult[0], "result": fitresult[1]} + + # write the csv file to pyarch + csvname = self.spectrumInfo["fittedDataFileFullPath"] + self._write_csv_file(fitresult, csvname) + + # write html report to pyarch + fname = os.path.basename(self.spectrumInfo["filename"]) + outfile = fname.split(".")[0] + outdir = os.path.dirname(self.spectrumInfo["annotatedPymcaXfeSpectrum"]) + + _kw = { + "outdir": outdir, + "outfile": outfile, + "fitresult": fitresult, + "plotdict": {"logy": False}, + } + + report = QtMcaAdvancedFitReport.QtMcaAdvancedFitReport(**_kw) + text = report.getText() + report.writeReport(text=text) + except Exception as exp: + msg = f"XRFSpectrum: problem fitting {exp}\n" + msg += f"Please check the raw data file {self.spectrumInfo['scanFileFullPath']}" + logging.getLogger("user_level_log").exception(msg) + self.spectrumCommandFailed() + return False + + def _write_csv_file(self, fitresult, fname=None): + """Write data to a csv file. + Args: + fitresult(dict): Data as dictionary. + Kwargs: + fname (str): Filename to write to (full path). + """ + fname = fname or self.spectrumInfo["fittedDataFileFullPath"] + if os.path.exists(fname): + os.remove(fname) + + # get the significant peaks + peaks_dict = {} + pars_len = len(fitresult["result"]["parameters"]) + grp_len = len(fitresult["result"]["groups"]) + nglobal = pars_len - grp_len + parameters = fitresult["result"]["fittedpar"][:nglobal] + [0.0] * grp_len + + for grp in fitresult["result"]["parameters"][nglobal:]: + idx = fitresult["result"]["parameters"].index(grp) + parameters[idx] = fitresult["result"]["fittedpar"][idx] + xmatrix = fitresult["result"]["xdata"] + ymatrix = self.mcafit.mcatheory(parameters, xmatrix) + ymatrix.shape = [len(ymatrix), 1] + label = "y" + grp + if self.mcafit.STRIP: + peaks_dict[label] = ymatrix + self.mcafit.zz + else: + peaks_dict[label] = ymatrix + peaks_dict[label].shape = (len(peaks_dict[label]),) + parameters[idx] = 0.0 + + delimiter = "," + header = '"channel"%s"Energy"%s"counts"%s"fit"%s"continuum"%s"pileup"' % ( + delimiter, + delimiter, + delimiter, + delimiter, + delimiter, + ) + + # add the peaks labels + for key in peaks_dict: + header += delimiter + ('"%s"' % key) + # logging.getLogger("user_level_log").info("Writing %s" % fname) + with open(fname, "w") as csv_fd: + csv_fd.write(header) + csv_fd.write("\n") + for i in range(fitresult["result"]["xdata"].size): + csv_fd.write( + "%.7g%s%.7g%s%.7g%s%.7g%s%.7g%s%.7g" + % ( + fitresult["result"]["xdata"][i], + delimiter, + fitresult["result"]["energy"][i], + delimiter, + fitresult["result"]["ydata"][i], + delimiter, + fitresult["result"]["yfit"][i], + delimiter, + fitresult["result"]["continuum"][i], + delimiter, + fitresult["result"]["pileup"][i], + ) + ) + for key in peaks_dict: + csv_fd.write("%s%.7g" % (delimiter, peaks_dict[key][i])) + + csv_fd.write("\n") diff --git a/mxcubecore/HardwareObjects/ESRF/ID232BeamDefiner.py b/mxcubecore/HardwareObjects/ESRF/ID232BeamDefiner.py index be8c980f61..e503205189 100644 --- a/mxcubecore/HardwareObjects/ESRF/ID232BeamDefiner.py +++ b/mxcubecore/HardwareObjects/ESRF/ID232BeamDefiner.py @@ -1,5 +1,8 @@ import ast +from enum import Enum +from gevent import Timeout, sleep + from mxcubecore.BaseHardwareObjects import HardwareObject from mxcubecore.HardwareObjects.abstract.AbstractNState import AbstractNState from bliss.common import event @@ -40,6 +43,10 @@ def init(self): self.size_by_name[name] = tuple([float(x) for x in ast.literal_eval(size)]) self.coef_by_name[name] = float(coef) + # check if we have values other that UKNOWN + if len(self.VALUES) == 1: + self._initialise_values() + def is_ready(self): return self.controller is not None @@ -59,9 +66,9 @@ def get_value(self): (int): The position index. """ try: - return self.pos_names.index(self.get_current_position_name()) - except ValueError: - return -1 + return self.VALUES[self.get_current_position_name()] + except (ValueError, KeyError): + return self.VALUES.UNKNOWN def _tf_state_updated(self, new_state=None): name = self.get_current_position_name() @@ -76,13 +83,17 @@ def connect_notify(self, signal): def get_predefined_positions_list(self): return self.pos_names + def get_current_status(self): + tf1_status = self.controller.tf.wago.get("stat")[::2] + tf2_status = self.controller.tf2.wago.get("stat")[::2] + return tf1_status, tf2_status + def get_current_position_name(self, *args): try: tf1_state = self.controller.tf.status_read()[1].split() tf2_state = self.controller.tf2.status_read()[1].split() except: return "UNKNOWN" - for name in self.pos_names: tf1_cfg = self.tf_cfg_by_name[name]["tf1"] tf2_cfg = self.tf_cfg_by_name[name]["tf2"] @@ -98,7 +109,7 @@ def get_current_position_name(self, *args): else: return name - def set_value(self, name): + def set_value(self, name, timeout=None): """Set the beam size. Args: name (str): position name @@ -108,3 +119,27 @@ def set_value(self, name): self.controller.tf.set(*tf1_cfg) self.controller.tf2.set(*tf2_cfg) + self.wait_ready((tf1_cfg,tf2_cfg), timeout) + + def wait_ready(self, status, timeout=None): + """Wait timeout seconds until status reached + Args: + timeout (float): Timeout [s]. Defaults to None. + """ + with Timeout(timeout, RuntimeError("Execution timeout")): + while status != self.get_current_status(): + sleep(0.5) + + def _initialise_values(self): + """Initialise the ValueEnum from the hardware + Raises: + RuntimeError: No aperture diameters defined. + """ + values = {} + for val in self.pos_names: + values[val] = [self.pos_names.index(val), self.size_by_name[val]] + + self.VALUES = Enum( + "ValueEnum", + dict(values, **{item.name: item.value for item in self.VALUES}), + ) diff --git a/mxcubecore/HardwareObjects/ESRF/MD2MultiCollect.py b/mxcubecore/HardwareObjects/ESRF/MD2MultiCollect.py index d5b0e1d7f5..d040d60497 100644 --- a/mxcubecore/HardwareObjects/ESRF/MD2MultiCollect.py +++ b/mxcubecore/HardwareObjects/ESRF/MD2MultiCollect.py @@ -94,8 +94,7 @@ def do_prepare_oscillation(self, *args, **kwargs): # move to DataCollection phase logging.getLogger("user_level_log").info("Moving MD2 to DataCollection") - # AB next 3 lines to speed up the data collection - # diffr.set_phase("DataCollection", wait=True, timeout=200) + # AB next line to speed up the data collection diffr.set_phase("DataCollection", wait=False, timeout=0) @task @@ -104,12 +103,14 @@ def data_collection_cleanup(self): self.close_fast_shutter() @task - def oscil(self, start, end, exptime, npass, wait=True): + def oscil(self, start, end, exptime, number_of_images, wait=True): diffr = self.get_object_by_role("diffractometer") # make sure the diffractometer is ready to do the scan diffr.wait_ready(100) if self.helical: - diffr.oscilScan4d(start, end, exptime, self.helical_pos, wait=True) + diffr.oscilScan4d( + start, end, exptime, number_of_images, self.helical_pos, wait=True + ) elif self.mesh: det = HWR.beamline.detector latency_time = det.get_property("latecy_time_mesh") or det.get_deadtime() @@ -149,7 +150,7 @@ def oscil(self, start, end, exptime, npass, wait=True): wait=True, ) else: - diffr.oscilScan(start, end, exptime, wait=True) + diffr.oscilScan(start, end, exptime, number_of_images, wait=True) @task def prepare_acquisition( diff --git a/mxcubecore/HardwareObjects/ExporterNState.py b/mxcubecore/HardwareObjects/ExporterNState.py index 71c1531fd7..fc098597cd 100644 --- a/mxcubecore/HardwareObjects/ExporterNState.py +++ b/mxcubecore/HardwareObjects/ExporterNState.py @@ -71,7 +71,7 @@ def init(self): }, value_channel, ) - self.value_channel.connect_signal("update", self.update_value) + self.value_channel.connect_signal("update", self._update_value) self.state_channel = self.add_channel( { @@ -104,6 +104,9 @@ def _wait_ready(self, timeout=None): while not self.get_state() == self.STATES.READY: sleep(0.5) + def _update_value(self, value): + super().update_value(self.value_to_enum(value)) + def _update_state(self, state=None): """To be used to update the state when emiting the "update" signal. Args: @@ -115,6 +118,7 @@ def _update_state(self, state=None): state = self.get_state() else: state = self._str2state(state) + return self.update_state(state) def _str2state(self, state): diff --git a/mxcubecore/HardwareObjects/FlexHCDMaintenance.py b/mxcubecore/HardwareObjects/FlexHCDMaintenance.py index 23a2a5efde..d808db5a3d 100644 --- a/mxcubecore/HardwareObjects/FlexHCDMaintenance.py +++ b/mxcubecore/HardwareObjects/FlexHCDMaintenance.py @@ -1,6 +1,7 @@ """ FLEX HCD maintenance mockup. """ + from mxcubecore.BaseHardwareObjects import Equipment import ast @@ -91,6 +92,7 @@ def _do_change_gripper(self, args): :rtype: None """ self._sc.change_gripper(gripper=args) + self.emit("gripperChanged") def _do_reset_sample_number(self): """ diff --git a/mxcubecore/HardwareObjects/Gphl/GphlWorkflow.py b/mxcubecore/HardwareObjects/Gphl/GphlWorkflow.py index fa67d37942..ecbda8a024 100644 --- a/mxcubecore/HardwareObjects/Gphl/GphlWorkflow.py +++ b/mxcubecore/HardwareObjects/Gphl/GphlWorkflow.py @@ -306,11 +306,14 @@ def get_available_workflows(self): def query_pre_strategy_params(self, choose_lattice=None): """Query pre_strategy parameters. + Used for both characterisation, diffractcal, and acquisition - :param data_model (GphlWorkflow): GphlWorkflow QueueModelObjecy - :param choose_lattice (ChooseLattice): GphlMessage.ChooseLattice - :return: -> dict + Args: + choose_lattice (:obj: `ChooseLattice`, optional): ChooseLattice message + + Returns: + dict: Parameter value dictionary """ resolution_decimals = 3 @@ -1373,11 +1376,14 @@ def query_collection_strategy(self, geometric_strategy): return result def setup_data_collection(self, payload, correlation_id): - """Query data collection parameters and return SampleCentred to ASTRA workflow + """ - :param payload (GphlMessages.GeometricStrategy): - :param correlation_id (int) Astra workflow correlation ID - :return (GphlMessages.SampleCentred): + Args: + payload (GphlMessages.GeometricStrategy: GeometricStrategy message + correlation_id (str) : Astra workflow correlation ID + + Returns: + GphlMessages.SampleCentred: Return message with collection parameters """ geometric_strategy = payload @@ -2295,7 +2301,15 @@ def process_centring_request(self, payload, correlation_id): logging.getLogger("HWR").warning( "No predefined positions for zoom motor." ) + elif True: + logging.getLogger("user_level_log").info( + "Sample re-centering now active - Zoom in before continuing." + ) + else: + # TODO The UI popup does not work in mxcubeweb + # NB Temporarily inactivated pending a fix + # Ask user to zoom info_text = """Automatic sample re-centering is now active Switch to maximum zoom before continuing""" diff --git a/mxcubecore/HardwareObjects/Gphl/GphlWorkflowConnection.py b/mxcubecore/HardwareObjects/Gphl/GphlWorkflowConnection.py index 4e13dfc6c8..64360646b6 100644 --- a/mxcubecore/HardwareObjects/Gphl/GphlWorkflowConnection.py +++ b/mxcubecore/HardwareObjects/Gphl/GphlWorkflowConnection.py @@ -495,11 +495,19 @@ def processMessage(self, py4j_message): if not self.msg_class_imported: try: - msg_class = self._gateway.jvm.py4j.reflection.ReflectionUtil.classForName("co.gphl.sdcp.astra.service.py4j.Py4jMessage") - java_gateway.java_import(self._gateway.jvm, "co.gphl.sdcp.astra.service.py4j.Py4jMessage") + msg_class = self._gateway.jvm.py4j.reflection.ReflectionUtil.classForName( + "co.gphl.sdcp.astra.service.py4j.Py4jMessage" + ) + java_gateway.java_import( + self._gateway.jvm, "co.gphl.sdcp.astra.service.py4j.Py4jMessage" + ) except Py4JJavaError: - msg_class = self._gateway.jvm.py4j.reflection.ReflectionUtil.classForName("co.gphl.sdcp.py4j.Py4jMessage") - java_gateway.java_import(self._gateway.jvm, "co.gphl.sdcp.py4j.Py4jMessage") + msg_class = self._gateway.jvm.py4j.reflection.ReflectionUtil.classForName( + "co.gphl.sdcp.py4j.Py4jMessage" + ) + java_gateway.java_import( + self._gateway.jvm, "co.gphl.sdcp.py4j.Py4jMessage" + ) logging.getLogger("HWR").debug( "GΦL workflow Py4jMessage class is: %s" % msg_class diff --git a/mxcubecore/HardwareObjects/LNLS/LNLSCollect.py b/mxcubecore/HardwareObjects/LNLS/LNLSCollect.py index c1e98a6a37..b91ce0df32 100644 --- a/mxcubecore/HardwareObjects/LNLS/LNLSCollect.py +++ b/mxcubecore/HardwareObjects/LNLS/LNLSCollect.py @@ -333,10 +333,14 @@ def set_detector_filenames( ): return - def prepare_oscillation(self, start, osc_range, exptime, npass): + def prepare_oscillation( + self, start, osc_range, exptime, number_of_images, shutterless, first_frame + ): return (start, start + osc_range) - def do_oscillation(self, start, end, exptime, shutterless, npass, first_frame): + def do_oscillation( + self, start, end, exptime, number_of_images, shutterless, first_frame + ): gevent.sleep(exptime) def start_acquisition(self, exptime, npass, first_frame): diff --git a/mxcubecore/HardwareObjects/LimaEigerDetector.py b/mxcubecore/HardwareObjects/LimaEigerDetector.py index daee7b0197..189e0d53f8 100644 --- a/mxcubecore/HardwareObjects/LimaEigerDetector.py +++ b/mxcubecore/HardwareObjects/LimaEigerDetector.py @@ -230,12 +230,14 @@ def set_detector_filenames(self, frame_number, start, filename): self.get_channel_object("saving_format").set_value("HDF5") def start_acquisition(self): + self.wait_ready() logging.getLogger("user_level_log").info("Preparing acquisition") self.get_command_object("prepare_acq")() logging.getLogger("user_level_log").info("Detector ready, continuing") self.get_command_object("start_acq")() def stop_acquisition(self): + self.update_state(self.STATES.BUSY) try: self.get_command_object("stop_acq")() except Exception: @@ -244,9 +246,11 @@ def stop_acquisition(self): time.sleep(1) self.get_command_object("reset")() self.wait_ready() + self.update_state(self.STATES.READY) def reset(self): self.stop_acquisition() + return True @property def status(self): diff --git a/mxcubecore/HardwareObjects/LimaPilatusDetector.py b/mxcubecore/HardwareObjects/LimaPilatusDetector.py index a670f7ae2b..fab9785645 100644 --- a/mxcubecore/HardwareObjects/LimaPilatusDetector.py +++ b/mxcubecore/HardwareObjects/LimaPilatusDetector.py @@ -162,11 +162,9 @@ def prepare_acquisition( mesh_num_lines, ): if mesh: - trigger_mode = "EXTERNAL_GATE" - # elif osc_range < 1e-4: - # trigger_mode = "INTERNAL_TRIGGER" + trigger_mode = self.get_property("mesh_trigger_mode", "EXTERNAL_GATE") else: - trigger_mode = "EXTERNAL_TRIGGER" + trigger_mode = self.get_property("osc_trigger_mode", "EXTERNAL_GATE") diffractometer_positions = HWR.beamline.diffractometer.get_positions() self.start_angles = list() diff --git a/mxcubecore/HardwareObjects/MD3UP.py b/mxcubecore/HardwareObjects/MD3UP.py index dedcab6ec9..e1dc6745ad 100644 --- a/mxcubecore/HardwareObjects/MD3UP.py +++ b/mxcubecore/HardwareObjects/MD3UP.py @@ -1,6 +1,7 @@ import math import numpy import logging +import time from mxcubecore.HardwareObjects import Microdiff from mxcubecore.HardwareObjects.sample_centring import CentringMotor @@ -83,18 +84,26 @@ def init(self): def setNbImages(self, number_of_images): self.scan_nb_frames = number_of_images - def oscilScan(self, start, end, exptime, wait=False): + def oscilScan(self, start, end, exptime, number_of_images, wait=False): if self.in_plate_mode(): scan_speed = math.fabs(end - start) / exptime low_lim, hi_lim = map(float, self.scanLimits(scan_speed)) if start < low_lim: raise ValueError("Scan start below the allowed value %f" % low_lim) elif end > hi_lim: - raise ValueError("Scan end above the allowed value %f" % hi_lim) - self.nb_frames.set_value(self.scan_nb_frames) + raise ValueError("Scan end abobe the allowed value %f" % hi_lim) + + dead_time = HWR.beamline.detector.get_deadtime() - params = "1\t%0.3f\t%0.3f\t%0.4f\t1" % (start, (end - start), exptime) + self.scan_detector_gate_pulse_enabled.set_value(True) + self.scan_detector_gate_pulse_readout_time.set_value(dead_time * 1000) + if self.get_property("md_set_number_of_frames", False): + self.nb_frames.set_value(number_of_images) + else: + self.nb_frames.set_value(1) + + scan_params = "1\t%0.3f\t%0.3f\t%0.4f\t1" % (start, (end - start), exptime) scan = self.add_command( { "type": "exporter", @@ -103,16 +112,15 @@ def oscilScan(self, start, end, exptime, wait=False): }, "startScanEx", ) - - self._wait_ready(300) - - scan(params) - + scan(scan_params) + print("oscil scan started at ----------->", time.time()) if wait: - # Timeout of 5 min - self._wait_ready(300) + self._wait_ready(20 * 60) # Timeout of 20 min + print("finished at ---------->", time.time()) - def oscilScan4d(self, start, end, exptime, motors_pos, wait=False): + def oscilScan4d( + self, start, end, exptime, number_of_images, motors_pos, wait=False + ): if self.in_plate_mode(): scan_speed = math.fabs(end - start) / exptime low_lim, hi_lim = map(float, self.scanLimits(scan_speed)) @@ -121,7 +129,10 @@ def oscilScan4d(self, start, end, exptime, motors_pos, wait=False): elif end > hi_lim: raise ValueError("Scan end abobe the allowed value %f" % hi_lim) - self.nb_frames.set_value(self.scan_nb_frames) + if self.get_property("md_set_number_of_frames", False): + self.nb_frames.set_value(number_of_images) + else: + self.nb_frames.set_value(1) params = "%0.3f\t%0.3f\t%f\t" % (start, (end - start), exptime) params += "%0.3f\t" % motors_pos["1"]["phiz"] @@ -160,20 +171,10 @@ def oscilScanMesh( mesh_range, wait=False, ): - self.scan_detector_gate_pulse_enabled.set_value(True) - - # Adding the servo time to the readout time to avoid any - # servo cycle jitter - servo_time = 0.110 - - self.scan_detector_gate_pulse_readout_time.set_value( - dead_time * 1000 + servo_time - ) - - # Prepositionning at the center of the grid + dead_time = HWR.beamline.detector.get_deadtime() + self.scan_detector_gate_pulse_readout_time.set_value(dead_time * 1000) self.move_motors(mesh_center.as_dict()) - positions = self.get_positions() params = "%0.3f\t" % (end - start) @@ -206,6 +207,58 @@ def oscilScanMesh( # Timeout of 30 min self._wait_ready(1800) + def timeresolved_mesh_scan(self, timeout=900): + """Start timeresolved mesh. + Args: + timeout(float): Timeout to wait the execution of the scan [s]. + Default value is 15 min + """ + scan = self.add_command( + { + "type": "exporter", + "exporter_address": self.exporter_addr, + "name": "start_timeresolved_mesh", + }, + "startContinuousTimeResolvedRasterScan", + ) + scan() + + # wait for the execution of the scan + self._wait_ready(timeout) + + def get_timeresolved_mesh_nbframes(self): + """Get the calculated number of frames for the continuous timeresolved + mesh scan. + Returns: + (int): Number of frames. + """ + cmd = self.add_channel( + { + "type": "exporter", + "exporter_address": self.exporter_addr, + "name": "timeresolved_mesh_nbframes", + }, + "ContinuousTimeResolvedRasterScanNbFrames", + ) + return cmd.get_value() + + def get_timeresolved_mesh_exptime(self): + """Get the calculated exposure time for the continuous timeresolved + mesh scan. + Returns: + (float): Exposure time [s]. + """ + + cmd = self.add_channel( + { + "type": "exporter", + "exporter_address": self.exporter_addr, + "name": "timeresolved_mesh_exptime", + }, + "ContinuousTimeResolvedRasterScanExposureTime", + ) + return cmd.get_value() + def get_centred_point_from_coord(self, x, y, return_by_names=None): self.pixelsPerMmY, self.pixelsPerMmZ = self.getCalibrationData( self.zoomMotor.get_value() diff --git a/mxcubecore/HardwareObjects/Microdiff.py b/mxcubecore/HardwareObjects/Microdiff.py index 4e0355ddb8..673afbb033 100644 --- a/mxcubecore/HardwareObjects/Microdiff.py +++ b/mxcubecore/HardwareObjects/Microdiff.py @@ -281,6 +281,10 @@ def _update_state(self, value): EXPORTER_TO_HWOBJ_STATE.get(value, HardwareObjectState.UNKNOWN) ) + def abort(self): + self.abort_cmd() + return True + def getMotorToExporterNames(self): MOTOR_TO_EXPORTER_NAME = { "focus": self.focusMotor.get_property("actuator_name"), @@ -397,6 +401,7 @@ def set_phase(self, phase, wait=False, timeout=None): msg = f"Changing phase to {phase}, using pmac script" logging.getLogger("user_level_log").info(msg) self.run_script(script) + self._wait_ready(600) else: self.move_phase(phase) if wait: @@ -435,21 +440,24 @@ def move_sync_motors(self, motors_dict, wait=False, timeout=None): self._wait_ready() # print "end moving motors =============", time.time() - def oscilScan(self, start, end, exptime, wait=False): + def oscilScan(self, start, end, exptime, number_of_images, wait=False): if self.in_plate_mode(): scan_speed = math.fabs(end - start) / exptime low_lim, hi_lim = map(float, self.scanLimits(scan_speed)) if start < low_lim: raise ValueError("Scan start below the allowed value %f" % low_lim) elif end > hi_lim: - raise ValueError("Scan end abobe the allowed value %f" % hi_lim) + raise ValueError("Scan end above the allowed value %f" % hi_lim) dead_time = HWR.beamline.detector.get_deadtime() self.scan_detector_gate_pulse_enabled.set_value(True) self.scan_detector_gate_pulse_readout_time.set_value(dead_time * 1000) - self.nb_frames.set_value(1) + if self.get_property("md_set_number_of_frames", False): + self.nb_frames.set_value(number_of_images) + else: + self.nb_frames.set_value(1) scan_params = "1\t%0.3f\t%0.3f\t%0.4f\t1" % (start, (end - start), exptime) scan = self.add_command( @@ -468,16 +476,22 @@ def oscilScan(self, start, end, exptime, wait=False): ) # timeout of 10 min # Changed on 20180406 Daniele, because of long exposure time set by users print("finished at ---------->", time.time()) - def oscilScan4d(self, start, end, exptime, motors_pos, wait=False): + def oscilScan4d( + self, start, end, exptime, number_of_images, motors_pos, wait=False + ): if self.in_plate_mode(): scan_speed = math.fabs(end - start) / exptime low_lim, hi_lim = map(float, self.scanLimits(scan_speed)) if start < low_lim: raise ValueError("Scan start below the allowed value %f" % low_lim) elif end > hi_lim: - raise ValueError("Scan end abobe the allowed value %f" % hi_lim) + raise ValueError("Scan end above the allowed value %f" % hi_lim) + + if self.get_property("md_set_number_of_frames", False): + self.nb_frames.set_value(number_of_images) + else: + self.nb_frames.set_value(1) - self.nb_frames.set_value(1) scan_params = "%0.3f\t%0.3f\t%f\t" % (start, (end - start), exptime) scan_params += "%0.3f\t" % motors_pos["1"]["phiy"] scan_params += "%0.3f\t" % motors_pos["1"]["phiz"] @@ -649,12 +663,12 @@ def get_positions(self): "sampx": float(self.sampleXMotor.get_value()), "sampy": float(self.sampleYMotor.get_value()), "zoom": self.zoomMotor.get_value().value, - "kappa": float(self.kappaMotor.get_value()) - if self.in_kappa_mode() - else None, - "kappa_phi": float(self.kappaPhiMotor.get_value()) - if self.in_kappa_mode() - else None, + "kappa": ( + float(self.kappaMotor.get_value()) if self.in_kappa_mode() else None + ), + "kappa_phi": ( + float(self.kappaPhiMotor.get_value()) if self.in_kappa_mode() else None + ), } return pos diff --git a/mxcubecore/HardwareObjects/abstract/AbstractMultiCollect.py b/mxcubecore/HardwareObjects/abstract/AbstractMultiCollect.py index 0db5e557f8..ebe55508ce 100644 --- a/mxcubecore/HardwareObjects/abstract/AbstractMultiCollect.py +++ b/mxcubecore/HardwareObjects/abstract/AbstractMultiCollect.py @@ -127,12 +127,14 @@ def prepare_intensity_monitors(self): pass @abc.abstractmethod - def do_oscillation(self, start, end, exptime, shutterless, npass, first_frame): + def do_oscillation( + self, start, end, exptime, number_of_images, shutterless, first_frame + ): pass @abc.abstractmethod def prepare_oscillation( - self, start, osc_range, exptime, number_of_images, shutterless, npass + self, start, osc_range, exptime, number_of_images, shutterless, first_frame ): pass @@ -751,6 +753,7 @@ def do_collect(self, owner, data_collect_parameters): "Setting resolution to %f", resolution ) try: + HWR.beamline.diffractometer.open_detector_cover() HWR.beamline.resolution.set_value(resolution, timeout=3500) except RuntimeError: logging.getLogger("user_level_log").info( @@ -949,7 +952,6 @@ def do_collect(self, owner, data_collect_parameters): exptime, wedge_size, data_collect_parameters.get("shutterless", True), - npass, j == wedge_size, ) @@ -964,8 +966,8 @@ def do_collect(self, owner, data_collect_parameters): osc_start, osc_end, exptime, + wedge_size, data_collect_parameters.get("shutterless", True), - npass, j == wedge_size, ) @@ -1033,7 +1035,7 @@ def do_collect(self, owner, data_collect_parameters): ), ): if last_image_saved <= 0: - last_image_saved = elf.last_image_saved( + last_image_saved = self.last_image_saved( _total_exptime, exptime, wedge_size ) @@ -1049,6 +1051,7 @@ def do_collect(self, owner, data_collect_parameters): frame = max( start_image_number + 1, start_image_number + last_image_saved - 1, + frame + 1, ) self.emit("collectImageTaken", frame) j = wedge_size - last_image_saved diff --git a/mxcubecore/HardwareObjects/mockup/ExporterNStateMockup.py b/mxcubecore/HardwareObjects/mockup/ExporterNStateMockup.py index cca1fb9589..e3a44811ec 100644 --- a/mxcubecore/HardwareObjects/mockup/ExporterNStateMockup.py +++ b/mxcubecore/HardwareObjects/mockup/ExporterNStateMockup.py @@ -51,7 +51,7 @@ def __init__(self, name): def init(self): """Initialise the device""" AbstractNState.init(self) - value = [e.value for e in self.VALUES][1] + value = self.VALUES.OUT self.update_state(self.STATES.READY) self.update_value(value) @@ -114,7 +114,7 @@ def _set_value(self, value): else: value = value.value - self._nominal_value = value + self._nominal_value = self.value_to_enum(value) self.update_state(self.STATES.READY) def get_value(self): @@ -123,4 +123,4 @@ def get_value(self): (Enum): Enum member, corresponding to the value or UNKNOWN. """ # _val = self._mock_value - return self.value_to_enum(self._nominal_value) + return self._nominal_value diff --git a/mxcubecore/HardwareObjects/mockup/ISPyBClientMockup.py b/mxcubecore/HardwareObjects/mockup/ISPyBClientMockup.py index d09d6b76a1..3cd11607a5 100644 --- a/mxcubecore/HardwareObjects/mockup/ISPyBClientMockup.py +++ b/mxcubecore/HardwareObjects/mockup/ISPyBClientMockup.py @@ -43,13 +43,6 @@ def init(self): Init method declared by HardwareObject. """ self.lims_rest = self.get_object_by_role("lims_rest") - self.authServerType = self.get_property("authServerType") or "ldap" - if self.authServerType == "ldap": - # Initialize ldap - self.ldap_connection = self.get_object_by_role("ldapServer") - if self.ldap_connection is None: - logging.getLogger("HWR").debug("LDAP Server is not available") - self.beamline_name = HWR.beamline.session.beamline_name try: diff --git a/mxcubecore/configuration/mockup/qt/beamline_config.yml b/mxcubecore/configuration/mockup/qt/beamline_config.yml index dca1310abd..19d440a5d6 100644 --- a/mxcubecore/configuration/mockup/qt/beamline_config.yml +++ b/mxcubecore/configuration/mockup/qt/beamline_config.yml @@ -35,7 +35,7 @@ _objects: - safety_shutter: safety-shutter-mockup.xml - fast_shutter: fast-shutter-mockup.xml - sample_changer: sample-changer-mockup.xml - # NBNB TODO remove plate_manipulater and treat as another smaple changer + # NBNB TODO remove plate_manipulater and treat as another sample changer - plate_manipulator: plate-manipulator-mockup.xml - diffractometer: diffractometer-mockup.xml - sample_view: sample-view.xml diff --git a/mxcubecore/model/queue_model_objects.py b/mxcubecore/model/queue_model_objects.py index 9e19f7ac29..7fc8357ef5 100644 --- a/mxcubecore/model/queue_model_objects.py +++ b/mxcubecore/model/queue_model_objects.py @@ -29,6 +29,12 @@ from mxcubecore.model import queue_model_enumerables +try: + from mxcubecore.model import crystal_symmetry + import ruamel.yaml as yaml +except Exception: + logging.getLogger("HWR").warning("Cannot import dependenices needed for GPHL workflows - GPhL workflows might not work") + # This module is used as a self contained entity by the BES # workflows, so we need to make sure that this module can be # imported eventhough HardwareRepository is not avilable. diff --git a/mxcubecore/queue_entry/base_queue_entry.py b/mxcubecore/queue_entry/base_queue_entry.py index 066bad48ee..13a12183ee 100644 --- a/mxcubecore/queue_entry/base_queue_entry.py +++ b/mxcubecore/queue_entry/base_queue_entry.py @@ -26,16 +26,14 @@ import sys import traceback import time -import logging import gevent +import copy from collections import namedtuple from mxcubecore import HardwareRepository as HWR from mxcubecore.model import queue_model_objects -from mxcubecore.model.queue_model_enumerables import ( - CENTRING_METHOD, -) +from mxcubecore.model.queue_model_enumerables import CENTRING_METHOD, EXPERIMENT_TYPE from mxcubecore.HardwareObjects import autoprocessing @@ -472,7 +470,7 @@ def execute(self): self.interleave_items.append(interleave_item) if task_model.inverse_beam_num_images is not None: - inverse_beam_item = copy(interleave_item) + inverse_beam_item = copy.deepcopy(interleave_item) inverse_beam_item["data_model"] = interleave_item[ "data_model" ].copy() diff --git a/pyproject.toml b/pyproject.toml index c052a30ce9..90bf8176f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "mxcubecore" -version = "1.76.0" +version = "1.79.0" license = "LGPL-3.0-or-later" description = "Core libraries for the MXCuBE application" authors = ["The MXCuBE collaboration "]