def on_calculation_exited(self, success: bool): """ This is called after calculation thread has exited. @param success: True if calculation was successful, False otherwise """ calculation_time = time.monotonic() - self.calculation_start_time if self.calculation_thread is not None: if self.calculation_thread.isRunning(): # Skipping because another thread is now running # Note: This happens all the time when calculation is interrupted and restarted through ModelAccess; # we see this because there is no reliable way to revoke the delayed "calculation_exited" signal # after another thread has already been started return else: # This happens when calculation finished and no other thread was started self.calculation_thread = None Debug( self, f".on_calculation_exited(): Success (took {calculation_time:.2f} s)", color=Theme.SuccessColor) else: Debug( self, f".on_calculation_exited(): Interrupted after {calculation_time:.2f} s", color=Theme.PrimaryColor) # Note: For some reason, most of the time we need an additional ("final-final") re-draw here; VisPy glitch? self.redraw() self.statusbar.disarm(success)
def recalculate(self): """ Re-calculates the model. """ Debug(self, ".recalculate()", color=Theme.SuccessColor) if self.calculation_thread is not None: Debug( self, ".recalculate(): WARNING: Killing orphaned calculation thread", color=Theme.WarningColor, force=True) self.interrupt_calculation() if self.initializing: self.initializing = False self.vispy_canvas.initializing = True self.redraw() self.statusbar.arm() # Create a new calculation thread and kick it off self.calculation_thread = CalculationThread(self) self.calculation_start_time = time.monotonic() self.calculation_thread.start()
def __init__(self, norm_id: str, comparison_id: str, _min: float, _max: float, permeability: float): """ Initializes the constraint. @param norm_id: Norm ID @param comparison_id: Comparison ID @param _min: Minimum value @param _max: Maximum value @param permeability: Relative permeability µ_r """ if norm_id not in self.Norm_ID_List: Debug(self, "Invalid norm ID", color=Theme.WarningColor, force=True) return if comparison_id not in self.Comparison_ID_List: Debug(self, "Invalid comparison ID", color=Theme.WarningColor, force=True) return self._is_angle = norm_id in self.Norm_ID_List_Degrees self._norm_id = norm_id self._comparison_id = comparison_id self._min = _min self._max = _max self.permeability = permeability
def on_selection_changed(self, _selected, _deselected): """ Gets called when the selection changed. @param _selected: Currently selected QItemSelection @param _deselected: Currently deselected QItemSelection """ if self.signalsBlocked(): Debug(self, f".on_selection_changed(): Blocked") return Debug(self, f".on_selection_changed()") self._selection_changed_callback()
def focusOutEvent(self, _event): """ Gets called when the table lost focus, or when a cell item is being edited, or when a cell widget is selected. When not editing, this clears the selection, triggering L{on_selection_changed} @param _event: Event """ if self.state() == QAbstractItemView.EditingState: Debug(self, f".focusOutEvent(): Ignored in editing mode") elif self.is_cell_widget_selected(): Debug(self, f".focusOutEvent(): Ignored for cell widget") else: Debug(self, f".focusOutEvent(): Clearing selection") self.clearSelection() self.set_style(border_color="black", border_width=1)
def _set_rotational_symmetry(self, parameters): """ This transformation replicates and rotates this curve `count` times about an `axis` with radius `radius`. Note: Intended to be called from the class constructor (doesn't automatically invalidate the wire) @param parameters: Dictionary containing the transformation parameters (number of replications, radius, axis and offset angle) """ Debug(self, "._set_rotational_symmetry()") axes = self.get_points_transformed().transpose() x, y, z = [], [], [] axis_other_1 = (parameters["axis"] + 1) % 3 axis_other_2 = (parameters["axis"] + 2) % 3 for a in np.linspace(0, 2 * np.pi, parameters["count"], endpoint=False): b = a + parameters["offset"] * np.pi / 180 x = np.append(x, axes[axis_other_1] * np.sin(b) - (axes[axis_other_2] + parameters["radius"]) * np.cos(b)) y = np.append(y, axes[axis_other_1] * np.cos(b) + (axes[axis_other_2] + parameters["radius"]) * np.sin(b)) z = np.append(z, axes[parameters["axis"]]) axes = [x, y, z] # Close the resulting loop for i in range(3): axes[i] = np.append(axes[i], axes[i][0]) self._points_transformed = np.array(axes).transpose() return self
def reinitialize(self): """ Re-initializes the widget. """ Debug(self, ".reinitialize()") self.blockSignals(True) for i in range(3): self.stretch_spinbox[i].setValue( self.gui.config.get_point("wire_stretch")[i]) self.rotational_symmetry_count_spinbox.setValue( self.gui.config.get_float("rotational_symmetry_count")) self.rotational_symmetry_radius_spinbox.setValue( self.gui.config.get_float("rotational_symmetry_radius")) for i, axis in enumerate(["X", "Y", "Z"]): if i == self.gui.config.get_int("rotational_symmetry_axis"): self.rotational_symmetry_axis_combobox.setCurrentIndex(i) self.rotational_symmetry_offset_spinbox.setValue( self.gui.config.get_float("rotational_symmetry_offset")) self.slicer_limit_spinbox.setValue( self.gui.config.get_float("wire_slicer_limit")) self.dc_spinbox.setValue(self.gui.config.get_float("wire_dc")) self.blockSignals(False) # Initially load wire from configuration self.set_wire(recalculate=False, readjust_sampling_volume=False, invalidate_self=False)
def clear_rows(self): """ Clears all table rows. """ Debug(self, f".clear_rows()") self.setRowCount(0)
def invalidate(self): """ Resets data, hiding from display. """ Debug(self, ".invalidate()", color=(128, 0, 0)) self._points_sliced = None self._length = None
def invalidate(self): """ Resets data, hiding from display. """ Debug(self, ".invalidate()", color=(128, 0, 0)) self._colors = None self._limits = None
def add_constraint(self, constraint): """ Adds some constraint to this volume's point generator. @param constraint: Constraint """ Debug(self, f".add_constraint()") self.constraints.append(constraint)
def set_defaults(self): """ Sets the default key-value pairs. Creates empty "User" section if not present. """ self._config["DEFAULT"] = Config.Default if "User" not in self._config: Debug(self, ".set_defaults(): Creating empty User section") self._config["User"] = {}
def __init__(self): """ Initializes parameters class. """ Debug(self, ": Init") self._energy = None self._self_inductance = None self._magnetic_dipole_moment = None
def invalidate(self): """ Resets data, hiding from display. """ Debug(self, ".invalidate()", color=(128, 0, 0)) self._energy = None self._self_inductance = None self._magnetic_dipole_moment = None
def select_cell(self, row=None, column=None): """ Selects a cell. Any parameter may be left set to None in order to load its value from the selection model. @param row: Row @param column: Column """ if row is None: row = self.selectionModel().currentIndex().row() if column is None: column = self.selectionModel().currentIndex().column() if row == -1 or column == -1: Debug(self, f".select_cell({row}, {column}): WARNING: Skipped", color=Theme.WarningColor, force=True) return item = self.item(row, column) if item is None: # Select cell widget Debug(self, f".select_cell({row}, {column}): Selecting cell widget") # widget = self.cellWidget(row, column) self.blockSignals(True) self.setCurrentCell(row, column) self.setFocus() self.blockSignals(False) else: # Select cell item Debug(self, f".select_cell({row}, {column}): Selecting cell item") item.setSelected(True) self.scrollToItem(item, QAbstractItemView.PositionAtCenter) self.selectionModel().setCurrentIndex( self.selectedIndexes()[0], QItemSelectionModel.SelectCurrent)
def get_result(self): """ Calculates the field at every point of the sampling volume. @return: (Total number of limited points, field) if successful, None if interrupted """ Debug(self, ".get_result()", color=Theme.PrimaryColor) total_limited = 0 vectors = [] # Fetch resulting vectors for i in range(len(self._sampling_volume_points)): tup = BiotSavart_JIT.worker(self._type, self._distance_limit, self._length_scale, self._current_elements, self._sampling_volume_points[i]) total_limited += tup[0] vector = tup[1] * self._sampling_volume_permeabilities[i] vectors.append(vector) # Signal progress update, handle interrupt (every 16 iterations to keep overhead low) if i & 0xf == 0: self._progress_callback(100 * (i + 1) / len(self._sampling_volume_points)) if QThread.currentThread().isInterruptionRequested(): Debug(self, ".get_result(): Interruption requested, exiting now", color=Theme.PrimaryColor) return None if self._type == 0 or self._type == 1: # Field is A-field or B-field vectors = np.array(vectors) * self._dc * Constants.mu_0 / 4 / np.pi self._progress_callback(100) return total_limited, vectors
def on_row_deleted(self, row: int): """ Gets called when a row was deleted. @param row: Row """ Debug(self, f".on_row_deleted({row})") self._row_deleted_callback(row) self.select_last_row()
def calculate_parameters(self, progress_callback): """ Calculates the parameters. @param progress_callback: Progress callback @return: True (currently non-interruptable) """ Debug(self, ".calculate_parameters()", color=Theme.PrimaryColor) return self.parameters.recalculate(self.wire, self.sampling_volume, self.field, progress_callback)
def calculate_wire(self, progress_callback): """ Calculates the wire. @param progress_callback: Progress callback @return: True if successful, False if interrupted """ Debug(self, ".calculate_wire()", color=Theme.PrimaryColor) self.invalidate(do_sampling_volume=True, do_field=True, do_metric=True) return self.wire.recalculate(progress_callback)
def invalidate(self): """ Resets data, hiding from display. """ Debug(self, ".invalidate()", color=(128, 0, 0)) self._points = None self._permeabilities = None self._labeled_indices = None self._neighbor_indices = None
def set_filename(self, filename: str): """ Sets the filename for the current session. @param filename: Filename """ self._filename = os.path.join( os.path.dirname(os.path.dirname(__file__)), filename) Debug(self, ".set_filename: file://" + self._filename.replace(" ", "%20"), force=True)
def reinitialize(self): """ Re-initializes the constraint editor. """ Debug(self, ".reinitialize()") # Initially load the constraints self.reload_constraints() self.update_table() self.clear_changed()
def quit(self): """ Quits the application. """ if self.calculation_thread != QThread.currentThread(): Debug(self, ".quit()") if self.calculation_thread is not None: self.interrupt_calculation() else: Debug( self, ".quit(): Called from calculation thread (assertion failed)") self.config.close() print() print("Goodbye!") # Unregister exit handler (used by Assert_Dialog to exit gracefully) atexit.unregister(self.quit)
def on_combobox_cell_edited(self, combobox, row, column): """ Gets called when a combobox cell has been edited. @param combobox: QCombobox @param row: Row @param column: Column """ Debug(self, f".on_combobox_cell_edited()") self._cell_edited_callback(combobox.currentText(), row, column)
def remove_key(self, key): """ Removes a key from the configuration. @param key: Key """ if not self._config.remove_option("User", key): Debug(self, f".remove_key({key}): WARNING: No such key", color=Theme.WarningColor, force=True)
def interrupt_calculation(self): """ Kills any running calculation. """ if self.calculation_thread is None: Debug( self, ".interrupt_calculation: WARNING: No calculation thread to interrupt", color=Theme.WarningColor, force=True) return if self.calculation_thread.isRunning(): Debug(self, ".interrupt_calculation(): Requesting interruption", color=Theme.PrimaryColor) self.calculation_thread.requestInterruption() if self.calculation_thread.wait(5000): Debug(self, ".interrupt_calculation(): Exited gracefully", color=Theme.PrimaryColor) else: Assert_Dialog(False, "Failed to terminate calculation thread") if self.calculation_thread is not None: if self.calculation_thread.isRunning(): Debug( self, ".interrupt_calculation(): WARNING: Terminating ungracefully", color=Theme.WarningColor, force=True) self.calculation_thread.terminate() self.calculation_thread.wait() else: Debug( self, ".interrupt_calculation: WARNING: Calculation thread should be running", color=Theme.WarningColor, force=True) self.calculation_thread = None
def recalculate(self, progress_callback) -> bool: """ Slices wire segments into smaller ones until segment lengths equal or undershoot slicer limit. @param progress_callback: Progress callback @return: True if successful, False if interrupted """ Debug(self, ".recalculate()", color=Theme.SuccessColor) points_sliced = [] length = 0 for i in range(len(self.get_points_transformed()) - 1): # Calculate direction and length of wire segment segment_direction = np.array(self.get_points_transformed()[i + 1] - self.get_points_transformed()[i]) segment_length = np.linalg.norm(segment_direction) length += segment_length # Calculate required number of slices (subdivisions) and perform linear interpolation slices = np.ceil(segment_length / self._slicer_limit).astype(int) linear = np.linspace(0, 1, slices, endpoint=False) points_sliced += [self.get_points_transformed()[i] + segment_direction * j for j in linear] # Signal progress update, handle interrupt (every 16 iterations to keep overhead low) if i & 0xf == 0: progress_callback(100 * (i + 1) / (len(self.get_points_transformed()) - 1)) if QThread.currentThread().isInterruptionRequested(): Debug(self, ".recalculate(): Interruption requested, exiting now", color=Theme.PrimaryColor) return False # Close the loop points_sliced.append(self.get_points_transformed()[-1]) self._points_sliced = np.array(points_sliced) self._length = length progress_callback(100) return True
def calculate_sampling_volume(self, label_resolution, progress_callback): """ Calculates the sampling volume. @param label_resolution: Label resolution @param progress_callback: Progress callback @return: True if successful, False if interrupted """ Debug(self, ".calculate_sampling_volume()", color=Theme.PrimaryColor) self.invalidate(do_field=True, do_metric=True) return self.sampling_volume.recalculate(label_resolution, progress_callback)
def save(self): """ Saves the configuration to file. """ Debug(self, ".save()", force=True) with open(self._filename, "w") as file: self._config.write(file) self._synced = True if self._changed_callback is not None: self._changed_callback()
def get_str(self, key: str) -> str: """ Reads a configuration value. Key must be in "Default" section and may be overridden in "User" section. @param key: Key @return: Value """ if self.DebugGetters: Debug(self, f".get_str({key})") value = self._config.get("User", key) return value