From 8316ae1ee441fb99aa0577073552c02a0e00680f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Schm=C3=B6lder?= Date: Wed, 26 Jun 2024 10:15:56 +0200 Subject: [PATCH 1/5] Add COBYQA optimizer --- CADETProcess/optimization/__init__.py | 3 +- CADETProcess/optimization/scipyAdapter.py | 76 ++++++++++++++++++++++- pyproject.toml | 2 +- tests/test_optimizer_behavior.py | 8 +++ 4 files changed, 85 insertions(+), 4 deletions(-) diff --git a/CADETProcess/optimization/__init__.py b/CADETProcess/optimization/__init__.py index 842e2832..a135fed4 100644 --- a/CADETProcess/optimization/__init__.py +++ b/CADETProcess/optimization/__init__.py @@ -36,6 +36,7 @@ TrustConstr COBYLA + COBYQA NelderMead SLSQP @@ -101,7 +102,7 @@ from .optimizationProblem import * from .parallelizationBackend import * from .optimizer import * -from .scipyAdapter import COBYLA, TrustConstr, NelderMead, SLSQP +from .scipyAdapter import COBYLA, COBYQA, TrustConstr, NelderMead, SLSQP from .pymooAdapter import NSGA2, U_NSGA3 import importlib diff --git a/CADETProcess/optimization/scipyAdapter.py b/CADETProcess/optimization/scipyAdapter.py index 6846f271..8cee2500 100644 --- a/CADETProcess/optimization/scipyAdapter.py +++ b/CADETProcess/optimization/scipyAdapter.py @@ -6,7 +6,7 @@ from CADETProcess import CADETProcessError from CADETProcess.dataStructure import ( - Bool, Switch, UnsignedInteger, UnsignedFloat + Bool, Switch, Float, UnsignedInteger, UnsignedFloat ) from CADETProcess.optimization import OptimizerBase, OptimizationProblem @@ -38,6 +38,7 @@ class SciPyInterface(OptimizerBase): See Also -------- COBYLA + COBYQA TrustConstr NelderMead SLSQP @@ -64,6 +65,7 @@ def _run(self, optimization_problem: OptimizationProblem, x0=None): See Also -------- COBYLA + COBYQA TrustConstr NelderMead SLSQP @@ -125,7 +127,7 @@ def callback_function(x, state=None): x0 = self.results.population_last.x[0, :] self.n_evals = self.results.n_evals options['maxiter'] = self.maxiter - self.n_evals - if str(self) == 'COBYLA': + if str(self) in ['COBYLA', 'COBYQA']: options['maxiter'] -= 1 with warnings.catch_warnings(): @@ -457,6 +459,76 @@ class COBYLA(SciPyInterface): _specific_options = ['rhobeg', 'tol', 'maxiter', 'disp', 'catol'] +class COBYQA(SciPyInterface): + """Wrapper for the COBYQA optimization method from the scipy optimization suite. + + It defines the solver options in the 'options' variable as a dictionary. + + Supports: + - Linear constraints + - Linear equality constraints + - Nonlinear constraints + - Bounds + + Parameters + ---------- + disp : bool, default False + Set to True to print information about the optimization procedure. + maxfev : int + Maximum number of function evaluations. + The default is None. + maxiter : int + Maximum number of iterations. + The default is None. + f_target : float + Target value for the objective function.The optimization procedure is + terminated when the objective function value of a feasible point (see + `feasibility_tol` below) is less than or equal to this target. + The default is `-np.inf` + feasibility_tol : float + Absolute tolerance for the constraint violation. + The default is 1e-8 + initial_tr_radius : float + Initial trust-region radius. Typically, this value should be in the order of one + tenth of the greatest expected change to the variables. + The default is 1.0 + final_tr_radius : float + Final trust-region radius. It should indicate the accuracy required in the final + values of the variables. If provided, this option overrides the value of `tol` + in the `minimize` function. + The default is 1e-6 + """ + + supports_linear_constraints = True + supports_linear_equality_constraints = True + supports_nonlinear_constraints = True + supports_bounds = True + + disp = Bool(default=False) + + maxfev = UnsignedInteger() + maxiter = UnsignedInteger() + f_target = Float(default=-np.inf) + feasibility_tol = UnsignedFloat(default=1e-8) + initial_tr_radius = UnsignedFloat(default=1.0) + final_tr_radius = UnsignedFloat(default=1e-6) + + x_tol = final_tr_radius # Alias for uniform interface + cv_nonlincon_tol = feasibility_tol # Alias for uniform interface + n_max_evals = maxfev # Alias for uniform interface + n_max_iter = maxiter # Alias for uniform interface + + _specific_options = [ + 'disp', + 'maxfev', + 'maxiter', + 'f_target', + 'feasibility_tol', + 'initial_tr_radius', + 'final_tr_radius', + ] + + class NelderMead(SciPyInterface): """Wrapper for the Nelder-Mead optimization method from the scipy optimization suite. diff --git a/pyproject.toml b/pyproject.toml index 0083582b..deb8fe53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ "pathos>=0.2.8", "psutil>=5.9.8", "pymoo>=0.6", - "scipy>=1.11", + "scipy>=1.14", ] [project.optional-dependencies] diff --git a/tests/test_optimizer_behavior.py b/tests/test_optimizer_behavior.py index b0af2265..9204acc4 100644 --- a/tests/test_optimizer_behavior.py +++ b/tests/test_optimizer_behavior.py @@ -4,6 +4,7 @@ from CADETProcess.optimization import ( OptimizerBase, + COBYQA, TrustConstr, COBYLA, NelderMead, @@ -80,6 +81,12 @@ def set_non_default_parameters(optimizer, problem): setattr(optimizer, pk, pv) +class COBYQA(COBYQA): + f_tol = F_TOL + x_tol = X_TOL + cv_tol = CV_NONLINCON_TOL + + class TrustConstr(TrustConstr): x_tol = X_TOL cv_nonlincon_tol = CV_NONLINCON_TOL @@ -161,6 +168,7 @@ def optimization_problem(request): @pytest.fixture( params=[ + COBYQA, TrustConstr, COBYLA, SLSQP, From 0cad9dd6eb9d32b2b29f09383b663f2e72f97023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Schm=C3=B6lder?= Date: Wed, 26 Jun 2024 10:17:23 +0200 Subject: [PATCH 2/5] Use COBYQA for fractionation --- CADETProcess/fractionation/fractionationOptimizer.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CADETProcess/fractionation/fractionationOptimizer.py b/CADETProcess/fractionation/fractionationOptimizer.py index c4510b75..96d1d717 100644 --- a/CADETProcess/fractionation/fractionationOptimizer.py +++ b/CADETProcess/fractionation/fractionationOptimizer.py @@ -7,7 +7,7 @@ from CADETProcess import SimulationResults from CADETProcess.fractionation import Fractionator from CADETProcess.optimization import OptimizerBase, OptimizationProblem -from CADETProcess.optimization import COBYLA +from CADETProcess.optimization import COBYQA from CADETProcess.performance import Mass, Purity @@ -48,13 +48,13 @@ def __init__(self, optimizer=None, log_level='WARNING'): ---------- optimizer: OptimizerBase, optional Optimizer for optimizing the fractionation times. - If no value is specified, a default COBYLA optimizer will be used. + If no value is specified, COBYQA is used by default. log_level: {'WARNING', 'INFO', 'DEBUG', 'ERROR'} Log level for the fractionation optimization process. The default is 'WARNING'. """ if optimizer is None: - optimizer = COBYLA() + optimizer = COBYQA() optimizer.tol = 1e-3 optimizer.catol = 5e-3 optimizer.rhobeg = 1e-4 @@ -294,7 +294,8 @@ def optimize_fractionation( The default is 1. obj_fun : function, optional Objective function used for OptimizationProblem. - If COBYLA is used, must return single objective. + Must return single objective if optimizer does not support multiple + objectives. If is None, the mass of all components is maximized. n_objectives : int, optional Number of objectives returned by obj_fun. The default is 1. From b5af0491628c2e271b5c6e741fcaede2b6992cc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Schm=C3=B6lder?= Date: Thu, 27 Jun 2024 10:20:06 +0200 Subject: [PATCH 3/5] Remove SLSQP and COBYLA in preparation for scipy's new callback interface SLSQP and COBYLA currently do not support scipy's new callback interface. --- CADETProcess/optimization/__init__.py | 4 +- CADETProcess/optimization/scipyAdapter.py | 100 +--------------------- tests/test_optimizer_behavior.py | 14 --- 3 files changed, 2 insertions(+), 116 deletions(-) diff --git a/CADETProcess/optimization/__init__.py b/CADETProcess/optimization/__init__.py index a135fed4..b257f0f5 100644 --- a/CADETProcess/optimization/__init__.py +++ b/CADETProcess/optimization/__init__.py @@ -35,10 +35,8 @@ :toctree: generated/ TrustConstr - COBYLA COBYQA NelderMead - SLSQP Pymoo ----- @@ -102,7 +100,7 @@ from .optimizationProblem import * from .parallelizationBackend import * from .optimizer import * -from .scipyAdapter import COBYLA, COBYQA, TrustConstr, NelderMead, SLSQP +from .scipyAdapter import COBYQA, TrustConstr, NelderMead from .pymooAdapter import NSGA2, U_NSGA3 import importlib diff --git a/CADETProcess/optimization/scipyAdapter.py b/CADETProcess/optimization/scipyAdapter.py index 8cee2500..a3551491 100644 --- a/CADETProcess/optimization/scipyAdapter.py +++ b/CADETProcess/optimization/scipyAdapter.py @@ -37,11 +37,9 @@ class SciPyInterface(OptimizerBase): See Also -------- - COBYLA COBYQA TrustConstr NelderMead - SLSQP CADETProcess.optimization.OptimizationProblem.evaluate_objectives options scipy.optimize.minimize @@ -64,11 +62,9 @@ def _run(self, optimization_problem: OptimizationProblem, x0=None): See Also -------- - COBYLA COBYQA TrustConstr NelderMead - SLSQP CADETProcess.optimization.OptimizationProblem.evaluate_objectives options scipy.optimize.minimize @@ -127,7 +123,7 @@ def callback_function(x, state=None): x0 = self.results.population_last.x[0, :] self.n_evals = self.results.n_evals options['maxiter'] = self.maxiter - self.n_evals - if str(self) in ['COBYLA', 'COBYQA']: + if str(self) == 'COBYQA': options['maxiter'] -= 1 with warnings.catch_warnings(): @@ -414,51 +410,6 @@ def __str__(self): return 'trust-constr' -class COBYLA(SciPyInterface): - """Wrapper for the COBYLA optimization method from the scipy optimization suite. - - It defines the solver options in the 'options' variable as a dictionary. - - Supports: - - Linear constraints - - Linear equality constraints - - Nonlinear constraints - - Parameters - ---------- - rhobeg : float, default 1 - Reasonable initial changes to the variables. - tol : float, default 0.0002 - Final accuracy in the optimization (not precisely guaranteed). - This is a lower bound on the size of the trust region. - disp : bool, default False - Set to True to print convergence messages. - If False, verbosity is ignored and set to 0. - maxiter : int, default 10000 - Maximum number of function evaluations. - catol : float, default 2e-4 - Absolute tolerance for constraint violations. - - """ - supports_linear_constraints = True - supports_linear_equality_constraints = True - supports_nonlinear_constraints = True - supports_bounds = True - - rhobeg = UnsignedFloat(default=1) - tol = UnsignedFloat(default=0.0002) - maxiter = UnsignedInteger(default=10000) - disp = Bool(default=False) - catol = UnsignedFloat(default=0.0002) - - x_tol = tol # Alias for uniform interface - cv_nonlincon_tol = catol # Alias for uniform interface - n_max_evals = maxiter # Alias for uniform interface - n_max_iter = maxiter # Alias for uniform interface - - _specific_options = ['rhobeg', 'tol', 'maxiter', 'disp', 'catol'] - - class COBYQA(SciPyInterface): """Wrapper for the COBYQA optimization method from the scipy optimization suite. @@ -575,52 +526,3 @@ class NelderMead(SciPyInterface): def __str__(self): return 'Nelder-Mead' - - -class SLSQP(SciPyInterface): - """Wrapper for the SLSQP optimization method from the scipy optimization suite. - - It defines the solver options in the 'options' variable as a dictionary. - - Supports: - - Linear constraints - - Linear equality constraints - - Nonlinear constraints - - Bounds - - Parameters - ---------- - ftol : float, default 1e-2 - Precision goal for the value of f in the stopping criterion. - eps : float, default 1e-6 - Step size used for numerical approximation of the Jacobian. - disp : bool, default False - Set to True to print convergence messages. - If False, verbosity is ignored and set to 0. - maxiter : int, default 1000 - Maximum number of iterations. - iprint: int, optional - The verbosity of fmin_slsqp : - iprint <= 0 : Silent operation - iprint == 1 : Print summary upon completion (default) - iprint >= 2 : Print status of each iterate and summary - """ - - supports_linear_constraints = True - supports_linear_equality_constraints = True - supports_nonlinear_constraints = True - supports_bounds = True - - ftol = UnsignedFloat(default=1e-2) - eps = UnsignedFloat(default=1e-6) - disp = Bool(default=False) - maxiter = UnsignedInteger(default=1000) - iprint = UnsignedInteger(ub=2, default=1) - - f_tol = ftol # Alias for uniform interface - n_max_evals = maxiter # Alias for uniform interface - n_max_iter = maxiter # Alias for uniform interface - - _specific_options = [ - 'ftol', 'eps', 'disp', 'maxiter', 'finite_diff_rel_step', 'iprint' - ] diff --git a/tests/test_optimizer_behavior.py b/tests/test_optimizer_behavior.py index 9204acc4..ab20bc3a 100644 --- a/tests/test_optimizer_behavior.py +++ b/tests/test_optimizer_behavior.py @@ -6,9 +6,7 @@ OptimizerBase, COBYQA, TrustConstr, - COBYLA, NelderMead, - SLSQP, U_NSGA3, GPEI, NEHVI, @@ -92,21 +90,11 @@ class TrustConstr(TrustConstr): cv_nonlincon_tol = CV_NONLINCON_TOL -class COBYLA(COBYLA): - x_tol = X_TOL - cv_nonlincon_tol = CV_NONLINCON_TOL - - class NelderMead(NelderMead): x_tol = X_TOL f_tol = F_TOL -class SLSQP(SLSQP): - x_tol = X_TOL - cv_lincon_tol = CV_LINCON_TOL - - class U_NSGA3(U_NSGA3): f_tol = F_TOL x_tol = X_TOL @@ -170,8 +158,6 @@ def optimization_problem(request): params=[ COBYQA, TrustConstr, - COBYLA, - SLSQP, NelderMead, U_NSGA3, GPEI, From 1246c035a6ce82e14b048e2e3e3d00fb08583565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Schm=C3=B6lder?= Date: Thu, 27 Jun 2024 11:03:58 +0200 Subject: [PATCH 4/5] Update callback function for scipy optimizers --- CADETProcess/optimization/scipyAdapter.py | 156 ++++++++++++---------- 1 file changed, 89 insertions(+), 67 deletions(-) diff --git a/CADETProcess/optimization/scipyAdapter.py b/CADETProcess/optimization/scipyAdapter.py index a3551491..bedde84d 100644 --- a/CADETProcess/optimization/scipyAdapter.py +++ b/CADETProcess/optimization/scipyAdapter.py @@ -1,12 +1,16 @@ import warnings from scipy import optimize -from scipy.optimize import OptimizeWarning +from scipy.optimize import OptimizeWarning, OptimizeResult import numpy as np from CADETProcess import CADETProcessError from CADETProcess.dataStructure import ( - Bool, Switch, Float, UnsignedInteger, UnsignedFloat + Bool, + Switch, + Float, + UnsignedInteger, + UnsignedFloat, ) from CADETProcess.optimization import OptimizerBase, OptimizationProblem @@ -45,9 +49,10 @@ class SciPyInterface(OptimizerBase): scipy.optimize.minimize """ + finite_diff_rel_step = UnsignedFloat() tol = UnsignedFloat() - jac = Switch(valid=['2-point', '3-point', 'cs'], default='2-point') + jac = Switch(valid=["2-point", "3-point", "cs"], default="2-point") def _run(self, optimization_problem: OptimizationProblem, x0=None): """Solve the optimization problem using any of the scipy methods. @@ -77,39 +82,33 @@ def _run(self, optimization_problem: OptimizationProblem, x0=None): def objective_function(x): return optimization_problem.evaluate_objectives( - x, untransform=True, get_dependent_values=True, ensure_minimization=True, + x, + untransform=True, + get_dependent_values=True, + ensure_minimization=True, )[0] - def callback_function(x, state=None): - """Internal callback to report progress after evaluation. - - Notes - ----- - Currently, this evaluates all functions again. This should not be a problem - since objectives and constraints are automatically cached. - - Unfortunately, only `trust-constr` returns a `state` which contains the - current best point. Hence, the internal pareto front is used. - """ + def callback(intermediate_result: OptimizeResult): + """Internal callback to report progress after evaluation.""" self.n_evals += 1 - x = x.tolist() - f = optimization_problem.evaluate_objectives( - x, + x_transformed = intermediate_result.x + f = intermediate_result.fun + + g = optimization_problem.evaluate_nonlinear_constraints( + x_transformed, untransform=True, get_dependent_values=True, - ensure_minimization=True, ) - g = optimization_problem.evaluate_nonlinear_constraints( - x, untransform=True, get_dependent_values=True, + cv_nonlincon = ( + optimization_problem.evaluate_nonlinear_constraints_violation( + x_transformed, + untransform=True, + get_dependent_values=True, + ) ) - cv = optimization_problem.evaluate_nonlinear_constraints_violation( - x, untransform=True, get_dependent_values=True, - ) - - self.run_post_processing(x, f, g, cv, self.n_evals) - return False + self.run_post_processing(x_transformed, f, g, cv_nonlincon, self.n_evals) if x0 is None: x0 = optimization_problem.create_initial_values( @@ -122,13 +121,13 @@ def callback_function(x, state=None): if self.results.n_gen > 0: x0 = self.results.population_last.x[0, :] self.n_evals = self.results.n_evals - options['maxiter'] = self.maxiter - self.n_evals - if str(self) == 'COBYQA': - options['maxiter'] -= 1 + options["maxiter"] = self.maxiter - self.n_evals + if str(self) == "COBYQA": + options["maxiter"] -= 1 with warnings.catch_warnings(): - warnings.filterwarnings('ignore', category=OptimizeWarning) - warnings.filterwarnings('ignore', category=RuntimeWarning) + warnings.filterwarnings("ignore", category=OptimizeWarning) + warnings.filterwarnings("ignore", category=RuntimeWarning) scipy_results = optimize.minimize( objective_function, x0=x0_transformed, @@ -138,7 +137,7 @@ def callback_function(x, state=None): constraints=self.get_constraint_objects(optimization_problem), bounds=self.get_bounds(optimization_problem), options=options, - callback=callback_function, + callback=callback, ) self.results.success = bool(scipy_results.success) @@ -160,7 +159,7 @@ def get_bounds(self, optimization_problem): return optimize.Bounds( optimization_problem.lower_bounds_independent_transformed, optimization_problem.upper_bounds_independent_transformed, - keep_feasible=True + keep_feasible=True, ) def get_constraint_objects(self, optimization_problem): @@ -204,7 +203,7 @@ def get_lincon_obj(self, optimization_problem): if optimization_problem.n_linear_constraints == 0: return None - lb = [-np.inf]*len(optimization_problem.b) + lb = [-np.inf] * len(optimization_problem.b) ub = optimization_problem.b_transformed return optimize.LinearConstraint( @@ -233,8 +232,7 @@ def get_lineqcon_obj(self, optimization_problem): ub = optimization_problem.beq_transformed + optimization_problem.eps_lineq return optimize.LinearConstraint( - optimization_problem.Aeq_independent_transformed, lb, ub, - keep_feasible=True + optimization_problem.Aeq_independent_transformed, lb, ub, keep_feasible=True ) def get_nonlincon_obj(self, optimization_problem): @@ -275,11 +273,14 @@ def makeConstraint(i): """ constr = optimize.NonlinearConstraint( lambda x: opt.evaluate_nonlinear_constraints_violation( - x, untransform=True, get_dependent_values=True, + x, + untransform=True, + get_dependent_values=True, )[i], - lb=-np.inf, ub=0, + lb=-np.inf, + ub=0, finite_diff_rel_step=self.finite_diff_rel_step, - keep_feasible=True + keep_feasible=True, ) return constr @@ -376,6 +377,7 @@ class TrustConstr(SciPyInterface): If True, then verbose will be set to 1 if it was 0. Default is False. """ + supports_linear_constraints = True supports_linear_equality_constraints = True supports_nonlinear_constraints = True @@ -388,26 +390,40 @@ class TrustConstr(SciPyInterface): initial_constr_penalty = UnsignedFloat(default=1.0) initial_barrier_parameter = UnsignedFloat(default=0.1) initial_barrier_tolerance = UnsignedFloat(default=0.1) - factorization_method = Switch(valid=[ - 'NormalEquation', 'AugmentedSystem', 'QRFactorization', 'SVDFactorization' - ]) + factorization_method = Switch( + valid=[ + "NormalEquation", + "AugmentedSystem", + "QRFactorization", + "SVDFactorization", + ] + ) maxiter = UnsignedInteger(default=1000) verbose = UnsignedInteger(default=0) disp = Bool(default=False) - x_tol = xtol # Alias for uniform interface - cv_nonlincon_tol = gtol # Alias for uniform interface - n_max_evals = maxiter # Alias for uniform interface - n_max_iter = maxiter # Alias for uniform interface + x_tol = xtol # Alias for uniform interface + cv_nonlincon_tol = gtol # Alias for uniform interface + n_max_evals = maxiter # Alias for uniform interface + n_max_iter = maxiter # Alias for uniform interface _specific_options = [ - 'gtol', 'xtol', 'barrier_tol', 'finite_diff_rel_step', - 'initial_constr_penalty', 'initial_tr_radius', 'initial_barrier_parameter', - 'initial_barrier_tolerance', 'factorization_method', 'maxiter', 'verbose', 'disp' + "gtol", + "xtol", + "barrier_tol", + "finite_diff_rel_step", + "initial_constr_penalty", + "initial_tr_radius", + "initial_barrier_parameter", + "initial_barrier_tolerance", + "factorization_method", + "maxiter", + "verbose", + "disp", ] def __str__(self): - return 'trust-constr' + return "trust-constr" class COBYQA(SciPyInterface): @@ -464,19 +480,19 @@ class COBYQA(SciPyInterface): initial_tr_radius = UnsignedFloat(default=1.0) final_tr_radius = UnsignedFloat(default=1e-6) - x_tol = final_tr_radius # Alias for uniform interface + x_tol = final_tr_radius # Alias for uniform interface cv_nonlincon_tol = feasibility_tol # Alias for uniform interface - n_max_evals = maxfev # Alias for uniform interface - n_max_iter = maxiter # Alias for uniform interface + n_max_evals = maxfev # Alias for uniform interface + n_max_iter = maxiter # Alias for uniform interface _specific_options = [ - 'disp', - 'maxfev', - 'maxiter', - 'f_target', - 'feasibility_tol', - 'initial_tr_radius', - 'final_tr_radius', + "disp", + "maxfev", + "maxiter", + "f_target", + "feasibility_tol", + "initial_tr_radius", + "final_tr_radius", ] @@ -506,6 +522,7 @@ class NelderMead(SciPyInterface): disp : Bool, optional Set to True to print convergence messages. """ + supports_bounds = True maxiter = UnsignedInteger(default=1000) @@ -515,14 +532,19 @@ class NelderMead(SciPyInterface): adaptive = Bool(default=True) disp = Bool(default=False) - x_tol = xatol # Alias for uniform interface - f_tol = fatol # Alias for uniform interface - n_max_evals = maxiter # Alias for uniform interface - n_max_iter = maxiter # Alias for uniform interface + x_tol = xatol # Alias for uniform interface + f_tol = fatol # Alias for uniform interface + n_max_evals = maxiter # Alias for uniform interface + n_max_iter = maxiter # Alias for uniform interface _specific_options = [ - 'maxiter', 'initial_simplex', 'xatol', 'fatol', 'adaptive', 'disp' + "maxiter", + "initial_simplex", + "xatol", + "fatol", + "adaptive", + "disp", ] def __str__(self): - return 'Nelder-Mead' + return "Nelder-Mead" From 510152e79a21e2a4d352b0b98afee4da93cb5000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Schm=C3=B6lder?= Date: Wed, 22 Jan 2025 15:39:42 +0100 Subject: [PATCH 5/5] fixup! Add functionality to round to significant digits. --- CADETProcess/transform.py | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/CADETProcess/transform.py b/CADETProcess/transform.py index f2d132be..60dd809c 100644 --- a/CADETProcess/transform.py +++ b/CADETProcess/transform.py @@ -140,7 +140,7 @@ def ub(self): """ pass - def transform(self, x): + def transform(self, x, significant_digits=None): """Transform the input parameter space to the output parameter space. Applies the transformation function _transform to x after performing input @@ -151,23 +151,33 @@ def transform(self, x): ---------- x : {float, array} Input parameter values. + significant_digits : int, optional + Number of significant figures to which variable can be rounded. + If None, variable is not rounded. The default is None. Returns ------- {float, array} Transformed parameter values. """ + x_round = round_to_significant_digits(x, digits=significant_digits) + if ( not self.allow_extended_input and - not np.all((self.lb_input <= x) * (x <= self.ub_input))): + not np.all((self.lb_input <= x_round) * (x_round <= self.ub_input)) + ): raise ValueError("Value exceeds input bounds.") - x = self._transform(x) + + x_trans = self._transform(x_round) + x_trans_round = round_to_significant_digits(x_trans, digits=significant_digits) + if ( not self.allow_extended_output and - not np.all((self.lb <= x) * (x <= self.ub))): + not np.all((self.lb <= x_trans_round) * (x_trans_round <= self.ub)) + ): raise ValueError("Value exceeds output bounds.") - return x + return x_trans_round @abstractmethod def _transform(self, x): @@ -187,7 +197,7 @@ def _transform(self, x): """ pass - def untransform(self, x, significant_digits=None): + def untransform(self, x_transformed, significant_digits=None): """Transform the output parameter space to the input parameter space. Applies the transformation function _untransform to x after performing output @@ -196,7 +206,7 @@ def untransform(self, x, significant_digits=None): Parameters ---------- - x : {float, array} + x_transformed : {float, array} Output parameter values. significant_digits : int, optional Number of significant figures to which variable can be rounded. @@ -207,22 +217,25 @@ def untransform(self, x, significant_digits=None): {float, array} Transformed parameter values. """ - x_ = round_to_significant_digits(x, digits=significant_digits) + x_round = round_to_significant_digits(x_transformed, digits=significant_digits) if ( not self.allow_extended_output and - not np.all((self.lb <= x_) * (x_ <= self.ub))): + not np.all((self.lb <= x_round) * (x_round <= self.ub)) + ): raise ValueError("Value exceeds output bounds.") - x_ = self._untransform(x_) - x_ = round_to_significant_digits(x_, digits=significant_digits) + x_untrans = self._untransform(x_round) + x_untrans_round = round_to_significant_digits(x_untrans, digits=significant_digits) if ( not self.allow_extended_input and - not np.all((self.lb_input <= x_) * (x_ <= self.ub_input))): + not np.all((self.lb_input <= x_untrans_round) * (x_untrans_round <= self.ub_input)) + ): raise ValueError("Value exceeds input bounds.") - return x_ + return x_untrans_round + @abstractmethod def _untransform(self, x):