3 Parameter estimation#

This notebook introduces the Estimator class and demonstrates it’s functions for parameter estimation using the example of a simple batch model (see Notebook 1. Modeling & Simulation).

from estim8.models import FmuModel
from estim8 import visualization, datatypes, Estimator, generalized_islands
import pandas as pd
import pygmo

3.1 Load the SimpleBatch FMU model#

SimpleBatchModel = FmuModel(path=r'../../../tests/test_data/SimpleBatch.fmu')
print(f"Model parameters: \n {SimpleBatchModel.parameters}")
print(f"Model observables: \n {SimpleBatchModel.observables}")
Model parameters: 
 {'Ks': 0.01, 'S0': 10.0, 'X0': 0.2, 'Y_XS': 0.35, 'mu_max': 0.4}
Model observables: 
 ['S', 'X', 'der(S)', 'der(X)', 'mu']

3.2 Load experimental data & data structure setup#

In order to setup an Experiment data structure (see Notebook 2), raw measurement data is loaded first. Here, the function pandas.read_excel is used to read the contents of an Excel sheet SimpleBatch_Data.xlsx, which contains artificial measurements for biomass concentration X \([g \cdot L^{-1}]\) and substrate concentration S \([g \cdot L^{-1}]\) over the duration of a batch cultivation.

# import datasheet
data = pd.read_excel(r'SimpleBatch_Data.xlsx', index_col=0, header=(0,1))
data.head(11)
Time X S
h g/L g/L
0.0 0.176200 NaN
0.1 0.318313 NaN
0.2 0.285270 NaN
0.3 0.218600 NaN
0.4 0.248210 NaN
0.5 0.268477 NaN
0.6 0.312500 NaN
0.7 0.150687 NaN
0.8 0.226870 NaN
0.9 0.258150 NaN
1.0 0.275126 9.632284

Before creating an Experiment object by passing the created DataFrame, the multi-index must be removed.

For this simple example, the Experiment’s default LinearErrorModel is used to describe the Measurement noise (see Notebook 2. Experimental data and error modeling). As the names of the columns in data directly correspond to the states in SimpleBatchModel.observables no observation_mapping must be defined.

# drop the multi-index header and show resulting dataframe
data.columns = data.columns.droplevel(1)
display(data)

# create an Experiment object
experiment = datatypes.Experiment(data) 
Time X S
0.0 0.176200 NaN
0.1 0.318313 NaN
0.2 0.285270 NaN
0.3 0.218600 NaN
0.4 0.248210 NaN
... ... ...
9.6 4.885956 NaN
9.7 4.807351 NaN
9.8 5.167874 NaN
9.9 5.220779 NaN
10.0 5.065959 0.0

101 rows × 2 columns

To compare the unfitted model’s precitions to experimental data, the plot_simulation function of the visualization submodule takes the keyword argument experiment.

# compare simulation to data
simulation = SimpleBatchModel.simulate(t0=0, t_end=10, stepsize=0.1)
_ = visualization.plot_simulation(simulation=simulation, experiment=experiment, observe=['S', 'X'])
../_images/8d0710a75cf3deee4e485cb6927494e826715b1c435cde6662031e8bd6b405ea.png

3.3 Problem definition#

Besides a model for forward simulation and experimental data, an estimation problem is defined by unknown parameters and and their bounds using a dictionary.

# define unknown parameters with upper and lower bounds
bounds = {
    'X0': [0.05, 0.15],
    'mu_max': [0.1, 0.9],
    'Y_XS': [0.1, 1]
}

3.4 The Estimator class#

The Estimator class collects all single parts of a problem defined by the user and manages the processes of model fitting. Creating an instance of this class requires the following arguments:

Argument

Type

Description

model

models.Estim8Model

The model class to be fitted. Must be a subclass of Estim8Model that runs the simulations.

bounds

dict[str, List[float]]

Dictionary defining the parameter bounds for estimation. Format: {“param_name”: [lower_bound, upper_bound]}.

data

Experiment | Dict[str, Experiment]

