From 7831bd94fe8ac9f1fdf3606d0a1cd69653c66ffc Mon Sep 17 00:00:00 2001 From: Andi Date: Thu, 21 Nov 2019 21:34:26 +0100 Subject: [PATCH 01/11] Added Aperture plus needed controls to control it. --- nionswift_plugin/usim/InstrumentDevice.py | 12 ++- .../usim/RonchigramCameraSimulator.py | 92 ++++++++++++++++++- 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/nionswift_plugin/usim/InstrumentDevice.py b/nionswift_plugin/usim/InstrumentDevice.py index a8a08b6..10f9f6a 100644 --- a/nionswift_plugin/usim/InstrumentDevice.py +++ b/nionswift_plugin/usim/InstrumentDevice.py @@ -336,6 +336,11 @@ def __create_built_in_controls(self): zlp_tare_control = Control("ZLPtare") zlp_offset_control = Control("ZLPoffset", -20, [(zlp_tare_control, 1.0)]) stage_position_m = Control2D("stage_position_m", ("x", "y")) + # VOA controls + c_aperture_offset = Control2D("CApertureOffset", ("x", "y")) + c_aperture = Control2D("CAperture", ("x", "y"), weighted_inputs=([(c_aperture_offset.x, 1.0)], [(c_aperture_offset.y, 1.0)])) + aperture_round = Control2D("ApertureRound", ("x", "y")) + s_voa = Control("S_VOA") c10 = Control("C10", 500 / 1e9) c12 = Control2D("C12", ("x", "y")) c21 = Control2D("C21", ("x", "y")) @@ -364,10 +369,11 @@ def __create_built_in_controls(self): c3_range = Control("C3Range") # dependent controls beam_shift_m_control = Control2D("beam_shift_m", ("x", "y"), (csh.x.output_value, csh.y.output_value), ([(csh.x, 1.0)], [(csh.y, 1.0)])) - return [stage_position_m, zlp_tare_control, zlp_offset_control, c10, c12, c21, c23, c30, c32, c34, c10Control, c12Control, - c21Control, c23Control, c30Control, c32Control, c34Control, csh, drift, + return [stage_position_m, zlp_tare_control, zlp_offset_control, c10, c12, c21, c23, c30, c32, c34, c10Control, + c12Control, c21Control, c23Control, c30Control, c32Control, c34Control, csh, drift, beam_shift_m_control, order_1_max_angle, order_2_max_angle, order_3_max_angle, order_1_patch, - order_2_patch, order_3_patch, c1_range, c2_range, c3_range] + order_2_patch, order_3_patch, c1_range, c2_range, c3_range, c_aperture, aperture_round, s_voa, + c_aperture_offset] @property def sample(self) -> SampleSimulator.Sample: diff --git a/nionswift_plugin/usim/RonchigramCameraSimulator.py b/nionswift_plugin/usim/RonchigramCameraSimulator.py index e61d52e..5ed64b3 100755 --- a/nionswift_plugin/usim/RonchigramCameraSimulator.py +++ b/nionswift_plugin/usim/RonchigramCameraSimulator.py @@ -1,4 +1,5 @@ # standard libraries +import typing import math import numpy import scipy.ndimage.interpolation @@ -214,12 +215,76 @@ def get_chi(coefficient_name): return r return numpy.zeros((height, width)) + + +def ellipse_radius(polar_angle: typing.Union[float, numpy.ndarray], a: float, b: float, rotation: float) -> typing.Union[float, numpy.ndarray]: + """ + Returns the radius of a point lying on an ellipse with the given parameters. The ellipse is described in polar + coordinates here, which makes it easy to incorporate a rotation. + + Parameters + ----------- + polar_angle : float or numpy.ndarray + Polar angle of a point to which the corresponding radius should be calculated (rad). + a : float + Length of the major half-axis of the ellipse. + b : float + Length of the minor half-axis of the ellipse. + rotation : Rotation of the ellipse with respect to the x-axis (rad). Counter-clockwise is positive. + + Returns + -------- + radius : float or numpy.ndarray + Radius of a point lying on an ellipse with the given parameters. + """ + + return a*b/numpy.sqrt((b*numpy.cos(polar_angle+rotation))**2+(a*numpy.sin(polar_angle+rotation))**2) + + +def draw_ellipse(image: numpy.ndarray, ellipse: typing.Tuple[float, float, float, float, float], *, + color: typing.Any=1.0) -> None: + """ + Draws an ellipse on a 2D-array. + + Parameters + ---------- + image : array + The array on which the ellipse will be drawn. Note that the data will be modified in place. + ellipse : tuple + A tuple describing an ellipse with the same moments as the aperture. The values must be (in this order): + [0] The y-coordinate of the center. + [1] The x-coordinate of the center. + [2] The length of the major half-axis + [3] The length of the minor half-axis + [4] The rotation of the ellipse in rad. + color : optional + The color to which the pixels inside the given ellipse will be set. Note that `color` will be cast to the + type of `image` automatically. If this is not possible, an exception will be raised. The default is 1.0. + + Returns + -------- + None + """ + shape = image.shape + assert len(shape) == 2, 'Can only draw an ellipse on a 2D-array.' + #coords = np.mgrid[-shape[0]/2:shape[0]/2:shape[0]*1j, -shape[1]/2:shape[1]/2:shape[1]*1j] + top = max(int(ellipse[0] - ellipse[2]), 0) + left = max(int(ellipse[1] - ellipse[2]), 0) + bottom = min(int(ellipse[0] + ellipse[2]) + 1, shape[0]) + right = min(int(ellipse[1] + ellipse[2]) + 1, shape[1]) + coords = numpy.mgrid[top-ellipse[0]:bottom-ellipse[0], left-ellipse[1]:right-ellipse[1]] + #coords[0] -= ellipse[0] + #coords[1] -= ellipse[1] + radii = numpy.sqrt(numpy.sum(coords**2, axis=0)) + polar_angles = numpy.arctan2(coords[0], coords[1]) + ellipse_radii = ellipse_radius(polar_angles, *ellipse[2:]) + image[top:bottom, left:right][radii DataAndMetadata.DataAndMetadata: # check if one of the arguments has changed since last call @@ -298,6 +386,8 @@ def get_frame_data(self, readout_area: Geometry.IntRect, binning_shape: Geometry aberrations["c34a"] = self.instrument.GetVal2D("C34Control").x aberrations["c34b"] = self.instrument.GetVal2D("C34Control").y data = self.__aberrations_controller.apply(aberrations, data) + if self.instrument.GetVal("S_VOA") > 0: + self._draw_aperture(data, binning_shape) intensity_calibration = Calibration.Calibration(units="counts") dimensional_calibrations = self.get_dimensional_calibrations(readout_area, binning_shape) From 769bd12a40d952993c10057afbb3fb20ab79dd68 Mon Sep 17 00:00:00 2001 From: Andi Date: Thu, 21 Nov 2019 21:56:38 +0100 Subject: [PATCH 02/11] Add UI elements to control aperture --- nionswift_plugin/usim/InstrumentPanel.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/nionswift_plugin/usim/InstrumentPanel.py b/nionswift_plugin/usim/InstrumentPanel.py index d8a156d..e252a2b 100644 --- a/nionswift_plugin/usim/InstrumentPanel.py +++ b/nionswift_plugin/usim/InstrumentPanel.py @@ -64,14 +64,14 @@ def close(self): class PositionWidget(Widgets.CompositeWidgetBase): - def __init__(self, ui, label: str, object, xy_property): + def __init__(self, ui, label: str, object, xy_property, unit="nm", multiplier=1E9): super().__init__(ui.create_row_widget()) stage_x_field = ui.create_line_edit_widget() - stage_x_field.bind_text(Control2DBinding(object, xy_property, "x", Converter.PhysicalValueToStringConverter("nm", 1E9))) + stage_x_field.bind_text(Control2DBinding(object, xy_property, "x", Converter.PhysicalValueToStringConverter(unit, multiplier))) stage_y_field = ui.create_line_edit_widget() - stage_y_field.bind_text(Control2DBinding(object, xy_property, "y", Converter.PhysicalValueToStringConverter("nm", 1E9))) + stage_y_field.bind_text(Control2DBinding(object, xy_property, "y", Converter.PhysicalValueToStringConverter(unit, multiplier))) row = self.content_widget @@ -132,6 +132,12 @@ def __init__(self, document_controller, instrument: InstrumentDevice.Instrument) slit_in_checkbox = ui.create_check_box_widget(_("Slit In")) slit_in_checkbox.bind_checked(Binding.PropertyBinding(instrument, "is_slit_in")) + + voa_in_checkbox = ui.create_check_box_widget(_("VOA In")) + voa_in_checkbox.bind_checked(ControlBinding(instrument, "S_VOA")) + + c_aperture_widget = PositionWidget(ui, _("CAperture"), instrument, "CAperture", unit="mrad", multiplier=1E3) + aperture_round_widget = PositionWidget(ui, _("ApertureRound"), instrument, "ApertureRound", unit="", multiplier=1) energy_offset_field = ui.create_line_edit_widget() energy_offset_field.bind_text(Binding.PropertyBinding(instrument, "energy_offset_eV", converter=Converter.FloatToStringConverter())) @@ -142,6 +148,8 @@ def __init__(self, document_controller, instrument: InstrumentDevice.Instrument) beam_row = ui.create_row_widget() beam_row.add_spacing(8) beam_row.add(blanked_checkbox) + beam_row.add_spacing(8) + beam_row.add(voa_in_checkbox) beam_row.add_stretch() eels_row = ui.create_row_widget() @@ -206,6 +214,8 @@ def __init__(self, document_controller, instrument: InstrumentDevice.Instrument) column.add(c32_widget) column.add(c34_widget) column.add(beam_row) + column.add(c_aperture_widget) + column.add(aperture_round_widget) column.add(eels_row) column.add_stretch() From 2266ccd27fe7fbc68ace032798de06fb48493e35 Mon Sep 17 00:00:00 2001 From: Andreas Date: Tue, 26 Nov 2019 18:43:26 +0100 Subject: [PATCH 03/11] Changed convergence angle to have a more realistic meaning. --- nionswift_plugin/usim/InstrumentDevice.py | 34 ++++++++++++------- .../usim/RonchigramCameraSimulator.py | 19 ++++++----- 2 files changed, 31 insertions(+), 22 deletions(-) mode change 100644 => 100755 nionswift_plugin/usim/InstrumentDevice.py diff --git a/nionswift_plugin/usim/InstrumentDevice.py b/nionswift_plugin/usim/InstrumentDevice.py old mode 100644 new mode 100755 index 10f9f6a..60e12e4 --- a/nionswift_plugin/usim/InstrumentDevice.py +++ b/nionswift_plugin/usim/InstrumentDevice.py @@ -297,11 +297,10 @@ def __init__(self, instrument_id: str): self.__sample_index = 0 # define the STEM geometry limits - self.stage_size_nm = 150 + self.stage_size_nm = 1000 self.max_defocus = 5000 / 1E9 self.__stage_position_m = Geometry.FloatPoint() - self.__convergence_angle_rad = 30 / 1000 self.__slit_in = False self.__energy_per_channel_eV = 0.5 self.__voltage = 100000 @@ -320,7 +319,7 @@ def __init__(self, instrument_id: str): self.add_control(control) self.__cameras = { - "ronchigram": RonchigramCameraSimulator.RonchigramCameraSimulator(self, self.__ronchigram_shape, self.counts_per_electron, self.__convergence_angle_rad), + "ronchigram": RonchigramCameraSimulator.RonchigramCameraSimulator(self, self.__ronchigram_shape, self.counts_per_electron, self.stage_size_nm), "eels": EELSCameraSimulator.EELSCameraSimulator(self, self.__eels_shape, self.counts_per_electron) } @@ -336,11 +335,23 @@ def __create_built_in_controls(self): zlp_tare_control = Control("ZLPtare") zlp_offset_control = Control("ZLPoffset", -20, [(zlp_tare_control, 1.0)]) stage_position_m = Control2D("stage_position_m", ("x", "y")) + # monochromator controls + mc_exists = Control("S_MC_InsideColumn", local_value=1) # Used by tuning to check if scope has a monochromator + slit_tilt = Control2D("SlitTilt", ("x", "y")) + slit_C10 = Control("Slit_C10") + slit_C12 = Control2D("Slit_C12", ("x", "y")) + slit_C21 = Control2D("Slit_C21", ("x", "y")) + slit_C23 = Control2D("Slit_C23", ("x", "y")) + slit_C30 = Control("Slit_C30") + slit_C32 = Control2D("Slit_C32", ("x", "y")) + slit_C34 = Control2D("Slit_C34", ("x", "y")) # VOA controls c_aperture_offset = Control2D("CApertureOffset", ("x", "y")) - c_aperture = Control2D("CAperture", ("x", "y"), weighted_inputs=([(c_aperture_offset.x, 1.0)], [(c_aperture_offset.y, 1.0)])) + c_aperture = Control2D("CAperture", ("x", "y"), weighted_inputs=([(c_aperture_offset.x, 1.0), (slit_tilt.x, 1.0)], + [(c_aperture_offset.y, 1.0), (slit_tilt.y, 1.0)])) aperture_round = Control2D("ApertureRound", ("x", "y")) s_voa = Control("S_VOA") + convergence_angle = Control("ConvergenceAngle", 0.04) c10 = Control("C10", 500 / 1e9) c12 = Control2D("C12", ("x", "y")) c21 = Control2D("C21", ("x", "y")) @@ -358,12 +369,9 @@ def __create_built_in_controls(self): csh = Control2D("CSH", ("x", "y")) drift = Control2D("Drift", ("x", "y")) # tuning parameters - order_1_max_angle = Control("Order1MaxAngle", 0.008) - order_2_max_angle = Control("Order2MaxAngle", 0.012) - order_3_max_angle = Control("Order3MaxAngle", 0.024) - order_1_patch = Control("Order1Patch", 0.006) - order_2_patch = Control("Order2Patch", 0.006) - order_3_patch = Control("Order3Patch", 0.006) + order_1_max_angle = Control("Order1MaxAngle", -1) + order_2_max_angle = Control("Order2MaxAngle", -1) + order_3_max_angle = Control("Order3MaxAngle", -1) c1_range = Control("C1Range") c2_range = Control("C2Range") c3_range = Control("C3Range") @@ -371,9 +379,9 @@ def __create_built_in_controls(self): beam_shift_m_control = Control2D("beam_shift_m", ("x", "y"), (csh.x.output_value, csh.y.output_value), ([(csh.x, 1.0)], [(csh.y, 1.0)])) return [stage_position_m, zlp_tare_control, zlp_offset_control, c10, c12, c21, c23, c30, c32, c34, c10Control, c12Control, c21Control, c23Control, c30Control, c32Control, c34Control, csh, drift, - beam_shift_m_control, order_1_max_angle, order_2_max_angle, order_3_max_angle, order_1_patch, - order_2_patch, order_3_patch, c1_range, c2_range, c3_range, c_aperture, aperture_round, s_voa, - c_aperture_offset] + beam_shift_m_control, order_1_max_angle, order_2_max_angle, order_3_max_angle, c1_range, c2_range, + c3_range, c_aperture, aperture_round, s_voa, c_aperture_offset, mc_exists, slit_tilt, slit_C10, + slit_C12, slit_C21, slit_C23, slit_C30, slit_C32, slit_C34, convergence_angle] @property def sample(self) -> SampleSimulator.Sample: diff --git a/nionswift_plugin/usim/RonchigramCameraSimulator.py b/nionswift_plugin/usim/RonchigramCameraSimulator.py index 5ed64b3..3aaa7bb 100755 --- a/nionswift_plugin/usim/RonchigramCameraSimulator.py +++ b/nionswift_plugin/usim/RonchigramCameraSimulator.py @@ -215,8 +215,8 @@ def get_chi(coefficient_name): return r return numpy.zeros((height, width)) - - + + def ellipse_radius(polar_angle: typing.Union[float, numpy.ndarray], a: float, b: float, rotation: float) -> typing.Union[float, numpy.ndarray]: """ Returns the radius of a point lying on an ellipse with the given parameters. The ellipse is described in polar @@ -240,7 +240,7 @@ def ellipse_radius(polar_angle: typing.Union[float, numpy.ndarray], a: float, b: return a*b/numpy.sqrt((b*numpy.cos(polar_angle+rotation))**2+(a*numpy.sin(polar_angle+rotation))**2) - + def draw_ellipse(image: numpy.ndarray, ellipse: typing.Tuple[float, float, float, float, float], *, color: typing.Any=1.0) -> None: """ @@ -284,16 +284,16 @@ def draw_ellipse(image: numpy.ndarray, ellipse: typing.Tuple[float, float, float class RonchigramCameraSimulator(CameraSimulator.CameraSimulator): depends_on = ["C10Control", "C12Control", "C21Control", "C23Control", "C30Control", "C32Control", "C34Control", "C34Control", "stage_position_m", "probe_state", "probe_position", "live_probe_position", "features", - "beam_shift_m", "is_blanked", "beam_current", "CAperture", "ApertureRound", "S_VOA"] + "beam_shift_m", "is_blanked", "beam_current", "CAperture", "ApertureRound", "S_VOA", "ConvergenceAngle"] - def __init__(self, instrument, ronchigram_shape: Geometry.IntSize, counts_per_electron: int, convergence_angle: float): + def __init__(self, instrument, ronchigram_shape: Geometry.IntSize, counts_per_electron: int, stage_size_nm: float): super().__init__(instrument, "ronchigram", ronchigram_shape, counts_per_electron) self.__last_sample = None self.__cached_frame = None max_defocus = instrument.max_defocus tv_pixel_angle = math.asin(instrument.stage_size_nm / (max_defocus * 1E9)) / ronchigram_shape.height self.__tv_pixel_angle = tv_pixel_angle - self.__convergence_angle_rad = convergence_angle + self.__stage_size_nm = stage_size_nm self.__max_defocus = max_defocus self.__data_scale = 1.0 self.__aperture_ellipse = None @@ -302,7 +302,7 @@ def __init__(self, instrument, ronchigram_shape: Geometry.IntSize, counts_per_el defocus_m = instrument.defocus_m self.__aberrations_controller = AberrationsController(ronchigram_shape.height, ronchigram_shape.width, theta, max_defocus, defocus_m) self.noise = Noise.PoissonNoise() - + def _draw_aperture(self, frame_data: numpy.ndarray, binning_shape: Geometry.IntSize): # TODO handle asymmetric binning binning = binning_shape[0] @@ -316,7 +316,8 @@ def _draw_aperture(self, frame_data: numpy.ndarray, binning_shape: Geometry.IntS excentricity = math.sqrt(1-1/(1+abs(excentricity))**4) direction = numpy.arctan2(aperture_round[0], aperture_round[1]) # Calculate a and b (the ellipse half-axes) from excentricity. Keep ellipse area constant - convergence_angle_pixels = 0.25* self.__convergence_angle_rad / self.__tv_pixel_angle / binning + convergence_angle = self.instrument.GetVal("ConvergenceAngle") + convergence_angle_pixels = convergence_angle / self.__tv_pixel_angle / binning a = math.sqrt(convergence_angle_pixels**2 / math.sqrt(1 - excentricity**2)) b = convergence_angle_pixels**2 / a self.__aperture_ellipse = ellipse_center + (a, b, direction) @@ -338,7 +339,7 @@ def get_frame_data(self, readout_area: Geometry.IntRect, binning_shape: Geometry height = readout_area.height width = readout_area.width offset_m = self.instrument.stage_position_m - full_fov_nm = abs(self.__max_defocus) * math.sin(self.__convergence_angle_rad) * 1E9 + full_fov_nm = self.__stage_size_nm fov_size_nm = Geometry.FloatSize(full_fov_nm * height / self._sensor_dimensions.height, full_fov_nm * width / self._sensor_dimensions.width) center_nm = Geometry.FloatPoint(full_fov_nm * (readout_area.center.y / self._sensor_dimensions.height- 0.5), full_fov_nm * (readout_area.center.x / self._sensor_dimensions.width - 0.5)) size = Geometry.IntSize(height, width) From eccb37b965f4dc668517324eea9c3d4f747ce708 Mon Sep 17 00:00:00 2001 From: Andi Date: Wed, 27 Nov 2019 15:00:08 +0100 Subject: [PATCH 04/11] Add functions to add inputs to controls and to change input weights. --- nionswift_plugin/usim/InstrumentDevice.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/nionswift_plugin/usim/InstrumentDevice.py b/nionswift_plugin/usim/InstrumentDevice.py index 60e12e4..c41d831 100755 --- a/nionswift_plugin/usim/InstrumentDevice.py +++ b/nionswift_plugin/usim/InstrumentDevice.py @@ -436,6 +436,22 @@ def get_control(self, control_name: str) -> typing.Union[Control, Control2D, Non else: control = self.__controls.get(control_name) return control + + def add_control_inputs(self, control_name: str, weighted_inputs: typing.List[typing.Tuple["Control", float]]) -> None: + control = self.get_control(control_name) + assert isinstance(control, Control) + for input, weight in weighted_inputs: + control.add_input(input, weight) + + def set_input_weight(self, control_name: str, input_name: str, new_weight: float) -> None: + control = self.get_control(control_name) + assert isinstance(control, Control) + input_control = self.get_control(input_name) + assert isinstance(input_control, Control) + inputs = [control_ for control_, _ in control.weighted_inputs] + if input_control not in inputs: + raise ValueError(f"{input_name} is not an input for {control_name}. Please add it first before attempting to change its strength.") + control.add_input(input_control, new_weight) @property def sequence_progress(self): From 72c08175352ef933a0d6a25c189688ab1f2cfa72 Mon Sep 17 00:00:00 2001 From: Andi Date: Sun, 1 Dec 2019 12:45:00 +0100 Subject: [PATCH 05/11] Add convergence angle control to intrument panel. --- nionswift_plugin/usim/InstrumentPanel.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/nionswift_plugin/usim/InstrumentPanel.py b/nionswift_plugin/usim/InstrumentPanel.py index e252a2b..48fb517 100644 --- a/nionswift_plugin/usim/InstrumentPanel.py +++ b/nionswift_plugin/usim/InstrumentPanel.py @@ -136,6 +136,9 @@ def __init__(self, document_controller, instrument: InstrumentDevice.Instrument) voa_in_checkbox = ui.create_check_box_widget(_("VOA In")) voa_in_checkbox.bind_checked(ControlBinding(instrument, "S_VOA")) + convergenve_angle_field = ui.create_line_edit_widget() + convergenve_angle_field.bind_text(ControlBinding(instrument, "ConvergenceAngle", converter=Converter.PhysicalValueToStringConverter(units="mrad", multiplier=1E3))) + c_aperture_widget = PositionWidget(ui, _("CAperture"), instrument, "CAperture", unit="mrad", multiplier=1E3) aperture_round_widget = PositionWidget(ui, _("ApertureRound"), instrument, "ApertureRound", unit="", multiplier=1) @@ -198,6 +201,13 @@ def __init__(self, document_controller, instrument: InstrumentDevice.Instrument) beam_current_row.add(ui.create_label_widget("Beam Current")) beam_current_row.add(beam_current_field) beam_current_row.add_stretch() + + convergence_angle_row = ui.create_row_widget() + convergence_angle_row.add_spacing(8) + convergence_angle_row.add_spacing(8) + convergence_angle_row.add(ui.create_label_widget("Convergence Angle")) + convergence_angle_row.add(convergenve_angle_field) + convergence_angle_row.add_stretch() column = self.content_widget @@ -214,6 +224,7 @@ def __init__(self, document_controller, instrument: InstrumentDevice.Instrument) column.add(c32_widget) column.add(c34_widget) column.add(beam_row) + column.add(convergence_angle_row) column.add(c_aperture_widget) column.add(aperture_round_widget) column.add(eels_row) From cd50f4cfc0bc159b4a755fd51bf644119cc4a731 Mon Sep 17 00:00:00 2001 From: Andi Date: Mon, 2 Dec 2019 07:26:36 +0100 Subject: [PATCH 06/11] Added get_input_weight function. --- nionswift_plugin/usim/InstrumentDevice.py | 67 ++++++++++++++--------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/nionswift_plugin/usim/InstrumentDevice.py b/nionswift_plugin/usim/InstrumentDevice.py index c41d831..0bcaf59 100755 --- a/nionswift_plugin/usim/InstrumentDevice.py +++ b/nionswift_plugin/usim/InstrumentDevice.py @@ -452,7 +452,17 @@ def set_input_weight(self, control_name: str, input_name: str, new_weight: float if input_control not in inputs: raise ValueError(f"{input_name} is not an input for {control_name}. Please add it first before attempting to change its strength.") control.add_input(input_control, new_weight) - + + def get_input_weight(self, control_name: str, input_name: str): + control = self.get_control(control_name) + assert isinstance(control, Control) + input_control = self.get_control(input_name) + assert isinstance(input_control, Control) + inputs = [control_ for control_, _ in control.weighted_inputs] + if input_control not in inputs: + raise ValueError(f"{input_name} is not an input for {control_name}. Please add it first before attempting to get its strength.") + return control.weighted_inputs[inputs.index(input_control)][1] + @property def sequence_progress(self): with self.__lock: @@ -625,6 +635,33 @@ def get_autostem_properties(self): } # these are required functions to implement the standard stem controller interface. + + def __resolve_control_name(self, s: str, set_val: typing.Optional[float]=None) -> typing.Tuple[bool, typing.Optional[float]]: + if "->" in s: + input_name, control_name = s.split("->") + if set_val is not None: + try: + self.set_input_weight(control_name, input_name, set_val) + except ValueError: + return False, None + else: + return True, None + else: + try: + value = self.get_input_weight(control_name, input_name) + except ValueError: + return False, None + else: + return True, value + else: + control = self.get_control(s) + if isinstance(control, Control): + if set_val is not None: + control.set_output_value(set_val) + return True, None + else: + return True, control.output_value + return False, None def TryGetVal(self, s: str) -> (bool, float): @@ -650,20 +687,8 @@ def parse_camera_values(p: str, s: str) -> (bool, float): return parse_camera_values("ronchigram", s[len("ronchigram_"):]) elif s.startswith("eels_"): return parse_camera_values("eels", s[len("eels_"):]) - elif "." in s: - split_s = s.split('.') - control = self.get_control(split_s[0]) # get the 2d control - if isinstance(control, Control2D): - axis = getattr(control, split_s[1], None) # get the control that holds the value for the right axis - if axis is not None: - value = axis.output_value # get the actual value - if value is not None: - return True, value else: - control = self.get_control(s) - if isinstance(control, Control): - return True, control.output_value - return False, None + return self.__resolve_control_name(s) def GetVal(self, s: str, default_value: float=None) -> float: good, d = self.TryGetVal(s) @@ -681,20 +706,8 @@ def SetVal(self, s: str, val: float) -> bool: elif s == "C_Blank": self.is_blanked = val != 0.0 return True - elif "." in s: - split_s = s.split('.') - control = self.get_control(split_s[0]) # get the 2d control - if isinstance(control, Control2D): - axis = getattr(control, split_s[1], None) # get the control that holds the value for the right axis - if axis is not None: - axis.set_output_value(val) # set the actual value - return True else: - control = self.get_control(s) - if isinstance(control, Control): - control.set_output_value(val) - return True - return False + return self.__resolve_control_name(s, set_val=val) def SetValWait(self, s: str, val: float, timeout_ms: int) -> bool: return self.SetVal(s, val) From e693abaf84e044d17c3a7d44ab4e092677940fd1 Mon Sep 17 00:00:00 2001 From: Andreas Date: Mon, 2 Dec 2019 11:56:37 +0100 Subject: [PATCH 07/11] Add tests for new functionality. --- nionswift_plugin/usim/InstrumentDevice.py | 18 ++++++++-------- .../usim/test/InstrumentDevice_test.py | 21 +++++++++++++++++++ 2 files changed, 30 insertions(+), 9 deletions(-) mode change 100644 => 100755 nionswift_plugin/usim/test/InstrumentDevice_test.py diff --git a/nionswift_plugin/usim/InstrumentDevice.py b/nionswift_plugin/usim/InstrumentDevice.py index 0bcaf59..c397f38 100755 --- a/nionswift_plugin/usim/InstrumentDevice.py +++ b/nionswift_plugin/usim/InstrumentDevice.py @@ -436,13 +436,13 @@ def get_control(self, control_name: str) -> typing.Union[Control, Control2D, Non else: control = self.__controls.get(control_name) return control - + def add_control_inputs(self, control_name: str, weighted_inputs: typing.List[typing.Tuple["Control", float]]) -> None: control = self.get_control(control_name) assert isinstance(control, Control) for input, weight in weighted_inputs: control.add_input(input, weight) - + def set_input_weight(self, control_name: str, input_name: str, new_weight: float) -> None: control = self.get_control(control_name) assert isinstance(control, Control) @@ -452,7 +452,7 @@ def set_input_weight(self, control_name: str, input_name: str, new_weight: float if input_control not in inputs: raise ValueError(f"{input_name} is not an input for {control_name}. Please add it first before attempting to change its strength.") control.add_input(input_control, new_weight) - + def get_input_weight(self, control_name: str, input_name: str): control = self.get_control(control_name) assert isinstance(control, Control) @@ -462,7 +462,7 @@ def get_input_weight(self, control_name: str, input_name: str): if input_control not in inputs: raise ValueError(f"{input_name} is not an input for {control_name}. Please add it first before attempting to get its strength.") return control.weighted_inputs[inputs.index(input_control)][1] - + @property def sequence_progress(self): with self.__lock: @@ -635,8 +635,8 @@ def get_autostem_properties(self): } # these are required functions to implement the standard stem controller interface. - - def __resolve_control_name(self, s: str, set_val: typing.Optional[float]=None) -> typing.Tuple[bool, typing.Optional[float]]: + + def __resolve_control_name(self, s: str, set_val: typing.Optional[float]=None) -> typing.Tuple[bool, typing.Optional[float]]: if "->" in s: input_name, control_name = s.split("->") if set_val is not None: @@ -655,13 +655,13 @@ def __resolve_control_name(self, s: str, set_val: typing.Optional[float]=None) - return True, value else: control = self.get_control(s) - if isinstance(control, Control): + if isinstance(control, (Control, ConvertedControl)): if set_val is not None: control.set_output_value(set_val) return True, None else: return True, control.output_value - return False, None + return False, None def TryGetVal(self, s: str) -> (bool, float): @@ -707,7 +707,7 @@ def SetVal(self, s: str, val: float) -> bool: self.is_blanked = val != 0.0 return True else: - return self.__resolve_control_name(s, set_val=val) + return self.__resolve_control_name(s, set_val=val)[0] def SetValWait(self, s: str, val: float, timeout_ms: int) -> bool: return self.SetVal(s, val) diff --git a/nionswift_plugin/usim/test/InstrumentDevice_test.py b/nionswift_plugin/usim/test/InstrumentDevice_test.py old mode 100644 new mode 100755 index 66f8249..bd7e39c --- a/nionswift_plugin/usim/test/InstrumentDevice_test.py +++ b/nionswift_plugin/usim/test/InstrumentDevice_test.py @@ -385,6 +385,27 @@ def test_accessing_non_exisiting_axis_fails(self): with self.assertRaises(AttributeError): getattr(instrument.get_control("C12"), "ne") + def test_get_drive_strength_with_arrow_syntax(self): + instrument = InstrumentDevice.Instrument("usim_stem_controller") + success, value = instrument.TryGetVal("CApertureOffset.x->CAperture.x") + self.assertTrue(success) + self.assertAlmostEqual(value, 1.0) + + def test_set_drive_strength_with_arrow_syntax(self): + instrument = InstrumentDevice.Instrument("usim_stem_controller") + success = instrument.SetVal("CApertureOffset.x->CAperture.x", 0.5) + self.assertTrue(success) + success = instrument.SetVal("CApertureOffset.y->CAperture.y", 0.2) + self.assertTrue(success) + value = instrument.GetVal("CApertureOffset.x->CAperture.x") + self.assertAlmostEqual(value, 0.5) + value = instrument.GetVal("CApertureOffset.y->CAperture.y") + self.assertAlmostEqual(value, 0.2) + + def test_get_drive_strength_fails_for_non_existing_drive(self): + instrument = InstrumentDevice.Instrument("usim_stem_controller") + success, value = instrument.TryGetVal("CApertureOffset.y->CAperture.x") + self.assertFalse(success) if __name__ == '__main__': unittest.main() From 1a4ac00c578641ec1b2336f480607bdb794ea887 Mon Sep 17 00:00:00 2001 From: Andreas Date: Mon, 2 Dec 2019 18:55:58 +0100 Subject: [PATCH 08/11] Allow using a Control as an input weight for other controls. --- nionswift_plugin/usim/InstrumentDevice.py | 38 +++++++++++++------ .../usim/RonchigramCameraSimulator.py | 6 ++- .../usim/test/InstrumentDevice_test.py | 15 ++++++++ 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/nionswift_plugin/usim/InstrumentDevice.py b/nionswift_plugin/usim/InstrumentDevice.py index c397f38..effbfb6 100755 --- a/nionswift_plugin/usim/InstrumentDevice.py +++ b/nionswift_plugin/usim/InstrumentDevice.py @@ -85,7 +85,8 @@ class Control: TODO: add hysteresis """ - def __init__(self, name: str, local_value: float = 0.0, weighted_inputs: typing.Optional[typing.List[typing.Tuple["Control", float]]] = None): + def __init__(self, name: str, local_value: float = 0.0, + weighted_inputs: typing.Optional[typing.List[typing.Tuple["Control", typing.Union[float, "Control"]]]] = None): self.name = name self.weighted_inputs = weighted_inputs if weighted_inputs else list() self.dependents = list() @@ -100,22 +101,28 @@ def __str__(self): @property def weighted_input_value(self) -> float: - return sum([weight * input.output_value for input, weight in self.weighted_inputs]) + return sum([(weight.output_value if isinstance(weight, Control) else weight) * input.output_value + for input, weight in self.weighted_inputs]) @property def output_value(self) -> float: return self.weighted_input_value + self.local_value - def add_input(self, input: "Control", weight: float) -> None: + def add_input(self, input: "Control", weight: typing.Union[float, "Control"]) -> None: # if input is already in the list of weighted inputs, overwrite it inputs = [control for control, _ in self.weighted_inputs] if input in inputs: input_index = inputs.index(input) - self.weighted_inputs[input_index] = (input, weight) + if isinstance(self.weighted_inputs[input_index][1], Control): + self.weighted_inputs[input_index][1].set_output_value(weight) + else: + self.weighted_inputs[input_index] = (input, weight) else: self.weighted_inputs.append((input, weight)) # we can always call add dependent because it checks if self is already in input's dependents input.add_dependent(self) + if isinstance(weight, Control): + weight.add_dependent(self) self._notify_change() def add_dependent(self, dependent: "Control") -> None: @@ -294,7 +301,7 @@ def __init__(self, instrument_id: str): self.property_changed_event = Event.Event() self.__camera_frame_event = threading.Event() self.__samples = [SampleSimulator.RectangleFlakeSample(), SampleSimulator.AmorphousSample()] - self.__sample_index = 0 + self.__sample_index = 1 # define the STEM geometry limits self.stage_size_nm = 1000 @@ -351,6 +358,7 @@ def __create_built_in_controls(self): [(c_aperture_offset.y, 1.0), (slit_tilt.y, 1.0)])) aperture_round = Control2D("ApertureRound", ("x", "y")) s_voa = Control("S_VOA") + s_moa = Control("S_MOA") convergence_angle = Control("ConvergenceAngle", 0.04) c10 = Control("C10", 500 / 1e9) c12 = Control2D("C12", ("x", "y")) @@ -377,11 +385,13 @@ def __create_built_in_controls(self): c3_range = Control("C3Range") # dependent controls beam_shift_m_control = Control2D("beam_shift_m", ("x", "y"), (csh.x.output_value, csh.y.output_value), ([(csh.x, 1.0)], [(csh.y, 1.0)])) + # AxisConverter is commonly used to convert between axis without affecting any hardware + axis_converter = Control2D("AxisConverter", ("x", "y")) return [stage_position_m, zlp_tare_control, zlp_offset_control, c10, c12, c21, c23, c30, c32, c34, c10Control, c12Control, c21Control, c23Control, c30Control, c32Control, c34Control, csh, drift, beam_shift_m_control, order_1_max_angle, order_2_max_angle, order_3_max_angle, c1_range, c2_range, - c3_range, c_aperture, aperture_round, s_voa, c_aperture_offset, mc_exists, slit_tilt, slit_C10, - slit_C12, slit_C21, slit_C23, slit_C30, slit_C32, slit_C34, convergence_angle] + c3_range, c_aperture, aperture_round, s_voa, s_moa, c_aperture_offset, mc_exists, slit_tilt, slit_C10, + slit_C12, slit_C21, slit_C23, slit_C30, slit_C32, slit_C34, convergence_angle, axis_converter] @property def sample(self) -> SampleSimulator.Sample: @@ -411,10 +421,10 @@ def live_probe_position(self, position): def control_changed(self, control: Control) -> None: self.property_changed_event.fire(control.name) - def create_control(self, name: str, local_value: float = 0.0, weighted_inputs: typing.Optional[typing.List[typing.Tuple["Control", float]]] = None): + def create_control(self, name: str, local_value: float = 0.0, weighted_inputs: typing.Optional[typing.List[typing.Tuple[Control, typing.Union[float, Control]]]] = None): return Control(name, local_value, weighted_inputs) - def create_2d_control(self, name: str, native_axis: stem_controller.AxisType, local_values: typing.Tuple[float, float] = (0.0, 0.0), weighted_inputs: typing.Optional[typing.Tuple[typing.List[typing.Tuple["Control", float]], typing.List[typing.Tuple["Control", float]]]] = None): + def create_2d_control(self, name: str, native_axis: stem_controller.AxisType, local_values: typing.Tuple[float, float] = (0.0, 0.0), weighted_inputs: typing.Optional[typing.Tuple[typing.List[typing.Tuple[Control, typing.Union[float, Control]]], typing.List[typing.Tuple[Control, typing.Union[float, Control]]]]] = None): return Control2D(name, native_axis, local_values, weighted_inputs) def add_control(self, control: typing.Union[Control, Control2D]) -> None: @@ -437,7 +447,8 @@ def get_control(self, control_name: str) -> typing.Union[Control, Control2D, Non control = self.__controls.get(control_name) return control - def add_control_inputs(self, control_name: str, weighted_inputs: typing.List[typing.Tuple["Control", float]]) -> None: + def add_control_inputs(self, control_name: str, + weighted_inputs: typing.List[typing.Tuple[Control, typing.Union[float, Control]]]) -> None: control = self.get_control(control_name) assert isinstance(control, Control) for input, weight in weighted_inputs: @@ -457,11 +468,14 @@ def get_input_weight(self, control_name: str, input_name: str): control = self.get_control(control_name) assert isinstance(control, Control) input_control = self.get_control(input_name) - assert isinstance(input_control, Control) + assert isinstance(input_control, Control), f"{input_name} is not of type 'Control' but {type(input_control)}." inputs = [control_ for control_, _ in control.weighted_inputs] if input_control not in inputs: raise ValueError(f"{input_name} is not an input for {control_name}. Please add it first before attempting to get its strength.") - return control.weighted_inputs[inputs.index(input_control)][1] + weight = control.weighted_inputs[inputs.index(input_control)][1] + if isinstance(weight, Control): + return weight.output_value + return weight @property def sequence_progress(self): diff --git a/nionswift_plugin/usim/RonchigramCameraSimulator.py b/nionswift_plugin/usim/RonchigramCameraSimulator.py index 3aaa7bb..2b53f0e 100755 --- a/nionswift_plugin/usim/RonchigramCameraSimulator.py +++ b/nionswift_plugin/usim/RonchigramCameraSimulator.py @@ -303,7 +303,7 @@ def __init__(self, instrument, ronchigram_shape: Geometry.IntSize, counts_per_el self.__aberrations_controller = AberrationsController(ronchigram_shape.height, ronchigram_shape.width, theta, max_defocus, defocus_m) self.noise = Noise.PoissonNoise() - def _draw_aperture(self, frame_data: numpy.ndarray, binning_shape: Geometry.IntSize): + def _draw_aperture(self, frame_data: numpy.ndarray, binning_shape: Geometry.IntSize, enlarge_by: float=0.0): # TODO handle asymmetric binning binning = binning_shape[0] position = self.instrument.GetVal2D("CAperture") @@ -316,7 +316,7 @@ def _draw_aperture(self, frame_data: numpy.ndarray, binning_shape: Geometry.IntS excentricity = math.sqrt(1-1/(1+abs(excentricity))**4) direction = numpy.arctan2(aperture_round[0], aperture_round[1]) # Calculate a and b (the ellipse half-axes) from excentricity. Keep ellipse area constant - convergence_angle = self.instrument.GetVal("ConvergenceAngle") + convergence_angle = self.instrument.GetVal("ConvergenceAngle") * (1 + enlarge_by) convergence_angle_pixels = convergence_angle / self.__tv_pixel_angle / binning a = math.sqrt(convergence_angle_pixels**2 / math.sqrt(1 - excentricity**2)) b = convergence_angle_pixels**2 / a @@ -389,6 +389,8 @@ def get_frame_data(self, readout_area: Geometry.IntRect, binning_shape: Geometry data = self.__aberrations_controller.apply(aberrations, data) if self.instrument.GetVal("S_VOA") > 0: self._draw_aperture(data, binning_shape) + elif self.instrument.GetVal("S_MOA") > 0: + self._draw_aperture(data, binning_shape, enlarge_by=0.1) intensity_calibration = Calibration.Calibration(units="counts") dimensional_calibrations = self.get_dimensional_calibrations(readout_area, binning_shape) diff --git a/nionswift_plugin/usim/test/InstrumentDevice_test.py b/nionswift_plugin/usim/test/InstrumentDevice_test.py index bd7e39c..6fee459 100755 --- a/nionswift_plugin/usim/test/InstrumentDevice_test.py +++ b/nionswift_plugin/usim/test/InstrumentDevice_test.py @@ -407,5 +407,20 @@ def test_get_drive_strength_fails_for_non_existing_drive(self): success, value = instrument.TryGetVal("CApertureOffset.y->CAperture.x") self.assertFalse(success) + def test_use_control_as_input_weight_works(self): + instrument = InstrumentDevice.Instrument("usim_stem_controller") + weight_control = instrument.create_control("weight_control") + instrument.add_control(weight_control) + input_control = instrument.create_control("input_control") + instrument.add_control(input_control) + test_control = instrument.create_control("test_control", weighted_inputs=[(input_control, weight_control)]) + instrument.add_control(test_control) + self.assertAlmostEqual(instrument.GetVal("test_control"), 0) + self.assertTrue(instrument.SetVal("input_control", 1.0)) + self.assertAlmostEqual(instrument.GetVal("test_control"), 0) + self.assertTrue(instrument.SetVal("weight_control", 1.0)) + self.assertAlmostEqual(instrument.GetVal("test_control"), 1.0) + + if __name__ == '__main__': unittest.main() From 918fd3bb9da3c31e60c072b7bce74ae01f5fb0a7 Mon Sep 17 00:00:00 2001 From: Andreas Date: Wed, 4 Dec 2019 17:14:54 +0100 Subject: [PATCH 09/11] Add option to use expressions as control inputs. --- nionswift_plugin/usim/CameraSimulator.py | 2 +- nionswift_plugin/usim/EELSCameraSimulator.py | 2 +- nionswift_plugin/usim/InstrumentDevice.py | 86 ++++++++++++++----- nionswift_plugin/usim/InstrumentPanel.py | 10 +-- .../usim/RonchigramCameraSimulator.py | 2 +- .../usim/test/InstrumentDevice_test.py | 32 ++++++- 6 files changed, 104 insertions(+), 30 deletions(-) mode change 100644 => 100755 nionswift_plugin/usim/CameraSimulator.py mode change 100644 => 100755 nionswift_plugin/usim/EELSCameraSimulator.py mode change 100644 => 100755 nionswift_plugin/usim/InstrumentPanel.py diff --git a/nionswift_plugin/usim/CameraSimulator.py b/nionswift_plugin/usim/CameraSimulator.py old mode 100644 new mode 100755 index 31852a0..b114063 --- a/nionswift_plugin/usim/CameraSimulator.py +++ b/nionswift_plugin/usim/CameraSimulator.py @@ -58,7 +58,7 @@ def get_frame_data(self, readout_area: Geometry.IntRect, binning_shape: Geometry raise NotImplementedError def get_total_counts(self, exposure_s: float) -> float: - beam_current_pa = self.instrument.beam_current * 1E12 + beam_current_pa = self.instrument.GetVal("BeamCurrent") * 1E12 e_per_pa = 6.242E18 / 1E12 return beam_current_pa * e_per_pa * exposure_s * self._counts_per_electron diff --git a/nionswift_plugin/usim/EELSCameraSimulator.py b/nionswift_plugin/usim/EELSCameraSimulator.py old mode 100644 new mode 100755 index 331fc90..11843ad --- a/nionswift_plugin/usim/EELSCameraSimulator.py +++ b/nionswift_plugin/usim/EELSCameraSimulator.py @@ -47,7 +47,7 @@ def plot_spectrum(feature, data: numpy.ndarray, multiplier: float, energy_calibr class EELSCameraSimulator(CameraSimulator.CameraSimulator): depends_on = ["is_slit_in", "probe_state", "probe_position", "live_probe_position", "is_blanked", "ZLPoffset", "stage_position_m", "beam_shift_m", "features", "energy_offset_eV", "energy_per_channel_eV", - "beam_current"] + "BeamCurrent"] def __init__(self, instrument, sensor_dimensions: Geometry.IntSize, counts_per_electron: int): super().__init__(instrument, "eels", sensor_dimensions, counts_per_electron) diff --git a/nionswift_plugin/usim/InstrumentDevice.py b/nionswift_plugin/usim/InstrumentDevice.py index effbfb6..23e5810 100755 --- a/nionswift_plugin/usim/InstrumentDevice.py +++ b/nionswift_plugin/usim/InstrumentDevice.py @@ -86,9 +86,11 @@ class Control: """ def __init__(self, name: str, local_value: float = 0.0, - weighted_inputs: typing.Optional[typing.List[typing.Tuple["Control", typing.Union[float, "Control"]]]] = None): + weighted_inputs: typing.Optional[typing.List[typing.Tuple["Control", typing.Union[float, typing.Callable]]]] = None): self.name = name self.weighted_inputs = weighted_inputs if weighted_inputs else list() + self.__variables = dict() + self.__expression = None self.dependents = list() self.local_value = float(local_value) for input, _ in self.weighted_inputs: @@ -101,14 +103,64 @@ def __str__(self): @property def weighted_input_value(self) -> float: - return sum([(weight.output_value if isinstance(weight, Control) else weight) * input.output_value - for input, weight in self.weighted_inputs]) + weighted_input = 0 + for input_, weight in self.weighted_inputs: + if isinstance(weight, Control): + weighted_input += weight.output_value * input_.output_value + else: + weighted_input += weight * input_.output_value + if self.__expression is not None: + weighted_input += self.__evaluate_expression() + return weighted_input @property def output_value(self) -> float: return self.weighted_input_value + self.local_value - def add_input(self, input: "Control", weight: typing.Union[float, "Control"]) -> None: + @property + def variables(self) -> dict: + return self.__variables + + def get_expression(self) -> str: + return self.__expression + + def set_expression(self, expression: str, variables: typing.Optional[dict]=None, instrument: typing.Optional["Instrument"]=None) -> None: + if variables is not None: + resolved_variables = dict() + for key, value in variables.items(): + if isinstance(value, str): + if instrument is None: + raise TypeError("An instrument controller instance is required when using string names as control identifiers.") + value = instrument.get_control(value) + if value is None: + raise ValueError(f"Cannot get value for name {key}.") + if isinstance(value, Control2D): + raise TypeError("2D controls cannot be used in expressions") + if isinstance(value, Control): + if value == self: + raise ValueError("An expression cannot include the control it is attached to.") + value.add_dependent(self) + resolved_variables[key] = value + self.__variables = resolved_variables + self.__expression = expression + self._notify_change() + + def __evaluate_expression(self): + variables = dict() + for key, value in self.__variables.items(): + if isinstance(value, Control): + value = value.output_value + variables[key] = value + try: + res = eval(self.__expression, globals(), variables) + except: + import traceback + traceback.print_exc() + return 0 + else: + return res + + def add_input(self, input: "Control", weight: typing.Union[float, typing.Callable]) -> None: # if input is already in the list of weighted inputs, overwrite it inputs = [control for control, _ in self.weighted_inputs] if input in inputs: @@ -251,8 +303,8 @@ def __init__(self, name: str, native_axis: stem_controller.AxisType, local_values: typing.Tuple[float, float] = (0.0, 0.0), - weighted_inputs: typing.Optional[typing.Tuple[typing.List[typing.Tuple["Control", float]], - typing.List[typing.Tuple["Control", float]]]] = None): + weighted_inputs: typing.Optional[typing.Tuple[typing.List[typing.Tuple["Control", typing.Union[float, typing.Callable]]], + typing.List[typing.Tuple["Control", typing.Union[float, typing.Callable]]]]] = None): self.name = name self.native_axis = native_axis if weighted_inputs is None: @@ -342,6 +394,7 @@ def __create_built_in_controls(self): zlp_tare_control = Control("ZLPtare") zlp_offset_control = Control("ZLPoffset", -20, [(zlp_tare_control, 1.0)]) stage_position_m = Control2D("stage_position_m", ("x", "y")) + beam_current = Control("BeamCurrent", 200e-12) # monochromator controls mc_exists = Control("S_MC_InsideColumn", local_value=1) # Used by tuning to check if scope has a monochromator slit_tilt = Control2D("SlitTilt", ("x", "y")) @@ -388,7 +441,7 @@ def __create_built_in_controls(self): # AxisConverter is commonly used to convert between axis without affecting any hardware axis_converter = Control2D("AxisConverter", ("x", "y")) return [stage_position_m, zlp_tare_control, zlp_offset_control, c10, c12, c21, c23, c30, c32, c34, c10Control, - c12Control, c21Control, c23Control, c30Control, c32Control, c34Control, csh, drift, + c12Control, c21Control, c23Control, c30Control, c32Control, c34Control, csh, drift, beam_current, beam_shift_m_control, order_1_max_angle, order_2_max_angle, order_3_max_angle, c1_range, c2_range, c3_range, c_aperture, aperture_round, s_voa, s_moa, c_aperture_offset, mc_exists, slit_tilt, slit_C10, slit_C12, slit_C21, slit_C23, slit_C30, slit_C32, slit_C34, convergence_angle, axis_converter] @@ -448,7 +501,7 @@ def get_control(self, control_name: str) -> typing.Union[Control, Control2D, Non return control def add_control_inputs(self, control_name: str, - weighted_inputs: typing.List[typing.Tuple[Control, typing.Union[float, Control]]]) -> None: + weighted_inputs: typing.List[typing.Tuple[Control, typing.Union[float, typing.Callable]]]) -> None: control = self.get_control(control_name) assert isinstance(control, Control) for input, weight in weighted_inputs: @@ -464,7 +517,7 @@ def set_input_weight(self, control_name: str, input_name: str, new_weight: float raise ValueError(f"{input_name} is not an input for {control_name}. Please add it first before attempting to change its strength.") control.add_input(input_control, new_weight) - def get_input_weight(self, control_name: str, input_name: str): + def get_input_weight(self, control_name: str, input_name: str) -> float: control = self.get_control(control_name) assert isinstance(control, Control) input_control = self.get_control(input_name) @@ -548,7 +601,7 @@ def counts_per_electron(self): return 40 def get_electrons_per_pixel(self, pixel_count: int, exposure_s: float) -> float: - beam_current_pa = self.__beam_current * 1E12 + beam_current_pa = self.GetVal("BeamCurrent") * 1E12 e_per_pa = 6.242E18 / 1E12 beam_e = beam_current_pa * e_per_pa e_per_pixel_per_second = beam_e / pixel_count @@ -585,15 +638,6 @@ def voltage(self, value: float) -> None: self.__voltage = value self.property_changed_event.fire("voltage") - @property - def beam_current(self) -> float: - return self.__beam_current - - @beam_current.setter - def beam_current(self, value: float) -> None: - self.__beam_current = value - self.property_changed_event.fire("beam_current") - @property def is_blanked(self) -> bool: return self.__blanked @@ -656,14 +700,14 @@ def __resolve_control_name(self, s: str, set_val: typing.Optional[float]=None) - if set_val is not None: try: self.set_input_weight(control_name, input_name, set_val) - except ValueError: + except (ValueError, AssertionError): return False, None else: return True, None else: try: value = self.get_input_weight(control_name, input_name) - except ValueError: + except (ValueError, AssertionError): return False, None else: return True, value diff --git a/nionswift_plugin/usim/InstrumentPanel.py b/nionswift_plugin/usim/InstrumentPanel.py old mode 100644 new mode 100755 index 48fb517..8dcb965 --- a/nionswift_plugin/usim/InstrumentPanel.py +++ b/nionswift_plugin/usim/InstrumentPanel.py @@ -105,7 +105,7 @@ def __init__(self, document_controller, instrument: InstrumentDevice.Instrument) voltage_field.bind_text(Binding.PropertyBinding(instrument, "voltage", converter=Converter.PhysicalValueToStringConverter(units="keV", multiplier=1E-3))) beam_current_field = ui.create_line_edit_widget() - beam_current_field.bind_text(Binding.PropertyBinding(instrument, "beam_current", converter=Converter.PhysicalValueToStringConverter(units="pA", multiplier=1E12))) + beam_current_field.bind_text(ControlBinding(instrument, "BeamCurrent", converter=Converter.PhysicalValueToStringConverter(units="pA", multiplier=1E12))) stage_position_widget = PositionWidget(ui, _("Stage"), instrument, "stage_position_m") @@ -132,13 +132,13 @@ def __init__(self, document_controller, instrument: InstrumentDevice.Instrument) slit_in_checkbox = ui.create_check_box_widget(_("Slit In")) slit_in_checkbox.bind_checked(Binding.PropertyBinding(instrument, "is_slit_in")) - + voa_in_checkbox = ui.create_check_box_widget(_("VOA In")) voa_in_checkbox.bind_checked(ControlBinding(instrument, "S_VOA")) - + convergenve_angle_field = ui.create_line_edit_widget() convergenve_angle_field.bind_text(ControlBinding(instrument, "ConvergenceAngle", converter=Converter.PhysicalValueToStringConverter(units="mrad", multiplier=1E3))) - + c_aperture_widget = PositionWidget(ui, _("CAperture"), instrument, "CAperture", unit="mrad", multiplier=1E3) aperture_round_widget = PositionWidget(ui, _("ApertureRound"), instrument, "ApertureRound", unit="", multiplier=1) @@ -201,7 +201,7 @@ def __init__(self, document_controller, instrument: InstrumentDevice.Instrument) beam_current_row.add(ui.create_label_widget("Beam Current")) beam_current_row.add(beam_current_field) beam_current_row.add_stretch() - + convergence_angle_row = ui.create_row_widget() convergence_angle_row.add_spacing(8) convergence_angle_row.add_spacing(8) diff --git a/nionswift_plugin/usim/RonchigramCameraSimulator.py b/nionswift_plugin/usim/RonchigramCameraSimulator.py index 2b53f0e..85ff05f 100755 --- a/nionswift_plugin/usim/RonchigramCameraSimulator.py +++ b/nionswift_plugin/usim/RonchigramCameraSimulator.py @@ -284,7 +284,7 @@ def draw_ellipse(image: numpy.ndarray, ellipse: typing.Tuple[float, float, float class RonchigramCameraSimulator(CameraSimulator.CameraSimulator): depends_on = ["C10Control", "C12Control", "C21Control", "C23Control", "C30Control", "C32Control", "C34Control", "C34Control", "stage_position_m", "probe_state", "probe_position", "live_probe_position", "features", - "beam_shift_m", "is_blanked", "beam_current", "CAperture", "ApertureRound", "S_VOA", "ConvergenceAngle"] + "beam_shift_m", "is_blanked", "BeamCurrent", "CAperture", "ApertureRound", "S_VOA", "ConvergenceAngle"] def __init__(self, instrument, ronchigram_shape: Geometry.IntSize, counts_per_electron: int, stage_size_nm: float): super().__init__(instrument, "ronchigram", ronchigram_shape, counts_per_electron) diff --git a/nionswift_plugin/usim/test/InstrumentDevice_test.py b/nionswift_plugin/usim/test/InstrumentDevice_test.py index 6fee459..e572788 100755 --- a/nionswift_plugin/usim/test/InstrumentDevice_test.py +++ b/nionswift_plugin/usim/test/InstrumentDevice_test.py @@ -39,7 +39,7 @@ def test_ronchigram_handles_dependencies_properly(self): instrument.SetValDelta("ZLPoffset", 1) self.assertFalse(camera._needs_recalculation) camera._needs_recalculation = False - instrument.beam_current += 1 + instrument.SetValDelta("BeamCurrent", 1) self.assertTrue(camera._needs_recalculation) def test_powerlaw(self): @@ -104,6 +104,7 @@ def test_eels_data_is_consistent_when_energy_offset_changes_with_negative_zlp_of def test_eels_data_thickness_is_consistent(self): instrument = InstrumentDevice.Instrument("usim_stem_controller") + instrument.sample_index = 0 instrument.get_scan_data(scan_base.ScanFrameParameters({"size": (256, 256), "pixel_time_us": 1, "fov_nm": 10}), 0) instrument.validate_probe_position() camera = instrument._get_camera_simulator("eels") @@ -421,6 +422,35 @@ def test_use_control_as_input_weight_works(self): self.assertTrue(instrument.SetVal("weight_control", 1.0)) self.assertAlmostEqual(instrument.GetVal("test_control"), 1.0) + def test_expression(self): + instrument = InstrumentDevice.Instrument("usim_stem_controller") + weight_control = instrument.create_control("weight_control") + instrument.add_control(weight_control) + input_control = instrument.create_control("input_control") + instrument.add_control(input_control) + other_control = instrument.create_control("other_control") + instrument.add_control(other_control) + test_control = instrument.create_control("test_control") + instrument.add_control(test_control) + test_control.set_expression("input_control*weight_control/2 + x", + variables={"input_control": "input_control", + "weight_control": weight_control, + "x": "other_control"}, + instrument=instrument) + self.assertAlmostEqual(instrument.GetVal("test_control"), 0) + self.assertTrue(instrument.SetVal("input_control", 1.0)) + self.assertAlmostEqual(instrument.GetVal("test_control"), 0) + self.assertTrue(instrument.SetVal("weight_control", 2.0)) + self.assertAlmostEqual(instrument.GetVal("test_control"), 1.0) + + def test_using_control_in_its_own_expression_raises_value_error(self): + instrument = InstrumentDevice.Instrument("usim_stem_controller") + test_control = instrument.create_control("test_control") + instrument.add_control(test_control) + test_control.add_dependent(test_control) + with self.assertRaises(ValueError): + test_control.set_expression('test_control', variables={'test_control': test_control}) + if __name__ == '__main__': unittest.main() From 33e194626047874659a05abba35dbb185b43b13e Mon Sep 17 00:00:00 2001 From: Andreas Date: Thu, 5 Dec 2019 17:47:13 +0100 Subject: [PATCH 10/11] Fixed a bug in Control.add_input. --- nionswift_plugin/usim/InstrumentDevice.py | 9 +++------ nionswift_plugin/usim/test/InstrumentDevice_test.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/nionswift_plugin/usim/InstrumentDevice.py b/nionswift_plugin/usim/InstrumentDevice.py index af131ea..adf85cc 100755 --- a/nionswift_plugin/usim/InstrumentDevice.py +++ b/nionswift_plugin/usim/InstrumentDevice.py @@ -161,15 +161,12 @@ def __evaluate_expression(self): else: return res - def add_input(self, input: "Control", weight: typing.Union[float, typing.Callable]) -> None: + def add_input(self, input: "Control", weight: typing.Union[float, "Control"]) -> None: # if input is already in the list of weighted inputs, overwrite it inputs = [control for control, _ in self.weighted_inputs] if input in inputs: input_index = inputs.index(input) - if isinstance(self.weighted_inputs[input_index][1], Control): - self.weighted_inputs[input_index][1].set_output_value(weight) - else: - self.weighted_inputs[input_index] = (input, weight) + self.weighted_inputs[input_index] = (input, weight) else: self.weighted_inputs.append((input, weight)) # we can always call add dependent because it checks if self is already in input's dependents @@ -513,7 +510,7 @@ def add_control_inputs(self, control_name: str, for input, weight in weighted_inputs: control.add_input(input, weight) - def set_input_weight(self, control_name: str, input_name: str, new_weight: float) -> None: + def set_input_weight(self, control_name: str, input_name: str, new_weight: typing.Union[float, Control]) -> None: control = self.get_control(control_name) assert isinstance(control, Control) input_control = self.get_control(input_name) diff --git a/nionswift_plugin/usim/test/InstrumentDevice_test.py b/nionswift_plugin/usim/test/InstrumentDevice_test.py index 75d9c69..1000037 100755 --- a/nionswift_plugin/usim/test/InstrumentDevice_test.py +++ b/nionswift_plugin/usim/test/InstrumentDevice_test.py @@ -459,6 +459,18 @@ def test_using_control_in_its_own_expression_raises_value_error(self): with self.assertRaises(ValueError): test_control.set_expression('test_control', variables={'test_control': test_control}) + def test_add_input_for_existing_control(self): + instrument = InstrumentDevice.Instrument("usim_stem_controller") + test_control = instrument.create_control("test_control") + other_control = instrument.create_control("other_control") + weight_control = instrument.create_control("weight_control") + instrument.add_control(test_control) + instrument.add_control(other_control) + instrument.add_control(weight_control) + instrument.add_control_inputs('test_control', [(other_control, weight_control)]) + # Add it a second time to test add existing control + instrument.add_control_inputs('test_control', [(other_control, weight_control)]) + if __name__ == '__main__': unittest.main() From 9fd4ee4ca16a100be12903046e7306b5805d70e3 Mon Sep 17 00:00:00 2001 From: Andreas Date: Fri, 6 Dec 2019 16:49:41 +0100 Subject: [PATCH 11/11] Updated Changes.rst. --- CHANGES.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) mode change 100644 => 100755 CHANGES.rst diff --git a/CHANGES.rst b/CHANGES.rst old mode 100644 new mode 100755 index cfd0efd..dd195f8 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,16 @@ Changelog (nionswift-usim) ========================== +0.2.2 (unreleased) +------------------ +- Add aperture that can be moved and "distorted" (i.e. dipole and quadropole effect simulation) +- Add functions to 'Instrument' that facilitate adding new inputs to existing controls +- Allow input weights for controls to be controls in addition to float +- Add option to attach a python expression as control input (only one expression per control can be set, +but it can be arbitrarily complex, as long as it can be evaluated by 'eval') +- Changed meaning of convergence angle to reflect its real meaning (in the simulator it only controls the size of +the aperture on the ronchigram camera, the effect on the scan is not simulated yet) + 0.2.1 (2019-11-27) ------------------ - Minor changes to be compatible with nionswift-instrumentation.