Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add aperture and new capabilities for linking controls. #19

Closed
wants to merge 12 commits into from
2 changes: 1 addition & 1 deletion nionswift_plugin/usim/CameraSimulator.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion nionswift_plugin/usim/EELSCameraSimulator.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,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)
Expand Down
212 changes: 155 additions & 57 deletions nionswift_plugin/usim/InstrumentDevice.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,12 @@ 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, 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:
Expand All @@ -101,13 +104,64 @@ def __str__(self):

@property
def weighted_input_value(self) -> float:
return sum([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: float) -> 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, "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:
Expand All @@ -117,6 +171,8 @@ def add_input(self, input: "Control", weight: float) -> None:
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:
Expand Down Expand Up @@ -245,8 +301,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:
Expand Down Expand Up @@ -295,14 +351,13 @@ 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
Brow71189 marked this conversation as resolved.
Show resolved Hide resolved

# define the STEM geometry limits
self.stage_size_nm = 150
self.stage_size_nm = 1000
Brow71189 marked this conversation as resolved.
Show resolved Hide resolved
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
Expand All @@ -322,7 +377,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)
}

Expand All @@ -338,6 +393,25 @@ 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"))
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), (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")
s_moa = Control("S_MOA")
convergence_angle = Control("ConvergenceAngle", 0.04)
c10 = Control("C10", 500 / 1e9)
c12 = Control2D("C12", ("x", "y"))
c21 = Control2D("C21", ("x", "y"))
Expand All @@ -355,21 +429,21 @@ 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")
# 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,
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]
# 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_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]

@property
def sample(self) -> SampleSimulator.Sample:
Expand Down Expand Up @@ -403,10 +477,10 @@ def _set_scan_context_probe_position(self, scan_context: stem_controller.ScanCon
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:
Expand All @@ -429,6 +503,36 @@ 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, typing.Union[float, typing.Callable]]]) -> 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: typing.Union[float, Control]) -> 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)

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)
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.")
weight = control.weighted_inputs[inputs.index(input_control)][1]
if isinstance(weight, Control):
return weight.output_value
return weight

@property
def sequence_progress(self):
with self.__lock:
Expand Down Expand Up @@ -498,7 +602,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
Expand Down Expand Up @@ -535,15 +639,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
Expand Down Expand Up @@ -600,6 +695,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, AssertionError):
return False, None
else:
return True, None
else:
try:
value = self.get_input_weight(control_name, input_name)
except (ValueError, AssertionError):
return False, None
else:
return True, value
else:
control = self.get_control(s)
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

def TryGetVal(self, s: str) -> (bool, float):

def parse_camera_values(p: str, s: str) -> (bool, float):
Expand All @@ -624,20 +746,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)
Expand All @@ -655,20 +765,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)[0]

def SetValWait(self, s: str, val: float, timeout_ms: int) -> bool:
return self.SetVal(s, val)
Expand Down
Loading