Experimental data for model fitting. Can be single experiment or multiple replicates as Experiment objects in form of dict{replicate_ID: Experiment}.

Optional keyword arguments are:

Kwarg

Type

Description

t

List[float], optional

Time vector specification as [t_0, t_end, step_size]. If None, will be automatically determined from the experimental data

metric

Literal[“SS”, “WSS”, “negLL”], optional

Loss function for optimization: “SS” (Sum of squared residuals), “WSS” (Weighted sum of squared residuals), or “negLL” (Negative log-likelihood). Default is “SS”

parameter_mapping

List[ParameterMapper] | ParameterMapping, optional

Defines parameter relationships between replicates. Can be list of mappings or complete ParameterMapping object. None means all parameters are applied to all model replicates (default)

# Instantiating an Estimator object
estimator = Estimator(
    model=SimpleBatchModel,     # model used for simulation
    bounds=bounds,              # unknown parameters and bounds
    data=experiment,            # experimental data
    t=[0, 10, 0.1],             # the timeframe for model simulation as [t_0, t_end, step_size],
    metric = 'SS'               # default objective function Sum of squared residuals
)

Estim8 supports a variety of optimzation algorithms from scipy.optimize, scikit-optimize and pygmo. Is is higly recommended to check out their documentations, e.g. for the use of solver hyperparameters and keyword arguments.

The optimization algorithm of choice is specified by passing a corresponding method key to the function Estimator.estimate.

3.4.1 scipy and scikit-optimize alogorithms and keys:#

Method key

Optimization function

Solver parallelization

local

scipy.optimize.minimize

de

scipy.optimize.differential_evolution

bh

scipy.optimize.basinhopping

shgo

scipy.optimize.shgo

dual_annealing

scipy.optimize.dual_annealing

gp

skopt.gp_minimize

In this example, the differential_evolution function from scipy.optimize is used to solve the estimation problem.

The estimation is started by calling the function Estimator.estimate. It takes at least the following arguments:

Argument

Type

Description

method

str or List[str]

the method key of the optimization algorithm. Pygmo algorithms are passed as a List[str] (see below).

max_iter

int

maximum iteration rounds for the solver algorithm

Additionally, keyword arguments include:

Kwarg

Type

Description

n_jobs

int

number of parallel jobs to run the optimization algorithm with.

optimizer_kwargs

dict

keyword arguments for the optimization function

Finally, the Estimator.estimate method returns the estimated parameters and an info-object containing additional information about the optimization procedure.

estimates, info = estimator.estimate(
    method              =   'de',           # method key of the optimization algorithm
    max_iter            =   1000,           # maximum number of iterations for the solver
    n_jobs              =   1,              # process pool size of the solver algorithm     
    optimizer_kwargs    =   {               # kewyword arguments to pass to the differential_evolution function:
                'disp': True,               # prints the evolution process
                "x0": [0.15, 0.3, 0.4],     # initial guess for the unknown parameters
                "popsize": 15,              # population size
            }
)

print(f" \nEstimated parameters: \n{estimates}")
print(f"Estimation info: \n{info}")
differential_evolution step 1: f(x)= 5.187519893589525
differential_evolution step 2: f(x)= 5.187519893589525
differential_evolution step 3: f(x)= 5.187519893589525
differential_evolution step 4: f(x)= 5.187519893589525
differential_evolution step 5: f(x)= 5.187519893589525
differential_evolution step 6: f(x)= 4.66017492209449
differential_evolution step 7: f(x)= 4.66017492209449
differential_evolution step 8: f(x)= 4.66017492209449
differential_evolution step 9: f(x)= 4.66017492209449
differential_evolution step 10: f(x)= 4.526663410674368
differential_evolution step 11: f(x)= 4.376752316743263
differential_evolution step 12: f(x)= 3.888030711038112
differential_evolution step 13: f(x)= 3.888030711038112
differential_evolution step 14: f(x)= 3.888030711038112
differential_evolution step 15: f(x)= 3.8872908160179627
differential_evolution step 16: f(x)= 3.8872908160179627
differential_evolution step 17: f(x)= 3.8872908160179627
differential_evolution step 18: f(x)= 3.8618782707797235
differential_evolution step 19: f(x)= 3.85049883342603
differential_evolution step 20: f(x)= 3.85049883342603
differential_evolution step 21: f(x)= 3.85049883342603
Polishing solution with 'L-BFGS-B'
 
