class OptimizationFluenceParameterWidget(OptimizationParameterWidget): """ Class that handles the fluence optimization parameters' ui. """ # Fluence specific properties upper_limits = bnd.bind("fluenceDoubleSpinBox") dis_c = bnd.bind("disCSpinBox") dis_m = bnd.bind("disMSpinBox") @property def lower_limits(self): return 0.0 @property def sol_size(self): return 1 @property def optimization_type(self) -> OptimizationType: return OptimizationType.FLUENCE def __init__(self, **kwargs): """Initialize the widget. Args: kwargs: property values to be shown in the widget. """ ui_file = gutils.get_ui_dir() / "ui_optimization_fluence_params.ui" self.fluenceDoubleSpinBox = ScientificSpinBox(10e12) super().__init__(ui_file, **kwargs) self.fluence_form_layout.addRow("Upper limit", self.fluenceDoubleSpinBox)
class PercentageRow(QtWidgets.QWidget, bnd.PropertyBindingWidget, metaclass=QtABCMeta): """PercentageRow is used to display percentage and area for each RecoilElement in the PercentageWidget. """ percentage = bnd.bind("percentageLabel", fget=label_to_percentage, fset=percentage_to_label) area = bnd.bind("areaLabel", fget=label_to_area, fset=area_to_label) selected = bnd.bind("selectedCheckbox") def __init__(self, label_text, color="red", **kwargs): """Initializes a PercentageRow. Args: label_text: text to be shown in the main label color: color of the Circle that is shown next to label kwargs: percentage and area. """ super().__init__() self.setMinimumHeight(20) layout = QtWidgets.QHBoxLayout() layout.setAlignment(Qt.AlignBottom) text_label = QtWidgets.QLabel(label_text) text_label.setMaximumWidth(100) text_label.setMinimumWidth(100) if platform.system() == "Linux": circle = Circle(color, None) else: circle = Circle(color, (1, 4, 4, 4)) circle.setMinimumWidth(25) circle.setMaximumWidth(25) self.percentageLabel = QtWidgets.QLabel() self.percentageLabel.setMinimumWidth(80) self.percentageLabel.setMaximumWidth(80) self.areaLabel = QtWidgets.QLabel() self.areaLabel.setMinimumWidth(60) self.areaLabel.setMaximumWidth(60) self.selectedCheckbox = QtWidgets.QCheckBox() self.selectedCheckbox.setToolTip( f"Deselect to ignore the element from percentage and area " f"calculations.") self.selectedCheckbox.setChecked(True) layout.addWidget(text_label) layout.addWidget(circle) layout.addWidget(self.percentageLabel) layout.addWidget(self.areaLabel) layout.addWidget(self.selectedCheckbox) self.set_properties(**kwargs) self.setLayout(layout)
class DepthProfileIgnoreElements(QtWidgets.QDialog): """ Dialog for ignoring elements in a depth profile. """ included_in_graph = bnd.bind("tree_elements") included_in_ratio = bnd.bind("tree_ratio") @property def ignored_from_graph(self): try: return self._get_ignored(set(self.included_in_graph)) except AttributeError: return set() @property def ignored_from_ratio(self): try: return self._get_ignored(set(self.included_in_ratio)) except AttributeError: return set() def _get_ignored(self, included): return {elem for elem in self._elements if elem not in included} def __init__(self, elements: List[Element], ignored_graph: Set[Element], ignored_ratio: Set[Element]): """Init the dialog. Args: elements: A list of elements in Depth Profile. ignored_graph: A list of elements ignored previously for the graph. ignored_ratio: A list of elements ignored previously for ratio calculation. """ super().__init__() uic.loadUi(gutils.get_ui_dir() / "ui_depth_profile_ignored.ui", self) self._elements = sorted(set(elements)) self.button_ok.clicked.connect(self.accept) self.button_cancel.clicked.connect(self.reject) # Fill the trees gutils.fill_tree(self.tree_elements.invisibleRootItem(), self._elements) gutils.fill_tree(self.tree_ratio.invisibleRootItem(), self._elements) self.included_in_graph = set(elem for elem in self._elements if elem not in ignored_graph) self.included_in_ratio = set(elem for elem in self._elements if elem not in ignored_ratio)
class OptimizationParameterWidget(QtWidgets.QWidget, PropertyBindingWidget, abc.ABC, metaclass=QtABCMeta): """Abstract base class for recoil and fluence optimization parameter widgets. """ # Common properties gen = bnd.bind("generationSpinBox") pop_size = bnd.bind("populationSpinBox") number_of_processes = bnd.bind("processesSpinBox") cross_p = bnd.bind("crossoverProbDoubleSpinBox") mut_p = bnd.bind("mutationProbDoubleSpinBox") stop_percent = bnd.bind("percentDoubleSpinBox") check_time = bnd.bind("timeSpinBox") check_max = bnd.bind("maxTimeEdit") check_min = bnd.bind("minTimeEdit") skip_simulation = bnd.bind("skip_sim_chk_box") @abc.abstractmethod def optimization_type(self) -> OptimizationType: pass def __init__(self, ui_file, **kwargs): """Initializes a optimization parameter widget. Args: ui_file: relative path to a ui_file kwargs: values to show in the widget """ super().__init__() uic.loadUi(ui_file, self) locale = QLocale.c() self.crossoverProbDoubleSpinBox.setLocale(locale) self.mutationProbDoubleSpinBox.setLocale(locale) self.percentDoubleSpinBox.setLocale(locale) self.skip_sim_chk_box.stateChanged.connect(self.enable_sim_params) self.set_properties(**kwargs) def enable_sim_params(self, *_): """Either enables or disables simulation parameters depending on the value of skip_simulation parameter. Args: *_: not used """ self.simGroupBox.setEnabled(not self.skip_simulation)
class TofeGraphSettingsWidget(QtWidgets.QDialog): """Graph settings dialog for the ToF-E histogram graph. """ color_scheme = bnd.bind("colorbox") def __init__(self, parent): """Inits ToF-E graph histogram graph settings dialog. Args: parent: MatplotlibHistogramWidget which settings are being changed. """ super().__init__() uic.loadUi(gutils.get_ui_dir() / "ui_tofe_graph_settings.ui", self) self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.parent = parent gutils.set_min_max_handlers( self.spin_range_x_min, self.spin_range_x_max ) gutils.set_min_max_handlers( self.spin_range_y_min, self.spin_range_y_max ) self.parent.show_yourself(self) # Connect and show self.OKButton.clicked.connect(self.accept_settings) self.cancelButton.clicked.connect(self.close) self.exec_() def accept_settings(self): """Accept changed settings and save them. """ self.parent.compression_x = self.bin_x.value() self.parent.compression_y = self.bin_y.value() self.parent.invert_X = \ self.invert_x.checkState() == QtCore.Qt.Checked self.parent.invert_Y = \ self.invert_y.checkState() == QtCore.Qt.Checked self.parent.show_axis_ticks = \ self.axes_ticks.checkState() == QtCore.Qt.Checked self.parent.transpose_axes = \ self.transposeAxesCheckBox.checkState() == QtCore.Qt.Checked self.parent.color_scheme = self.color_scheme if self.radio_range_auto.isChecked(): self.parent.axes_range_mode = 0 elif self.radio_range_manual.isChecked(): self.parent.axes_range_mode = 1 x_range_min = self.spin_range_x_min.value() x_range_max = self.spin_range_x_max.value() y_range_min = self.spin_range_y_min.value() y_range_max = self.spin_range_y_max.value() self.parent.axes_range = [(x_range_min, x_range_max), (y_range_min, y_range_max)] self.parent.on_draw() self.close()
class FoilWidget(QtWidgets.QWidget): """Class for creating a foil widget for detector settings. """ foil_deletion = pyqtSignal(QtWidgets.QWidget) # Distance in nanometers from previous foil or Target if this is the first # foil distance_from_previous = bnd.bind("distanceDoubleSpinBox") # Distance from Target in nanometers cumulative_distance = bnd.bind("distanceLabel") name = bnd.bind("foilButton") def __init__(self, foil): """ Initializes the foil widget. Args: foil: foil object """ super().__init__() uic.loadUi(gutils.get_ui_dir() / "ui_foil_widget.ui", self) locale = QLocale.c() self.distanceDoubleSpinBox.setLocale(locale) self.deleteButton.clicked.connect(self._delete_foil) self.name = foil.name self.distance_from_previous = 0 self.cumulative_distance = foil.distance def _delete_foil(self): """ Delete a foil. """ confirm_box = QtWidgets.QMessageBox() confirm_box.setIcon(QtWidgets.QMessageBox.Warning) yes_button = confirm_box.addButton(QtWidgets.QMessageBox.Yes) confirm_box.addButton(QtWidgets.QMessageBox.Cancel) confirm_box.setText("Are you sure you want to delete the foil?") confirm_box.setWindowTitle("Confirm") confirm_box.exec() if confirm_box.clickedButton() == yes_button: self.foil_deletion.emit(self)
class Foo(bnd.PropertyTrackingWidget): foo = bnd.bind("_foo", getattr, setattr, track_change=True) def __init__(self): self.foo = 1 def get_original_property_values(self): return {"_foo": 2}
class Foo(bnd.PropertyTrackingWidget): bar = bnd.bind("_bar", getattr, setattr, track_change=True) def __init__(self): self._bar = None self._orig = {} def get_original_property_values(self): return self._orig
class Foo(bnd.PropertyTrackingWidget): @property def foo(self): return self._foo @foo.setter def foo(self, value): self._foo = value bar = bnd.bind("_bar", getattr, setattr, track_change=True) baz = bnd.bind("_baz", getattr, setattr, track_change=False) def __init__(self): self.orig_props = {} self.set_properties(foo=1, bar=2, baz=3) def get_original_property_values(self): return self.orig_props
class OptimizationRecoilParameterWidget(OptimizationParameterWidget): """ Class that handles the recoil optimization parameters' ui. """ # Recoil specific properties upper_limits = bnd.multi_bind( ("upperXDoubleSpinBox", "upperYDoubleSpinBox")) lower_limits = bnd.multi_bind( ("lowerXDoubleSpinBox", "lowerYDoubleSpinBox")) # sol_size values are unique (5, 7, 9 or 11) so they can be used in # two-way binding sol_size = bnd.bind("recoilTypeComboBox", fget=sol_size_from_combobox, fset=sol_size_to_combobox) recoil_type = bnd.bind("recoilTypeComboBox", fget=recoil_from_combobox, twoway=False) @property def optimization_type(self) -> OptimizationType: return OptimizationType.RECOIL def __init__(self, **kwargs): """Initialize the widget. Args: kwargs: property values to be shown in the widget. """ ui_file = gutils.get_ui_dir() / "ui_optimization_recoil_params.ui" super().__init__(ui_file, **kwargs) self.radio_buttons() locale = QLocale.c() self.upperXDoubleSpinBox.setLocale(locale) self.lowerXDoubleSpinBox.setLocale(locale) self.upperYDoubleSpinBox.setLocale(locale) self.lowerYDoubleSpinBox.setLocale(locale)
class DepthProfileDialog(QtWidgets.QDialog): """ Dialog for making a depth profile. """ # TODO replace these global variables with PropertySavingWidget checked_cuts = {} x_unit = DepthProfileUnit.ATOMS_PER_SQUARE_CM line_zero = False line_scale = False systerr = 0.0 status_msg = bnd.bind("label_status") used_cuts = bnd.bind("treeWidget") cross_sections = bnd.bind("label_cross") tof_slope = bnd.bind("label_calibslope") tof_offset = bnd.bind("label_caliboffset") depth_stop = bnd.bind("label_depthstop") depth_steps = bnd.bind("label_depthnumber") depth_bin = bnd.bind("label_depthbin") depth_scale = bnd.bind("label_depthscale") used_efficiency_files = bnd.bind("label_efficiency_files") systematic_error = bnd.bind("spin_systerr") show_scale_line = bnd.bind("check_scaleline") show_zero_line = bnd.bind("check_0line") reference_density = bnd.bind("sbox_reference_density") x_axis_units = bnd.bind("group_x_axis_units") def __init__(self, parent: BaseTab, measurement: Measurement, global_settings: GlobalSettings, statusbar: Optional[QtWidgets.QStatusBar] = None): """Inits depth profile dialog. Args: parent: a MeasurementTabWidget. measurement: a Measurement object global_settings: a GlobalSettings object statusbar: a QStatusBar object """ super().__init__() uic.loadUi(gutils.get_ui_dir() / "ui_depth_profile_params.ui", self) self.parent = parent self.measurement = measurement self.statusbar = statusbar # Connect buttons self.OKButton.clicked.connect(self._accept_params) self.cancelButton.clicked.connect(self.close) locale = QLocale.c() self.spin_systerr.setLocale(locale) self.sbox_reference_density.setLocale(locale) m_name = self.measurement.name if m_name not in DepthProfileDialog.checked_cuts: DepthProfileDialog.checked_cuts[m_name] = set() gutils.fill_cuts_treewidget(self.measurement, self.treeWidget.invisibleRootItem(), use_elemloss=True) self.used_cuts = DepthProfileDialog.checked_cuts[m_name] gutils.set_btn_group_data(self.group_x_axis_units, DepthProfileUnit) self.x_axis_units = DepthProfileDialog.x_unit if self.x_axis_units == DepthProfileUnit.NM: self._show_reference_density() else: self._hide_reference_density() self.radioButtonNm.clicked.connect(self._show_reference_density) self.radioButtonAtPerCm2.clicked.connect(self._hide_reference_density) self.systematic_error = DepthProfileDialog.systerr self.show_scale_line = DepthProfileDialog.line_scale self.show_zero_line = DepthProfileDialog.line_zero self.cross_sections = global_settings.get_cross_sections() self._show_measurement_settings() self._show_efficiency_files() self.exec_() @gutils.disable_widget def _accept_params(self, *_): """Accept given parameters. Args: *_: unused event args """ self.status_msg = "" sbh = StatusBarHandler(self.statusbar) sbh.reporter.report(10) try: output_dir = self.measurement.get_depth_profile_dir() # Get the filepaths of the selected items used_cuts = self.used_cuts DepthProfileDialog.checked_cuts[self.measurement.name] = set( used_cuts) # TODO could take care of RBS selection here elements = [ Element.from_string(fp.name.split(".")[1]) for fp in used_cuts ] x_unit = self.x_axis_units DepthProfileDialog.x_unit = x_unit DepthProfileDialog.line_zero = self.show_zero_line DepthProfileDialog.line_scale = self.show_scale_line DepthProfileDialog.systerr = self.systematic_error sbh.reporter.report(20) # If items are selected, proceed to generating the depth profile if used_cuts: self.status_msg = "Please wait. Creating depth profile." if self.parent.depth_profile_widget: self.parent.del_widget(self.parent.depth_profile_widget) # If reference density changed, update value to measurement if x_unit == DepthProfileUnit.NM: _, _, _, profile, measurement = \ self.measurement.get_used_settings() if profile.reference_density != self.reference_density: profile.reference_density = self.reference_density measurement.to_file() self.parent.depth_profile_widget = DepthProfileWidget( self.parent, output_dir, used_cuts, elements, x_unit, DepthProfileDialog.line_zero, DepthProfileDialog.line_scale, DepthProfileDialog.systerr, progress=sbh.reporter.get_sub_reporter( lambda x: 30 + 0.6 * x)) sbh.reporter.report(90) icon = self.parent.icon_manager.get_icon( "depth_profile_icon_2_16.png") self.parent.add_widget(self.parent.depth_profile_widget, icon=icon) self.close() else: self.status_msg = "Please select .cut file[s] to create " \ "depth profiles." except Exception as e: error_log = f"Exception occurred when trying to create depth " \ f"profiles: {e}" logging.getLogger(self.measurement.name).error(error_log) finally: sbh.reporter.report(100) def _show_reference_density(self): """ Add a filed for modifying the reference density. """ self.label_reference_density.setVisible(True) self.sbox_reference_density.setVisible(True) def _hide_reference_density(self): """ Remove reference density form dialog if it is there. """ self.label_reference_density.setVisible(False) self.sbox_reference_density.setVisible(False) def _show_efficiency_files(self): """Update efficiency files to UI which are used. """ detector, *_ = self.measurement.get_used_settings() self.used_efficiency_files = df.get_efficiency_text( self.treeWidget, detector) def _show_measurement_settings(self): """Show some important setting values in the depth profile parameter dialog for the user. """ detector, _, _, profile, _ = self.measurement.get_used_settings() self.tof_slope = detector.tof_slope self.tof_offset = detector.tof_offset self.depth_stop = profile.depth_step_for_stopping self.depth_steps = profile.number_of_depth_steps self.depth_bin = profile.depth_step_for_output self.depth_scale = f"{profile.depth_for_concentration_from} - " \ f"{profile.depth_for_concentration_to}" self.reference_density = profile.reference_density
class SimulationSettingsWidget(QtWidgets.QWidget, PropertyTrackingWidget, PropertySavingWidget, metaclass=QtABCMeta): """Class for creating a simulation settings tab. """ # TODO name, desc should perhaps not be tracked name = bnd.bind("nameLineEdit", track_change=True) description = bnd.bind("descriptionPlainTextEdit", track_change=True) simulation_type = bnd.bind("typeOfSimulationComboBox", track_change=True) simulation_mode = bnd.bind("modeComboBox", track_change=True) number_of_ions = bnd.bind("numberOfIonsSpinBox") number_of_ions_in_presimu = bnd.bind("numberOfPreIonsSpinBox") number_of_scaling_ions = bnd.bind("numberOfScalingIonsSpinBox", track_change=True) number_of_recoils = bnd.bind("numberOfRecoilsSpinBox", track_change=True) minimum_scattering_angle = bnd.bind("minimumScatterAngleDoubleSpinBox", track_change=True) minimum_main_scattering_angle = bnd.bind( "minimumMainScatterAngleDoubleSpinBox", track_change=True) minimum_energy_of_ions = bnd.bind("minimumEnergyDoubleSpinBox", track_change=True) # Seed and modification time are not tracked for changes seed_number = bnd.bind("seedSpinBox") modification_time = bnd.bind("dateLabel", fget=bnd.unix_time_from_label, fset=bnd.unix_time_to_label) settings_updated = pyqtSignal() def __init__(self, element_simulation: ElementSimulation, preset_folder=None): """ Initializes the widget. Args: element_simulation: Element simulation object. """ super().__init__() uic.loadUi(gutils.get_ui_dir() / "ui_request_simulation_settings.ui", self) # By default, disable the widget, so caller has to enable it. Without # this, name and description fields would always be enabled when the # widget loads. self.setEnabled(False) self.element_simulation = element_simulation self.set_spinbox_maximums() gutils.fill_combobox(self.modeComboBox, SimulationMode) gutils.fill_combobox(self.typeOfSimulationComboBox, SimulationType) self.fields_are_valid = False iv.set_input_field_red(self.nameLineEdit) self.nameLineEdit.textChanged.connect( lambda: iv.check_text(self.nameLineEdit, qwidget=self)) self.nameLineEdit.textEdited.connect( lambda: iv.sanitize_file_name(self.nameLineEdit)) self.nameLineEdit.setEnabled(False) locale = QLocale.c() self.minimumScatterAngleDoubleSpinBox.setLocale(locale) self.minimumMainScatterAngleDoubleSpinBox.setLocale(locale) self.minimumEnergyDoubleSpinBox.setLocale(locale) self.__original_property_values = {} self.set_properties( name=self.element_simulation.name, description=self.element_simulation.description, modification_time=self.element_simulation.modification_time, **self.element_simulation.get_settings()) if preset_folder is not None: self.preset_widget = PresetWidget.add_preset_widget( preset_folder / "simulation", "sim", lambda w: self.layout().insertWidget(0, w), save_callback=self.save_properties_to_file, load_callback=self.load_properties_from_file) else: self.preset_widget = None def get_property_file_path(self) -> Path: raise NotImplementedError def save_on_close(self) -> bool: return False def save_properties_to_file(self, file_path: Path): def err_func(err: Exception): if self.preset_widget is not None: self.preset_widget.set_status_msg( f"Failed to save preset: {err}") self._save_json_file(file_path, self.get_properties(), True, error_func=err_func) if self.preset_widget is not None: self.preset_widget.load_files(selected=file_path) def load_properties_from_file(self, file_path: Path): # TODO create a base class for settings widgets to get rid of this # copy-paste code def err_func(err: Exception): if self.preset_widget is not None: self.preset_widget.set_status_msg( f"Failed to load preset: {err}") bnd.PropertySavingWidget.load_properties_from_file(self, file_path, error_func=err_func) def get_original_property_values(self): """Returns a dictionary of original property values. """ return self.__original_property_values def setEnabled(self, b): """Either enables or disables widgets input fields. """ super().setEnabled(b) try: # setEnabled is called when ui file is being loaded and these # attributes do not yet exist, so we have to catch the exception. self.formLayout.setEnabled(b) self.generalParametersGroupBox.setEnabled(b) self.physicalParametersGroupBox.setEnabled(b) except AttributeError: pass def set_spinbox_maximums(self, int_max=2147483647, float_max=1000000000000000013287555072.00): """Set maximum values to spinbox components. """ self.numberOfIonsSpinBox.setMaximum(int_max) self.numberOfPreIonsSpinBox.setMaximum(int_max) self.seedSpinBox.setMaximum(int_max) self.numberOfRecoilsSpinBox.setMaximum(int_max) self.numberOfScalingIonsSpinBox.setMaximum(int_max) self.minimumScatterAngleDoubleSpinBox.setMaximum(float_max) self.minimumMainScatterAngleDoubleSpinBox.setMaximum(float_max) self.minimumEnergyDoubleSpinBox.setMaximum(float_max) def update_settings(self): """ Update simulation settings. """ params = self.get_properties() self.element_simulation.name = params.pop("name") self.element_simulation.description = params.pop("description") params.pop("modification_time") if self.simulation_type != self.element_simulation.simulation_type: if self.simulation_type == SimulationType.ERD: new_type = "rec" old_type = ".sct" else: new_type = "sct" old_type = ".rec" for recoil in self.element_simulation.recoil_elements: recoil.type = new_type try: path_to_rec = Path(self.element_simulation.directory, f"{recoil.get_full_name()}{old_type}") os.remove(path_to_rec) except OSError: pass recoil.to_file(self.element_simulation.directory) self.element_simulation.set_settings(**params) self.settings_updated.emit()
class SimulationControlsWidget(QtWidgets.QWidget, GUIObserver): """Class for creating simulation controls widget for the element simulation. """ recoil_name = bnd.bind("controls_group_box") process_count = bnd.bind("processes_spinbox") finished_processes = bnd.bind("finished_processes_label", fget=_process_count_from_label, fset=_process_count_to_label) observed_atoms = bnd.bind("observed_atom_count_label") simulation_state = bnd.bind("state_label") mcerd_error = bnd.bind("mcerd_error_lbl") ion_settings_error = bnd.bind("ion_settings_label") # TODO these styles could use some brush up... PRESIM_PROGRESS_STYLE = """ QProgressBar::chunk:horizontal { background: #b8112a; } """ SIM_PROGRESS_STYLE = """ QProgressBar::chunk:horizontal { background: #0ec95c; } """ def __init__(self, element_simulation: ElementSimulation, recoil_dist_widget, recoil_name_changed=None, settings_updated=None, ion_division=IonDivision.BOTH, min_presim_ions=0, min_sim_ions=0): """ Initializes a SimulationControlsWidget. Args: element_simulation: An ElementSimulation class object. recoil_dist_widget: RecoilAtomDistributionWidget. recoil_name_changed: signal that indicates that a recoil name has changed. ion_division: ion division mode """ super().__init__() GUIObserver.__init__(self) uic.loadUi(gutils.get_ui_dir() / "ui_simulation_controls.ui", self) self.element_simulation = element_simulation self.element_simulation.subscribe(self) self.recoil_dist_widget = recoil_dist_widget self.progress_bars = {} self.recoil_name = \ self.element_simulation.get_main_recoil().get_full_name() self.show_status(self.element_simulation.get_current_status()) self.finished_processes = 0, self.process_count self.run_button.clicked.connect(self.start_simulation) self.run_button.setIcon(icons.get_reinhardt_icon("player_play.svg")) self.stop_button.clicked.connect(self.stop_simulation) self.stop_button.setIcon(icons.get_reinhardt_icon("player_stop.svg")) self.enable_buttons() self.mcerd_error_lbl.hide() self.ion_settings_label.hide() self.__unsub = None self._ion_division = ion_division self._min_presim_ions = min_presim_ions self._min_sim_ions = min_sim_ions self.show_ion_settings_label() self.processes_spinbox.valueChanged.connect( self.show_ion_settings_label) self._recoil_name_changed = recoil_name_changed if self._recoil_name_changed is not None: self._recoil_name_changed.connect(self._set_name) self._settings_updated = settings_updated if self._settings_updated is not None: self._settings_updated.connect(self.settings_update_handler) self._settings_updated[GlobalSettings].connect( self.settings_update_handler) def closeEvent(self, event): """Disconnects self from recoil_name_changed signal and closes the widget. """ # FIXME not being called. This is a problem as there may be memory # leaks. try: self._recoil_name_changed.disconnect(self._set_name) except (AttributeError, TypeError): pass try: self.settings_updated.disconnect(self.settings_update_handler) except (AttributeError, TypeError): pass super().closeEvent(event) def _set_name(self, _, recoil_elem): """Sets the name shown in group box title to the name of the given recoil element if the recoil element is the same as the main recoil. """ if recoil_elem is self.element_simulation.get_main_recoil(): self.recoil_name = recoil_elem.get_full_name() def enable_buttons(self, starting=False): """Switches the states of run and stop button depending on the state of the ElementSimulation object. """ if self.element_simulation.is_optimization_running(): start_enabled, stop_enabled = False, False else: start_enabled = not ( self.element_simulation.is_simulation_running() and starting) stop_enabled = not ( start_enabled or self.element_simulation.is_optimization_running()) self.run_button.setEnabled(start_enabled) self.stop_button.setEnabled(stop_enabled) self.processes_spinbox.setEnabled(start_enabled) def start_simulation(self): """Calls ElementSimulation's start method. """ # Ask the user if they want to write old simulation results over (if # they exist), or continue status = self.element_simulation.get_current_status() if status[ElementSimulation.STATE] == SimulationState.DONE: reply = QtWidgets.QMessageBox.question( self, "Confirmation", "Do you want to continue this simulation?\n\n" "If you do, old simulation results will be preserved.\n" "Otherwise they will be deleted.", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) if reply == QtWidgets.QMessageBox.Cancel: return # If clicked Cancel don't start simulation elif reply == QtWidgets.QMessageBox.No: use_old_erd_files = False else: use_old_erd_files = True elif status[ElementSimulation.STATE] == SimulationState.NOTRUN: use_old_erd_files = False else: self.mcerd_error = "Simulation currently running. Cannot start a " \ "new one." self.mcerd_error_lbl.show() return self.mcerd_error_lbl.hide() # Lock full edit self.element_simulation.lock_edit() if self.recoil_dist_widget.current_element_simulation is \ self.element_simulation: self.recoil_dist_widget.full_edit_on = False self.recoil_dist_widget.update_plot() self.finished_processes = 0, self.process_count self.remove_progress_bars() observable = self.element_simulation.start( self.process_count, use_old_erd_files=use_old_erd_files, ion_division=self._ion_division) if observable is not None: self.__unsub = observable.pipe( ops.scan(lambda acc, x: { **x, "started": x[MCERD.IS_RUNNING] and not acc["started"] }, seed={"started": False})).subscribe(self) else: self.mcerd_error = "Could not start simulation. Check that there " \ "is no other simulation running for this " \ "recoil element" self.mcerd_error_lbl.show() def show_status(self, status): """Updates the status of simulation in the GUI Args: status: status of the ElementSimulation object """ self.observed_atoms = status[ElementSimulation.ATOMS] self.simulation_state = status[ElementSimulation.STATE] def settings_update_handler(self, settings: Optional[GlobalSettings] = None): """Updates SimulationControlsWidget when a setting has been updated. """ if settings is not None: self._ion_division = settings.get_ion_division() self._min_presim_ions = settings.get_min_presim_ions() self._min_sim_ions = settings.get_min_simulation_ions() self.show_ion_settings_label() def show_ion_settings_label(self): """Shows a warning label if ion counts are below the user defined treshold. """ settings, _, _ = self.element_simulation.get_mcerd_params() presim_ions, sim_ions = self._ion_division.get_ion_counts( settings["number_of_ions_in_presimu"], settings["number_of_ions"], self.process_count) txt = "" if self._min_presim_ions > presim_ions: txt += "Not enough pre-simulation ions. " if self._min_sim_ions > sim_ions: txt += "Not enough simulation ions." if txt: self.ion_settings_error = txt self.ion_settings_label.show() else: self.ion_settings_label.hide() def stop_simulation(self): """ Calls ElementSimulation's stop method. """ self.element_simulation.stop() def on_next_handler(self, status): """Callback function that receives status from an ElementSimulation Args: status: status update sent by ElementSimulation or observable stream """ if status[MCERD.MSG] == MCERD.PRESIM_FINISHED: style = SimulationControlsWidget.SIM_PROGRESS_STYLE else: style = None self.update_progress_bar(status[MCERD.SEED], status[MCERD.PERCENTAGE], stylesheet=style) self.finished_processes = (status[ElementSimulation.FINISHED], status[ElementSimulation.TOTAL]) if status["started"]: self.enable_buttons(starting=True) self.show_status(status) def on_error_handler(self, err): """Called when observable (either ElementSimulation or the rx stream reports an error. """ self.mcerd_error = err self.mcerd_error_lbl.show() self.enable_buttons() self.show_status(self.element_simulation.get_current_status()) if self.__unsub is not None: self.__unsub.dispose() @QtCore.pyqtSlot() @QtCore.pyqtSlot(object) def on_completed_handler(self, status=None): """This method is called when the ElementSimulation has run all of its simulation processes. GUI is updated to show the status and button states are switched accordingly. """ if status is None: self.show_status(self.element_simulation.get_current_status()) if self.__unsub is not None: self.__unsub.dispose() else: self.show_status(status) for process_bar in self.progress_bars.values(): # TODO this only affects the number, adjust the color too process_bar.setEnabled(False) self.enable_buttons() def remove_progress_bars(self): """Removes all progress bars and seed labels. """ self.progress_bars = {} for i in reversed(range(self.process_layout.count())): self.process_layout.itemAt(i).widget().deleteLater() def update_progress_bar(self, seed: int, value: int, stylesheet=None): """Updates or adds a progress bar for a simulation process that uses the given seed. Args: seed: seed of the simulation process value: value to be shown in the progress bar. stylesheet: stylesheet given to to the progress bar. """ if seed not in self.progress_bars: if stylesheet is None: stylesheet = SimulationControlsWidget.PRESIM_PROGRESS_STYLE progress_bar = QtWidgets.QProgressBar() progress_bar.setStyleSheet(stylesheet) # Align the percentage display to the right side of the # progress bar. progress_bar.setAlignment(Qt.AlignRight | Qt.AlignVCenter) # Make sure it fills the horizontal space by setting size policy # to expanding. progress_bar.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) self.progress_bars[seed] = progress_bar self.process_layout.addRow(QtWidgets.QLabel(str(seed)), progress_bar) else: progress_bar = self.progress_bars[seed] if stylesheet is not None: progress_bar.setStyleSheet(stylesheet) progress_bar.setValue(value)
class SimulationSettingsDialog(QtWidgets.QDialog): """ Dialog class for handling the simulation parameter input. """ use_request_settings = bnd.bind("defaultSettingsCheckBox") def __init__(self, tab, simulation: Simulation, icon_manager): """ Initializes the dialog. Args: tab: A SimulationTabWidget. simulation: A Simulation object whose parameters are handled. icon_manager: An icon manager. """ super().__init__() uic.loadUi(gutils.get_ui_dir() / "ui_specific_settings.ui", self) self.tab = tab self.simulation = simulation self.icon_manager = icon_manager self.setWindowTitle("Simulation Settings") self.setAttribute(QtCore.Qt.WA_DeleteOnClose) screen_geometry = QtWidgets.QDesktopWidget.availableGeometry( QtWidgets.QApplication.desktop()) self.resize(int(self.geometry().width() * 1.2), int(screen_geometry.size().height() * 0.8)) self.defaultSettingsCheckBox.stateChanged.connect( self._change_used_settings) self.OKButton.clicked.connect(self._save_settings_and_close) self.applyButton.clicked.connect(self._update_parameters) self.cancelButton.clicked.connect(self.close) preset_folder = gutils.get_preset_dir( self.simulation.request.global_settings) # Add measurement settings view to the settings view self.measurement_settings_widget = MeasurementSettingsWidget( self.simulation, preset_folder=preset_folder) self.tabs.addTab(self.measurement_settings_widget, "Measurement") self.measurement_settings_widget.beam_selection_ok.connect( lambda b: self.OKButton.setEnabled(b)) # Add detector settings view to the settings view detector_object = self.simulation.detector if detector_object is None: detector_object = self.simulation.request.default_detector self.detector_settings_widget = DetectorSettingsWidget( detector_object, self.simulation.request, self.icon_manager) # 2 is calibration tab that is not needed calib_tab_widget = self.detector_settings_widget.tabs.widget(2) self.detector_settings_widget.tabs.removeTab(2) calib_tab_widget.deleteLater() self.tabs.addTab(self.detector_settings_widget, "Detector") self.use_request_settings = self.simulation.use_request_settings # TODO # self.measurement_settings_widget.nameLineEdit.setText( # self.simulation.measurement_setting_file_name) # self.measurement_settings_widget.descriptionPlainTextEdit \ # .setPlainText( # self.simulation.measurement_setting_file_description) # self.measurement_settings_widget.dateLabel.setText(time.strftime( # "%c %z %Z", time.localtime(self.simulation.modification_time))) self.tabs.currentChanged.connect(lambda: df.check_for_red(self)) self.exec() def _change_used_settings(self): """Set specific settings enabled or disabled based on the "Use request settings" checkbox. """ check_box = self.sender() if check_box.isChecked(): self.tabs.setEnabled(False) else: self.tabs.setEnabled(True) def _remove_extra_files(self): gf.remove_matching_files(self.simulation.directory, exts={".measurement", ".target"}) gf.remove_matching_files(self.simulation.directory / "Detector", exts={".detector"}) def use_request_settings_toggled(self) -> bool: """Check if "use request settings" has been toggled.""" return self.use_request_settings != self.simulation.use_request_settings def values_changed(self) -> bool: """Check if measurement or detector settings have changed.""" if self.measurement_settings_widget.are_values_changed(): return True if self.detector_settings_widget.values_changed(): return True return False def _update_parameters(self): """ Update Simulation's Run, Detector and Target objects. If simulation specific parameters are in use, save them into a file. """ if self.measurement_settings_widget.isotopeComboBox.currentIndex() \ == -1: QtWidgets.QMessageBox.critical( self, "Warning", "No isotope selected.\n\n" "Please select an isotope for the beam element.", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok) return False if not self.simulation.measurement_setting_file_name: self.simulation.measurement_setting_file_name = \ self.simulation.name if not self.tabs.currentWidget().fields_are_valid: QtWidgets.QMessageBox.critical( self, "Warning", "Some of the setting values have not been set.\n" "Please input values in fields indicated in red.", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok) return False if self.use_request_settings_toggled() \ or (not self.use_request_settings and self.values_changed()): # User has switched from simulation settings to request settings, # or vice versa. Confirm if the user wants to delete old simulations # OR # If values that require rerunning simulations, prompt user # delete previous or currently running simulation results (if those # exists) if not df.delete_element_simulations( self, self.simulation, tab=self.tab, msg="simulation settings"): return False # Copy request settings without checking their validity. They # have been checked once in request settings anyway. if self.use_request_settings: self.simulation.use_request_settings = True # Remove simulation-specific efficiency files if self.simulation.detector is not \ self.simulation.request.default_detector: self.simulation.detector.remove_efficiency_files() self.simulation.clone_request_settings() self._remove_extra_files() self.simulation.to_file() return True try: # Update simulation settings self.simulation.use_request_settings = False # Set Detector object to settings widget self.detector_settings_widget.obj = self.simulation.detector # Update settings self.measurement_settings_widget.update_settings() self.detector_settings_widget.update_settings() self._remove_extra_files() self.simulation.to_file() return True except TypeError: QtWidgets.QMessageBox.question( self, "Warning", "Some of the setting values have not been set.\n" "Please input setting values to save them.", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok) return False def _save_settings_and_close(self): """Saves settings and closes the dialog if __update_parameters returns True. """ if self._update_parameters(): self.tab.check_default_settings_clicked() self.close()
class PresetWidget(QWidget, bnd.PropertyBindingWidget, metaclass=gutils.QtABCMeta): PRESET_SUFFIX = ".preset" MAX_COUNT = 10 NONE_TEXT = "<None>" preset = bnd.bind("preset_combobox") status_msg = bnd.bind("status_label") save_file = pyqtSignal(Path) load_file = pyqtSignal(Path) def __init__(self, folder: Path, prefix: str, enable_load_btn=False): """Initializes a new PresetWidget. Args: folder: folder where preset files are stored prefix: default prefix for preset files enable_load_btn: whether 'Load preset' file is shown or not. If not, load_file signal is fired when the combobox index changes. """ QWidget.__init__(self) uic.loadUi(gutils.get_ui_dir() / "ui_preset_widget.ui", self) self._folder = folder self._prefix = prefix self.save_btn: QPushButton self.load_btn: QPushButton self.preset_combobox: QComboBox self.save_btn.clicked.connect(self._emit_save_file) self._load_btn_enabled = enable_load_btn self.load_btn.setVisible(self._load_btn_enabled) self.load_btn.setEnabled(self._load_btn_enabled) if self._load_btn_enabled: self.load_btn.clicked.connect( lambda: self._emit_file_to_load(self.preset)) self.status_label.setVisible(False) self.preset_combobox: QComboBox self.preset_combobox.setContextMenuPolicy(Qt.ActionsContextMenu) self.preset_combobox.currentIndexChanged.connect(self._index_changed) self._action_rename = QAction(self.preset_combobox) self._action_rename.setText("Rename") self._action_rename.triggered.connect(self._rename_file) self._action_remove = QAction(self.preset_combobox) self._action_remove.setText("Remove") self._action_remove.triggered.connect(self._remove_file) self.preset_combobox.addAction(self._action_rename) self.preset_combobox.addAction(self._action_remove) self.preset_combobox.installEventFilter(self) self.load_files() self._activate_actions(self.preset) def _index_changed(self): """Event handler for index changes in the combobox. Actions are disabled if the selected item is 'None'. If 'Load preset' button is not enabled, this will also emit a load_file signal. '""" self.set_status_msg("") preset = self.preset self._activate_actions(preset) if not self._load_btn_enabled: self._emit_file_to_load(preset) def _activate_actions(self, preset: Optional[Path]): """Activates or deactives actions depending on whether the given preset is None or not. """ self._action_remove.setEnabled(preset is not None) self._action_rename.setEnabled(preset is not None) def _emit_file_to_load(self, preset: Optional[Path]): """Emits a load file signal if the given preset is not 'None'. """ if preset is not None: self.load_file.emit(preset) def _emit_save_file(self): """Emits save_file signal if new file name can be generated. """ next_file = self.get_next_available_file( starting_index=self.preset_combobox.count() - 1) if next_file is None: self.set_status_msg("Could not generate a name for preset.") else: self.save_file.emit(next_file) def load_files(self, max_count=MAX_COUNT, selected: Optional[Path] = None): """Loads preset files to combobox. Args: max_count: maximum number of files to load selected: path to a file that will be selected after loading """ def text_func(preset_path: Optional[Path]): if preset_path is None: return PresetWidget.NONE_TEXT return preset_path.stem preset_files = PresetWidget.get_preset_files(self._folder, max_count, keep=selected) if not self._load_btn_enabled: # If load button is not used, add 'None' as the first element in # the combobox preset_files = [None, *preset_files] gutils.fill_combobox(self.preset_combobox, preset_files, text_func=text_func, block_signals=True) self.preset = selected def _rename_file(self): """Activates edit if preset is selected. """ self.preset_combobox: QComboBox self.preset_combobox.setEditable(self.preset is not None) def eventFilter(self, source: QObject, event: QEvent) -> bool: """Filters combobox events. If combobox is currently editable, capture FocusOut event and try to rename selected file. """ self.preset_combobox: QComboBox if source is self.preset_combobox and isinstance(event, QFocusEvent): if self.preset_combobox.isEditable(): cur_txt = self.preset_combobox.currentText() self.preset_combobox.setEditable(False) try: cur_preset = self.preset new_file = Path(self._folder, f"{cur_txt}{PresetWidget.PRESET_SUFFIX}") if new_file != cur_preset and self.is_valid_preset( self._folder, new_file): cur_preset.rename(new_file) self.load_files(selected=new_file) except OSError as e: self.set_status_msg(f"Failed to rename preset: {e}") return super().eventFilter(source, event) def _remove_file(self): """Removes file with a confirmation. """ file = self.preset if file is None: return reply = QMessageBox.question(self, "Delete preset", "Delete selected preset?", QMessageBox.Yes | QMessageBox.Cancel, QMessageBox.Yes) if reply == QMessageBox.Yes: try: file.unlink() except OSError as e: self.set_status_msg(f"Failed to remove preset: {e}") self.load_files() self._activate_actions(self.preset) def set_status_msg(self, msg: Any): """Sets the status message and shows or hides the label. """ self.status_msg = msg self.status_label.setVisible(bool(msg)) @classmethod def add_preset_widget(cls, folder: Path, prefix, add_func: Callable, save_callback: Optional[Callable] = None, load_callback: Optional[Callable] = None) -> \ "PresetWidget": """Creates a PresetWidget, adds it to a parent widget by calling the add_func and connects save and load callbacks if they are provided. Returns the created widget. """ widget = cls(folder, prefix) add_func(widget) if save_callback is not None: widget.save_file.connect(save_callback) if load_callback is not None: widget.load_file.connect(load_callback) return widget def get_next_available_file(self, starting_index=0, max_iterations=100) \ -> Optional[Path]: """Returns the next available file name or None, if no available file name was found within maximum number of iterations. """ def file_name_generator(): for i in range(starting_index, max_iterations): fname = f"{self._prefix}-{i + 1:03}{PresetWidget.PRESET_SUFFIX}" yield self._folder / fname try: return fp.find_available_file_path(file_name_generator()) except ValueError: return None @staticmethod def get_preset_files(folder: Path, max_count: int = MAX_COUNT, keep: Optional[Path] = None) -> List[Path]: """Returns a sorted list of .preset files from the given folder. Args: folder: folder path max_count: maximum number of files to return keep: file that is guaranteed to be in the list as long as it is valid .preset file """ if PresetWidget.is_valid_preset(folder, keep) and keep.is_file(): files = [keep] else: files = [] try: with os.scandir(folder) as scdir: for entry in scdir: if len(files) >= max_count: break path = Path(entry.path) if PresetWidget.is_valid_preset(folder, path) and \ path != keep and path.is_file(): files.append(path) except OSError: pass return sorted(files) @staticmethod def is_valid_preset(folder: Path, file: Optional[Path]) -> bool: """Checks if the given file is a valid .preset file. """ # TODO might want to add checks for characters that are forbidden # on one platform but allowed on another to ensure the portability # of presets. if file is None: return False if not file.stem: return False if file.suffix != PresetWidget.PRESET_SUFFIX: return False return file.resolve().parent == folder.resolve()
class ElementLossesDialog(QtWidgets.QDialog): """Class to handle element losses dialogs. """ checked_cuts = {} reference_cut = {} split_count = 10 y_scale = 1 status_msg = bnd.bind("label_status") used_reference_cut = bnd.bind("referenceCut") used_cuts = bnd.bind("targetCutTree") def __init__(self, parent, measurement: Measurement, statusbar: Optional[QtWidgets.QStatusBar] = None): """Inits element losses class. Args: parent: A MeasurementTabWidget. """ super().__init__() uic.loadUi(gutils.get_ui_dir() / "ui_element_losses_params.ui", self) self.parent = parent self.measurement = measurement self.statusbar = statusbar self.cuts = [] self.OKButton.clicked.connect(self.__accept_params) self.cancelButton.clicked.connect(self.close) # self.referenceCut.currentIndexChanged.connect(self.__load_targets) # TODO: Reads cut files twice. Requires Refactor. # Reference cuts m_name = self.measurement.name if m_name not in ElementLossesDialog.reference_cut: ElementLossesDialog.reference_cut[m_name] = None cuts, _ = self.measurement.get_cut_files() gutils.fill_combobox( self.referenceCut, cuts, text_func=lambda fp: fp.name) self.used_reference_cut = ElementLossesDialog.reference_cut[m_name] # Cuts and element losses if m_name not in ElementLossesDialog.checked_cuts: ElementLossesDialog.checked_cuts[m_name] = set() gutils.fill_cuts_treewidget( self.measurement, self.targetCutTree.invisibleRootItem(), use_elemloss=True) self.used_cuts = ElementLossesDialog.checked_cuts[m_name] self.partitionCount.setValue(ElementLossesDialog.split_count) self.radioButton_0max.setChecked(ElementLossesDialog.y_scale == 0) self.radioButton_minmax.setChecked(ElementLossesDialog.y_scale == 1) self.exec_() @gutils.disable_widget def __accept_params(self, *_): """Called when OK button is pressed. Creates a elementlosses widget and adds it to the parent (mdiArea). Args: *_: unused event args """ self.status_msg = "" sbh = StatusBarHandler(self.statusbar) y_axis_0_scale = self.radioButton_0max.isChecked() reference_cut = self.used_reference_cut split_count = self.partitionCount.value() m_name = self.measurement.name used_cuts = self.used_cuts ElementLossesDialog.checked_cuts[m_name] = set(used_cuts) if y_axis_0_scale: y_scale = 0 else: y_scale = 1 ElementLossesDialog.reference_cut[m_name] = \ self.referenceCut.currentText() ElementLossesDialog.split_count = split_count ElementLossesDialog.y_scale = y_scale sbh.reporter.report(25) if used_cuts: if self.parent.elemental_losses_widget: self.parent.del_widget(self.parent.elemental_losses_widget) self.parent.elemental_losses_widget = ElementLossesWidget( self.parent, self.measurement, reference_cut, used_cuts, split_count, y_scale, statusbar=self.statusbar, progress=sbh.reporter.get_sub_reporter( lambda x: 25 + 0.70 * x )) icon = self.parent.icon_manager \ .get_icon("elemental_losses_icon_16.png") self.parent.add_widget(self.parent.elemental_losses_widget, icon=icon) msg = f"Created Element Losses. Splits: {split_count} " \ f"Reference cut: {reference_cut} " \ f"List of cuts: {used_cuts}" self.measurement.log(msg) log_info = "Elemental Losses split counts:\n" split_counts = self.parent.elemental_losses_widget.split_counts splitinfo = "\n".join( ["{0}: {1}".format( key, ", ".join(str(v) for v in split_counts[key])) for key in split_counts]) self.measurement.log(log_info + splitinfo) sbh.reporter.report(100) self.close() else: self.status_msg = "Please select .cut file[s] to create element " \ "losses." sbh.reporter.report(100)
class OptimizationParameterWidget(QtWidgets.QWidget, PropertyBindingWidget, abc.ABC, metaclass=QtABCMeta): """Abstract base class for recoil and fluence optimization parameter widgets. """ # Common properties gen = bnd.bind("generationSpinBox") pop_size = bnd.bind("populationSpinBox") number_of_processes = bnd.bind("processesSpinBox") cross_p = bnd.bind("crossoverProbDoubleSpinBox") mut_p = bnd.bind("mutationProbDoubleSpinBox") stop_percent = bnd.bind("percentDoubleSpinBox") check_time = bnd.bind("timeSpinBox") check_max = bnd.bind("maxTimeEdit") check_min = bnd.bind("minTimeEdit") skip_simulation = bnd.bind("skip_sim_chk_box") @abc.abstractmethod def optimization_type(self) -> OptimizationType: pass def __init__(self, ui_file, **kwargs): """Initializes a optimization parameter widget. Args: ui_file: relative path to a ui_file kwargs: values to show in the widget """ super().__init__() uic.loadUi(ui_file, self) locale = QLocale.c() self.crossoverProbDoubleSpinBox.setLocale(locale) self.mutationProbDoubleSpinBox.setLocale(locale) self.percentDoubleSpinBox.setLocale(locale) self.skip_sim_chk_box.stateChanged.connect(self.enable_sim_params) self.set_properties(**kwargs) def enable_sim_params(self, *_): """Either enables or disables simulation parameters depending on the value of skip_simulation parameter. Args: *_: not used """ self.simGroupBox.setEnabled(not self.skip_simulation) def radio_buttons(self): """Radio buttons for optimization parameters Sum and Area """ self.optimize_by_area = True self.radios = QtWidgets.QButtonGroup(self) self.radios.buttonToggled[QtWidgets.QAbstractButton, bool].connect(self.isChecked) self.radios.addButton(self.areaRadioButton) self.radios.addButton(self.sumRadioButton) def isChecked(self, button, checked): """Choose whether to optimize by Sum or Area """ if checked and button.text() == 'Sum': self.optimize_by_area = False else: self.optimize_by_area = True
def __init__(self, tab, measurement: Measurement, icon_manager): """ Initializes the dialog. Args: measurement: A Measurement object whose parameters are handled. icon_manager: An icon manager. """ super().__init__() uic.loadUi(gutils.get_ui_dir() / "ui_specific_settings.ui", self) self.warning_text = bnd.bind('warning_text') self.tab = tab self.measurement = measurement self.icon_manager = icon_manager self.setWindowTitle("Measurement Settings") self.setAttribute(QtCore.Qt.WA_DeleteOnClose) screen_geometry = QtWidgets.QDesktopWidget.availableGeometry( QtWidgets.QApplication.desktop()) self.resize(int(self.geometry().width() * 1.2), int(screen_geometry.size().height() * 0.8)) self.defaultSettingsCheckBox.stateChanged.connect( self._change_used_settings) self.OKButton.clicked.connect(self._save_settings_and_close) self.applyButton.clicked.connect(self._update_parameters) self.cancelButton.clicked.connect(self.close) preset_folder = gutils.get_preset_dir( self.measurement.request.global_settings) # Add measurement settings view to the settings view self.measurement_settings_widget = MeasurementSettingsWidget( self.measurement, preset_folder=preset_folder) self.tabs.addTab(self.measurement_settings_widget, "Measurement") self.measurement_settings_widget.beam_selection_ok.connect( self.OKButton.setEnabled) # Add detector settings view to the settings view self.detector_settings_widget = DetectorSettingsWidget( self.measurement.detector, self.measurement.request, self.icon_manager, self.measurement_settings_widget.tmp_run) self.tabs.addTab(self.detector_settings_widget, "Detector") self.use_request_settings = self.measurement.use_request_settings # TODO these should be set in the widget, not here self.measurement_settings_widget.nameLineEdit.setText( self.measurement.measurement_setting_file_name) self.measurement_settings_widget.descriptionPlainTextEdit.setPlainText( self.measurement.measurement_setting_file_description) self.measurement_settings_widget.dateLabel.setText( time.strftime("%c %z %Z", time.localtime(self.measurement.modification_time))) # Add profile settings view to the settings view self.profile_settings_widget = ProfileSettingsWidget( self.measurement, preset_folder=preset_folder) self.tabs.addTab(self.profile_settings_widget, "Profile") self.tabs.currentChanged.connect(lambda: df.check_for_red(self)) self.exec()
class DepthProfileDialog(QtWidgets.QDialog): """ Dialog for making a depth profile. """ # TODO replace these global variables with PropertySavingWidget checked_cuts = {} x_unit = DepthProfileUnit.ATOMS_PER_SQUARE_CM line_zero = False line_scale = False used_eff = False systerr = 0.0 status_msg = bnd.bind("label_status") used_cuts = bnd.bind("treeWidget") cross_sections = bnd.bind("label_cross") tof_slope = bnd.bind("label_calibslope") tof_offset = bnd.bind("label_caliboffset") depth_stop = bnd.bind("label_depthstop") depth_steps = bnd.bind("label_depthnumber") depth_bin = bnd.bind("label_depthbin") depth_scale = bnd.bind("label_depthscale") used_efficiency_files = bnd.bind("label_efficiency_files") warning = bnd.bind("label_warning_text") systematic_error = bnd.bind("spin_systerr") show_scale_line = bnd.bind("check_scaleline") show_used_eff = bnd.bind("show_eff") show_zero_line = bnd.bind("check_0line") reference_density = bnd.bind("sbox_reference_density") x_axis_units = bnd.bind("group_x_axis_units") def __init__(self, parent: BaseTab, measurement: Measurement, global_settings: GlobalSettings, statusbar: Optional[QtWidgets.QStatusBar] = None): """Inits depth profile dialog. Args: parent: a MeasurementTabWidget. measurement: a Measurement object global_settings: a GlobalSettings object statusbar: a QStatusBar object """ super().__init__() uic.loadUi(gutils.get_ui_dir() / "ui_depth_profile_params.ui", self) # Basic stuff self.parent = parent self.measurement = measurement self.statusbar = statusbar # Connect buttons self.OKButton.clicked.connect(self._accept_params) self.cancelButton.clicked.connect(self.close) locale = QLocale.c() self.spin_systerr.setLocale(locale) self.sbox_reference_density.setLocale(locale) m_name = self.measurement.name if m_name not in DepthProfileDialog.checked_cuts: DepthProfileDialog.checked_cuts[m_name] = set() gutils.fill_cuts_treewidget(self.measurement, self.treeWidget.invisibleRootItem(), use_elemloss=True) self.used_cuts = DepthProfileDialog.checked_cuts[m_name] self._update_label() self.treeWidget.itemClicked.connect(self._update_label) gutils.set_btn_group_data(self.group_x_axis_units, DepthProfileUnit) self.x_axis_units = DepthProfileDialog.x_unit if self.x_axis_units == DepthProfileUnit.NM: self._show_reference_density() else: self._hide_reference_density() self.radioButtonNm.clicked.connect(self._show_reference_density) self.radioButtonAtPerCm2.clicked.connect(self._hide_reference_density) self.systematic_error = DepthProfileDialog.systerr # Checkboxes self.systematic_error = DepthProfileDialog.systerr self.show_scale_line = DepthProfileDialog.line_scale self.show_zero_line = DepthProfileDialog.line_zero self.show_used_eff = DepthProfileDialog.used_eff self.cross_sections = global_settings.get_cross_sections() self._show_measurement_settings() self._show_efficiency_files() # Does not work correctly if self is replaced with DepthProfileDialog self.eff_files_str = self.used_efficiency_files self.exec_() def _update_label(self): if len(self.used_cuts) <= 1: self.label_warning_text.setText('') return else: cuts = [] for cut in self.used_cuts: cuts.append(cut.suffixes[0]) indices = [] for c in cuts: indices.append( [i for i, sublist in enumerate(cuts) if sublist == c]) indices = [ list(sublist) for sublist in set( tuple(sublist) for sublist in indices) ] indices_length = [len(x) for x in indices] if any(length > 1 for length in indices_length): files = [] for index in indices: if len(index) < 2: continue files.append( (self.used_cuts[index[0]].suffixes[0].replace('.', ''))) warning_message = "Multiple .cut-files selected for " \ "following element(s): {} \nCheck the " \ "elemental losses if you are not sure " \ "what you are doing. " if len(files) > 1: # If there are multiple elements files = ' and '.join(files) warning_message = warning_message.format(files) else: # If theres is only one element warning_message = warning_message.format(files[0]) self.label_warning_text.setText(warning_message) self.label_warning_text.setStyleSheet("color: red") return self.label_warning_text.setText('') return @gutils.disable_widget def _accept_params(self, *_): """Accept given parameters. Args: *_: unused event args """ self.status_msg = "" sbh = StatusBarHandler(self.statusbar) sbh.reporter.report(10) try: output_dir = self.measurement.get_depth_profile_dir() # Get the filepaths of the selected items used_cuts = self.used_cuts DepthProfileDialog.checked_cuts[self.measurement.name] = set( used_cuts) # TODO could take care of RBS selection here elements = [ Element.from_string(fp.name.split(".")[1]) for fp in used_cuts ] x_unit = self.x_axis_units DepthProfileDialog.x_unit = x_unit DepthProfileDialog.line_zero = self.show_zero_line DepthProfileDialog.line_scale = self.show_scale_line DepthProfileDialog.systerr = self.systematic_error DepthProfileDialog.used_eff = self.show_used_eff DepthProfileDialog.eff_files_str = self.eff_files_str sbh.reporter.report(20) # If items are selected, proceed to generating the depth profile if used_cuts: self.status_msg = "Please wait. Creating depth profile." if self.parent.depth_profile_widget: self.parent.del_widget(self.parent.depth_profile_widget) # If reference density changed, update value to measurement if x_unit == DepthProfileUnit.NM: _, _, _, profile, measurement = \ self.measurement.get_used_settings() if profile.reference_density != self.reference_density: profile.reference_density = self.reference_density measurement.to_file() self.parent.depth_profile_widget = DepthProfileWidget( self.parent, output_dir, used_cuts, elements, x_unit, DepthProfileDialog.line_zero, DepthProfileDialog.used_eff, DepthProfileDialog.line_scale, DepthProfileDialog.systerr, DepthProfileDialog.eff_files_str, progress=sbh.reporter.get_sub_reporter( lambda x: 30 + 0.6 * x)) sbh.reporter.report(90) icon = self.parent.icon_manager.get_icon( "depth_profile_icon_2_16.png") self.parent.add_widget(self.parent.depth_profile_widget, icon=icon) self.close() else: self.status_msg = "Please select .cut file[s] to create " \ "depth profiles." except Exception as e: error_log = f"Exception occurred when trying to create depth " \ f"profiles: {e}" self.measurement.log_error(error_log) finally: sbh.reporter.report(100) def _show_reference_density(self): """ Add a filed for modifying the reference density. """ self.label_reference_density.setVisible(True) self.sbox_reference_density.setVisible(True) def _hide_reference_density(self): """ Remove reference density form dialog if it is there. """ self.label_reference_density.setVisible(False) self.sbox_reference_density.setVisible(False) def _show_efficiency_files(self): """Update efficiency files to UI which are used. """ detector, *_ = self.measurement.get_used_settings() self.used_efficiency_files = df.get_efficiency_text( self.treeWidget, detector) def _show_measurement_settings(self): """Show some important setting values in the depth profile parameter dialog for the user. """ detector, _, _, profile, _ = self.measurement.get_used_settings() self.tof_slope = detector.tof_slope self.tof_offset = detector.tof_offset self.depth_stop = profile.depth_step_for_stopping self.depth_steps = profile.number_of_depth_steps self.depth_bin = profile.depth_step_for_output self.depth_scale = f"{profile.depth_for_concentration_from} - " \ f"{profile.depth_for_concentration_to}" self.reference_density = profile.reference_density
class CalibrationDialog(QtWidgets.QDialog): """A dialog for the time of flight calibration """ bin_width = bnd.bind("binWidthSpinBox") selected_cut_file = bnd.bind("cutFilesTreeWidget", fget=bnd.get_selected_tree_item, fset=bnd.set_selected_tree_item) POINTS_OBJECT_FILENAME = 'points.pkl' def __init__(self, measurements: List[Measurement], detector: Detector, run: Run, parent_settings_widget=None): """Inits the calibration dialog class. Args: measurements: A string list representing measurements files. detector: A Detector class object. run: Run object. parent_settings_widget: A widget this dialog was opened from. """ super().__init__() uic.loadUi(gutils.get_ui_dir() / "ui_calibration_dialog.ui", self) self.measurements = measurements self.run = run self.detector = detector self.parent_settings_widget = parent_settings_widget self.tof_calibration = TOFCalibration() # Go through all the measurements and their cut files and list them. for measurement in self.measurements: item = QtWidgets.QTreeWidgetItem([measurement.name]) cuts, _ = measurement.get_cut_files() gutils.fill_tree(item, cuts, data_func=lambda fp: CutFile(cut_file_path=fp), text_func=lambda fp: fp.name) self.cutFilesTreeWidget.addTopLevelItem(item) item.setExpanded(True) # Resize columns to fit the content nicely for column in range(0, self.cutFilesTreeWidget.columnCount()): self.cutFilesTreeWidget.resizeColumnToContents(column) self.curveFittingWidget = CalibrationCurveFittingWidget( self, self.selected_cut_file, self.tof_calibration, self.detector, self.bin_width, 0, self.run) old_params = None # Get old parameters from the parent dialog if parent_settings_widget is not None: try: f1 = self.parent_settings_widget.tof_offset f2 = self.parent_settings_widget.tof_slope old_params = f1, f2 except ValueError as e: print(f"Can't get old calibration parameters from the " f"settings dialog: {e}.") self.linearFittingWidget = CalibrationLinearFittingWidget( self, self.tof_calibration, old_params) self.fittingResultsLayout.addWidget(self.curveFittingWidget) self.calibrationResultsLayout.addWidget(self.linearFittingWidget) # Set up connections self.cutFilesTreeWidget.itemSelectionChanged.connect( self.__update_curve_fit) self.pointsTreeWidget.itemClicked.connect(self.__set_state_for_point) self.acceptPointButton.clicked.connect(self.__accept_point) self.binWidthSpinBox.valueChanged.connect(self.__update_curve_fit) self.binWidthSpinBox.setKeyboardTracking(False) self.acceptCalibrationButton.clicked.connect(self.accept_calibration) self.cancelButton.clicked.connect(self.close) self.removePointButton.clicked.connect(self.remove_selected_points) self.tofChannelLineEdit.editingFinished.connect( lambda: self.set_calibration_point( float(self.tofChannelLineEdit.text()))) # Set the validator for lineEdit so user can't give invalid values double_validator = QtGui.QDoubleValidator() self.tofChannelLineEdit.setValidator(double_validator) self.timer = QtCore.QTimer() self.timer.timeout.connect(self.label_remove_timeout) try: self.__load_object(self.POINTS_OBJECT_FILENAME) for p in self.tof_calibration.tof_points: self.__accept_points(p) except OSError: pass self.exec_() def showEvent(self, _): """Called after dialog is shown. Size is adjusted so that all elements fit nicely on screen. """ self.adjustSize() def remove_selected_points(self): """Remove selected items from point tree widget """ removed_something = False root = self.pointsTreeWidget.invisibleRootItem() for item in self.pointsTreeWidget.selectedItems(): if item and hasattr(item, "point"): removed_something = True self.tof_calibration.remove_point(item.point) (item.parent() or root).removeChild(item) if removed_something: self.__change_selected_points() self.__save_object(self.POINTS_OBJECT_FILENAME) def set_calibration_point(self, tof): """Set Cut file front edge estimation to specific value. Args: tof: Float representing front edge of linear fit estimation. """ self.curveFittingWidget.matplotlib.set_calibration_point_externally( tof) def set_calibration_parameters_to_parent(self): """Set calibration parameters to parent dialog's calibration parameters fields. """ if self.parent_settings_widget is not None: self.parent_settings_widget.tof_slope = float( self.slopeLineEdit.text()) self.parent_settings_widget.tof_offset = float( self.offsetLineEdit.text()) return True return False def accept_calibration(self): """Accept calibration (parameters). """ calib_ok = "Calibration parameters accepted.\nYou can now close the " \ "window." calib_no = "Couldn't set parameters to\nthe settings dialog." calib_inv = "Invalid calibration parameters." results = self.tof_calibration.get_fit_parameters() if results[0] and results[1]: if self.set_calibration_parameters_to_parent(): self.acceptCalibrationLabel.setText(calib_ok) else: self.acceptCalibrationLabel.setText(calib_no) else: self.acceptCalibrationLabel.setText(calib_inv) def __update_curve_fit(self): """Redraws everything in the curve fitting graph. Updates the bin width too. """ self.__change_accept_point_label("") if self.selected_cut_file is not None: self.curveFittingWidget.matplotlib.change_bin_width(self.bin_width) try: self.curveFittingWidget.matplotlib.change_cut( self.selected_cut_file) except ValueError as e: QtWidgets.QMessageBox.critical(self, "Warning", str(e)) # TODO: Clear plot def __set_state_for_point(self, tree_item): """Sets if the tof calibration point is drawn to the linear fit graph Args: tree_item: QtWidgets.QTreeWidgetItem """ if tree_item and hasattr(tree_item, "point"): tree_item.point.point_used = tree_item.checkState(0) self.__change_selected_points() self.__enable_accept_calibration_button() def __accept_points(self, point): """ Called when 'accept point' button is clicked. Adds the calibration point to the point set for linear fitting and updates the treewidget of points. """ self.__add_point_to_tree(point) self.__change_selected_points() self.__enable_accept_calibration_button() self.__change_accept_point_label("Point accepted.") def __accept_point(self): """ Called when 'accept point' button is clicked. Adds the calibration point to the point set for linear fitting and updates the treewidget of points. """ point = self.curveFittingWidget.matplotlib.tof_calibration_point if point and not self.tof_calibration.point_exists(point): self.tof_calibration.add_point(point) self.__add_point_to_tree(point) self.__change_selected_points() self.__enable_accept_calibration_button() self.__change_accept_point_label("Point accepted.") self.__save_object(self.POINTS_OBJECT_FILENAME) else: self.__change_accept_point_label("Point already exists.") def __load_object(self, filename): file_to_open = self.detector.path.parent / filename with open(file_to_open, 'rb') as output: self.tof_calibration.tof_points = pickle.load(output) def __save_object(self, filename): file_to_open = self.detector.path.parent / filename with open(file_to_open, 'wb') as output: pickle.dump(self.tof_calibration.tof_points, output, pickle.HIGHEST_PROTOCOL) def __change_accept_point_label(self, text): """Sets text for the 'acceptPointLabel' label and starts timer. Args: text: String to be set to the label. """ self.acceptPointLabel.setText(text) self.timer.start(1500) def label_remove_timeout(self): """Timeout event method to remove label text. """ self.acceptPointLabel.setText("") self.timer.stop() def __enable_accept_calibration_button(self): """Let press accept calibration only if there are parameters available. """ if self.tof_calibration.slope or self.tof_calibration.offset: self.acceptCalibrationButton.setEnabled(True) else: self.acceptCalibrationButton.setEnabled(False) def __add_point_to_tree(self, tof_calibration_point): """Adds a ToF Calibration point to the pointsTreeWidget and sets the QTreeWidgetItem's attribute 'point' as the given TOFCalibrationPoint. """ item = QtWidgets.QTreeWidgetItem([tof_calibration_point.get_name()]) item.point = tof_calibration_point item.setCheckState(0, QtCore.Qt.Checked) self.pointsTreeWidget.addTopLevelItem(item) def __change_selected_points(self): """Redraws the linear fitting graph. """ self.linearFittingWidget.matplotlib.on_draw() # Redraw
class OptimizationDialog(QtWidgets.QDialog, PropertySavingWidget, metaclass=QtABCMeta): """User may either optimize fluence or recoil atom distribution. Optimization is done by comparing simulated spectrum to measured spectrum. """ ch: float = bnd.bind("histogramTicksDoubleSpinBox") use_efficiency: bool = bnd.bind("eff_file_check_box") selected_cut_file: Path = bnd.bind("measurementTreeWidget", fget=bnd.get_selected_tree_item, fset=bnd.set_selected_tree_item) selected_element_simulation: ElementSimulation = bnd.bind( "simulationTreeWidget", fget=bnd.get_selected_tree_item, fset=bnd.set_selected_tree_item) auto_adjust_x: bool = bnd.bind("auto_adjust_x_box") @property def fluence_parameters(self) -> Dict[str, Any]: return self.fluence_widget.get_properties() @fluence_parameters.setter def fluence_parameters(self, value: Dict[str, Any]): self.fluence_widget.set_properties(**value) @property def recoil_parameters(self) -> Dict[str, Any]: return self.recoil_widget.get_properties() @recoil_parameters.setter def recoil_parameters(self, value: Dict[str, Any]): self.recoil_widget.set_properties(**value) def __init__(self, simulation: Simulation, parent): """Initializes an OptimizationDialog that displays various optimization parameters. Args: simulation: a Simulation object parent: a SimulationTabWidget """ super().__init__() self.simulation = simulation self.tab = parent self.current_mode = OptimizationType.RECOIL uic.loadUi(gutils.get_ui_dir() / "ui_optimization_params.ui", self) self.recoil_widget = OptimizationRecoilParameterWidget() self.fluence_widget = OptimizationFluenceParameterWidget() self.load_properties_from_file() locale = QLocale.c() self.histogramTicksDoubleSpinBox.setLocale(locale) self.pushButton_OK.setEnabled(False) self.pushButton_Cancel.clicked.connect(self.close) self.pushButton_OK.clicked.connect(self.start_optimization) self.radios = QtWidgets.QButtonGroup(self) self.radios.buttonToggled[QtWidgets.QAbstractButton, bool].connect(self.choose_optimization_mode) self.parametersLayout.addWidget(self.recoil_widget) self.parametersLayout.addWidget(self.fluence_widget) self.fluence_widget.hide() self.radios.addButton(self.fluenceRadioButton) self.radios.addButton(self.recoilRadioButton) gutils.fill_tree(self.simulationTreeWidget.invisibleRootItem(), simulation.element_simulations, text_func=lambda elem_sim: elem_sim.get_full_name()) self.simulationTreeWidget.itemSelectionChanged.connect( self._enable_ok_button) self.simulationTreeWidget.itemSelectionChanged.connect(self._adjust_x) self.auto_adjust_x_box.clicked.connect(self._adjust_x) self._fill_measurement_widget() self.measurementTreeWidget.itemSelectionChanged.connect( self._enable_ok_button) self.eff_file_check_box.clicked.connect(self._enable_efficiency_label) self._update_efficiency_label() self.exec_() def closeEvent(self, event): """Overrides the QDialogs closeEvent. Saves current parameters to file so they shown next time the dialog is opened. """ params = self.get_properties() # Remove non-serializable values params.pop("selected_element_simulation") params.pop("selected_cut_file") self.save_properties_to_file(values=params) QtWidgets.QDialog.closeEvent(self, event) def _update_efficiency_label(self): """Updates the text of efficiency label. """ self.efficiency_label.setText( df.get_multi_efficiency_text( self.measurementTreeWidget, self.simulation.sample.get_measurements(), data_func=lambda tpl: tpl[0])) def _enable_efficiency_label(self): """Enables or disables efficiency label. """ self.efficiency_label.setEnabled(self.use_efficiency) def _fill_measurement_widget(self): """Add calculated tof_list files to tof_list_tree_widget by measurement under the same sample. """ for sample in self.simulation.request.samples.samples: for measurement in sample.measurements.measurements.values(): if self.simulation.sample is measurement.sample: root = QtWidgets.QTreeWidgetItem() root.setText(0, measurement.name) self.measurementTreeWidget.addTopLevelItem(root) cuts, elem_losses = measurement.get_cut_files() gutils.fill_tree(root, cuts, data_func=lambda c: (c, measurement), text_func=lambda c: c.name) loss_node = QtWidgets.QTreeWidgetItem(["Element losses"]) gutils.fill_tree(loss_node, elem_losses, data_func=lambda c: (c, measurement), text_func=lambda c: c.name) root.addChild(loss_node) root.setExpanded(True) def get_property_file_path(self) -> Path: """Returns absolute path to the file that is used for saving and loading parameters. """ return Path(self.simulation.directory, ".parameters", ".optimization_parameters") def _enable_ok_button(self, *_): """Enables OK button if both ElementSimulation and cut file have been selected. """ self.pushButton_OK.setEnabled( self.selected_cut_file is not None and self.selected_element_simulation is not None) def _adjust_x(self): """Adjusts the upper limit value on x axis based on the distribution length of the main recoil of currently selected ElementSimulation. """ if not self.auto_adjust_x: return elem_sim = self.selected_element_simulation if elem_sim is None: return _, max_x = elem_sim.get_main_recoil().get_range() _, prev_y = self.recoil_widget.upper_limits self.recoil_widget.upper_limits = max_x, prev_y def choose_optimization_mode(self, button, checked): """Choose whether to optimize recoils or fluence. Show correct widget. """ if checked: if button.text() == "Recoil": self.current_mode = OptimizationType.RECOIL self.fluence_widget.hide() self.recoil_widget.show() else: self.recoil_widget.hide() self.fluence_widget.show() self.current_mode = OptimizationType.FLUENCE def start_optimization(self): """Find necessary cut file and make energy spectrum with it, and start optimization with given parameters. """ elem_sim = self.selected_element_simulation cut, measurement = self.selected_cut_file # Delete previous results widget if it exists if self.tab.optimization_result_widget: self.tab.del_widget(self.tab.optimization_result_widget) self.tab.optimization_result_widget = None # Delete previous energy spectra if there are any df.delete_optim_espe(self, elem_sim) self.close() if self.current_mode == OptimizationType.RECOIL: params = self.recoil_widget.get_properties() else: params = self.fluence_widget.get_properties() # TODO move following code to the result widget nsgaii = Nsgaii(element_simulation=elem_sim, measurement=measurement, cut_file=cut, ch=self.ch, **params, use_efficiency=self.use_efficiency) # Optimization running thread ct = CancellationToken() optimization_thread = threading.Thread( target=nsgaii.start_optimization, kwargs={"cancellation_token": ct}) # Create necessary results widget result_widget = self.tab.add_optimization_results_widget( elem_sim, cut.name, self.current_mode, ct=ct) elem_sim.optimization_widget = result_widget nsgaii.subscribe(result_widget) optimization_thread.daemon = True optimization_thread.start()
class PercentageWidget(QtWidgets.QWidget): """ Class for a widget that calculates the percentages for given recoils and intervals. """ interval_type = bnd.bind("comboBox") status_msg = bnd.bind("label_status") def __init__(self, recoil_elements: List[RecoilElement], icon_manager, distribution_changed=None, interval_changed=None, get_limits=None): """ Initialize the widget. Args: recoil_elements: List of recoil elements. icon_manager: Icon manager. """ super().__init__() uic.loadUi(gutils.get_ui_dir() / "ui_percentage_widget.ui", self) # Stores the PercentageRow objects for each recoil self._percentage_rows = {recoil: None for recoil in recoil_elements} self.icon_manager = icon_manager self.setWindowTitle("Percentages") gutils.fill_combobox(self.comboBox, IntervalType) self.comboBox.currentIndexChanged.connect( self.__show_percents_and_areas) self.icon_manager.set_icon(self.absRelButton, "depth_profile_rel.svg") self.__relative_values = True self.absRelButton.setToolTip( "Toggle between relative and absolute values.") self.absRelButton.clicked.connect(self.__show_abs_or_rel_values) self.__dist_changed_sig = distribution_changed self.__interval_changed_sig = interval_changed self.get_limits = get_limits if self.get_limits is not None: if distribution_changed is not None: distribution_changed.connect(self._dist_changed) if interval_changed is not None: interval_changed.connect(self._limits_changed) self.__show_percents_and_areas() def closeEvent(self, event): """Overrides QWidget's closeEvent. Disconnects slots from signals. """ try: self.__dist_changed_sig.disconnect(self._dist_changed) except (TypeError, AttributeError): # Signal was either already disconnected or None pass try: self.__interval_changed_sig.disconnect(self._limits_changed) except (TypeError, AttributeError): # Signal was either already disconnected or None pass event.accept() def row_selected(self, recoil: RecoilElement) -> bool: """Checks if the given recoil has a row that is selected. """ row = self._percentage_rows.get(recoil, None) # Row is None when it has not been created yet. # Row will be created after area has been calculated # for the first time so we can return True return row is None or row.selected def _calculate_areas_and_percentages(self, rounding=2) -> Tuple[Dict, Dict]: """Calculate areas and percents for recoil elements within the given interval. Args: rounding: rounding accuracy of percentage calculation Return: areas and percentages as a dict """ if self.get_limits is not None: limits = self.get_limits() else: return None, None interval_type = self.interval_type if interval_type is IntervalType.NO_LIMITS: def get_range(_): return None, None elif interval_type is IntervalType.COMMON: def get_range(_): try: start, end = limits["common"] except (ValueError, KeyError): start, end = None, None return start, end else: def get_range(rec: RecoilElement): try: start, end = limits[rec] except (ValueError, KeyError): start, end = None, None return start, end areas = {} percentages = {} for recoil in self._percentage_rows: if not self.row_selected(recoil): area = 0 else: s, e = get_range(recoil) area = recoil.calculate_area(s, e) if not self.__relative_values: # TODO label text needs to reformatted when using absolute # values. See previous version of Potku for reference. area *= recoil.reference_density areas[recoil] = area for recoil, percentage in zip( areas, mf.calculate_percentages(areas.values(), rounding=rounding)): percentages[recoil] = percentage return areas, percentages def __show_abs_or_rel_values(self): """Show recoil area in absolute or relative format. """ if self.__relative_values: self.icon_manager.set_icon(self.absRelButton, "depth_profile_abs.svg") else: self.icon_manager.set_icon(self.absRelButton, "depth_profile_rel.svg") self.__relative_values = not self.__relative_values self.__show_percents_and_areas() def __show_percents_and_areas(self): """Show the percentages of the recoil elements. """ areas, percentages = self._calculate_areas_and_percentages() if areas is None or percentages is None: return for row_idx, recoil in enumerate(sorted(percentages)): try: self._percentage_rows[recoil].percentage = \ percentages[recoil] self._percentage_rows[recoil].area = \ areas[recoil] except (KeyError, AttributeError): row = PercentageRow(recoil.get_full_name(), recoil.color, percentage=percentages[recoil], area=areas[recoil]) self._percentage_rows[recoil] = row row.selectedCheckbox.stateChanged.connect( self.__show_percents_and_areas) self.gridLayout.addWidget(row, row_idx, 0) def _dist_changed(self, recoil, _): """Callback that is executed when RecoilElement distribution is changed. Checks that the recoil is being displayed in the Widget and then calls _show_percents_and_areas. Args: recoil: RecoilElement which distribution has changed. _: unused ElementSimulation object. """ if recoil in self._percentage_rows: self.__show_percents_and_areas() def _limits_changed(self): """Updates the common_interval with given x values and calls __show_percents_and_areas. """ self.__show_percents_and_areas()
class MeasurementSettingsWidget(QtWidgets.QWidget, bnd.PropertyTrackingWidget, bnd.PropertySavingWidget, metaclass=QtABCMeta): """Class for creating a measurement settings tab. """ # Signal that indicates whether the beam selection was ok or not (i.e. can # the selected element be used in measurements or simulations) beam_selection_ok = pyqtSignal(bool) measurement_setting_file_name = bnd.bind("nameLineEdit") measurement_setting_file_description = bnd.bind("descriptionPlainTextEdit") beam_ion = bnd.multi_bind( ["beamIonButton", "isotopeComboBox"], fget=element_from_gui, fset=element_to_gui, track_change=True ) beam_energy = bnd.bind("energyDoubleSpinBox", track_change=True) beam_energy_distribution = bnd.bind( "energyDistDoubleSpinBox", track_change=True) beam_charge = bnd.bind("beamChargeSpinBox") beam_spot_size = bnd.multi_bind( ["spotSizeXdoubleSpinBox", "spotSizeYdoubleSpinBox"], track_change=True) beam_divergence = bnd.bind("divergenceDoubleSpinBox", track_change=True) beam_profile = bnd.bind("profileComboBox", track_change=True) run_fluence = bnd.bind("fluenceDoubleSpinBox") run_current = bnd.bind("currentDoubleSpinBox") run_time = bnd.bind("timeDoubleSpinBox") run_charge = bnd.bind("runChargeDoubleSpinBox") target_theta = bnd.bind("targetThetaDoubleSpinBox", track_change=True) detector_theta = bnd.bind("detectorThetaDoubleSpinBox", track_change=True) def __init__(self, obj: Union[Measurement, Simulation], preset_folder=None): """Initializes the widget. Args: obj: object that uses these settings, either a Measurement or a Simulation. """ super().__init__() uic.loadUi(gutils.get_ui_dir() / "ui_measurement_settings_tab.ui", self) self.fluenceDoubleSpinBox = ScientificSpinBox() image = gf.get_root_dir() / "images" / "measurement_setup_angles.png" pixmap = QtGui.QPixmap(str(image)) self.picture.setScaledContents(True) self.picture.setPixmap(pixmap) self.obj = obj self.__original_property_values = {} locale = QLocale.c() self.energyDoubleSpinBox.setLocale(locale) self.energyDistDoubleSpinBox.setLocale(locale) self.spotSizeXdoubleSpinBox.setLocale(locale) self.spotSizeYdoubleSpinBox.setLocale(locale) self.divergenceDoubleSpinBox.setLocale(locale) self.currentDoubleSpinBox.setLocale(locale) self.timeDoubleSpinBox.setLocale(locale) self.runChargeDoubleSpinBox.setLocale(locale) self.targetThetaDoubleSpinBox.setLocale(locale) self.detectorThetaDoubleSpinBox.setLocale(locale) self.detectorFiiDoubleSpinBox.setLocale(locale) self.targetFiiDoubleSpinBox.setLocale(locale) # Fii angles are currently not used so disable their spin boxes self.detectorFiiDoubleSpinBox.setEnabled(False) self.targetFiiDoubleSpinBox.setEnabled(False) gutils.fill_combobox(self.profileComboBox, Profile) # Copy of measurement's/simulation's run or default run # TODO should default run also be copied? if not self.obj.run: self.tmp_run = self.obj.request.default_run else: self.tmp_run = copy.deepcopy(self.obj.run) self.isotopeInfoLabel.setVisible(False) self.beamIonButton.clicked.connect(self.change_element) self.fields_are_valid = False iv.set_input_field_red(self.nameLineEdit) self.nameLineEdit.textChanged.connect( lambda: iv.check_text(self.nameLineEdit, qwidget=self)) self.nameLineEdit.textEdited.connect(self.__validate) self.nameLineEdit.setEnabled(False) self.run_form_layout: QtWidgets.QFormLayout self.run_form_layout.insertRow(3, "Fluence", self.fluenceDoubleSpinBox) self.fluenceDoubleSpinBox.scientificLineEdit.setContextMenuPolicy( Qt.ActionsContextMenu) self.actionMultiply = QtWidgets.QAction( self.fluenceDoubleSpinBox.scientificLineEdit) self.actionMultiply.triggered.connect(self.__multiply_fluence) self.fluenceDoubleSpinBox.scientificLineEdit.addAction( self.actionMultiply) self.actionUndo = QtWidgets.QAction( self.fluenceDoubleSpinBox.scientificLineEdit) self.actionUndo.setText("Undo multiply") self.actionUndo.triggered.connect(self.__undo_fluence) self.actionUndo.setEnabled(bool(self.tmp_run.previous_fluence)) self.fluenceDoubleSpinBox.scientificLineEdit.addAction(self.actionUndo) self.clipboard = QGuiApplication.clipboard() self._ratio = None self.clipboard.changed.connect(self.__update_multiply_action) self.__update_multiply_action() self.energyDoubleSpinBox.setToolTip("Energy set in MeV with.") if preset_folder is not None: self.preset_widget = PresetWidget.add_preset_widget( preset_folder / "measurement", "mea", lambda w: self.layout().insertWidget(0, w), save_callback=self.save_properties_to_file, load_callback=self.load_properties_from_file ) else: self.preset_widget = None self.show_settings() def get_property_file_path(self) -> Path: raise NotImplementedError def save_on_close(self) -> bool: return False def save_properties_to_file(self, file_path: Path): def err_func(err: Exception): if self.preset_widget is not None: self.preset_widget.set_status_msg( f"Failed to save preset: {err}") values = self.get_properties() values["beam_ion"] = str(values["beam_ion"]) self._save_json_file( file_path, values, True, error_func=err_func) if self.preset_widget is not None: self.preset_widget.load_files(selected=file_path) def load_properties_from_file(self, file_path: Path): # TODO create a base class for settings widgets to get rid of this # copy-paste code def err_func(err: Exception): if self.preset_widget is not None: self.preset_widget.set_status_msg( f"Failed to load preset: {err}") values = self._load_json_file(file_path, error_func=err_func) if not values: return try: values["beam_ion"] = Element.from_string(values["beam_ion"]) self.set_properties(**values) except KeyError as e: err_func(f"file contained no {e} key.") except ValueError as e: err_func(e) def get_original_property_values(self): """Returns the values of the properties when they were first set. """ return self.__original_property_values def show_settings(self): """Show measurement settings. """ self.measurement_setting_file_name = \ self.obj.measurement_setting_file_name self.measurement_setting_file_description = \ self.obj.measurement_setting_file_description self.dateLabel.setText(time.strftime("%c %z %Z", time.localtime( self.obj.modification_time))) run_params = { f"run_{key}": value for key, value in self.tmp_run.get_settings().items() } bean_params = { f"beam_{key}": value for key, value in self.tmp_run.beam.get_settings().items() } self.set_properties(**run_params, **bean_params) self.detector_theta = self.obj.detector.detector_theta self.target_theta = self.obj.target.target_theta def check_angles(self): """ Check that detector angle is bigger than target angle. This is a must for measurement. Simulation can handle target angles greater than the detector angle. Return: Whether it is ok to use current angle settings. """ if self.target_theta > self.detector_theta: reply = QtWidgets.QMessageBox.question( self, "Warning", "Measurement cannot use a target angle that is " "bigger than the detector angle (for simulation " "this is possible).\n\n" "Do you want to use these settings anyway?", QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) if reply == QtWidgets.QMessageBox.Cancel: return False return True def get_run_and_beam_parameters(self): """Returns run and beam related properties as separate dictionaries with prefixes removed from each key. """ run_params, beam_params = {}, {} for k, v in self.get_properties().items(): if k.startswith("run_"): run_params[k[4:]] = v elif k.startswith("beam_"): beam_params[k[5:]] = v return run_params, beam_params def update_settings(self): """Update measurement settings. """ isotope_index = self.isotopeComboBox.currentIndex() if isotope_index != -1: self.obj.measurement_setting_file_name = \ self.measurement_setting_file_name self.obj.measurement_setting_file_description = \ self.measurement_setting_file_description run_params, beam_params = self.get_run_and_beam_parameters() self.obj.run.set_settings(**run_params) self.obj.run.beam.set_settings(**beam_params) self.obj.run.previous_fluence = self.tmp_run.previous_fluence self.obj.detector.detector_theta = self.detector_theta self.obj.target.target_theta = self.target_theta else: QtWidgets.QMessageBox.critical( self, "Warning", "No isotope selected.\n\n" "Please select an isotope for the beam element.", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok) def other_values_changed(self): """ Check whether measurement values that don't require running a simulation again have been changed. Return: True or False. """ # TODO make it possible to group TrackingProperties (for example into # 'critical' and 'noncritical' properties) if self.obj.measurement_setting_file_name != \ self.measurement_setting_file_name: return True if self.obj.measurement_setting_file_description != \ self.measurement_setting_file_description: return True if self.obj.run.beam.charge != self.beam_charge: return True if self.obj.run.current != self.run_current: return True if self.obj.run.time != self.run_time: return True if self.obj.run.charge != self.run_charge: return True if self.obj.run.fluence != self.run_fluence: return True return False def save_to_tmp_run(self): """ Save run and beam parameters to tmp_run object. """ isotope_index = self.isotopeComboBox.currentIndex() # TODO: Show a message box, don't just quietly do nothing if isotope_index != -1: run_params, beam_params = self.get_run_and_beam_parameters() self.tmp_run.set_settings(**run_params) self.tmp_run.beam.set_settings(**beam_params) else: QtWidgets.QMessageBox.critical( self, "Warning", "No isotope selected.\n\n" "Please select an isotope for the beam element.", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok) def __validate(self): """Validate the measurement settings file name. """ text = self.measurement_setting_file_name regex = "^[A-Za-z0-9-ÖöÄäÅå]*" valid_text = iv.validate_text_input(text, regex) self.measurement_setting_file_name = valid_text def __multiply_fluence(self): """Multiply fluence with clipboard's value. """ try: new_fluence = round(self._ratio * self.run_fluence, 2) self.tmp_run.previous_fluence.append(self.run_fluence) self.run_fluence = new_fluence self.actionUndo.setEnabled(True) except ValueError: pass def __undo_fluence(self): """Undo latest change to fluence. """ try: old_value = self.tmp_run.previous_fluence.pop() self.run_fluence = old_value except IndexError: pass # Enable undo if there are still previous values self.actionUndo.setEnabled(bool(self.tmp_run.previous_fluence)) def __update_multiply_action(self): """Update the value with which the multiplication is done. """ try: self._ratio = float(self.clipboard.text()) except ValueError: if self._ratio is None: self._ratio = 1.0 self.actionMultiply.setText(f"Multiply with value in clipboard\n" f"({self._ratio})") def change_element(self): """Opens element selection dialog and loads selected element's isotopes to the combobox. """ dialog = ElementSelectionDialog() if dialog.element: self.beamIonButton.setText(dialog.element) # TODO use IsotopeSelectionWidget gutils.load_isotopes(dialog.element, self.isotopeComboBox) # Check if no isotopes if self.isotopeComboBox.count() == 0: self.isotopeInfoLabel.setVisible(True) self.fields_are_valid = False iv.set_input_field_red(self.isotopeComboBox) self.beam_selection_ok.emit(False) else: self.isotopeInfoLabel.setVisible(False) iv.check_text(self.nameLineEdit, qwidget=self) self.isotopeComboBox.setStyleSheet( "background-color: %s" % "None") self.beam_selection_ok.emit(True) else: self.beam_selection_ok.emit(False)
class EnergySpectrumParamsDialog(QtWidgets.QDialog): """An EnergySpectrumParamsDialog. """ checked_cuts = {} bin_width = 0.025 use_efficiency = bnd.bind("use_eff_checkbox") status_msg = bnd.bind("label_status") measurement_cuts = bnd.bind("treeWidget") used_bin_width = bnd.bind("histogramTicksDoubleSpinBox") external_files = bnd.bind("external_tree_widget") tof_list_files = bnd.bind("tof_list_tree_widget") used_recoil = bnd.bind("treeWidget") def __init__(self, parent: BaseTab, spectrum_type: str = _MESU, element_simulation: Optional[ElementSimulation] = None, simulation: Optional[Simulation] = None, measurement: Optional[Measurement] = None, recoil_widget=None, statusbar: Optional[QtWidgets.QStatusBar] = None, spectra_changed=None): """Inits energy spectrum dialog. Args: parent: A TabWidget. spectrum_type: Whether spectrum is for measurement of simulation. element_simulation: ElementSimulation object. recoil_widget: RecoilElement widget. statusbar: QStatusBar spectra_changed: pyQtSignal that is emitted when recoil atom distribution is changed. """ super().__init__() uic.loadUi(gutils.get_ui_dir() / "ui_energy_spectrum_params.ui", self) self.parent = parent if spectrum_type == EnergySpectrumWidget.MEASUREMENT: if measurement is None: raise ValueError( f"Must provide a Measurement when spectrum type is " f"{spectrum_type}") elif spectrum_type is EnergySpectrumWidget.SIMULATION: if simulation is None: raise ValueError( f"Must provide a Simulation when spectrum type is " f"{spectrum_type}") if element_simulation is None: raise ValueError( f"Must provide an ElementSimulation when spectrum is " f"{spectrum_type}") else: raise ValueError(f"Unexpected spectrum type: {spectrum_type}") self.spectrum_type = spectrum_type self.measurement = measurement self.simulation = simulation self.element_simulation = element_simulation self.statusbar = statusbar self.result_files = [] self.use_eff_checkbox.stateChanged.connect( lambda *_: self.label_efficiency_files.setEnabled(self. use_efficiency)) self.use_efficiency = True locale = QLocale.c() self.histogramTicksDoubleSpinBox.setLocale(locale) # Connect buttons self.pushButton_Cancel.clicked.connect(self.close) self.external_tree_widget = QtWidgets.QTreeWidget() if self.spectrum_type == EnergySpectrumWidget.MEASUREMENT: EnergySpectrumParamsDialog.bin_width = \ self.measurement.profile.channel_width self.pushButton_OK.clicked.connect( lambda: self.__accept_params(spectra_changed=spectra_changed)) m_name = self.measurement.name if m_name not in EnergySpectrumParamsDialog.checked_cuts: EnergySpectrumParamsDialog.checked_cuts[m_name] = set() gutils.fill_cuts_treewidget(self.measurement, self.treeWidget.invisibleRootItem(), use_elemloss=True) self.measurement_cuts = \ EnergySpectrumParamsDialog.checked_cuts[m_name] self.importPushButton.setVisible(False) else: EnergySpectrumParamsDialog.bin_width = \ self.element_simulation.channel_width self._set_simulation_files(recoil_widget) self._set_measurement_files() self._set_external_files() # Change the bin width label text self.histogramTicksLabel.setText( "Simulation and measurement histogram bin width:") self.pushButton_OK.clicked.connect( self.__calculate_selected_spectra) self.importPushButton.clicked.connect(self.__import_external_file) self.used_bin_width = EnergySpectrumParamsDialog.bin_width # FIXME .eff files not shown in sim mode self.__update_eff_files() self.exec_() def showEvent(self, event): """Adjust size after dialog has been shown. """ self.adjustSize() super().showEvent(event) def _set_simulation_files(self, recoil_widget): """Sets up the simulation files in a QTreeWidget. """ header_item = QtWidgets.QTreeWidgetItem() header_item.setText(0, "Simulated element (observed atoms)") self.treeWidget.setHeaderItem(header_item) for elem_sim in self.simulation.element_simulations: root = QtWidgets.QTreeWidgetItem( [f"{elem_sim.get_full_name()} ({elem_sim.get_atom_count()})"]) gutils.fill_tree(root, elem_sim.recoil_elements, data_func=lambda rec: (elem_sim, rec, None), text_func=lambda rec: rec.get_full_name()) if elem_sim.is_optimization_finished(): gutils.fill_tree(root, elem_sim.optimization_recoils, data_func=lambda rec: (elem_sim, rec, OptimizationType.RECOIL), text_func=lambda rec: rec.get_full_name()) self.treeWidget.addTopLevelItem(root) root.setExpanded(True) self.used_recoil = {(recoil_widget.element_simulation, recoil_widget.recoil_element, None)} def _set_measurement_files(self): """Sets up the .cut file list. """ self.tof_list_tree_widget = QtWidgets.QTreeWidget() self.tof_list_tree_widget.setSizePolicy( QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) header = QtWidgets.QTreeWidgetItem() header.setText(0, "Pre-calculated elements") self.gridLayout_2.addWidget(self.tof_list_tree_widget, 0, 1) self.tof_list_tree_widget.setHeaderItem(header) # Add calculated tof_list files to tof_list_tree_widget by # measurement under the same sample. for measurement in self.simulation.sample.get_measurements(): root = QtWidgets.QTreeWidgetItem([measurement.name]) cuts, elem_loss = measurement.get_cut_files() gutils.fill_tree(root, cuts, data_func=lambda c: (c, measurement), text_func=lambda c: c.name) self.tof_list_tree_widget.addTopLevelItem(root) elem_loss_root = QtWidgets.QTreeWidgetItem(["Element losses"]) gutils.fill_tree(elem_loss_root, elem_loss, data_func=lambda c: (c, measurement), text_func=lambda c: c.name) root.addChild(elem_loss_root) root.setExpanded(True) self.tof_list_files = {} def _set_external_files(self): """Sets up the external file QTreeWidget. """ # Add a view for adding external files to draw self.external_tree_widget.setSizePolicy( QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) header = QtWidgets.QTreeWidgetItem() header.setText(0, "External files") self.gridLayout_2.addWidget(self.external_tree_widget, 0, 2) self.external_tree_widget.setHeaderItem(header) gutils.fill_tree(self.external_tree_widget.invisibleRootItem(), self.simulation.request.get_imported_files(), text_func=lambda fp: fp.name) self.external_files = {} def get_selected_measurements(self): """Returns a dictionary that contains selected measurements, cut files belonging to each measurement, and the corresponding result file. """ mesus = self.tof_list_files used_measurements = {} # TODO result file is probably not needed here for c, m in mesus: used_measurements.setdefault(m, []).append({ "cut_file": c, "result_file": Path(m.get_energy_spectra_dir(), f"{c.stem}.no_foil.hist") }) return used_measurements def get_selected_simulations(self): """Returns a dictionary that contains selected simulations and list of recoil elements and corresponding result files. """ used_simulations = {} for elem_sim, rec, optim in self.used_recoil: # TODO optim type may not be necessary used_simulations.setdefault(elem_sim, []).append({ "recoil_element": rec, "optimization_type": optim }) return used_simulations @gutils.disable_widget def __calculate_selected_spectra(self, *_): """Calculate selected spectra. """ EnergySpectrumParamsDialog.bin_width = self.used_bin_width sbh = StatusBarHandler(self.statusbar) # Get all used_simulations = self.get_selected_simulations() used_measurements = self.get_selected_measurements() used_externals = self.external_files sbh.reporter.report(33) # Calculate espes for simulations for elem_sim, lst in used_simulations.items(): for d in lst: _, espe_file = elem_sim.calculate_espe(**d, write_to_file=True, ch=self.bin_width) self.result_files.append(espe_file) sbh.reporter.report(66) # Calculate espes for measurements. 'no_foil' parameter is used to # make the results comparable with simulation espes. Basically # this increases the calculated energy values, shifting the espe # histograms to the right on the x axis. for mesu, lst in used_measurements.items(): self.result_files.extend(d["result_file"] for d in lst) # TODO use the return values instead of reading the files further # down the execution path EnergySpectrum.calculate_measured_spectra( mesu, [d["cut_file"] for d in lst], self.bin_width, use_efficiency=self.use_efficiency, no_foil=True) # Add external files self.result_files.extend(used_externals) sbh.reporter.report(100) msg = f"Created Energy Spectrum. " \ f"Bin width: {self.bin_width} " \ f"Used files: {', '.join(str(f) for f in self.result_files)}" self.simulation.log(msg) self.close() @gutils.disable_widget def __accept_params(self, spectra_changed=None): """Accept given parameters and cut files. """ self.status_msg = "" width = self.used_bin_width m_name = self.measurement.name selected_cuts = self.measurement_cuts EnergySpectrumParamsDialog.checked_cuts[m_name] = set( self.measurement_cuts) EnergySpectrumParamsDialog.bin_width = width if selected_cuts: self.status_msg = "Please wait. Creating energy spectrum." if self.parent.energy_spectrum_widget: self.parent.del_widget(self.parent.energy_spectrum_widget) self.parent.energy_spectrum_widget = EnergySpectrumWidget( self.parent, spectrum_type=self.spectrum_type, use_cuts=selected_cuts, bin_width=width, use_efficiency=self.use_efficiency, statusbar=self.statusbar, spectra_changed=spectra_changed) # Check that matplotlib attribute exists after creation of energy # spectrum widget. # If it doesn't exists, that means that the widget hasn't been # initialized properly and the program should show an error dialog. if hasattr(self.parent.energy_spectrum_widget, "matplotlib_layout"): icon = self.parent.icon_manager.get_icon( "energy_spectrum_icon_16.png") self.parent.add_widget(self.parent.energy_spectrum_widget, icon=icon) cuts = ", ".join(str(cut) for cut in selected_cuts) msg = f"Created Energy Spectrum. " \ f"Bin width: {width}. " \ f"Cut files: {cuts}" self.measurement.log(msg) log_info = "Energy Spectrum graph points:\n" data = self.parent.energy_spectrum_widget.energy_spectrum_data splitinfo = "\n".join([ "{0}: {1}".format( key, ", ".join("({0};{1})".format(round(v[0], 2), v[1]) for v in data[key])) for key in data.keys() ]) self.measurement.log(log_info + splitinfo) else: QtWidgets.QMessageBox.critical( self, "Error", "An error occurred while trying to create energy spectrum.", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok) self.close() else: self.status_msg = "Please select .cut file[s] to create energy " \ "spectra." def __import_external_file(self): """ Import an external file that matches the format of hist and simu files. """ QtWidgets.QMessageBox.information( self, "Notice", "The external file needs to have the following format:\n\n" "energy count\nenergy count\nenergy count\n...\n\n" "to match the simulation and measurement energy spectra files.", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok) file_path = fdialogs.open_file_dialog( self, self.element_simulation.request.directory, "Select a file to import", "") if file_path is None: return name = file_path.name new_file_name = \ self.element_simulation.request.get_imported_files_folder() / name if new_file_name.exists(): QtWidgets.QMessageBox.critical( self, "Error", "A file with that name already exists.", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok) return shutil.copyfile(file_path, new_file_name) item = QtWidgets.QTreeWidgetItem() item.setText(0, new_file_name.name) item.setData(0, QtCore.Qt.UserRole, new_file_name) item.setCheckState(0, QtCore.Qt.Checked) self.external_tree_widget.addTopLevelItem(item) def __update_eff_files(self): """Update efficiency files to UI which are used. """ if self.spectrum_type == _SIMU: # Simulation energy spectrum can contain cut files from multiple # Measurements which each can have different Detector an thus # different efficiency files label_txt = df.get_multi_efficiency_text( self.tof_list_tree_widget, self.simulation.sample.get_measurements(), data_func=lambda tpl: tpl[0]) else: detector = self.measurement.get_detector_or_default() label_txt = df.get_efficiency_text(self.treeWidget, detector) self.label_efficiency_files.setText(label_txt)
class RecoilInfoDialog(QtWidgets.QDialog, bnd.PropertyBindingWidget, metaclass=gutils.QtABCMeta): """Dialog for editing the name, description and reference density of a recoil element. """ # TODO possibly track name changes name = bnd.bind("nameLineEdit") description = bnd.bind("descriptionLineEdit") reference_density = bnd.bind("scientific_spinbox") @property def color(self): return self.tmp_color.name() def __init__(self, recoil_element: RecoilElement, colormap, element_simulation: ElementSimulation): """Inits a recoil info dialog. Args: recoil_element: A RecoilElement object. colormap: Colormap for elements. element_simulation: Element simulation that has the recoil element. """ super().__init__() self.recoil_element = recoil_element self.element_simulation = element_simulation self.tmp_color = QColor(self.recoil_element.color) self.colormap = colormap value = self.recoil_element.reference_density self.scientific_spinbox = ScientificSpinBox(value=value, minimum=0.01, maximum=9.99e23) uic.loadUi(gutils.get_ui_dir() / "ui_recoil_info_dialog.ui", self) self.okPushButton.clicked.connect(self.__accept_settings) self.cancelPushButton.clicked.connect(self.close) self.colorPushButton.clicked.connect(self.__change_color) self.fields_are_valid = True iv.set_input_field_red(self.nameLineEdit) self.nameLineEdit.textChanged.connect( lambda: iv.check_text(self.nameLineEdit, qwidget=self)) self.nameLineEdit.textEdited.connect(self.__validate) self.name = recoil_element.name self.description = recoil_element.description self.formLayout.insertRow( 4, QtWidgets.QLabel(r"Reference density [at./cm<sup>3</sup>]:"), self.scientific_spinbox) self.formLayout.removeRow(self.widget) self.description = recoil_element.description self.isOk = False self.dateLabel.setText( time.strftime( "%c %z %Z", time.localtime(self.recoil_element.modification_time))) title = f"Recoil element: " \ f"{self.recoil_element.element.get_prefix()}" self.infoGroupBox.setTitle(title) self.__set_color_button_color(self.recoil_element.element.symbol) self.exec_() def __density_valid(self): """ Check if density value is valid and in limits. Return: True or False. """ try: self.scientific_spinbox.get_value() return True except TypeError: return False def __accept_settings(self): """Function for accepting the current settings and closing the dialog window. """ if not self.fields_are_valid or not self.__density_valid(): QtWidgets.QMessageBox.critical( self, "Warning", "Some of the setting values are invalid.\n" "Please input values in fields indicated in red.", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok) return if self.name != self.recoil_element.name: # Check that the new name is not already in use if self.name in ( r.name for r in # has_recoil self.element_simulation.recoil_elements): QtWidgets.QMessageBox.critical( self, "Warning", "Name of the recoil element is already in use. Please use " "a different name", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok) return # If current recoil is used in a running simulation if self.recoil_element is \ self.element_simulation.get_main_recoil(): if (self.element_simulation.is_simulation_running() or self.element_simulation.is_optimization_running()) and \ self.name != self.recoil_element.name: reply = QtWidgets.QMessageBox.question( self, "Recoil used in simulation", "This recoil is used in a simulation that is " "currently running.\nIf you change the name of " "the recoil, the running simulation will be " "stopped.\n\n" "Do you want to save changes anyway?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) if reply == QtWidgets.QMessageBox.No or reply == \ QtWidgets.QMessageBox.Cancel: return else: self.element_simulation.stop() self.isOk = True self.close() def __change_color(self): """ Change the color of the recoil element. """ dialog = QtWidgets.QColorDialog(self) color = dialog.getColor(self.tmp_color) if color.isValid(): self.tmp_color = color self.__change_color_button_color( self.recoil_element.element.symbol) def __change_color_button_color(self, element: str): """ Change color button's color. Args: element: String representing element name. """ df.set_btn_color(self.colorPushButton, self.tmp_color, self.colormap, element) def __set_color_button_color(self, element): """Set default color of element to color button. Args: element: String representing element. """ self.colorPushButton.setEnabled(True) self.tmp_color = QColor(self.recoil_element.color) self.__change_color_button_color(element) def __validate(self): """ Validate the recoil name. """ text = self.name regex = "^[A-Za-z0-9-ÖöÄäÅå]*" valid_text = iv.validate_text_input(text, regex) self.name = valid_text
class PointCoordinatesWidget(QtWidgets.QWidget): """ Class for handling point coordinates spin boxes. """ x_coord = bnd.bind("x_coordinate_box") y_coord = bnd.bind("y_coordinate_box") coord_changed = pyqtSignal() def __init__(self, parent, optimize=False, full_edit_changed=None): """Initializes the widget. Args: parent: RecoilAtomDistributionWidget. """ super().__init__() self.parent = parent vertical_layout = QtWidgets.QVBoxLayout() vertical_layout.setContentsMargins(0, 0, 0, 0) horizontal_layout_x = QtWidgets.QHBoxLayout() horizontal_layout_x.setContentsMargins(0, 0, 0, 0) horizontal_layout_y = QtWidgets.QHBoxLayout() horizontal_layout_y.setContentsMargins(0, 0, 0, 0) # Point x coordinate spinbox self.x_coordinate_box = QtWidgets.QDoubleSpinBox(self) # Set decimal pointer to . self.x_coordinate_box.setLocale(self.parent.locale) self.x_coordinate_box.setToolTip("X coordinate of selected point") self.x_coordinate_box.setSingleStep(0.1) self.x_coordinate_box.setDecimals(2) self.x_coordinate_box.setMinimum(0) self.x_coordinate_box.setMaximum(1000000000000) self.x_coordinate_box.setMaximumWidth(62) self.x_coordinate_box.setKeyboardTracking(False) if not optimize: self.x_coordinate_box.valueChanged.connect( self.parent.set_selected_point_x) self.x_coordinate_box.setContextMenuPolicy(Qt.ActionsContextMenu) self.actionXMultiply = QtWidgets.QAction(self) self.actionXMultiply.setText("Multiply coordinate...") self.actionXMultiply.triggered.connect( lambda: self.__multiply_coordinate(self.x_coordinate_box)) self.x_coordinate_box.addAction(self.actionXMultiply) self.set_x_enabled(False) # X label label_x = QtWidgets.QLabel("x:") # Point y coordinate spinbox self.y_coordinate_box = QtWidgets.QDoubleSpinBox(self) # Set decimal pointer to . self.y_coordinate_box.setLocale(self.parent.locale) self.y_coordinate_box.setToolTip("Y coordinate of selected point") self.y_coordinate_box.setSingleStep(0.1) self.y_coordinate_box.setDecimals(4) self.y_coordinate_box.setMaximum(1000000000000) self.y_coordinate_box.setMaximumWidth(62) self.y_coord = 1.0 self.set_y_min() self.y_coordinate_box.setKeyboardTracking(False) if not optimize: self.y_coordinate_box.valueChanged.connect( self.parent.set_selected_point_y) self.y_coordinate_box.setContextMenuPolicy(Qt.ActionsContextMenu) self.actionYMultiply = QtWidgets.QAction(self) self.actionYMultiply.setText("Multiply coordinate...") self.actionYMultiply.triggered.connect( lambda: self.__multiply_coordinate(self.y_coordinate_box)) self.y_coordinate_box.addAction(self.actionYMultiply) self.y_coordinate_box.editingFinished.connect( self.coord_changed.emit) self.x_coordinate_box.editingFinished.connect( self.coord_changed.emit) self.set_y_enabled(False) # Y label label_y = QtWidgets.QLabel("y:") if platform.system() == "Darwin" or platform.system() == "Linux": self.x_coordinate_box.setMinimumWidth(70) self.y_coordinate_box.setMinimumWidth(70) horizontal_layout_x.addWidget(label_x) horizontal_layout_x.addWidget(self.x_coordinate_box) horizontal_layout_y.addWidget(label_y) horizontal_layout_y.addWidget(self.y_coordinate_box) vertical_layout.addLayout(horizontal_layout_x) vertical_layout.addLayout(horizontal_layout_y) self.setLayout(vertical_layout) self._full_edit_changed = full_edit_changed if full_edit_changed is not None: full_edit_changed.connect(self.set_y_min) def closeEvent(self, event): """Disconnects full edit signal when closing. """ try: self._full_edit_changed.disconnect(self.set_y_min) except AttributeError: pass super().closeEvent(event) def set_y_min(self, point: Optional[Point] = None): """Sets the minimum value of y spinbox depending on whether full edit is on or not. """ try: if self.parent.full_edit_on: minimum = 0.0 else: minimum = min( self.parent.get_minimum_concentration(), self.y_coord, point.get_y() if point is not None else self.y_coord) self.y_coordinate_box.setMinimum(minimum) except AttributeError: self.y_coordinate_box.setMinimum(0.0) def set_x_enabled(self, b: bool): """Enables or disables x coordinate spinbox. """ self.x_coordinate_box.setEnabled(b) def set_y_enabled(self, b: bool): """Enables or disables y coordinate spinbox. """ self.y_coordinate_box.setEnabled(b) def __multiply_coordinate(self, spinbox: QtWidgets.QDoubleSpinBox): """Multiply the spinbox's value with the value in clipboard. Args: spinbox: Spinbox whose value is multiplied. """ dialog = MultiplyCoordinateDialog(self.parent.ratio_str) if dialog.used_multiplier: multiplier = dialog.used_multiplier # Make backlog entry self.parent.current_recoil_element.save_current_points( self.parent.full_edit_on) if spinbox is self.x_coordinate_box: for point in reversed(self.parent.selected_points): if point.get_y() == 0.0: if self.parent.editing_restricted(): continue coord = point.get_x() new_coord = round(multiplier * coord, 3) if new_coord > self.parent.target_thickness: new_coord = self.parent.target_thickness self.parent.set_selected_point_x(new_coord, point) else: for point in reversed(self.parent.selected_points): if point.get_y() == 0.0: if self.parent.editing_restricted(): continue coord = point.get_y() new_coord = round(multiplier * coord, 3) self.parent.set_selected_point_y(new_coord, point)
class LayerPropertiesDialog(QtWidgets.QDialog, bnd.PropertyTrackingWidget, metaclass=gutils.QtABCMeta): """Dialog for adding a new layer or editing an existing one. """ name = bnd.bind("nameEdit", track_change=True) thickness = bnd.bind("thicknessEdit", track_change=True) density = bnd.bind("densityEdit", track_change=True) def __init__(self, tab, layer=None, modify=False, simulation=None, first_layer=False): """Inits a layer dialog. Args: tab: A SimulationTabWidget layer: Layer object to be modified. None if creating a new layer. modify: If dialog is used to modify a layer. simulation: A Simulation object. first_layer: Whether the dialog is used to add the first layer. """ super().__init__() uic.loadUi(gutils.get_ui_dir() / "ui_layer_dialog.ui", self) self.tab = tab self.layer = layer self.ok_pressed = False self.simulation = simulation self.amount_mismatch = True self.fields_are_valid = True iv.set_input_field_red(self.nameEdit) self.nameEdit.textChanged.connect( lambda: iv.check_text(self.nameEdit, self)) self.nameEdit.textEdited.connect( lambda: iv.sanitize_file_name(self.nameEdit)) # Connect buttons to events self.addElementButton.clicked.connect(self.__add_element_layout) self.okButton.clicked.connect(self.__save_layer) self.cancelButton.clicked.connect(self.close) self.thicknessEdit.setMinimum(0.01) self.densityEdit.setMinimum(0.01) self.__original_properties = {} if self.layer: self.__show_layer_info() else: self.__add_element_layout() if first_layer: self.groupBox_2.hide() self.__close = True self.thicknessEdit.setLocale(QLocale.c()) self.densityEdit.setLocale(QLocale.c()) if modify: self.groupBox_2.hide() self.placement_under = True if platform.system() == "Darwin": self.setMinimumWidth(450) if platform.system() == "Linux": self.setMinimumWidth(470) self.exec_() def get_original_property_values(self): """Returns the original values of the properties. """ return self.__original_properties def __save_layer(self): """Function for adding a new layer with given settings. """ self.__check_if_settings_ok() self.__accept_settings() if self.__close: self.close() def __show_layer_info(self): """ Show information of the current layer. """ self.set_properties(name=self.layer.name, thickness=self.layer.thickness, density=self.layer.density) for elem in self.layer.elements: self.__add_element_layout(elem) def get_element_widgets(self): """Returns all ElementLayout child objects that the widget has.""" return [ child for child in self.scrollAreaWidgetContents.layout().children() if isinstance(child, ElementLayout) ] def __check_if_settings_ok(self): """Check that all the settings are okay. Return: True if the settings are okay and false if some required fields are empty. """ settings_ok = True help_sum = 0 elem_widgets = self.get_element_widgets() # Check if 'scrollArea' is empty (no elements). if not elem_widgets: iv.set_input_field_red(self.scrollArea) settings_ok = False # Check that the element specific settings are okay. for widget in elem_widgets: elem = widget.get_selected_element() if elem is None: settings_ok = False else: help_sum += elem.amount if isclose(help_sum, 1.0, rel_tol=1e-6) or isclose(help_sum, 100.0, rel_tol=1e-6): for widget in elem_widgets: iv.set_input_field_white(widget.amount_spinbox) self.amount_mismatch = False else: for widget in elem_widgets: iv.set_input_field_red(widget.amount_spinbox) settings_ok = False self.amount_mismatch = True self.fields_are_valid = settings_ok def elements_changed(self): """ Check if elements have been changed in the layer. """ return not all(e1 == e2 for e1, e2 in itertools.zip_longest( self.find_elements(), self.layer.elements )) def find_elements(self): """ Find all the layer's element from the dialog. """ return [ child.get_selected_element() for child in self.get_element_widgets() ] def __accept_settings(self): """Function for accepting the current settings and closing the dialog window. """ if not self.fields_are_valid: if self.amount_mismatch: hint = "(Hint: element amounts need to sum up to either 1 or " \ "100.)" else: hint = "" QtWidgets.QMessageBox.critical(self, "Warning", "Some of the parameter values have" " not been set.\n\n" + "Please input values in fields " "indicated in red.\n" + hint, QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok) self.__close = False return if self.layer and not (self.are_values_changed() or self.elements_changed()): self.__close = True self.fields_are_valid = True self.ok_pressed = False # No update needed return if self.simulation is not None: if not df.delete_element_simulations(self, self.simulation, tab=self.tab, msg="target"): self.__close = False return elements = self.find_elements() if self.layer: self.layer.name = self.name self.layer.elements = elements self.layer.thickness = self.thickness self.layer.density = self.density else: self.layer = Layer(self.name, elements, self.thickness, self.density) if self.comboBox.currentText().startswith("Under"): self.placement_under = True else: self.placement_under = False self.ok_pressed = True self.__close = True def __missing_information_message(self, empty_fields): """Show the user a message about missing information. Args: empty_fields: Input fields that are empty. """ fields = "" for field in empty_fields: fields += " • " + field + "\n" QtWidgets.QMessageBox.critical( self.parent(), "Required information missing", "The following fields are still empty:\n\n" + fields + "\nFill out the required information in order to continue.", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok) def __add_element_layout(self, element=None): """Add element widget into view. """ el = ElementLayout(self.scrollAreaWidgetContents, element, self) el.selection_changed.connect(self.__check_if_settings_ok) self.scrollArea.setStyleSheet("") self.scrollAreaWidgetContents.layout().addLayout(el)
class DetectorSettingsWidget(QtWidgets.QWidget, bnd.PropertyTrackingWidget, metaclass=gutils.QtABCMeta): """Class for creating a detector settings tab. """ # Key that is used to store the folder of the most recently added eff file EFF_FILE_FOLDER_KEY = "efficiency_folder" efficiency_files = bnd.bind("efficiencyListWidget") name = bnd.bind("nameLineEdit") modification_time = bnd.bind("dateLabel", fget=bnd.unix_time_from_label, fset=bnd.unix_time_to_label) description = bnd.bind("descriptionLineEdit") detector_type = bnd.bind("typeComboBox", track_change=True) angle_slope = bnd.bind("angleSlopeLineEdit", track_change=True) angle_offset = bnd.bind("angleOffsetLineEdit", track_change=True) tof_slope = bnd.bind("scientific_tof_slope", track_change=True) tof_offset = bnd.bind("scientific_tof_offset", track_change=True) timeres = bnd.bind("timeResSpinBox", track_change=True) virtual_size = bnd.multi_bind( ("virtualSizeXSpinBox", "virtualSizeYSpinBox"), track_change=True) def __init__(self, obj: Detector, request: Request, icon_manager, run=None): """Initializes a DetectorSettingsWidget object. Args: obj: a Detector object. request: Which request it belongs to. icon_manager: IconManager object. run: Run object. None if detector is default detector. """ super().__init__() uic.loadUi(gutils.get_ui_dir() / "ui_request_detector_settings.ui", self) self.obj = obj self.request = request self.icon_manager = icon_manager self.run = run self.__original_properties = {} # Temporary foils list which holds all the information given in the # foil dialog # If user presses ok or apply, these values will be saved into # request's default detector self.tmp_foil_info = [] # List of foil indexes that are timing foils self.tof_foils = [] # Add foil widgets and foil objects self.detector_structure_widgets = [] self.foils_layout = self._add_default_foils(self.obj) self.detectorScrollAreaContents.layout().addLayout(self.foils_layout) self.newFoilButton.clicked.connect( lambda: self._add_new_foil(self.foils_layout)) self.addEfficiencyButton.clicked.connect(self.__add_efficiency) self.removeEfficiencyButton.clicked.connect(self.__remove_efficiency) self.plotEfficiencyButton.clicked.connect(self.__plot_efficiency) self.efficiencyListWidget.itemSelectionChanged.connect( self._enable_remove_btn) self._enable_remove_btn() # Calibration settings # TODO: Require saving affected cuts if beam setting has been changed self.executeCalibrationButton.clicked.connect( self.__open_calibration_dialog) self.executeCalibrationButton.setEnabled( not self.request.samples.measurements.is_empty()) gutils.fill_combobox(self.typeComboBox, DetectorType) self.fields_are_valid = False iv.set_input_field_red(self.nameLineEdit) self.nameLineEdit.textChanged.connect( lambda: iv.check_text(self.nameLineEdit, qwidget=self)) self.nameLineEdit.textEdited.connect( lambda: iv.sanitize_file_name(self.nameLineEdit)) self.nameLineEdit.setEnabled(False) locale = QLocale.c() self.timeResSpinBox.setLocale(locale) self.virtualSizeXSpinBox.setLocale(locale) self.virtualSizeYSpinBox.setLocale(locale) self.angleSlopeLineEdit.setLocale(locale) self.angleOffsetLineEdit.setLocale(locale) # Create scientific spinboxes for tof slope and tof offset self.formLayout_2.removeRow(self.slopeLineEdit) self.formLayout_2.removeRow(self.offsetLineEdit) self.scientific_tof_slope = ScientificSpinBox(minimum=-math.inf, maximum=math.inf) self.scientific_tof_offset = ScientificSpinBox(minimum=-math.inf, maximum=math.inf) self.formLayout_2.insertRow(0, "ToF slope [s/channel]:", self.scientific_tof_slope) self.formLayout_2.insertRow(1, "ToF offset[s]:", self.scientific_tof_offset) if platform.system() == "Darwin": self.scientific_tof_offset.scientificLineEdit.setFixedWidth(170) self.scientific_tof_slope.scientificLineEdit.setFixedWidth(170) # Save as and load self.saveButton.clicked.connect(self.__save_file) self.loadButton.clicked.connect(self.__load_file) self.show_settings() def get_original_property_values(self): """Returns the original values of the widget's properties. """ return self.__original_properties def __load_file(self): """Load settings from file. """ file = fdialogs.open_file_dialog(self, self.request.default_folder, "Select detector file", "Detector File (*.detector)") if file is None: return temp_detector = Detector.from_file(file, self.request, False) self.obj.set_settings(**temp_detector.get_settings()) self.tmp_foil_info = [] self.tof_foils = [] self.detector_structure_widgets = [] # Remove old widgets for i in range(self.detectorScrollAreaContents.layout().count()): layout_item = self.detectorScrollAreaContents.layout().itemAt(i) if layout_item == self.foils_layout: self.detectorScrollAreaContents.layout().removeItem( layout_item) for j in reversed(range(layout_item.count())): layout_item.itemAt(j).widget().deleteLater() # Add foil widgets and foil objects self.foils_layout = self._add_default_foils(temp_detector) self.detectorScrollAreaContents.layout().addLayout(self.foils_layout) self.show_settings() def __save_file(self): """Opens file dialog and sets and saves the settings to a file. """ file = fdialogs.save_file_dialog(self, self.request.default_folder, "Save detector file", "Detector File (*.detector)") if file is None: return file = file.with_suffix(".detector") if not self.some_values_changed(): self.obj.to_file(file) else: # Make temp detector, modify it according to widget values, # and write it to file. temp_detector = copy.deepcopy(self.obj) original_obj = self.obj self.obj = temp_detector self.update_settings() self.obj.to_file(file) self.obj = original_obj def show_settings(self): """Show Detector settings. """ # Detector settings self.set_properties(**self.obj.get_settings()) self.efficiency_files = self.obj.get_efficiency_files() # Detector foils self.tmp_foil_info = copy.deepcopy(self.obj.foils) self.calculate_distance() # Tof foils self.tof_foils = copy.deepcopy(self.obj.tof_foils) def update_settings(self): """Update detector settings. """ self.obj.set_settings(**self.get_properties()) # Detector foils self.calculate_distance() self.obj.foils = self.tmp_foil_info # Tof foils self.obj.tof_foils = self.tof_foils def some_values_changed(self): """Check if any detector settings values have been changed. Return: True or False. """ if self.values_changed(): return True if self.other_values_changed(): return True return False def values_changed(self): """Check if detector settings values that require rerunning of the simulation have been changed. Return: True or False. """ if self.are_values_changed(): return True # TODO refactor foils # Detector foils self.calculate_distance() if self.foils_changed(): return True # Tof foils if self.obj.tof_foils != self.tof_foils: return True return False def other_values_changed(self): """Check if detector settings values that don't require rerunning simulations have been changed. Return: True or False. """ if self.obj.name != self.name: return True if self.obj.description != self.description: return True return False def foils_changed(self): """Check if detector foils have been changed. Return: True or False. """ if len(self.obj.foils) != len(self.tmp_foil_info): return True for i in range(len(self.obj.foils)): foil = self.obj.foils[i] tmp_foil = self.tmp_foil_info[i] if type(foil) != type(tmp_foil): return True if foil.name != tmp_foil.name: return True if foil.distance != tmp_foil.distance: return True if foil.transmission != tmp_foil.transmission: return True # Check layers if self.layers_changed(foil, tmp_foil): return True if type(foil) is CircularFoil: if foil.diameter != tmp_foil.diameter: return True else: if foil.size != tmp_foil.size: return True return False def layers_changed(self, foil1, foil2): """Check if foil1 has different layers than foil2. Args: foil1: Foil object. foil2: Foil object. Return: True or False. """ if len(foil1.layers) != len(foil2.layers): return True for i in range(len(foil1.layers)): layer1 = foil1.layers[i] layer2 = foil2.layers[i] if layer1.name != layer2.name: return True if layer1.thickness != layer2.thickness: return True if layer1.density != layer2.density: return True if layer1.start_depth != layer2.start_depth: return True # Check layer elements if self.layer_elements_changed(layer1, layer2): return True return False @staticmethod def layer_elements_changed(layer1, layer2): """Check if layer1 elements are different than layer2 elements. Args: layer1: Layer object. layer2: Layer object. Return: True or False. """ if len(layer1.elements) != len(layer2.elements): return True for i in range(len(layer1.elements)): elem1 = layer1.elements[i] elem2 = layer2.elements[i] if elem1 != elem2: return True return False def _add_new_foil(self, layout, new_foil=None): """Add a new foil into detector. Args: layout: Layout into which the foil widget is added. new_foil: New Foil object to be added. """ if self.tmp_foil_info: prev_distance = self.tmp_foil_info[-1].distance else: prev_distance = 0.0 if new_foil is None: new_foil = CircularFoil(distance=prev_distance) foil_widget = FoilWidget(new_foil) foil_widget.distance_from_previous = new_foil.distance - prev_distance foil_widget.distanceDoubleSpinBox.valueChanged.connect( self.calculate_distance) foil_widget.foil_deletion.connect(self.delete_foil) foil_widget.foilButton.clicked.connect(self._open_foil_dialog) foil_widget.timingFoilCheckBox.stateChanged.connect( self._check_and_add) layout.addWidget(foil_widget) self.tmp_foil_info.append(new_foil) self.detector_structure_widgets.append(foil_widget) if len(self.tof_foils) >= 2: foil_widget.timingFoilCheckBox.setEnabled(False) return foil_widget def _add_default_foils(self, detector: Detector): """Add default foils as widgets. Return: Layout that holds the default foil widgets. """ layout = QtWidgets.QHBoxLayout() foils = detector.foils for i in range(len(foils)): foil_widget = self._add_new_foil(layout, foils[i]) for index in detector.tof_foils: if index == i: foil_widget.timingFoilCheckBox.setChecked(True) return layout def _check_and_add(self): """Check if foil widget needs to be added into tof_foils list or removed from it. """ check_box = self.sender() for i in range(len(self.detector_structure_widgets)): if self.detector_structure_widgets[i].timingFoilCheckBox is \ self.sender(): if check_box.isChecked(): if self.tof_foils: if self.tof_foils[0] > i: self.tof_foils.insert(0, i) else: self.tof_foils.append(i) if len(self.tof_foils) >= 2: self._disable_checkboxes() else: self.tof_foils.append(i) else: self.tof_foils.remove(i) if 0 < len(self.tof_foils) < 2: self._enable_checkboxes() break def _disable_checkboxes(self): """Disable selection of foil widgets as tof foil if they are not in the tof_foils list. """ for i in range(len(self.detector_structure_widgets)): if i not in self.tof_foils: widget = self.detector_structure_widgets[i] widget.timingFoilCheckBox.setEnabled(False) def _enable_checkboxes(self): """Allow all foil widgets to be selected as tof foil. """ for i in range(len(self.detector_structure_widgets)): widget = self.detector_structure_widgets[i] widget.timingFoilCheckBox.setEnabled(True) def _enable_remove_btn(self): """Sets the remove button either enabled or not depending whether an item is selected in the efficiency file list. """ self.removeEfficiencyButton.setEnabled( bool(self.efficiencyListWidget.currentItem())) def _open_foil_dialog(self): """Open the FoilDialog which is used to modify the Foil object. """ foil_name = self.sender().text() foil_object_index = -1 for i in range(len(self.tmp_foil_info)): if foil_name == self.tmp_foil_info[i].name: foil_object_index = i break FoilDialog(self.tmp_foil_info, foil_object_index, self.icon_manager) self.sender().setText(self.tmp_foil_info[foil_object_index].name) def __add_efficiency(self): """Opens a dialog that allows the user to add efficiency files to the Efficiency_files folder of the detector. """ eff_folder = gutils.get_potku_setting( DetectorSettingsWidget.EFF_FILE_FOLDER_KEY, self.request.default_folder) new_eff_files = fdialogs.open_files_dialog(self, eff_folder, "Select efficiency files", "Efficiency File (*.eff)") if not new_eff_files: return used_eff_files = { Detector.get_used_efficiency_file_name(f) for f in self.efficiency_files } for eff_file in new_eff_files: used_eff_file = Detector.get_used_efficiency_file_name(eff_file) if used_eff_file not in used_eff_files: try: self.obj.add_efficiency_file(eff_file) used_eff_files.add(used_eff_file) except OSError as e: QtWidgets.QMessageBox.critical( self, "Error", f"Failed to add the efficiency file: {e}\n", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok) else: QtWidgets.QMessageBox.critical( self, "Error", f"There already is an efficiency file for element " f"{used_eff_file.stem}.\n", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok) self.efficiency_files = self.obj.get_efficiency_files() # Store the folder where we previously fetched an eff-file gutils.set_potku_setting(DetectorSettingsWidget.EFF_FILE_FOLDER_KEY, str(eff_file.parent)) def __remove_efficiency(self): """Removes efficiency files from detector's efficiency directory and updates settings view. """ self.efficiencyListWidget: QtWidgets.QListWidget selected_items = self.efficiencyListWidget.selectedItems() if selected_items: reply = QtWidgets.QMessageBox.question( self, "Confirmation", "Are you sure you want to delete selected efficiencies?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) if reply == QtWidgets.QMessageBox.No or reply == \ QtWidgets.QMessageBox.Cancel: return for item in selected_items: selected_eff_file = item.data(QtCore.Qt.UserRole) self.obj.remove_efficiency_file(selected_eff_file) self.efficiency_files = self.obj.get_efficiency_files() self._enable_remove_btn() def __plot_efficiency(self): """ Open efficiency plot widget """ self.eff_folder = gutils.get_potku_setting( DetectorSettingsWidget.EFF_FILE_FOLDER_KEY, self.request.default_folder) self.efficiency_files = self.obj.get_efficiency_files() self.efficiency_files_list = [] i = 0 for file in self.efficiency_files: file_name = gf.get_root_dir() / self.eff_folder / str( self.efficiency_files[i]) self.efficiency_files_list.append(file_name) i += 1 EfficiencyDialog(self.efficiency_files_list, self) def __open_calibration_dialog(self): """ Open the CalibrationDialog. """ measurements = [ self.request.samples.measurements.get_key_value(key) for key in self.request.samples.measurements.measurements ] CalibrationDialog(measurements, self.obj, self.run, self) def calculate_distance(self): """ Calculate the distances of the foils from the target. """ distance = 0 for foil, widget in zip(self.tmp_foil_info, self.detector_structure_widgets): distance += widget.distance_from_previous widget.cumulative_distance = distance foil.distance = distance def delete_foil(self, foil_widget): """Delete a foil from widgets and Foil objects. Args: foil_widget: Widget to be deleted. Its index is used to delete Foil objects as well. """ index_of_item_to_be_deleted = self.detector_structure_widgets.index( foil_widget) del (self.detector_structure_widgets[index_of_item_to_be_deleted]) foil_to_be_deleted = self.tmp_foil_info[index_of_item_to_be_deleted] # Check if foil to be deleted is in tof_foils and remove it fro the # tof_foils list if it is. if index_of_item_to_be_deleted in self.tof_foils: self.tof_foils.remove(index_of_item_to_be_deleted) if 0 < len(self.tof_foils) < 2: self._enable_checkboxes() self.tmp_foil_info.remove(foil_to_be_deleted) for i in range(len(self.tof_foils)): if self.tof_foils[i] > index_of_item_to_be_deleted: self.tof_foils[i] = self.tof_foils[i] - 1 self.foils_layout.removeWidget(foil_widget) foil_widget.deleteLater() self.calculate_distance()
class GlobalSettingsDialog(QtWidgets.QDialog): """ A GlobalSettingsDialog. """ color_scheme = bnd.bind("combo_tofe_colors") tofe_invert_x = bnd.bind("check_tofe_invert_x") tofe_invert_y = bnd.bind("check_tofe_invert_y") tofe_transposed = bnd.bind("check_tofe_transpose") tofe_bin_x = bnd.multi_bind( ("spin_tofe_bin_x_min", "spin_tofe_bin_x_max") ) tofe_bin_y = bnd.multi_bind( ("spin_tofe_bin_y_min", "spin_tofe_bin_y_max") ) comp_x = bnd.bind("spin_tofe_compression_x") comp_y = bnd.bind("spin_tofe_compression_y") depth_iters = bnd.bind("spin_depth_iterations") presim_ions = bnd.bind("presim_spinbox") sim_ions = bnd.bind("sim_spinbox") ion_division = bnd.bind("ion_division_radios") min_concentration = bnd.bind("min_conc_spinbox") coinc_count = bnd.bind("line_coinc_count") cross_section = bnd.bind("cross_section_radios") save_window_geometries = bnd.bind("window_geom_chkbox") settings_updated = QtCore.pyqtSignal(GlobalSettings) def __init__(self, settings: GlobalSettings): """Constructor for the program """ super().__init__() uic.loadUi(gutils.get_ui_dir() / "ui_global_settings.ui", self) self.settings = settings self.__added_timings = {} # Placeholder for timings self.setAttribute(QtCore.Qt.WA_DeleteOnClose) gutils.set_min_max_handlers( self.spin_tofe_bin_x_min, self.spin_tofe_bin_x_max ) gutils.set_min_max_handlers( self.spin_tofe_bin_y_min, self.spin_tofe_bin_y_max ) gutils.set_btn_group_data(self.ion_division_radios, IonDivision) gutils.set_btn_group_data(self.cross_section_radios, CrossSection) gutils.fill_combobox(self.combo_tofe_colors, ToFEColorScheme) # Connect UI buttons self.OKButton.clicked.connect(self.__accept_changes) self.cancelButton.clicked.connect(self.close) buttons = self.findChild(QtWidgets.QButtonGroup, "elementButtons") buttons.buttonClicked.connect(self.__change_element_color) self.min_conc_spinbox.setLocale(QtCore.QLocale.c()) if platform.system() == "Darwin": self.gridLayout.setVerticalSpacing(15) self.gridLayout.setHorizontalSpacing(15) self.__set_values() def closeEvent(self, event): """Disconnects settings updated signal before closing. """ try: self.settings_updated.disconnect() except AttributeError: pass super().closeEvent(event) def __set_values(self): """Set settings values to dialog. """ for button in self.groupBox_3.findChildren(QtWidgets.QPushButton): self.set_btn_color( button, self.settings.get_element_color(button.text())) label_adc = QtWidgets.QLabel("ADC") label_low = QtWidgets.QLabel("Low") label_high = QtWidgets.QLabel("High") self.grid_timing.addWidget(label_adc, 0, 0) self.grid_timing.addWidget(label_low, 1, 0) self.grid_timing.addWidget(label_high, 2, 0) for i in range(3): timing = self.settings.get_import_timing(i) label = QtWidgets.QLabel(f"{i}") spin_low = self.__create_spinbox(timing[0]) spin_high = self.__create_spinbox(timing[1]) self.__added_timings[i] = CoincTiming(i, spin_low, spin_high) self.grid_timing.addWidget(label, 0, i + 1) self.grid_timing.addWidget(spin_low, 1, i + 1) self.grid_timing.addWidget(spin_high, 2, i + 1) self.coinc_count = self.settings.get_import_coinc_count() # self.__set_cross_sections() self.cross_section = self.settings.get_cross_sections() # ToF-E graph settings self.tofe_invert_x = self.settings.get_tofe_invert_x() self.tofe_invert_y = self.settings.get_tofe_invert_y() self.tofe_transposed = self.settings.get_tofe_transposed() # TODO binding for bin mode tofe_bin_mode = self.settings.get_tofe_bin_range_mode() self.radio_tofe_bin_auto.setChecked(tofe_bin_mode == 0) self.radio_tofe_bin_manual.setChecked(tofe_bin_mode == 1) self.tofe_bin_x = self.settings.get_tofe_bin_range_x() self.tofe_bin_y = self.settings.get_tofe_bin_range_y() self.comp_x = self.settings.get_tofe_compression_x() self.comp_y = self.settings.get_tofe_compression_y() self.depth_iters = self.settings.get_num_iterations() self.presim_ions = self.settings.get_min_presim_ions() self.sim_ions = self.settings.get_min_simulation_ions() self.ion_division = self.settings.get_ion_division() self.min_conc_spinbox.setMinimum(GlobalSettings.MIN_CONC_LIMIT) self.min_concentration = self.settings.get_minimum_concentration() self.save_window_geometries = gutils.get_potku_setting( BaseTab.SAVE_WINDOW_GEOM_KEY, True ) self.color_scheme = self.settings.get_tofe_color() @staticmethod def __create_spinbox(default): """ Create a spinbox. """ spinbox = QtWidgets.QSpinBox() spinbox.stepBy(1) spinbox.setMinimum(-1000) spinbox.setMaximum(1000) spinbox.setValue(int(default)) return spinbox def __accept_changes(self): """Accept changed settings and save. """ for button in self.groupBox_3.findChildren(QtWidgets.QPushButton): self.settings.set_element_color(button.text(), button.color) for key, coinc_timing in self.__added_timings.items(): self.settings.set_import_timing( key, coinc_timing.low.value(), coinc_timing.high.value()) self.settings.set_import_coinc_count(self.coinc_count) self.settings.set_cross_sections(self.cross_section) # ToF-E graph settings self.settings.set_tofe_invert_x(self.tofe_invert_x) self.settings.set_tofe_invert_y(self.tofe_invert_y) self.settings.set_tofe_transposed(self.tofe_transposed) self.settings.set_tofe_color(self.color_scheme) if self.radio_tofe_bin_auto.isChecked(): self.settings.set_tofe_bin_range_mode(0) elif self.radio_tofe_bin_manual.isChecked(): self.settings.set_tofe_bin_range_mode(1) self.settings.set_tofe_bin_range_x(*self.tofe_bin_x) self.settings.set_tofe_bin_range_y(*self.tofe_bin_y) self.settings.set_tofe_compression_x(self.comp_x) self.settings.set_tofe_compression_y(self.comp_y) self.settings.set_num_iterations(self.depth_iters) self.settings.set_min_presim_ions(self.presim_ions) self.settings.set_min_simulation_ions(self.sim_ions) self.settings.set_ion_division(self.ion_division) self.settings.set_minimum_concentration(self.min_concentration) gutils.set_potku_setting( BaseTab.SAVE_WINDOW_GEOM_KEY, self.save_window_geometries) # Save config and close self.settings.save_config() self.settings_updated.emit(self.settings) self.close() def __change_request_directory(self): """Change default request directory. """ folder = QtWidgets.QFileDialog.getExistingDirectory( self, "Select default request directory", directory=self.requestPathLineEdit.text()) if folder: self.requestPathLineEdit.setText(folder) def __change_element_color(self, button): """Change color of element button. Args: button: QPushButton """ dialog = QtWidgets.QColorDialog(self) self.color = dialog.getColor( QtGui.QColor(button.color), self, f"Select Color for Element: {button.text()}") if self.color.isValid(): self.set_btn_color(button, self.color.name()) @staticmethod def set_btn_color(button, color_name): """Change button text color. Args: button: QPushButton color_name: String representing color. """ color = QtGui.QColor(color_name) style = df.get_btn_stylesheet(color) button.color = color.name() if not button.isEnabled(): return # Do not set color for disabled buttons. button.setStyleSheet(style)
class MeasurementSettingsDialog(QtWidgets.QDialog): """ Dialog class for handling the measurement parameter input. """ use_request_settings = bnd.bind("defaultSettingsCheckBox") def __init__(self, tab, measurement: Measurement, icon_manager): """ Initializes the dialog. Args: measurement: A Measurement object whose parameters are handled. icon_manager: An icon manager. """ super().__init__() uic.loadUi(gutils.get_ui_dir() / "ui_specific_settings.ui", self) self.warning_text = bnd.bind('warning_text') self.tab = tab self.measurement = measurement self.icon_manager = icon_manager self.setWindowTitle("Measurement Settings") self.setAttribute(QtCore.Qt.WA_DeleteOnClose) screen_geometry = QtWidgets.QDesktopWidget.availableGeometry( QtWidgets.QApplication.desktop()) self.resize(int(self.geometry().width() * 1.2), int(screen_geometry.size().height() * 0.8)) self.defaultSettingsCheckBox.stateChanged.connect( self._change_used_settings) self.OKButton.clicked.connect(self._save_settings_and_close) self.applyButton.clicked.connect(self._update_parameters) self.cancelButton.clicked.connect(self.close) preset_folder = gutils.get_preset_dir( self.measurement.request.global_settings) # Add measurement settings view to the settings view self.measurement_settings_widget = MeasurementSettingsWidget( self.measurement, preset_folder=preset_folder) self.tabs.addTab(self.measurement_settings_widget, "Measurement") self.measurement_settings_widget.beam_selection_ok.connect( self.OKButton.setEnabled) # Add detector settings view to the settings view self.detector_settings_widget = DetectorSettingsWidget( self.measurement.detector, self.measurement.request, self.icon_manager, self.measurement_settings_widget.tmp_run) self.tabs.addTab(self.detector_settings_widget, "Detector") self.use_request_settings = self.measurement.use_request_settings # TODO these should be set in the widget, not here self.measurement_settings_widget.nameLineEdit.setText( self.measurement.measurement_setting_file_name) self.measurement_settings_widget.descriptionPlainTextEdit.setPlainText( self.measurement.measurement_setting_file_description) self.measurement_settings_widget.dateLabel.setText( time.strftime("%c %z %Z", time.localtime(self.measurement.modification_time))) # Add profile settings view to the settings view self.profile_settings_widget = ProfileSettingsWidget( self.measurement, preset_folder=preset_folder) self.tabs.addTab(self.profile_settings_widget, "Profile") self.tabs.currentChanged.connect(lambda: df.check_for_red(self)) self.exec() def _change_used_settings(self): check_box = self.sender() if check_box.isChecked(): self.tabs.setEnabled(False) else: self.tabs.setEnabled(True) def _remove_extra_files(self): gf.remove_matching_files(self.measurement.directory, exts={".measurement", ".profile", ".target"}) gf.remove_matching_files(self.measurement.directory / "Detector", exts={".detector"}) def _update_parameters(self): if self.measurement_settings_widget.isotopeComboBox.currentIndex() \ == -1: QtWidgets.QMessageBox.critical( self, "Warning", "No isotope selected.\n\nPlease select an isotope for the beam " "element.", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok) return False if not self.measurement.measurement_setting_file_name: self.measurement.measurement_setting_file_name = \ self.measurement.name # Copy request settings without checking their validity. They # have been checked once in request settings anyway. if self.use_request_settings: self.measurement.use_request_settings = True # Remove measurement-specific efficiency files if self.measurement.detector is not \ self.measurement.request.default_detector: self.measurement.detector.remove_efficiency_files() self.measurement.clone_request_settings() self._remove_extra_files() self.measurement.to_file() return True # Check the target and detector angles if not self.measurement_settings_widget.check_angles(): return False if not self.tabs.currentWidget().fields_are_valid: QtWidgets.QMessageBox.critical( self, "Warning", "Some of the setting values have not been set.\n" "Please input values in fields indicated in red.", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok) return False # Use Measurement specific settings try: self.measurement.use_request_settings = False # Set Detector object to settings widget self.detector_settings_widget.obj = self.measurement.detector # Update settings self.measurement_settings_widget.update_settings() self.detector_settings_widget.update_settings() self.profile_settings_widget.update_settings() self._remove_extra_files() self.measurement.to_file() return True except TypeError: QtWidgets.QMessageBox.question( self, "Warning", "Some of the setting values have not been set.\n" "Please input setting values to save them.", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok) return False def _save_settings_and_close(self): """ Save settings and close dialog if __update_parameters returns True. """ if self._update_parameters(): self.tab.check_default_settings_clicked() self.close()