Source code for pymead.core.param

import typing

import networkx
import numpy as np
from PyQt6.QtCore import QSignalBlocker

from pymead.core.pymead_obj import PymeadObj


[docs] class Param(PymeadObj): """ Base-level parameter in ``pymead``. Sub-classed by ``DesVar`` (design variable). Provides operator overloading and getter/setter methods. .. note:: Instances of this class should never be created directly. Instead, parameters can be created by creating a ``GeometryCollection`` and calling its ``add_param`` method. """
[docs] def __init__(self, value: float or int, name: str, lower: float or None = None, upper: float or None = None, sub_container: str = "params", setting_from_geo_col: bool = False, point=None, root=None, rotation_handle=None, enabled: bool = True, equation_str: str = None, geo_col=None, bspline=None): """ Parameters ========== value: float or int Starting value for the parameter. name: str Name of the parameter lower: float Lower bound for the parameter upper: float Upper bound for the parameter """ super().__init__(sub_container=sub_container, geo_col=geo_col) self.dtype = type(value).__name__ self._value = None self._lower = None self._upper = None self._enabled = None self.at_boundary = False self.bspline = bspline self.point = point self.root = root self.set_enabled(enabled) self.rotation_handle = rotation_handle if rotation_handle is not None: self.rotation_handle.rotation_param = self self.geo_objs = [] self.param_graph = None self.equation_str = equation_str self.equation = None self.equation_dict = None self.geo_cons = [] self.dims = [] self.setting_from_geo_col = setting_from_geo_col if upper is not None and lower is not None: if upper < lower: raise ValueError(f"Specified upper bound ({upper}) smaller than the specified lower bound ({lower})") if np.isclose(upper - lower, 0.0): raise ValueError(f"Specified upper bound ({upper}) too close to the specified lower bound ({lower})") if lower is not None and lower > value: raise ValueError(f"Specified lower bound ({lower}) larger than current parameter value ({value})" f"for {name}") if upper is not None and upper < value: raise ValueError(f"Specified upper bound ({upper}) smaller than current parameter value ({value})" f"for {name}") kwargs = dict(direct_user_request=False) if isinstance(self, LengthParam) else {} self.set_value(value, **kwargs) if lower is not None: self.set_lower(lower) if upper is not None: self.set_upper(upper) self.set_name(name)
def _get_value_spin(self): """ Gets the GUI spinbox associated with this parameter. Returns ------- pymead.gui.parameter_tree.ValueSpin Spinbox associated with this parameter """ return self.tree_item.treeWidget().itemWidget(self.tree_item, 1)
[docs] def value(self, bounds_normalized: bool = False): """ Returns the design variable value. Parameters ========== bounds_normalized: bool Whether to return the value as normalized by the set bounds. If the value is at the lower bound, 0.0 will be returned. If the value is at the upper bound, 1.0 will be returned. Otherwise, a number between 0.0 and 1.0 will be returned. Default: ``False``. Returns ======= float The design variable value """ if bounds_normalized: if self.lower() is None or self.upper() is None: raise ValueError("Lower and upper bounds must be set to extract a bounds-normalized value.") return (self._value - self._lower) / (self._upper - self._lower) else: return self._value
[docs] def set_value(self, value: float or int, bounds_normalized: bool = False, force: bool = False, param_graph_update: bool = False, from_request_move: bool = False): """ Sets the design variable value, adjusting the value to fit inside the bounds if necessary. Parameters ---------- value: float or int Design variable value bounds_normalized: bool Whether the specified value is normalized by the bounds (e.g., 0 if the value is equal to the lower bound, 1 if the value is equal to the upper bound, or a float between 0.0 and 1.0 if the value is somewhere between the bounds). Default: ``False`` force: bool Whether to force the change in value. This keyword argument should never be set to ``True`` when using the API. Default: ``False`` param_graph_update: bool Whether this value is being set as part of a parameter graph update. Used to prevent the parameter graph from triggering multiple updates for the same parameter. This keyword argument should never be set to ``True`` when using the API. Default: ``False`` from_request_move: bool Whether this method was called from ``Point.request_move``. Default: ``False`` """ old_point_vals = {k: v.as_array() for k, v in self.geo_col.container()["points"].items()} \ if self.geo_col is not None else {} def get_curves_to_update(_points_to_update): _curves_to_update = [] for point in _points_to_update: for curve in point.curves: if curve not in _curves_to_update: _curves_to_update.append(curve) return _curves_to_update def get_airfoils_to_update(_curves_to_update): _airfoils_to_update = [] # Account for blunt trailing edges curve_list_copy = _curves_to_update.copy() for curve in curve_list_copy: for point in curve.points(): for sub_curve in point.curves: if sub_curve in _curves_to_update: continue _curves_to_update.append(sub_curve) for curve in _curves_to_update: if curve.airfoil is None: continue if curve.airfoil not in _airfoils_to_update: _airfoils_to_update.append(curve.airfoil) return _airfoils_to_update def rotate_cluster(new_v): if self.gcs is None: return points_to_update, root = self.gcs.rotate_cluster(self.rotation_handle, new_rotation_angle=new_v) constraints_to_update = [] for point in networkx.dfs_preorder_nodes(self.gcs, source=root): for geo_con in point.geo_cons: if geo_con not in constraints_to_update: constraints_to_update.append(geo_con) for geo_con in constraints_to_update: if geo_con.canvas_item is not None: geo_con.canvas_item.update() _curves_to_update = get_curves_to_update(points_to_update) _airfoils_to_update = get_airfoils_to_update(_curves_to_update) # Update airfoil-relative points for _airfoil in _airfoils_to_update: _airfoil.update_relative_points(old_point_vals) # Visual updates to geometric objects for point in points_to_update: if point.canvas_item is not None: point.canvas_item.updateCanvasItem(point.x().value(), point.y().value()) for _curve in _curves_to_update: _curve.update() for _airfoil in _airfoils_to_update: _airfoil.update_coords() if _airfoil.canvas_item is not None: _airfoil.canvas_item.generatePicture() points_solved = [] if not force: if bounds_normalized: if self.lower() is None or self.upper() is None: raise ValueError("Lower and upper bounds must be set to assign a bounds-normalized value.") value = value * (self._upper - self._lower) + self._lower # Bounds clipping if self._lower is not None and value < self._lower: # If below the lower bound, # set the value equal to the lower bound self._value = self._lower self.at_boundary = True elif self._upper is not None and value > self._upper: # If above the upper bound, # set the value equal to the upper bound self._value = self._upper self.at_boundary = True else: # Otherwise, use the default behavior for Param. self._value = value self.at_boundary = False if self.rotation_handle is not None: rotate_cluster(self) if self.at_boundary: return if self.gcs is not None and self.geo_cons: points_solved = [] for gc in self.geo_cons: points_solved.extend(self.gcs.solve(gc)) curves_to_update = get_curves_to_update(points_solved) airfoils_to_update = get_airfoils_to_update(curves_to_update) # Update airfoil-relative points for airfoil in airfoils_to_update: airfoil.update_relative_points(old_point_vals) if not from_request_move: self.gcs.update_canvas_items(list(set(points_solved))) if self.bspline is not None: self.bspline.update() airfoils_to_update = get_airfoils_to_update([self.bspline]) for airfoil in airfoils_to_update: airfoil.update_coords() if airfoil.canvas_item is not None: airfoil.canvas_item.generatePicture() if self.param_graph is not None and not param_graph_update and self in self.param_graph.nodes: for node in networkx.dfs_preorder_nodes(self.param_graph, source=self): if not node.equation_str: continue node.evaluate_equation() else: self._value = value if self.tree_item is not None: value_spin = self._get_value_spin() with QSignalBlocker(value_spin): self.tree_item.treeWidget().itemWidget(self.tree_item, 1).setValue(self.value()) return list(set(points_solved))
[docs] def lower(self) -> float: """ Returns the lower bound for the design variable Returns ------- float DV lower bound """ return self._lower
[docs] def upper(self) -> float: """ Returns the upper bound for the design variable Returns ------- float DV upper bound """ return self._upper
[docs] def set_lower(self, lower: float, force: bool = False): """ Sets the lower bound for the design variable. If called from outside ``DesVar.__init__()``, adjust the design variable value to fit inside the bounds if necessary. Parameters ---------- lower: float Lower bound for the design variable force: bool Setting this argument to ``True`` ignores the check for lower bound greater than value. Default: ``False`` """ if lower > self.value() and not force: return self._lower = lower if self.tree_item is not None: value_spin = self._get_value_spin() with QSignalBlocker(value_spin): value_spin.setMinimum(self.lower())
[docs] def set_upper(self, upper: float, force: bool = False): """ Sets the upper bound for the design variable. If called from outside ``DesVar.__init__()``, adjust the design variable value to fit inside the bounds if necessary. Parameters ---------- upper: float Upper bound for the design variable force: bool Setting this argument to ``True`` ignores the check for upper bound less than value. Default: ``False`` """ if upper < self.value() and not force: return self._upper = upper if self.tree_item is not None: value_spin = self._get_value_spin() with QSignalBlocker(value_spin): value_spin.setMaximum(self.upper())
[docs] def enabled(self) -> bool: """ Whether this parameter is enabled (whether it can change) Returns ------- bool Whether this parameter is enabled """ return self._enabled
[docs] def set_enabled(self, enabled: bool): """ Sets this parameter to be enabled or disabled Parameters ---------- enabled: bool ``True`` if this parameter should be enabled, ``False`` otherwise """ self._enabled = enabled if self.tree_item is not None: value_spin = self._get_value_spin() with QSignalBlocker(value_spin): value_spin.setEnabled(enabled)
[docs] def update_equation(self, equation_str: str = None): """ Updates the equation defining the value of this parameter. Parameters ---------- equation_str: str or None The equation to assign to the parameter. If ``None``, the parameter will no longer be defined by an equation and the value is free to change independently of all other parameters. Default: ``None`` """ if not equation_str: # Handles both the None and empty-string cases self.equation_str = equation_str self.equation = None self.equation_dict = None return if self.param_graph is None: return self.equation = "def f(): return " self.equation_dict = {"p": {}} equation_split = equation_str.split() param_names = [param.name() for param in self.param_graph.param_list] for idx, sub_str in enumerate(equation_split): if sub_str[0] != "$": self.equation += sub_str continue param_name = sub_str.strip("$") if param_name in param_names: self.equation_dict["p"][param_name] = self.param_graph.param_list[param_names.index(param_name)] else: raise EquationCompileError("Failed to compile") self.equation += f"p['{param_name}'].value()" for param in self.equation_dict["p"].values(): self.param_graph.add_edge(param, self) if len(self.param_graph.nodes) != 0 and not networkx.is_forest(self.param_graph): # Revert to the original equation string self.update_equation(self.equation_str) raise EquationCompileError("The dependencies for this equation create a closed loop") self.evaluate_equation() self.equation_str = equation_str
[docs] def evaluate_equation(self): """ Evaluates this parameter's equation, taking into account other parameter values. Raises an ``EquationCompileError`` if the equation cannot be compiled due to a naming or syntax error. """ try: exec(self.equation, self.equation_dict) self.set_value(self.equation_dict["f"](), param_graph_update=True) except (NameError, SyntaxError) as e: raise EquationCompileError(str(e))
[docs] def get_dict_rep(self): return {"value": float(self.value()) if "float" in self.dtype else int(self.value()), "lower": self.lower(), "upper": self.upper(), "unit_type": None, "enabled": self.enabled(), "equation_str": self.equation_str}
@classmethod def set_from_dict_rep(cls, d: dict): if "lower" not in d.keys(): d["lower"] = None if "upper" not in d.keys(): d["upper"] = None return cls(value=d["value"], name=d["name"], lower=d["lower"], upper=d["upper"]) def __repr__(self): return f"{self.__class__.__name__} {self.name()}<v={self.value()}>" def __add__(self, other): return self.__class__(value=self.value() + other.value(), name="forward_add_result") def __radd__(self, other): return self.__class__(value=other.value() + self.value(), name="reverse_add_result") def __sub__(self, other): return self.__class__(value=self.value() - other.value(), name="forward_subtract_result") def __rsub__(self, other): return self.__class__(value=other.value() - self.value(), name="reverse_subtract_result") def __mul__(self, other): return self.__class__(value=self.value() * other.value(), name="forward_multiplication_result") def __rmul__(self, other): return self.__class__(value=other.value() * self.value(), name="reverse_multiplication_result") def __floordiv__(self, other): return self.__class__(value=self.value() // other.value(), name="floor_division_result") def __truediv__(self, other): return self.__class__(value=self.value() / other.value(), name="true_division_result") def __abs__(self): return self.__class__(value=abs(self.value()), name="absolute_value_result") def __pow__(self, power, modulo=None): return self.__class__(value=self.value() ** power, name="power_result")
[docs] class LengthParam(Param): """ Length-type parameter in ``pymead``. Adds unit functionality and prevents negative values except in the case of points. .. note:: Instances of this class should never be created directly. Instead, parameters can be created by creating a ``GeometryCollection`` and calling its ``add_param`` method with ``unit_type="length"``. """
[docs] def __init__(self, value: float, name: str, lower: float or None = None, upper: float or None = None, sub_container: str = "params", setting_from_geo_col: bool = False, point=None, root=None, rotation_handle=None, enabled: bool = True, equation_str: str = None, geo_col=None): self._unit = None name = "Length-1" if name is None else name super().__init__(value=value, name=name, lower=lower, upper=upper, sub_container=sub_container, setting_from_geo_col=setting_from_geo_col, point=point, root=root, rotation_handle=rotation_handle, enabled=enabled, equation_str=equation_str, geo_col=geo_col)
[docs] def unit(self) -> str or None: """ The value of this parameter's length unit. Returns ------- str or None ``None`` if the unit should be the default, ``str`` otherwise. """ return self._unit
[docs] def set_unit(self, unit: str or None = None, old_unit: str = None, modify_value: bool = True) -> float or None: """ This method sets the length unit to be used, called the first time when adding a parameter via ``GeometryCollection.add_pymead_obj_by_ref``. Parameters ---------- unit: str or None The new unit to switch to. If ``None``, the current length unit will be used. Default: ``None`` old_unit: str or None The old unit to switch from. If ``None``, no changes will be made to the bounds or value of the param. Default: ``None`` modify_value: bool Whether to scale the parameter value based on the conversion to the new unit. Default: ``True`` Returns ------- float or None If ``old_unit==None``, ``None`` will be returned. Otherwise, the value of the parameter is returned """ if unit is not None: self._unit = unit else: self._unit = self.geo_col.units.current_length_unit() if old_unit is None: return lower = self.lower() upper = self.upper() value = self.value() if lower is not None: base_lower = self.geo_col.units.convert_length_to_base(lower, old_unit) self.set_lower(self.geo_col.units.convert_length_from_base(base_lower, unit), force=True) if upper is not None: base_upper = self.geo_col.units.convert_length_to_base(upper, old_unit) self.set_upper(self.geo_col.units.convert_length_from_base(base_upper, unit), force=True) base_value = self.geo_col.units.convert_length_to_base(value, old_unit) new_value = self.geo_col.units.convert_length_from_base(base_value, unit) if modify_value: self.set_value(new_value, direct_user_request=False) if self.tree_item is not None: self.tree_item.treeWidget().itemWidget(self.tree_item, 1).setSuffix(f" {unit}") return new_value
[docs] def set_lower(self, lower: float, force: bool = False): if self.point is None and lower < 0.0: lower = 0.0 return super().set_lower(lower, force=force)
[docs] def set_value(self, value: float, bounds_normalized: bool = False, force: bool = False, param_graph_update: bool = False, from_request_move: bool = False, direct_user_request: bool = True): # Negative lengths are prohibited unless this represents a point if self.point is None and value < 0.0: value = self.value() # Can only happen if not bounds-normalized, so no need to pass this argument if direct_user_request and self.point and self.point.x() and self.point.y(): value = value * (self._upper - self._lower) + self._lower if bounds_normalized else value if self is self.point.x(): self.point.request_move(xp=value, yp=self.point.y().value()) if self is self.point.y(): self.point.request_move(xp=self.point.x().value(), yp=value) return return super().set_value(value, bounds_normalized=bounds_normalized, force=force, param_graph_update=param_graph_update, from_request_move=from_request_move)
[docs] def get_dict_rep(self): return {"value": float(self.value()), "lower": self.lower(), "upper": self.upper(), "unit_type": "length", "enabled": self.enabled(), "equation_str": self.equation_str}
[docs] class AngleParam(Param): r""" Angle-type parameter in ``pymead``. Adds unit functionality and transforms values into the range :math:`[0, 2\pi)`. .. note:: Instances of this class should never be created directly. Instead, parameters can be created by creating a ``GeometryCollection`` and calling its ``add_param`` method with ``unit_type="angle"``. """
[docs] def __init__(self, value: float, name: str, lower: float or None = None, upper: float or None = None, sub_container: str = "params", setting_from_geo_col: bool = False, point=None, root=None, rotation_handle=None, enabled: bool = True, equation_str: str = None, geo_col=None): self._unit = None name = "Angle-1" if name is None else name super().__init__(value=value, name=name, lower=lower, upper=upper, sub_container=sub_container, setting_from_geo_col=setting_from_geo_col, point=point, root=root, rotation_handle=rotation_handle, enabled=enabled, equation_str=equation_str, geo_col=geo_col)
[docs] def unit(self): """ The value of this parameter's angle unit. Returns ------- str or None ``None`` if the unit should be the default, ``str`` otherwise. """ return self._unit
def set_unit(self, unit: str or None = None, old_unit: str or None = None): if unit is not None: self._unit = unit else: self._unit = self.geo_col.units.current_angle_unit() if old_unit is None: return lower = self.lower() upper = self.upper() value = self.value() if lower is not None: base_lower = self.geo_col.units.convert_angle_to_base(lower, old_unit) self.set_lower(self.geo_col.units.convert_angle_from_base(base_lower, unit), force=True) if upper is not None: base_upper = self.geo_col.units.convert_angle_to_base(upper, old_unit) self.set_upper(self.geo_col.units.convert_angle_from_base(base_upper, unit), force=True) base_value = self.geo_col.units.convert_angle_to_base(value, old_unit) self.set_value(self.geo_col.units.convert_angle_from_base(base_value, unit)) if self.tree_item is not None: self.tree_item.treeWidget().itemWidget(self.tree_item, 1).setSuffix(f" {unit}")
[docs] def rad(self) -> float: """ Returns the value of the angle parameter in radians, the base angle unit of pymead Returns ------- float Angle in radians """ return self.geo_col.units.convert_angle_to_base(self._value, self.unit())
[docs] def set_value(self, value: float, bounds_normalized: bool = False, force: bool = False, param_graph_update: bool = False, from_request_move: bool = False): if self.unit() is None: self.set_unit() if self.__class__.__name__ == "AngleParam": # Do not include this restriction for angle design variables new_value = self.geo_col.units.convert_angle_to_base(value, self.unit()) zero_to_2pi_value = new_value % (2 * np.pi) new_value = self.geo_col.units.convert_angle_from_base(zero_to_2pi_value, self.unit()) else: new_value = value return super().set_value(new_value, bounds_normalized=bounds_normalized, force=force, param_graph_update=param_graph_update, from_request_move=from_request_move)
[docs] def get_dict_rep(self): return {"value": float(self.value()), "lower": self.lower(), "upper": self.upper(), "unit_type": "angle", "root": self.root.name() if self.root is not None else None, "rotation_handle": self.rotation_handle.name() if self.rotation_handle is not None else None, "enabled": self.enabled(), "equation_str": self.equation_str}
[docs] def default_lower(value: float) -> float: """ Sets the default value for a design variable lower bound based on the design variable's value. For values close to zero, an offset is used. For other values, a multiplier is used. Parameters ---------- value: float The value of the design variable Returns ------- float The default lower bound to use for the design variable """ if -0.1 <= value <= 0.1: return value - 0.02 else: if value < 0.0: return 1.2 * value else: return 0.8 * value
[docs] def default_upper(value: float): """ Sets the default value for a design variable upper bound based on the design variable's value. For values close to zero, an offset is used. For other values, a multiplier is used. Parameters ---------- value: float The value of the design variable Returns ------- float The default upper bound to use for the design variable """ if -0.1 <= value <= 0.1: return value + 0.02 else: if value < 0.0: return 0.8 * value else: return 1.2 * value
[docs] class DesVar(Param): """ Design variable class; subclasses the base-level Param. Adds lower and upper bound default behavior. """
[docs] def __init__(self, value: float, name: str, lower: float or None = None, upper: float or None = None, sub_container: str = "desvar", setting_from_geo_col: bool = False, point=None, root=None, rotation_handle=None, enabled: bool = True, equation_str: str = None, assignable: bool = True, bspline=None): """ Parameters ---------- value: float Starting value of the design variable name: str Name of the design variable lower: float or None Lower bound for the design variable. If ``None``, a reasonable value is chosen. Default: ``None``. upper: float or None Upper bound for the design variable. If ``None``, a reasonable value is chosen. Default: ``None``. setting_from_geo_col: bool Whether this method is being called directly from the geometric collection. Default: ``False``. """ self._assignable = None # Default behavior for lower bound if lower is None: lower = default_lower(value) # Default behavior for upper bound if upper is None: upper = default_upper(value) super().__init__(value=value, name=name, lower=lower, upper=upper, sub_container=sub_container, setting_from_geo_col=setting_from_geo_col, point=point, root=root, rotation_handle=rotation_handle, enabled=enabled, equation_str=equation_str, bspline=bspline) self.assignable = assignable
@property def assignable(self) -> bool: """ Whether this design variable can be assigned a value from a list of design variable values. Currently used just for Fan Pressure Ratio design variables. Returns ------- bool Whether this design variable can be assigned a value from a list of design variable values """ return self._assignable @assignable.setter def assignable(self, assignable: bool): """ Sets whether this design variable can be assigned a value from a list of design variable values. Currently used just for Fan Pressure Ratio design variables. Parameters ---------- assignable: bool Whether this design variable should be assignable from a list of design variable values """ self._assignable = assignable
[docs] class LengthDesVar(LengthParam): """ Design variable class for length values. Adds lower and upper bound default behavior. """
[docs] def __init__(self, value: float, name: str, lower: float or None = None, upper: float or None = None, setting_from_geo_col: bool = False, point=None, root=None, rotation_handle=None, enabled: bool = True, equation_str: str = None): """ Parameters ========== value: float Starting value of the design variable name: str Name of the design variable lower: float or None Lower bound for the design variable. If ``None``, a reasonable value is chosen. Default: ``None``. upper: float or None Upper bound for the design variable. If ``None``, a reasonable value is chosen. Default: ``None``. setting_from_geo_col: bool Whether this method is being called directly from the geometric collection. Default: ``False``. """ self.assignable = True # Default behavior for lower bound if lower is None: lower = default_lower(value) # Default behavior for upper bound if upper is None: upper = default_upper(value) super().__init__(value=value, name=name, lower=lower, upper=upper, setting_from_geo_col=setting_from_geo_col, sub_container="desvar", point=point, root=root, rotation_handle=rotation_handle, enabled=enabled, equation_str=equation_str)
[docs] def get_dict_rep(self): return {"value": float(self.value()), "lower": self.lower(), "upper": self.upper(), "unit_type": "length", "enabled": self.enabled(), "equation_str": self.equation_str}
[docs] class AngleDesVar(AngleParam): """ Design variable class for angle values; subclasses the base-level Param. Adds lower and upper bound default behavior. """
[docs] def __init__(self, value: float, name: str, lower: float or None = None, upper: float or None = None, setting_from_geo_col: bool = False, point=None, root=None, rotation_handle=None, enabled: bool = True, equation_str: str = None): """ Parameters ========== value: float Starting value of the design variable name: str Name of the design variable lower: float or None Lower bound for the design variable. If ``None``, a reasonable value is chosen. Default: ``None``. upper: float or None Upper bound for the design variable. If ``None``, a reasonable value is chosen. Default: ``None``. setting_from_geo_col: bool Whether this method is being called directly from the geometric collection. Default: ``False``. """ self.assignable = True # Default behavior for lower bound if lower is None: lower = default_lower(value) # Default behavior for upper bound if upper is None: upper = default_upper(value) super().__init__(value=value, name=name, lower=lower, upper=upper, sub_container="desvar", setting_from_geo_col=setting_from_geo_col, point=point, root=root, rotation_handle=rotation_handle, enabled=enabled, equation_str=equation_str)
[docs] def set_value(self, value: float, bounds_normalized: bool = False, force: bool = False, param_graph_update: bool = False, from_request_move: bool = False): r""" In this special case of ``set_value`` for an ``AngleDesVar``, we skip over the call to the ``set_value`` method in ``AngleParam`` and directly call the ``set_value`` method in ``Param`` (the grandparent class). The reason for this is that ``AngleParam`` always keeps the angle between 0 and :math:`2 \pi`, which is not logical behavior for a bounded variable. This method eliminates that restriction. """ return Param.set_value(self, value, bounds_normalized=bounds_normalized, force=force, param_graph_update=param_graph_update, from_request_move=from_request_move)
[docs] def get_dict_rep(self): return {"value": float(self.value()), "lower": self.lower(), "upper": self.upper(), "unit_type": "angle", "root": self.root.name() if self.root is not None else None, "rotation_handle": self.rotation_handle.name() if self.rotation_handle is not None else None, "enabled": self.enabled(), "equation_str": self.equation_str}
[docs] class ParamSequence:
[docs] def __init__(self, params: typing.List[Param]): self._params = None self.set_params(params)
def __getitem__(self, idx): if isinstance(idx, slice): return self.generate_from_slice(self, idx) else: return self.params()[idx] def __setitem__(self, idx, val): self.params()[idx] = val def __len__(self): return len(self.params()) @classmethod def generate_from_slice(cls, original_param_seq, s): return cls(params=original_param_seq.params()[s].copy()) @classmethod def generate_from_array(cls, arr: np.ndarray): assert arr.ndim == 1 return cls(params=[Param(value=arr_val, name=f"Param-{idx}") for idx, arr_val in enumerate(arr)]) def params(self): return self._params def param_idx_from_ref(self, param: Param): return self.params().index(param) def reverse(self): self.params().reverse() def set_params(self, params: typing.List[Param]): self._params = params def insert_param(self, idx: int, param: Param): self._params.insert(idx, param) def append_param(self, param: Param): self._params.append(param) def remove_param(self, idx: int): self._params.pop(idx) def as_array(self): return np.array([p.value() for p in self.params()]) def extract_subsequence(self, indices: list): return ParamSequence(params=[self.params()[idx] for idx in indices])
[docs] class EquationCompileError(Exception): pass