Estimated parameters: 
{'X0': 0.10411706298070048, 'mu_max': 0.493794907806968, 'Y_XS': 0.49733042846371794}
Estimation info: 
             message: Optimization terminated successfully.
             success: True
                 fun: 3.85049883342603
                   x: [ 1.041e-01  4.938e-01  4.973e-01]
                 nit: 21
                nfev: 1058
          population: [[ 1.041e-01  4.938e-01  4.973e-01]
                       [ 1.054e-01  4.925e-01  4.977e-01]
                       ...
                       [ 1.007e-01  4.985e-01  4.964e-01]
                       [ 1.084e-01  4.881e-01  4.985e-01]]
 population_energies: [ 3.850e+00  3.853e+00 ...  3.863e+00  3.863e+00]

3.4.2 Visualizing estimation results#

To inspect the optimization results the plot_estimates function from the visualization submodule is used:

# compare fitted model to data
_ = visualization.plot_estimates(estimates=estimates, estimator=estimator, only_measured=True)
../_images/9c380c0a881dbdce2bace2fafbd7e924c3e54fa764bdd6fef0489f47670ab44e.png

3.4.3 Parallelized parameter estimation#

In many cases the parameter estimation time can be reduced by parallelizing the optimization function using multiple CPUs. In this example, 4 parallel jobs are used for the differential evolution algorithm by setting n_jobs=4 when calling the estimate function:

