diff --git a/documentation/docs/guides/gui-usage.md b/documentation/docs/guides/gui-usage.md index f860e807..b152f2ae 100644 --- a/documentation/docs/guides/gui-usage.md +++ b/documentation/docs/guides/gui-usage.md @@ -115,6 +115,8 @@ While the optimization is running, the values of the variables, objectives, and #### Specify variable range Each optimzation variable must have defined bounds that specify the valid search space. These ranges serve dual purposes: they constrain the optimization algorithm to explore only physically meaningful parameter values, and they enforce safety limits to prevent damage to the equipment. Variable ranges are defined in the Environment + VOCS section. +Use **Set Variable Range** above the table to apply a range option to all variables, or click a row's gear button to adjust one variable. The individual range dialog offers three relative options and `exact_bounds`; relative bounds are calculated from the selected option and the variable's current value. + ![Variable Range](/img/gui/variablerange.png) #### Incorporate algorithm parameters diff --git a/documentation/docs/guides/minimode.md b/documentation/docs/guides/minimode.md index 9aabdc2e..55bf7063 100644 --- a/documentation/docs/guides/minimode.md +++ b/documentation/docs/guides/minimode.md @@ -95,12 +95,17 @@ When "Show checked only" is unchecked, the table scrolls to display the first se ### Variable Range Dialog -![Mini Mode Variable Dialog](/img/guides/minimode-variable2.png) +![Mini Mode Variable Dialog](/img/guides/minimode-variable-dialog.png) Clicking the gear icon on a variable row opens a dialog with: - The current value of the variable +- The current low and high bounds for the variable - Options for range selection method + - ratio wrt current value: Bounds are calculated as a fraction of the current value on either side of the current value + - ratio wrt full range: Bounds are calculated as a fraction of the full range on either side of the current value + - delta around current value: Bounds are calculated as a delta on either side of the current value + - exact bounds: the input numerical bounds are used. - A **bounds preview bar** showing what the resulting bounds will be - Hard limits displayed (but not editable) diff --git a/documentation/docs/guides/templates.md b/documentation/docs/guides/templates.md index f5173a71..988c4c8a 100644 --- a/documentation/docs/guides/templates.md +++ b/documentation/docs/guides/templates.md @@ -36,13 +36,15 @@ vocs: # XOPT VOCS # Important note about variable bounds: the bounds set here will be used # if relative_to_current is set to false. If relative_to_current is true, - # these bounds will not be used and the bounds for each variable will be - # determined based on the vrange_limit_options below. + # the bounds for each variable will be determined based on the + # vrange_limit_options below and the value of each variable when the template + # is loaded. In that case, bounds specified here will be used as hard + # variable limits. vrange_limit_options: {} # for each variable: - # variable: {limit_option_idx: 0 or 1 or 2, ratio_curr: ratio (float), ratio_full: ratio (float), delta: abs value (float)} + # variable: {limit_option_idx: 0 or 1 or 2 or 3, ratio_curr: ratio (float), ratio_full: ratio (float), delta: abs value (float), exact_bounds: [lower, upper] (list[float, float])} # # For example: # QUAD:LTUH:620:BCTRL: @@ -50,17 +52,25 @@ vrange_limit_options: {} # ratio_curr: 0.1 # ratio_full: 0.1 # delta: 1.2 + # exact_bounds: [45.0, 57.0] # Will set the variable range for QUAD:LTUH:620:BCTRL to a delta (option 2) of +- 1.2 from the current value of the variable. # # Note that ratio_curr is the ratio with respect to the current value, # ratio_full is the ratio with respect to the full variable range, and - # delta is an absolute delta around the current value. Include values - # for all three options even if you do not plan to use them. + # delta is an absolute delta around the current value. # limit_option_idx sets the desired option: 0 will use ratio_curr, - # 1 is ratio_full, 2 is delta. + # 1 is ratio_full, 2 is delta, 3 will use exact_bounds + # + # The first three methods will calculate bounds around the current variable value when the template + # is loaded based on the selected option. If limit_option_idx == 3 for exact_bounds, + # the specified bounds will be used regardless of the current value of the variable. If the + # exact_bounds option is selected without specifying numerical bounds in vrange_limit_options, + # the GUI will use the hard bounds defined above in vocs/variables as a fallback. relative_to_current: true # (bool) true or false. If true, variable ranges will be set - # for each variable based on vrange_limit_options. If False, variable + # for each variable based on vrange_limit_options, and initial points will be calculated + # automatically when the variable is added. The upper and lower bounds specified in the + # vocs/variables dictionary will be used as hard bounds. If False, variable # ranges will be set to the specified upper and lower bounds from the # variables dictionary in vocs. initial_point_actions: [{}] # list of dictionaries diff --git a/documentation/static/img/guides/minimode-variable-dialog.png b/documentation/static/img/guides/minimode-variable-dialog.png new file mode 100644 index 00000000..fc003e90 Binary files /dev/null and b/documentation/static/img/guides/minimode-variable-dialog.png differ diff --git a/src/badger/gui/components/routine_page.py b/src/badger/gui/components/routine_page.py index 1ad3810a..f02a678e 100644 --- a/src/badger/gui/components/routine_page.py +++ b/src/badger/gui/components/routine_page.py @@ -45,6 +45,7 @@ LessThanConstraint, MaximizeObjective, MinimizeObjective, + ContinuousVariable, ) from pydantic import ValidationError from PyQt5.QtCore import Qt, QTimer, pyqtSignal @@ -1521,7 +1522,27 @@ def set_vrange(self, set_all=True): for vname in vname_selected: self.ratio_var_ranges[vname] = copy.deepcopy(self.limit_option) - def set_ind_vrange(self, vname, config): + def set_ind_vrange(self, vname: str, config: dict) -> None: + """ + Apply a variable-specific range policy to a single environment variable. + + The selected mode is determined by ``config["limit_option_idx"]`` and can + represent a fixed percentage of the current value, a percentage of the full + hard-range width, a fixed absolute delta around the current value, or an exact + bound pair. The method reads the live environment value for ``vname``, computes + the candidate bounds, clips them to the hard limits from ``config["lower_bound"]`` + and ``config["upper_bound"]``, and applies the result to the GUI variable table. + Initial points for that variable are recalculated with the new bounds. + + Parameters + ---------- + vname : str + Name of the variable whose bounds should be updated. + config : dict + Bounds configuration for the variable, including the hard limits, + ``limit_option_idx``, ``ratio_full``, ``ratio_curr``, ``delta``, and + ``exact_bounds``. + """ logger.info( f"Setting individual variable range for {vname} with config: {config}" ) @@ -1531,6 +1552,7 @@ def set_ind_vrange(self, vname, config): "ratio_full": config["ratio_full"], "ratio_curr": config["ratio_curr"], "delta": config["delta"], + "exact_bounds": config.get("exact_bounds", hard_bounds), } option_idx = option["limit_option_idx"] @@ -1538,7 +1560,7 @@ def set_ind_vrange(self, vname, config): env = self.create_env() curr = env.get_variables([vname])[vname] - # 0: ratio with current value, 1: ratio with full range, 2: delta around current value + # 0: ratio with current value, 1: ratio with full range, 2: delta around current value, 3: exact bounds if option_idx == 1: ratio = option["ratio_full"] delta = 0.5 * ratio * (hard_bounds[1] - hard_bounds[0]) @@ -1548,6 +1570,9 @@ def set_ind_vrange(self, vname, config): delta = option["delta"] bounds = [curr - delta, curr + delta] bounds = np.clip(bounds, hard_bounds[0], hard_bounds[1]).tolist() + elif option_idx == 3: + bounds = sorted(option.get("exact_bounds", hard_bounds)) + bounds = np.clip(bounds, hard_bounds[0], hard_bounds[1]).tolist() else: ratio = option["ratio_curr"] sign = np.sign(curr) @@ -1612,6 +1637,23 @@ def update_init_table(self, force=False): self._fill_init_table() def calc_auto_bounds(self): + """ + Compute auto-derived bounds for all selected variables. + + The method of calculation is determined for each variable by that variable's + configured `limit_option_idx`, stored in `self.ratio_var_ranges[var_name]`. + - For `option_idx == 1`, bounds are calculated as a fraction of the full + variable range. + - For `option_idx == 2`, bounds are calculated as a delta on either side of the + current value. + - For `option_idx == 3`, the stored exact numerical bounds are used. + - Otherwise, (`option_idx == 0`) the ratio_curr mode calculated bounds as a + fraction of the current value around the current point. + In every case, the resulting bounds are + clipped to the variable's hard limits and the `clipped` dictionary records + whether a bound was reduced by that clip. + """ + logger.info("Calculating auto bounds for selected variables.") vname_selected = [] vrange = {} @@ -1634,7 +1676,7 @@ def calc_auto_bounds(self): limit_option = self.limit_option option_idx = limit_option["limit_option_idx"] - # 0: ratio with current value, 1: ratio with full range, 2: delta around current value + # 0: ratio with current value, 1: ratio with full range, 2: delta around current value, 3: exact bounds if option_idx == 1: ratio = limit_option["ratio_full"] hard_bounds = vrange[name] @@ -1650,6 +1692,13 @@ def calc_auto_bounds(self): bounds = np.clip(bounds, hard_bounds[0], hard_bounds[1]).tolist() vrange[name] = bounds logger.info(f"Auto bounds for {name} (delta): {bounds}") + elif option_idx == 3: + hard_bounds = vrange[name] + exact_bounds = limit_option.get("exact_bounds") + bounds = sorted(exact_bounds) if exact_bounds else list(hard_bounds) + bounds = np.clip(bounds, hard_bounds[0], hard_bounds[1]).tolist() + vrange[name] = bounds + logger.info(f"Auto bounds for {name} (exact): {bounds}") else: ratio = limit_option["ratio_curr"] hard_bounds = vrange[name] @@ -1749,10 +1798,17 @@ def handle_var_config(self, vname): except KeyError: option = self.limit_option + current_bounds = self.env_box.var_table.bounds.get(vname, bounds) + if current_bounds is None: + current_bounds = bounds + if isinstance(current_bounds, ContinuousVariable): + current_bounds = current_bounds.domain + configs = { "current_value": curr, "lower_bound": bounds[0], "upper_bound": bounds[1], + "current_bounds": current_bounds, **option, } diff --git a/src/badger/gui/mini/components/var_table.py b/src/badger/gui/mini/components/var_table.py index ac353204..f4cc9a23 100644 --- a/src/badger/gui/mini/components/var_table.py +++ b/src/badger/gui/mini/components/var_table.py @@ -227,16 +227,13 @@ def __init__( lower, upper = bounds delta = 0.5 * abs(upper - lower) - is_centered = self._is_centered(value, lower, upper) if is_clipped: lower_delta = abs(value - lower) upper_delta = abs(upper - value) delta = max(lower_delta, upper_delta) - self.line_edit = QLineEdit( - f"±{delta:.3f}{'*' if is_clipped or not is_centered else ''}" - ) + self.line_edit = QLineEdit(f"±{delta:.3f}{'*' if is_clipped else ''}") self.line_edit.setReadOnly(True) self.line_edit.setAlignment( Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter @@ -264,7 +261,7 @@ def __init__( layout.addWidget(self.line_edit) layout.addWidget(self._button_stack) - if is_clipped or not is_centered: + if is_clipped: self.setToolTip("Requested bounds are clipped by hardware limits") def set_selected(self, is_selected: bool): diff --git a/src/badger/gui/mini/pages/routine_page.py b/src/badger/gui/mini/pages/routine_page.py index 5c2e9a23..392ffe48 100644 --- a/src/badger/gui/mini/pages/routine_page.py +++ b/src/badger/gui/mini/pages/routine_page.py @@ -39,6 +39,7 @@ LessThanConstraint, MaximizeObjective, MinimizeObjective, + ContinuousVariable, ) from pydantic import ValidationError @@ -1459,7 +1460,27 @@ def set_vrange(self, set_all=True): self.ratio_var_ranges[vname] = copy.deepcopy(self.limit_option) self.env_box.var_table.set_scan_range_options() - def set_ind_vrange(self, vname, config): + def set_ind_vrange(self, vname: str, config: dict) -> None: + """ + Apply a variable-specific range policy to a single environment variable. + + The selected mode is determined by ``config["limit_option_idx"]`` and can + represent a fixed percentage of the current value, a percentage of the full + hard-range width, a fixed absolute delta around the current value, or an exact + bound pair. The method reads the live environment value for ``vname``, computes + the candidate bounds, clips them to the hard limits from ``config["lower_bound"]`` + and ``config["upper_bound"]``, and applies the result to the GUI variable table. + Initial points for that variable are recalculated with the new bounds. + + Parameters + ---------- + vname : str + Name of the variable whose bounds should be updated. + config : dict + Bounds configuration for the variable, including the hard limits, + ``limit_option_idx``, ``ratio_full``, ``ratio_curr``, ``delta``, and + ``exact_bounds``. + """ logger.info( f"Setting individual variable range for {vname} with config: {config}" ) @@ -1469,6 +1490,7 @@ def set_ind_vrange(self, vname, config): "ratio_full": config["ratio_full"], "ratio_curr": config["ratio_curr"], "delta": config["delta"], + "exact_bounds": config["exact_bounds"], } option_idx = option["limit_option_idx"] @@ -1476,17 +1498,24 @@ def set_ind_vrange(self, vname, config): env = self.create_env() curr = env.get_variables([vname])[vname] - # 0: ratio with current value, 1: ratio with full range, 2: delta around current value + # set bounds based on selected option if option_idx == 1: + # ratio with full range ratio = option["ratio_full"] delta = 0.5 * ratio * (hard_bounds[1] - hard_bounds[0]) bounds = [curr - delta, curr + delta] new_bounds = np.clip(bounds, hard_bounds[0], hard_bounds[1]).tolist() elif option_idx == 2: + # delta around current value delta = option["delta"] bounds = [curr - delta, curr + delta] new_bounds = np.clip(bounds, hard_bounds[0], hard_bounds[1]).tolist() + elif option_idx == 3: + # set exact bounds + bounds = option.get("exact_bounds", hard_bounds) + new_bounds = np.clip(bounds, hard_bounds[0], hard_bounds[1]).tolist() else: + # ratio around current value ratio = option["ratio_curr"] sign = np.sign(curr) bounds = [ @@ -1515,10 +1544,21 @@ def set_ind_vrange(self, vname, config): self.ratio_var_ranges[vname] = copy.deepcopy(option) self.env_box.var_table.set_scan_range_options() - def adjust_variable_range_options(self, ratio: float, var_name: str = None): + def adjust_variable_range_options( + self, ratio: float, var_name: str | None = None + ) -> None: """ Scale variable ranges by ratio and recalculate bounds + Variable range options: + - `option_idx == 1`, bounds are calculated as a fraction of the full + variable range. + - `option_idx == 2`, bounds are calculated as a delta on either side of the + current value. + - `option_idx == 3`, the stored exact numerical bounds are used. + - Otherwise, (`option_idx == 0`) the ratio_curr mode calculated bounds as a + fraction of the current value around the current point. + Parameters ---------- ratio : float @@ -1545,16 +1585,25 @@ def adjust_variable_range_options(self, ratio: float, var_name: str = None): # get copy of selected vrange option option = copy.copy(self.ratio_var_ranges.get(vname, self.limit_option)) option_idx = option["limit_option_idx"] - - if option_idx == 1: - key = "ratio_full" - elif option_idx == 2: - key = "delta" + if option_idx == 3: + # exact bounds: scale span by ratio around the current center. + exact_bounds = list(option.get("exact_bounds", [0.0, 0.0])) + lo, hi = sorted(exact_bounds) + center = 0.5 * (lo + hi) + half_span = 0.5 * (hi - lo) * ratio + option["exact_bounds"] = [center - half_span, center + half_span] else: - key = "ratio_curr" + # relative bounds options + if option_idx == 1: + key = "ratio_full" + elif option_idx == 2: + key = "delta" + else: + key = "ratio_curr" + + # update selected option with multiplication by ratio + option[key] = option[key] * ratio - # update selected option with multiplication by ratio - option[key] = option[key] * ratio self.ratio_var_ranges[vname] = option # recalculate bounds @@ -1612,7 +1661,32 @@ def update_init_table(self, force=False): self.clear_init_table(reset_actions=False) self._fill_init_table() - def calc_auto_bounds(self): + def calc_auto_bounds(self) -> tuple[dict, dict]: + """ + Compute auto-derived bounds for all selected variables. + + Returns + ------- + vrange, clipped: tuple[dict[str, list[float, float]], dict[str, bool]] + A tuple of dictionaires. Each dictionary is keyed by variable name. + `vrange` is a dict of [low, high] bounds for each variable. + `clipped` gives a bool for each variable indicating whether or not the + calculated bounds were clipped by hard limits. + + + The method of calculation is determined for each variable by that variable's + configured `limit_option_idx`, stored in `self.ratio_var_ranges[var_name]`. + - For `option_idx == 1`, bounds are calculated as a fraction of the full + variable range. + - For `option_idx == 2`, bounds are calculated as a delta on either side of the + current value. + - For `option_idx == 3`, the stored exact numerical bounds are used. + - Otherwise, (`option_idx == 0`) the ratio_curr mode calculated bounds as a + fraction of the current value around the current point. + In every case, the resulting bounds are + clipped to the variable's hard limits and the `clipped` dictionary records + whether a bound was reduced by that clip. + """ logger.info("Calculating auto bounds for selected variables.") vname_selected = [] vrange = {} @@ -1636,7 +1710,9 @@ def calc_auto_bounds(self): limit_option = self.limit_option option_idx = limit_option["limit_option_idx"] - # 0: ratio with current value, 1: ratio with full range, 2: delta around current value + # 0: ratio with current value, 1: ratio with full range, 2: delta around current value, 3: exact bounds + # Note that options 0, 1, and 2 are recalculated relative to current variable value, option 3 will not + # recalculate, this function just makes sure they are within the hard limits for the variable. if option_idx == 1: ratio = limit_option["ratio_full"] hard_bounds = vrange[name] @@ -1654,6 +1730,14 @@ def calc_auto_bounds(self): clipped[name] = bounds != new_bounds vrange[name] = new_bounds logger.info(f"Auto bounds for {name} (delta): {new_bounds}") + elif option_idx == 3: + exact_bounds = limit_option.get("exact_bounds") + hard_bounds = vrange[name] + bounds = sorted(exact_bounds) if exact_bounds else list(hard_bounds) + new_bounds = np.clip(bounds, hard_bounds[0], hard_bounds[1]).tolist() + clipped[name] = bounds != new_bounds + vrange[name] = new_bounds + logger.info(f"Auto bounds for {name} (exact): {new_bounds}") else: ratio = limit_option["ratio_curr"] hard_bounds = vrange[name] @@ -1757,10 +1841,18 @@ def handle_var_config(self, vname): except KeyError: option = self.limit_option + current_bounds = self.env_box.var_table.bounds.get(vname, bounds) + if current_bounds is None: + # default to env bounds if bounds not set + current_bounds = bounds + if isinstance(current_bounds, ContinuousVariable): + current_bounds = current_bounds.domain + configs = { "current_value": curr, "lower_bound": bounds[0], "upper_bound": bounds[1], + "current_bounds": current_bounds, **option, } diff --git a/src/badger/gui/windows/ind_lim_vrange_dialog.py b/src/badger/gui/windows/ind_lim_vrange_dialog.py index 6f0c0db3..d0b591b1 100644 --- a/src/badger/gui/windows/ind_lim_vrange_dialog.py +++ b/src/badger/gui/windows/ind_lim_vrange_dialog.py @@ -48,6 +48,7 @@ def init_ui(self): info_group = QFrame() vbox_info = QVBoxLayout(info_group) vbox_info.setContentsMargins(2, 2, 2, 2) + vbox_info.setSpacing(2) # Current value row hbox_current = QHBoxLayout() @@ -69,6 +70,22 @@ def init_ui(self): # Add the rows to the info group vbox_info.addLayout(hbox_current) + if "current_bounds" in self.configs: + # Display current bounds + hbox_bounds = QHBoxLayout() + hbox_bounds.setContentsMargins(0, 0, 0, 0) + lbl_bounds = QLabel("Current bounds:") + lbl_bounds.setFixedWidth(128) + current_bounds = self.configs["current_bounds"] + bounds_text = f"[{current_bounds[0]:.5f}, {current_bounds[1]:.5f}]" + lbl_current_bounds = QLabel(bounds_text) + lbl_current_bounds.setStyleSheet("color: #788D9C;") + hbox_bounds.addWidget(lbl_bounds) + hbox_bounds.addStretch() + hbox_bounds.addWidget(lbl_current_bounds) + hbox_bounds.addSpacing(8) + vbox_info.addLayout(hbox_bounds) + # Add the info group to the main layout vbox.addWidget(info_group) @@ -103,6 +120,7 @@ def init_ui(self): "ratio wrt current value", "ratio wrt full range", "delta around current value", + "exact bounds", ] ) cb.setCurrentIndex(self.configs.get("limit_option_idx", 0)) @@ -155,9 +173,55 @@ def init_ui(self): hbox_delta.addWidget(lbl) hbox_delta.addWidget(sb_delta, 1) + # Absolute bounds config (used in non-relative mode) + bounds_config = QWidget() + vbox_bounds = QVBoxLayout(bounds_config) + vbox_bounds.setContentsMargins(0, 0, 0, 0) + + # Get hard limits (full variable range) + hard_lower = float(self.configs.get("lower_bound", -1e10)) + hard_upper = float(self.configs.get("upper_bound", 1e10)) + + # Get exact bounds from configs, or use current or hard bounds if not specified. + exact_bounds = list( + self.configs.get( + "exact_bounds", + self.configs.get("current_bounds", [hard_lower, hard_upper]), + ) + ) + self.configs["exact_bounds"] = exact_bounds + + hbox_bounds_lower = QHBoxLayout() + hbox_bounds_lower.setContentsMargins(0, 0, 0, 0) + lbl_lower = QLabel("Lower") + self.sb_bounds_lower = sb_bounds_lower = QDoubleSpinBox() + sb_bounds_lower.setMinimum(hard_lower) + sb_bounds_lower.setMaximum(hard_upper) + sb_bounds_lower.setDecimals(6) + sb_bounds_lower.setSingleStep((hard_upper - hard_lower) / 50) + sb_bounds_lower.setValue(exact_bounds[0]) + hbox_bounds_lower.addWidget(lbl_lower) + hbox_bounds_lower.addWidget(sb_bounds_lower, 1) + + hbox_bounds_upper = QHBoxLayout() + hbox_bounds_upper.setContentsMargins(0, 0, 0, 0) + lbl_upper = QLabel("Upper") + self.sb_bounds_upper = sb_bounds_upper = QDoubleSpinBox() + sb_bounds_upper.setMinimum(hard_lower) + sb_bounds_upper.setMaximum(hard_upper) + sb_bounds_upper.setDecimals(6) + sb_bounds_upper.setSingleStep((hard_upper - hard_lower) / 50) + sb_bounds_upper.setValue(exact_bounds[1]) + hbox_bounds_upper.addWidget(lbl_upper) + hbox_bounds_upper.addWidget(sb_bounds_upper, 1) + + vbox_bounds.addLayout(hbox_bounds_lower) + vbox_bounds.addLayout(hbox_bounds_upper) + stacks.addWidget(ratio_curr_config) stacks.addWidget(ratio_full_config) stacks.addWidget(delta_config) + stacks.addWidget(bounds_config) stacks.setCurrentIndex(self.configs.get("limit_option_idx", 0)) vbox_config.addWidget(stacks) @@ -214,6 +278,8 @@ def config_logic(self): self.sb_ratio_curr.valueChanged.connect(self.ratio_curr_changed) self.sb_ratio_full.valueChanged.connect(self.ratio_full_changed) self.sb_delta.valueChanged.connect(self.delta_changed) + self.sb_bounds_lower.valueChanged.connect(self.bounds_lower_changed) + self.sb_bounds_upper.valueChanged.connect(self.bounds_upper_changed) self.update_bounds_preview() def _clip(self, value: float, lower: float, upper: float) -> float: @@ -229,7 +295,14 @@ def update_bounds_preview(self) -> None: hard_upper = self.configs.get("upper_bound", 0) option_idx = self.cb.currentIndex() - if option_idx == 1: + if option_idx == 0: + ratio = self.sb_ratio_curr.value() + sign = math.copysign(1.0, curr) if curr != 0 else 0.0 + bounds = [ + curr * (1 - 0.5 * sign * ratio), + curr * (1 + 0.5 * sign * ratio), + ] + elif option_idx == 1: ratio = self.sb_ratio_full.value() delta = 0.5 * ratio * (hard_upper - hard_lower) bounds = [curr - delta, curr + delta] @@ -237,11 +310,9 @@ def update_bounds_preview(self) -> None: delta = self.sb_delta.value() bounds = [curr - delta, curr + delta] else: - ratio = self.sb_ratio_curr.value() - sign = math.copysign(1.0, curr) if curr != 0 else 0.0 bounds = [ - curr * (1 - 0.5 * sign * ratio), - curr * (1 + 0.5 * sign * ratio), + float(self.sb_bounds_lower.value()), + float(self.sb_bounds_upper.value()), ] bounds = [ @@ -262,10 +333,6 @@ def update_bounds_preview(self) -> None: def update_config(self): try: - # lower = float(self.lbl_hard_lower.text()) - # upper = float(self.lbl_hard_upper.text()) - # self.configs["lower_bound"] = lower - # self.configs["upper_bound"] = upper # Fill in default values if not exist if "limit_option_idx" not in self.configs: self.configs["limit_option_idx"] = self.cb.currentIndex() @@ -275,6 +342,10 @@ def update_config(self): self.configs["ratio_full"] = self.sb_ratio_full.value() if "delta" not in self.configs: self.configs["delta"] = self.sb_delta.value() + lower = float(self.sb_bounds_lower.value()) + upper = float(self.sb_bounds_upper.value()) + # Keep exact bounds ordered to avoid invalid [lower, upper] ranges. + self.configs["exact_bounds"] = [min(lower, upper), max(lower, upper)] except ValueError: pass # Optionally handle invalid input @@ -290,6 +361,14 @@ def delta_changed(self, delta): self.configs["delta"] = delta self.update_bounds_preview() + def bounds_lower_changed(self, lower): + self.configs["exact_bounds"][0] = lower + self.update_bounds_preview() + + def bounds_upper_changed(self, upper): + self.configs["exact_bounds"][1] = upper + self.update_bounds_preview() + def set(self): self.update_config() self.apply_config(self.name, self.configs)