From 21433ab99cd65d41c950c4f2807b18e196920e51 Mon Sep 17 00:00:00 2001 From: Yoel Date: Wed, 6 May 2026 10:55:22 -0500 Subject: [PATCH] fix model convergence bug; add tests for model optimization methods --- biosteam/evaluation/_model.py | 7 ++- biosteam/evaluation/_prediction.py | 2 +- tests/test_model.py | 79 +++++++++++++++++++++++++++++- 3 files changed, 81 insertions(+), 7 deletions(-) diff --git a/biosteam/evaluation/_model.py b/biosteam/evaluation/_model.py index fbab1cc2..55486e07 100644 --- a/biosteam/evaluation/_model.py +++ b/biosteam/evaluation/_model.py @@ -910,6 +910,7 @@ def optimize(self, else: optimizer_options = {} if isinstance(convergence_model, str): + if convergence_options is None: convergence_options = {} convergence_model = ConvergenceModel( system=self.system, parameters=parameters, @@ -942,10 +943,8 @@ def optimize(self, ) else: raise ValueError(f'invalid optimization method {method!r}') - if isinstance(convergence_model, str): - return result, convergence_model - else: - return result + result.convergence_model = convergence_model + return result def evaluate(self, notify=0, file=None, autosave=0, autoload=False, convergence_model=None, **kwargs): diff --git a/biosteam/evaluation/_prediction.py b/biosteam/evaluation/_prediction.py index 3859baa9..09eda542 100644 --- a/biosteam/evaluation/_prediction.py +++ b/biosteam/evaluation/_prediction.py @@ -599,7 +599,7 @@ def load_responses(self): sample = [i.baseline for i in parameters] evaluate = self.evaluate_system_convergence baseline_1 = evaluate(sample) - values = [] + values = [baseline_1] values_at_bounds = [] samples = [sample] for i, p in enumerate(parameters): diff --git a/tests/test_model.py b/tests/test_model.py index 844c8229..4451cb3a 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -339,7 +339,7 @@ def set_M2_tau(i): D, p = model.kolmogorov_smirnov_d(thresholds=[1, 1.5]) # Just make sure it works for now # TODO: Add tests that make sense for comparing statistics -def test_model_optimization_differential_evolution(): +def test_model_optimization_no_system(): import biosteam as bst import numpy as np model = bst.Model(bst.System()) @@ -368,6 +368,80 @@ def objective(): ) assert_allclose(solution.x, [0.5, 0.5], rtol=1e-3, atol=1e-3) +def test_model_optimization_with_system(): + import biosteam as bst + from numpy.testing import assert_allclose + bst.settings.set_thermo([ + bst.Chemical('F', default=True, search_db=False, phase='l') + ]) + with bst.System() as sys: + feed = bst.Stream(F=1) + recycle = bst.Stream() + product1 = bst.Stream() + product2 = bst.Stream() + mixer = bst.Mixer(ins=(feed, recycle)) + splitter1 = bst.Splitter(ins=mixer-0, outs=('stream', product1), split=0.5) + splitter2 = bst.Splitter(ins=splitter1-0, outs=(recycle, product2), split=0.5) + + sys.set_tolerance(mol=1e-16, rmol=1e-16) + model = bst.Model(sys) + + @model.optimized_parameter(bounds=(0.01, 0.99), baseline=0.1) + def split1(x1): + splitter1.split = x1 + + @model.indicator + def objective(): + y1 = product1.imol['F'] + y2 = product2.imol['F'] + return y1**2 - y1 + y2**2 - y2 + + simple_convergence_models = ( + None, 'linear regressor', 'intercept linear regressor', + ) + methods = ( + 'cobyla', 'cobyqa', 'trust-constr', 'slsqp', + 'L-BFGS-B', 'shgo', 'differential evolution' + ) + results = {} + for method in methods: + for convergence_model in simple_convergence_models: + for local_weighted in (True, False): + recycle.empty() + solution = model.optimize( + objective, + method=method, + convergence_model=convergence_model, + convergence_options=dict(save_prediction=True, + local_weighted=local_weighted) + ) + y1 = product1.imol['F'] + y2 = product2.imol['F'] + assert_allclose([y1, y2], [0.5, 0.5], rtol=1e-3, atol=1e-3) + assert_allclose(solution.x, [0.6666667451749273], rtol=1e-3, atol=1e-3) + if convergence_model is None: continue + _, summary = solution.convergence_model.R2() + results[method, convergence_model, local_weighted] = R2 = summary['predicted']['recycle.F'] + assert R2 > 0.7 + advanced_convergence_models = ('svr', 'linear svr') + results = {} + for method in methods: + for convergence_model in advanced_convergence_models: + recycle.empty() + solution = model.optimize( + objective, + method=method, + convergence_model=convergence_model, + convergence_options=dict(save_prediction=True, nfits=None, recess=0) + ) + y1 = product1.imol['F'] + y2 = product2.imol['F'] + assert_allclose([y1, y2], [0.5, 0.5], rtol=1e-3, atol=1e-3) + assert_allclose(solution.x, [0.6666667451749273], rtol=1e-3, atol=1e-3) + _, summary = solution.convergence_model.R2() + results[method, convergence_model] = R2 = summary['predicted']['recycle.F'] + assert R2 > 0.7 + if __name__ == '__main__': test_parameter_hook() test_pearson_r() @@ -379,4 +453,5 @@ def objective(): test_model_exception_hook() test_parameters_from_df() test_kolmogorov_smirnov_d() - test_model_optimization_differential_evolution() \ No newline at end of file + test_model_optimization_no_system() + test_model_optimization_with_system() \ No newline at end of file