estimates, info = estimator.estimate(
    method              =   'de',           # method key of the optimization algorithm
    max_iter            =   1000,           # maximum number of iterations for the solver
    n_jobs              =   4,              # process pool size of the solver algorithm     
    optimizer_kwargs    =   {               # kewyword arguments to pass to the differential_evolution function:
                'disp': True,               # prints the evolution process
                "x0": [0.15, 0.3, 0.4],     # initial guess for the unknown parameters in the same order as unknowns
                "popsize": 15,              # population size
            }
)
c:\Users\Tobia\miniforge-pypy3\envs\testim8\lib\site-packages\scipy\optimize\_differentialevolution.py:487: UserWarning: differential_evolution: the 'workers' keyword has overridden updating='immediate' to updating='deferred'
  with DifferentialEvolutionSolver(func, bounds, args=args,
differential_evolution step 1: f(x)= 28.605241567077343
differential_evolution step 2: f(x)= 22.78821992617205
differential_evolution step 3: f(x)= 10.065425007814639
differential_evolution step 4: f(x)= 6.433987917222236
differential_evolution step 5: f(x)= 4.4973053733108
differential_evolution step 6: f(x)= 4.182347539947269
differential_evolution step 7: f(x)= 4.182347539947269
differential_evolution step 8: f(x)= 4.182347539947269
differential_evolution step 9: f(x)= 4.182347539947269
differential_evolution step 10: f(x)= 4.104307101346693
differential_evolution step 11: f(x)= 4.059027522127302
differential_evolution step 12: f(x)= 3.8602607304329655
differential_evolution step 13: f(x)= 3.8602607304329655
differential_evolution step 14: f(x)= 3.8602607304329655
differential_evolution step 15: f(x)= 3.8602607304329655
differential_evolution step 16: f(x)= 3.8602607304329655
differential_evolution step 17: f(x)= 3.8593992688392422
differential_evolution step 18: f(x)= 3.850042247355441
differential_evolution step 19: f(x)= 3.850042247355441
differential_evolution step 20: f(x)= 3.850042247355441
differential_evolution step 21: f(x)= 3.850042247355441
differential_evolution step 22: f(x)= 3.850042247355441
differential_evolution step 23: f(x)= 3.850042247355441
differential_evolution step 24: f(x)= 3.850042247355441
Polishing solution with 'L-BFGS-B'

3.4.4 Advanced parameter estimation with pygmo#

pygmo is a scientific Python library for massively parallel optimization, implementing a range of state-of-the-art bio-inspired and evolutionary algorithms. Using the asynchronous, generalized islands model, these algorithms can be easily mixed via a topology, yielding a “super algorithm” that exploits algorithmic cooperation. estim8 features the following algorithms:

method key

algorithm

scipy_optimize

A wrapper around scipy.optimize.minimize

de1220

Self-adaptive Differential Evolution, pygmo flavour (pDE)

bee_colony

Artificial Bee Colony

gaco

Extended Ant Colony Optimization algorithm

pso

Particle Swarm Optimization

sga

A Simple Genetic Algorithm

sea

(N+1)-ES simple evolutionary algorithm

compass_search

Compass Search

gwo

Grey Wolf Optimizer (gwo)

cmaes

Covariance Matrix Evolutionary Strategy (CMA-ES)

simulated_annealing

Simulated Annealing (Corana’s version)

nsga2

Non dominated Sorting Genetic Algorithm (NSGA-II)

mbh

Monotonic Basin Hopping (generalized)

ihs

Improved harmony search algorithm

xnes

Exponential Evolution Strategies

de

Differential Evolution

For using pygmo’s Generalized Islands approach, the estimate function is called with a list of algorithms keys in the method argument. A list of implemented topologies can be found here.

# algorithms for islands in heterogenous archipelago and keyword arguments (see pygmo documentation for more details)
algorithms_and_kwargs = {
    'sga'   : dict(gen=10, cr=.90, eta_c=1., m=0.02, param_m=1., param_s=2, crossover='exponential', mutation='polynomial', selection='tournament', seed=42),
    'pso'   : dict(gen=10, omega=0.7298, eta1=2.05, eta2=2.05, max_vel=0.5, variant=5, neighb_type=2, neighb_param=4, memory=False, seed=42),
    'de1220': dict(gen=10, allowed_variants=[2, 3, 7, 10, 13, 14, 15, 16], variant_adptv=1, ftol=1e-6, xtol=1e-6, memory=False, seed=42),
    'sea'   : {}    # here we use the default kwargs listed above
    }

# print the default algorithm kwargs for pygmo.sea algorithm
print(f"Default kwargs for pygmo.sea: {generalized_islands.PygmoHelpers.algo_default_kwargs['sea']} \n")


estimates_1, info_1 = estimator.estimate(   
    method = list(algorithms_and_kwargs.keys()),                # a list of algorithm keys defined above
    n_jobs=6,                                                   # process pool size of the solver algorithm
    max_iter=5,                                                # number of evolutions
    optimizer_kwargs = {
        'pop_size'      :   30,                                     # population size of each island
        'algos_kwargs'  :   list(algorithms_and_kwargs.values()),   # algorithm kwargs
        'topology'      :   pygmo.fully_connected(),                           # the archipelagos topology, default is pygmo.fully_connected()
        'report'        :   2                                       # prints out progress during evolutions and collects a trace of each island champion per evolution
    }
)
Default kwargs for pygmo.sea: {'gen': 40} 

>>> Created Island 1 using <pygmo.core.sga object at 0x000001B07CCC4330>
>>> Created Island 2 using <pygmo.core.pso object at 0x000001B07CC911B0>
>>> Created Island 3 using <pygmo.core.de1220 object at 0x000001B07CCC2C30>
>>> Created Island 4 using <pygmo.core.sea object at 0x000001B07CC74D30>
2025-07-17 14:32:53,404 - estim8.generalized_islands - INFO - ## Evolution 1 of island 1857519952432 completed:
      Algorithm: SEA: (N+1)-EA Simple Evolutionary Algorithm
      Champion loss: 9.05e+00
2025-07-17 14:33:00,228 - estim8.generalized_islands - INFO - ## Evolution 2 of island 1857519952432 completed:
      Algorithm: SEA: (N+1)-EA Simple Evolutionary Algorithm
      Champion loss: 4.58e+00
2025-07-17 14:33:00,645 - estim8.generalized_islands - INFO - ## Evolution 1 of island 1857519949888 completed:
      Algorithm: PSO: Particle Swarm Optimization
      Champion loss: 4.04e+00
2025-07-17 14:33:00,968 - estim8.generalized_islands - INFO - ## Evolution 1 of island 1857519944368 completed:
      Algorithm: SGA: Genetic Algorithm
      Champion loss: 6.60e+00
2025-07-17 14:33:01,086 - estim8.generalized_islands - INFO - ## Evolution 1 of island 1857519951424 completed:
      Algorithm: sa-DE1220: Self-adaptive Differential Evolution 1220
      Champion loss: 4.33e+00
2025-07-17 14:33:07,222 - estim8.generalized_islands - INFO - ## Evolution 3 of island 1857519952432 completed:
      Algorithm: SEA: (N+1)-EA Simple Evolutionary Algorithm
      Champion loss: 4.58e+00
2025-07-17 14:33:08,316 - estim8.generalized_islands - INFO - ## Evolution 4 of island 1857519952432 completed:
      Algorithm: SEA: (N+1)-EA Simple Evolutionary Algorithm
      Champion loss: 4.58e+00
2025-07-17 14:33:09,169 - estim8.generalized_islands - INFO - ## Evolution 2 of island 1857519949888 completed:
      Algorithm: PSO: Particle Swarm Optimization
      Champion loss: 4.04e+00
2025-07-17 14:33:09,401 - estim8.generalized_islands - INFO - ## Evolution 2 of island 1857519944368 completed:
      Algorithm: SGA: Genetic Algorithm
      Champion loss: 5.88e+00
2025-07-17 14:33:09,422 - estim8.generalized_islands - INFO - ## Evolution 5 of island 1857519952432 completed:
      Algorithm: SEA: (N+1)-EA Simple Evolutionary Algorithm
      Champion loss: 4.33e+00
2025-07-17 14:33:09,600 - estim8.generalized_islands - INFO - ## Evolution 2 of island 1857519951424 completed:
      Algorithm: sa-DE1220: Self-adaptive Differential Evolution 1220
      Champion loss: 3.89e+00
2025-07-17 14:33:17,126 - estim8.generalized_islands - INFO - ## Evolution 3 of island 1857519949888 completed:
      Algorithm: PSO: Particle Swarm Optimization
      Champion loss: 4.04e+00
2025-07-17 14:33:17,425 - estim8.generalized_islands - INFO - ## Evolution 3 of island 1857519944368 completed:
      Algorithm: SGA: Genetic Algorithm
      Champion loss: 3.92e+00
2025-07-17 14:33:17,609 - estim8.generalized_islands - INFO - ## Evolution 3 of island 1857519951424 completed:
      Algorithm: sa-DE1220: Self-adaptive Differential Evolution 1220
      Champion loss: 3.85e+00
2025-07-17 14:33:25,178 - estim8.generalized_islands - INFO - ## Evolution 4 of island 1857519949888 completed:
      Algorithm: PSO: Particle Swarm Optimization
      Champion loss: 4.04e+00
2025-07-17 14:33:25,587 - estim8.generalized_islands - INFO - ## Evolution 4 of island 1857519944368 completed:
      Algorithm: SGA: Genetic Algorithm
      Champion loss: 3.88e+00
2025-07-17 14:33:25,722 - estim8.generalized_islands - INFO - ## Evolution 4 of island 1857519951424 completed:
      Algorithm: sa-DE1220: Self-adaptive Differential Evolution 1220
      Champion loss: 3.85e+00
2025-07-17 14:33:33,186 - estim8.generalized_islands - INFO - ## Evolution 5 of island 1857519949888 completed:
      Algorithm: PSO: Particle Swarm Optimization
      Champion loss: 4.04e+00
2025-07-17 14:33:33,653 - estim8.generalized_islands - INFO - ## Evolution 5 of island 1857519944368 completed:
      Algorithm: SGA: Genetic Algorithm
      Champion loss: 3.88e+00
2025-07-17 14:33:33,759 - estim8.generalized_islands - INFO - ## Evolution 5 of island 1857519951424 completed:
      Algorithm: sa-DE1220: Self-adaptive Differential Evolution 1220
      Champion loss: 3.85e+00
# compare fitted model to data
_ = visualization.plot_estimates(estimates=estimates, estimator=estimator, only_measured=True)
../_images/a6ebe1bff6fcd58485fca67ede7612fbe5e1417e4f77dd6daa9f27102c1a676e.png

When calling the estimate function with report>=1, the info object provides an attribute evo_trace which is a pandas.DataFrame with data about the archipelago’s evolutions. This is usefull e.g. when comparing the performance of different algorithms or the archipelago’s topology setup.

In the following example a second approach using the same algorithms but fully unconnected islands (topology: pygmo.fully_connected()) is used for optimization. The evolution traces of both approaches are then compared:

# 2. Archipelago optimization with unconnected islands
estimates_2, info_2 = estimator.estimate(   
    method = list(algorithms_and_kwargs.keys()),                # a list of algorithm keys defined above
    n_jobs=6,                                                   # process pool size of the solver algorithm
    max_iter=5,                                                # number of evolutions
    optimizer_kwargs = {
        'pop_size'      :   30,                                     # population size of each island
        'algos_kwargs'  :   list(algorithms_and_kwargs.values()),   # algorithm kwargs
        'topology'      :   pygmo.unconnected(),                    # run with unconnected islands
        'report'        :   1                                       # collects a trace of each island champion per evolution
    }
)
>>> Created Island 1 using <pygmo.core.sga object at 0x000001B07C894270>
>>> Created Island 2 using <pygmo.core.pso object at 0x000001B07CCE9930>
>>> Created Island 3 using <pygmo.core.de1220 object at 0x000001B07CCEAEF0>
>>> Created Island 4 using <pygmo.core.sea object at 0x000001B07CCEADF0>
# plot the evolution traces of both approaches
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 2, figsize=(12, 4), sharey=True)

