diff --git a/snlohelper/base_function.py b/snlohelper/base_function.py new file mode 100644 index 0000000000000000000000000000000000000000..cd011c7db9f5e8c1a7136ade3e82c801ff5181e9 --- /dev/null +++ b/snlohelper/base_function.py @@ -0,0 +1,137 @@ +import time +from typing import Any, Optional, Protocol + +from .utils import Position, gui, scale, get_content_complete, set_value +from .main_window import Functions, open_function + + +class BaseFunction(Protocol): + """Base class for a function window.""" + + _function: Functions + # Positions + _run_pos: Position # of the run field + _result_pos: Position # of the results field + _close_pos: Position + _configuration_pos: dict[str, list[Position]] # of the configuration fields + + def open(self) -> None: + """Open the function.""" + open_function(self._function) + + def run(self) -> None: + """Click 'Run'.""" + gui.click(*scale(*self._run_pos)) + + def close(self) -> None: + """Click 'x'.""" + self.open() + gui.click(*scale(*self._close_pos)) + + def configure( + self, data: Optional[dict[str, Any | list[Any] | tuple[Any, ...]]] = None + ) -> None: + """Configure the values and leave the config window open. + + If any value is "None", that field will not be changed. This is useful, if you want to + change a single value in a row. + For example `data={'Wavelengths (nm)': [1064.5, None, None]}` will set the first wavelength + to 1064.5 nm while leaving the other wavelengths untouched. + """ + self.open() + if data is None: + return + for key, value in data.items(): + positions = self._configuration_pos[key] + if isinstance(value, (list, tuple)): + for i, val in enumerate(value): + if val is not None: + set_value(positions[i], val) + else: + if value is not None: + set_value(positions[0], value) + + def get_configuration(self) -> dict[str, list[float | str]]: + """Read the current configuration.""" + self.open() + data = {} + for key, positions in self._configuration_pos.items(): + d = [] + for pos in positions: + val = get_content_complete(pos) + try: + d.append(float(val)) + except ValueError: + d.append(val) + data[key] = d + return data + + def read_results(self) -> list[str]: + return get_content_complete(self._result_pos).split("\r\n") + + def interpret_results(self, rows: list[str]) -> dict[str, Any]: + """Interpret the results and return them as a dictionary.""" + data = {} + for row in rows: + text, values = row.split("=") + content = [] + for element in values.split(): + try: + val = float(element) + except ValueError: + val = element + content.append(val) + data[text.strip()] = content + return data + + def run_and_read( + self, + waiting_time: float = 1, + max_tries: int = 10, + interval: float = 0.5, + waiting_line_count: int = 3, + ) -> dict[str, Any]: + """Run an analysis and return the result.""" + self.run() + time.sleep(waiting_time) + for _ in range(max_tries): + rows = self.read_results() + if len(rows) > waiting_line_count: + break + time.sleep(interval) + + # interpret results and save as dictionary: + return self.interpret_results(rows) + + def configure_run_read( + self, data: Optional[dict[str, Any]] = None, **kwargs + ) -> dict[str, Any]: + """Configure and run an analysis and return the result.""" + self.configure(data) + return self.run_and_read(**kwargs) + + +def generate_position_dict( + first_position: Position, + configuration_names: list[str], + columns: int = 3, + column_distance: int = 60, +) -> dict[str, list[Position]]: + """Generate a position dictionary for functions. + + This utility makes it easier to generate a position matrix with three fields per entry. + Adjust it afterwards according to the number of entries. + + :param first_position: Position (tuple) of the top left configuration field. + :param configuration_names: List of all the names (in order) of the configuration. + """ + # Notes regarding positions for mix: horizontal distance is 60 px, vertical distance 16 pixels + positions = {} + i = 0 + for name in configuration_names: + positions[name] = [ + (first_position[0] + j * column_distance, first_position[1] + 16 * i) + for j in range(columns) + ] + i += 1 + return positions diff --git a/snlohelper/mix_methods.py b/snlohelper/mix_methods.py index bd44c306b1ac28cc12702d78fa265b88ae1247bb..9deae485a728c13362abb8ce64859ab7f21c3d48 100644 --- a/snlohelper/mix_methods.py +++ b/snlohelper/mix_methods.py @@ -1,102 +1,26 @@ -import time -from typing import Any, Optional +from typing import Any, Optional, Protocol -from .utils import Position, gui, scale, set_value, get_content_complete -from .main_window import Functions, open_function +from .utils import Position, gui, scale +from .base_function import BaseFunction -class MixMethods: +class MixMethods(BaseFunction, Protocol): """Parent class for mix methods. Subclass it for specific methods. You should define the positions and the result interpretation. """ - _function: Functions - # Positions _accept_pos: Position - _run_pos: Position _change_inputs_pos: Position - _result_pos: Position # of the results field - _configuration_pos: dict[str, list[Position]] # of the configuration fields - - def open(self) -> None: - """Open the function.""" - open_function(self._function) def accept(self) -> None: """Click 'Accept'.""" gui.click(*scale(*self._accept_pos)) - def run(self) -> None: - """Click 'Run'.""" - gui.click(*scale(*self._run_pos)) - def change_inputs(self) -> None: """Click 'Change Inputs'.""" gui.click(*scale(*self._change_inputs_pos)) - def configure(self, data: Optional[dict[str, Any]] = None) -> None: - """Configure the values and leave the config window open. - - If any value is "None", that field will not be changed. This is useful, if you want to - change a single value in a row. - For example `data={'Wavelengths (nm)': [1064.5, None, None]}` will set the first wavelength - to 1064.5 nm while leaving the other wavelengths untouched. - """ - self.open() - if data is None: - return - for key, value in data.items(): - positions = self._configuration_pos[key] - for i, val in enumerate(value): - if val is not None: - set_value(positions[i], val) - - def get_configuration(self) -> dict[str, Any]: - """Read the current configuration.""" - self.open() - data = {} - for key, positions in self._configuration_pos.items(): - d = [] - for pos in positions: - val = get_content_complete(pos) - try: - d.append(float(val)) - except ValueError: - d.append(val) - data[key] = d - return data - - def interpret_results(self, rows: list[str]) -> dict[str, Any]: - """Interpret the results and return them as a dictionary.""" - data = {} - for row in rows: - text, values = row.split("=") - data[text.strip()] = [float(i) for i in values.split()] - return data - - def read_results(self) -> list[str]: - return get_content_complete(self._result_pos).split("\r\n") - - def run_and_read( - self, - waiting_time: float = 1, - max_tries: int = 10, - interval: float = 0.5, - successful_line_count: int = 3, - ) -> dict[str, Any]: - """Run an analysis and return the result.""" - self.run() - time.sleep(waiting_time) - for _ in range(max_tries): - rows = self.read_results() - if len(rows) > successful_line_count: - break - time.sleep(interval) - - # interpret results and save as dictionary: - return self.interpret_results(rows) - def configure_run_read( self, data: Optional[dict[str, Any]] = None, **kwargs ) -> dict[str, float | list[float]]: @@ -104,26 +28,3 @@ class MixMethods: self.configure(data) self.accept() return self.run_and_read(**kwargs) - - -def generate_position_dict( - first_configuration_field: Position, configuration_names: list[str] -) -> dict[str, list[Position]]: - """Generate a position dictionary for mix methods. - - This utility makes it easier to generate a position matrix with three fields per entry. - Adjust it afterwards according to the number of entries. - - :param first_configuration_field: Position (tuple) of the top left configuration field. - :param configuration_names: List of all the names (in order) of the configuration. - """ - # Notes regarding positions: horizontal distance is 60 px, vertical distance 16 pixels - positions = {} - i = 0 - for name in configuration_names: - positions[name] = [ - (first_configuration_field[0], first_configuration_field[1] + 16 * i), - (first_configuration_field[0] + 60, first_configuration_field[1] + 16 * i), - (first_configuration_field[0] + 120, first_configuration_field[1] + 16 * i), - ] - return positions diff --git a/snlohelper/ref_index.py b/snlohelper/ref_index.py new file mode 100644 index 0000000000000000000000000000000000000000..6432588abf483e4723ffa7ede8e27c45bfe1134c --- /dev/null +++ b/snlohelper/ref_index.py @@ -0,0 +1,39 @@ +from typing import Any + +from .base_function import BaseFunction, Functions + + +class RefractiveIndex(BaseFunction): + _function = Functions.REF_INDEX + _run_pos = (295, 187) + _result_pos = (160, 260) + _close_pos = (365, 41) + _configuration_pos = { + "Crystal": [(170, 90)], + "Temperature": [(290, 90)], + "theta": [(170, 146)], + "phi": [(300, 146)], + "Wavelength": [(170, 190)], + } + + def run_and_read( + self, + waiting_time: float = 0.1, + max_tries: int = 10, + interval: float = 0.1, + waiting_line_count: int = 3, + ) -> dict[str, Any]: + return super().run_and_read(waiting_time, max_tries, interval, waiting_line_count) + + def refractive_indices( + self, Crystal=None, Temperature=None, theta=None, phi=None, Wavelength=None + ) -> list[float]: + kwargs = { + "Crystal": Crystal, + "Temperature": Temperature, + "theta": theta, + "phi": phi, + "Wavelength": Wavelength, + } + results = self.configure_run_read(kwargs) + return results["Refractive index (o,e)"] diff --git a/snlohelper/two_d_mix_lp.py b/snlohelper/two_d_mix_lp.py index e1abf34df3a5c6ee0d4eaf3a1b7a106aec3477e2..8b8cdb95f37c2a27b3cb7c736a5551cab46c07d3 100644 --- a/snlohelper/two_d_mix_lp.py +++ b/snlohelper/two_d_mix_lp.py @@ -8,6 +8,7 @@ class TwoDMixLP(MixMethods): _function = Functions.TWOD_MIX_LP _accept_pos = (506, 520) _run_pos = (140, 220) + _close_pos = (540, 140) _change_inputs_pos = (373, 166) _result_pos = (133, 293) @@ -31,7 +32,7 @@ class TwoDMixLP(MixMethods): "beta blue (cm/W)": [(400, 420), (460, 420), (520, 420)], "Walkoff angles (mrad)": [(400, 440), (460, 440), (520, 440)], "Offset in wo dir. (mm)": [(400, 453), (460, 453), (520, 453)], - "Rad. curv. (mm/air“)": [(400, 473), (460, 473), (520, 473)], + "Rad. curv. (mm/air)": [(400, 473), (460, 473), (520, 473)], "# of integ/grid points": [(400, 486), (460, 486), (520, 486)], "Crystal/grid sizes (mm)": [(400, 500), (460, 500), (520, 500)], "Deff (pm/V)": [(400, 520)], diff --git a/snlohelper/two_d_mix_sp.py b/snlohelper/two_d_mix_sp.py index b15561404e5c1a540a0801c4d6026cb012eb1fa2..f153542720a9e1566321b491da26b30b6bc664ff 100644 --- a/snlohelper/two_d_mix_sp.py +++ b/snlohelper/two_d_mix_sp.py @@ -8,6 +8,7 @@ class TwoDMixSP(MixMethods): _function = Functions.TWOD_MIX_SP _accept_pos = (506, 600) _run_pos = (140, 200) + _close_pos = (540, 140) _change_inputs_pos = (150, 266) _result_pos = (133, 293)