for ax, info in zip(axes, [info_1, info_2]):
    islands_traces = info.evo_trace.groupby("algorithm")
    for group in islands_traces.groups:
        data = islands_traces.get_group(group)
        ax.plot(data.index.get_level_values("evolution"), data["champion_loss"], label=group)

    ax.set_xlabel("evolution")
    ax.set_ylabel("champion loss")
    ax.set_xticks(data.index.get_level_values("evolution"))
    ax.legend()

axes[0].set_title("Ring topology")
axes[1].set_title("fully_connected topology")

fig.tight_layout()
../_images/75d76f2092ce847a5188d1c24f0f2cdd2628bf0017bbc2e93a334369d0ff0723.png

Continuing an estimation with pygmo#

In case the number of evolutions turns out insufficient, the optimization of the archipelago can be continued by simply running estimate(method=info ..) using the info object returned by the preceeding estimation:

estimates, info = estimator.estimate(
    method      =   info_1,
    max_iter    =   5,          # run 5 more evolutions
)
2025-07-17 14:34:29,916 - estim8.generalized_islands - INFO - ## Evolution 6 of island 1857519952432 completed:
      Algorithm: SEA: (N+1)-EA Simple Evolutionary Algorithm
      Champion loss: 4.33e+00
2025-07-17 14:34:31,035 - estim8.generalized_islands - INFO - ## Evolution 7 of island 1857519952432 completed:
      Algorithm: SEA: (N+1)-EA Simple Evolutionary Algorithm
      Champion loss: 4.33e+00
2025-07-17 14:34:32,155 - estim8.generalized_islands - INFO - ## Evolution 8 of island 1857519952432 completed:
      Algorithm: SEA: (N+1)-EA Simple Evolutionary Algorithm
      Champion loss: 3.86e+00
2025-07-17 14:34:33,240 - estim8.generalized_islands - INFO - ## Evolution 9 of island 1857519952432 completed:
      Algorithm: SEA: (N+1)-EA Simple Evolutionary Algorithm
      Champion loss: 3.86e+00
2025-07-17 14:34:34,311 - estim8.generalized_islands - INFO - ## Evolution 10 of island 1857519952432 completed:
      Algorithm: SEA: (N+1)-EA Simple Evolutionary Algorithm
      Champion loss: 3.85e+00
2025-07-17 14:34:36,752 - estim8.generalized_islands - INFO - ## Evolution 6 of island 1857519949888 completed:
      Algorithm: PSO: Particle Swarm Optimization
      Champion loss: 3.88e+00
2025-07-17 14:34:36,760 - estim8.generalized_islands - INFO - ## Evolution 6 of island 1857519944368 completed:
      Algorithm: SGA: Genetic Algorithm
      Champion loss: 3.88e+00
2025-07-17 14:34:37,104 - estim8.generalized_islands - INFO - ## Evolution 6 of island 1857519951424 completed:
      Algorithm: sa-DE1220: Self-adaptive Differential Evolution 1220
      Champion loss: 3.85e+00
2025-07-17 14:34:45,683 - estim8.generalized_islands - INFO - ## Evolution 7 of island 1857519949888 completed:
      Algorithm: PSO: Particle Swarm Optimization
      Champion loss: 3.85e+00
2025-07-17 14:34:45,698 - estim8.generalized_islands - INFO - ## Evolution 7 of island 1857519944368 completed:
      Algorithm: SGA: Genetic Algorithm
      Champion loss: 3.88e+00
2025-07-17 14:34:45,809 - estim8.generalized_islands - INFO - ## Evolution 7 of island 1857519951424 completed:
      Algorithm: sa-DE1220: Self-adaptive Differential Evolution 1220
      Champion loss: 3.85e+00
2025-07-17 14:34:54,102 - estim8.generalized_islands - INFO - ## Evolution 8 of island 1857519949888 completed:
      Algorithm: PSO: Particle Swarm Optimization
      Champion loss: 3.85e+00
2025-07-17 14:34:54,119 - estim8.generalized_islands - INFO - ## Evolution 8 of island 1857519944368 completed:
      Algorithm: SGA: Genetic Algorithm
      Champion loss: 3.85e+00
2025-07-17 14:34:54,292 - estim8.generalized_islands - INFO - ## Evolution 8 of island 1857519951424 completed:
      Algorithm: sa-DE1220: Self-adaptive Differential Evolution 1220
      Champion loss: 3.85e+00
2025-07-17 14:35:02,810 - estim8.generalized_islands - INFO - ## Evolution 9 of island 1857519949888 completed:
      Algorithm: PSO: Particle Swarm Optimization
      Champion loss: 3.85e+00
2025-07-17 14:35:02,817 - estim8.generalized_islands - INFO - ## Evolution 9 of island 1857519951424 completed:
      Algorithm: sa-DE1220: Self-adaptive Differential Evolution 1220
      Champion loss: 3.85e+00
2025-07-17 14:35:02,868 - estim8.generalized_islands - INFO - ## Evolution 9 of island 1857519944368 completed:
      Algorithm: SGA: Genetic Algorithm
      Champion loss: 3.85e+00
2025-07-17 14:35:11,039 - estim8.generalized_islands - INFO - ## Evolution 10 of island 1857519949888 completed:
      Algorithm: PSO: Particle Swarm Optimization
      Champion loss: 3.85e+00
2025-07-17 14:35:11,082 - estim8.generalized_islands - INFO - ## Evolution 10 of island 1857519951424 completed:
      Algorithm: sa-DE1220: Self-adaptive Differential Evolution 1220
      Champion loss: 3.85e+00
2025-07-17 14:35:11,440 - estim8.generalized_islands - INFO - ## Evolution 10 of island 1857519944368 completed:
      Algorithm: SGA: Genetic Algorithm
      Champion loss: 3.85e+00