from estim8.models import FmuModel
from estim8.error_models import BaseErrorModel, LinearErrorModel
from estim8 import visualization, datatypes, Estimator, utils
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# load and init model
SimpleBatchModel = FmuModel(path=r'../../../tests/test_data/SimpleBatch.fmu')
# import datasheet
data = pd.read_excel(r'SimpleBatch_Data.xlsx', index_col=0, header=[0, 1])
data.columns = data.columns.droplevel(1)
7 Federated worker setup#
Parameter estimation times depend one a series of factors, e.g. the dimensionality of the estimation problem or the solution landscape. In most cases, the forward simulation to evaluate a candidate solution vector \(\theta\) is the time-limiting step, which is further stretched with increased numbers of experimental replicates. To address this issue, estim8 introduces as setup employing federated computing principles for differentiable cost functions, effectively yielding a second parallelization layer for the simulation of model replicates.
Essentially, federated Workers are launched as gRPC services bound to a unique host and port network address. Evaluation calls for replicate specific parameter sets in the objective function are then distributed to these workers using PyTensor and pytensor_federated.
Using a federated worker setup, heavy computations can be effectively distributed among machines, exploiting parallel computer architectures like workstations or computer clusters.
We start by creating an artificial noisy dataset of 3 replicates like in Notebook 4. Modeling experimental replicates.
# define number of replicates and IDs
replicate_IDs = [str(i) for i in range(1,4)]
# create a parameter mapping
replicate_specific_parameters = ["X0"]
parameter_mapping = utils.ModelHelpers.ParameterMapping(
mappings=[utils.ModelHelpers.ParameterMapper(
global_name = param,
replicate_ID = r_ID,
value = 0.1 + np.random.random()/10 # use a random turbulence
)
for r_ID in replicate_IDs
for param in replicate_specific_parameters],
default_parameters = SimpleBatchModel.parameters,
replicate_IDs = replicate_IDs
)
# create 2 artificial datasets
def make_experiment(parameters, model: FmuModel, original_data: pd.DataFrame, error_model: BaseErrorModel = LinearErrorModel(), rID: str = datatypes.Constants.SINGLE_ID, resample=4):
simulation = model.simulate(t0=0, t_end=10, stepsize=0.1, parameters=parameters)
measurements = []
for obs in original_data.columns:
model_prediction = simulation[obs]
# get timepoints of measurements from datesheet above
timepoints = original_data[obs].dropna().index
values = model_prediction.interpolate(timepoints)
# get noisy values by resampling the data according to erro models distribution
for _ in range(resample):
values = error_model.get_sampling(values=values,errors=error_model.generate_error_data(values),n_samples= 1)[0]
measurements.append(
datatypes.Measurement(
name = model_prediction.name,
timepoints=timepoints,
values=values,
error_model=error_model,
replicate_ID=rID
)
)
return datatypes.Experiment(measurements, replicate_ID=rID)
# create noisy data
artificial_data = dict()
for r_ID in replicate_IDs:
replicate_parameters = parameter_mapping.replicate_handling(
replicate_ID=r_ID
)
artificial_data[r_ID] = make_experiment(parameters=replicate_parameters, original_data=data, model=SimpleBatchModel, error_model=LinearErrorModel(offset=0.05), rID=r_ID)
# plot data
fig, axes = plt.subplots(
ncols=2,
nrows=1,
figsize=(15, 5)
)
for (r_ID, experiment), color in zip(artificial_data.items(),sns.color_palette("colorblind") ):
for ax, measurement in zip(axes, experiment.measurements):
visualization.plot_measurement(ax=ax, measurement=measurement, color=color, ecolor=color)
ax.set_xlabel('Time [h]')
ax.legend()
7.1 Federated workers on a single machine#
To use this setup on a single local machine, pass the keyword argument federated_workersto the Estimator class methods.
7.1.1 Parameter estimation#
# define unknowns and bounds
bounds = {f"X0_{r_ID}": [0.01, 0.5] for r_ID in replicate_IDs}
bounds["mu_max"] = [0.1, 0.8]
# instantiate Estimator
estimator = Estimator(
model=SimpleBatchModel,
bounds=bounds,
data=artificial_data,
metric="WSS",
parameter_mapping=parameter_mapping,
t=[0, 10, 0.1]
)
estimates, info = estimator.estimate(
method="de",
max_iter=1000,
optimizer_kwargs={"disp": True, "updating": "deferred"},
n_jobs=1,
federated_workers=4 # use 4 federated workers for parallel simulation
)
_ = visualization.plot_estimates(estimates, estimator, only_measured=True)
Launching 4 workers...
2025-07-08 15:39:49,565 [INFO] Compiling new CVM
2025-07-08 15:39:50,623 [INFO] New version 0.31
testing workers..
2025-07-08 15:39:50,641 [INFO] Client 2637425474400-13036-20432 connected to localhost:9500
2025-07-08 15:39:50,675 [INFO] Closing evaluation stream
2025-07-08 15:39:50,677 [INFO] Client 2637425474400-13036-20432 connected to localhost:9501
2025-07-08 15:39:50,695 [INFO] Closing evaluation stream
2025-07-08 15:39:50,697 [INFO] Client 2637425474400-13036-20432 connected to localhost:9502
2025-07-08 15:39:50,718 [INFO] Closing evaluation stream
2025-07-08 15:39:50,720 [INFO] Client 2637425474400-13036-20432 connected to localhost:9503
2025-07-08 15:39:50,741 [INFO] Closing evaluation stream
Worker test succesfull.
2025-07-08 15:39:52,072 [INFO] Client 2637474644096-13036-20432 connected to localhost:9502
2025-07-08 15:39:52,670 [INFO] Client 2637474644768-13036-20432 connected to localhost:9500
2025-07-08 15:39:52,777 [INFO] Client 2637454610112-13036-20432 connected to localhost:9501
differential_evolution step 1: f(x)= 37569.55510066514
differential_evolution step 2: f(x)= 24583.83695090932
differential_evolution step 3: f(x)= 11156.159922117642
differential_evolution step 4: f(x)= 7299.926225031093
differential_evolution step 5: f(x)= 3068.615904905357
differential_evolution step 6: f(x)= 3068.615904905357
differential_evolution step 7: f(x)= 3068.615904905357
differential_evolution step 8: f(x)= 3068.615904905357
differential_evolution step 9: f(x)= 3068.615904905357
differential_evolution step 10: f(x)= 3068.615904905357
differential_evolution step 11: f(x)= 3068.615904905357
differential_evolution step 12: f(x)= 2854.0730651228037
differential_evolution step 13: f(x)= 2854.0730651228037
differential_evolution step 14: f(x)= 2854.0730651228037
differential_evolution step 15: f(x)= 2010.4198754013782
differential_evolution step 16: f(x)= 2010.4198754013782
differential_evolution step 17: f(x)= 1945.2532895988563
differential_evolution step 18: f(x)= 1945.2532895988563
differential_evolution step 19: f(x)= 1664.2769607199823
differential_evolution step 20: f(x)= 1664.2769607199823
differential_evolution step 21: f(x)= 1664.2769607199823
differential_evolution step 22: f(x)= 1658.3824490737886
differential_evolution step 23: f(x)= 1545.348363024132
differential_evolution step 24: f(x)= 1545.348363024132
differential_evolution step 25: f(x)= 1545.348363024132
differential_evolution step 26: f(x)= 1545.348363024132
differential_evolution step 27: f(x)= 1470.0027204271298
differential_evolution step 28: f(x)= 1470.0027204271298
differential_evolution step 29: f(x)= 1470.0027204271298
differential_evolution step 30: f(x)= 1470.0027204271298
differential_evolution step 31: f(x)= 1470.0027204271298
differential_evolution step 32: f(x)= 1470.0027204271298
differential_evolution step 33: f(x)= 1470.0027204271298
differential_evolution step 34: f(x)= 1469.6481764359626
differential_evolution step 35: f(x)= 1463.029944203497
differential_evolution step 36: f(x)= 1463.029944203497
differential_evolution step 37: f(x)= 1463.029944203497
differential_evolution step 38: f(x)= 1455.5773211254996
differential_evolution step 39: f(x)= 1455.5773211254996
differential_evolution step 40: f(x)= 1455.5773211254996
differential_evolution step 41: f(x)= 1455.5773211254996
differential_evolution step 42: f(x)= 1455.5773211254996
differential_evolution step 43: f(x)= 1455.3783608607623
differential_evolution step 44: f(x)= 1455.3783608607623
differential_evolution step 45: f(x)= 1455.3783608607623
differential_evolution step 46: f(x)= 1453.9301324253631
differential_evolution step 47: f(x)= 1453.9301324253631
differential_evolution step 48: f(x)= 1453.9301324253631
Polishing solution with 'L-BFGS-B'
Shutting down workers...
7.1.2 Uncertainty quantification methods#
To scale up the processes, similar to the estimate method above, one can define a number of federated_workers.
# monte carlo sampling
mc_samples = estimator.mc_sampling(
method='de',
max_iter=1000,
n_jobs=1,
mcs_at_once=4,
federated_workers=16,
n_samples=100
)
Launching 16 workers...
testing workers..
2025-07-08 15:42:06,512 [INFO] Client 2637436813632-13036-20432 connected to localhost:9500
2025-07-08 15:42:06,558 [INFO] Closing evaluation stream
2025-07-08 15:42:06,561 [INFO] Client 2637436813632-13036-20432 connected to localhost:9501
2025-07-08 15:42:06,580 [INFO] Closing evaluation stream
2025-07-08 15:42:06,582 [INFO] Client 2637436813632-13036-20432 connected to localhost:9502
2025-07-08 15:42:06,606 [INFO] Closing evaluation stream
2025-07-08 15:42:06,610 [INFO] Client 2637436813632-13036-20432 connected to localhost:9503
2025-07-08 15:42:06,634 [INFO] Closing evaluation stream
2025-07-08 15:42:06,651 [INFO] Client 2637436813632-13036-20432 connected to localhost:9504
2025-07-08 15:42:06,671 [INFO] Closing evaluation stream
2025-07-08 15:42:06,673 [INFO] Client 2637436813632-13036-20432 connected to localhost:9505
2025-07-08 15:42:06,691 [INFO] Closing evaluation stream
2025-07-08 15:42:06,693 [INFO] Client 2637436813632-13036-20432 connected to localhost:9506
2025-07-08 15:42:06,716 [INFO] Closing evaluation stream
2025-07-08 15:42:06,718 [INFO] Client 2637436813632-13036-20432 connected to localhost:9507
2025-07-08 15:42:06,755 [INFO] Closing evaluation stream
2025-07-08 15:42:06,757 [INFO] Client 2637436813632-13036-20432 connected to localhost:9508
2025-07-08 15:42:06,779 [INFO] Closing evaluation stream
2025-07-08 15:42:06,782 [INFO] Client 2637436813632-13036-20432 connected to localhost:9509
2025-07-08 15:42:06,800 [INFO] Closing evaluation stream
2025-07-08 15:42:06,802 [INFO] Client 2637436813632-13036-20432 connected to localhost:9510
2025-07-08 15:42:06,823 [INFO] Closing evaluation stream
2025-07-08 15:42:06,825 [INFO] Client 2637409725008-13036-20432 connected to localhost:9511
2025-07-08 15:42:06,845 [INFO] Closing evaluation stream
2025-07-08 15:42:06,847 [INFO] Client 2637409725008-13036-20432 connected to localhost:9512
2025-07-08 15:42:06,865 [INFO] Closing evaluation stream
2025-07-08 15:42:06,867 [INFO] Client 2637436813632-13036-20432 connected to localhost:9513
2025-07-08 15:42:06,886 [INFO] Closing evaluation stream
2025-07-08 15:42:06,888 [INFO] Client 2637436813632-13036-20432 connected to localhost:9514
2025-07-08 15:42:06,909 [INFO] Closing evaluation stream
2025-07-08 15:42:06,911 [INFO] Client 2637436813632-13036-20432 connected to localhost:9515
2025-07-08 15:42:06,930 [INFO] Closing evaluation stream
Worker test succesfull.
---- Sample 1 completed
---- Sample 2 completed
---- Sample 3 completed
---- Sample 4 completed
---- Sample 5 completed
---- Sample 6 completed
---- Sample 7 completed
---- Sample 8 completed
---- Sample 9 completed
---- Sample 10 completed
---- Sample 11 completed
---- Sample 12 completed
---- Sample 13 completed
---- Sample 14 completed
---- Sample 15 completed
---- Sample 16 completed
---- Sample 17 completed
---- Sample 18 completed
---- Sample 19 completed
---- Sample 20 completed
---- Sample 21 completed
---- Sample 22 completed
---- Sample 23 completed
---- Sample 24 completed
---- Sample 25 completed
---- Sample 26 completed
---- Sample 27 completed
---- Sample 28 completed
---- Sample 29 completed
---- Sample 30 completed
---- Sample 31 completed
---- Sample 32 completed
---- Sample 33 completed
---- Sample 34 completed
---- Sample 35 completed
---- Sample 36 completed
---- Sample 37 completed
---- Sample 38 completed
---- Sample 39 completed
---- Sample 40 completed
---- Sample 41 completed
---- Sample 42 completed
---- Sample 43 completed
---- Sample 44 completed
---- Sample 45 completed
---- Sample 46 completed
---- Sample 47 completed
---- Sample 48 completed
---- Sample 49 completed
---- Sample 50 completed
---- Sample 51 completed
---- Sample 52 completed
---- Sample 53 completed
---- Sample 54 completed
---- Sample 55 completed
---- Sample 56 completed
---- Sample 57 completed
---- Sample 58 completed
---- Sample 59 completed
---- Sample 60 completed
---- Sample 61 completed
---- Sample 62 completed
---- Sample 63 completed
---- Sample 64 completed
---- Sample 65 completed
---- Sample 66 completed
---- Sample 67 completed
---- Sample 68 completed
---- Sample 69 completed
---- Sample 70 completed
---- Sample 71 completed
---- Sample 72 completed
---- Sample 73 completed
---- Sample 74 completed
---- Sample 75 completed
---- Sample 76 completed
---- Sample 77 completed
---- Sample 78 completed
---- Sample 79 completed
---- Sample 80 completed
---- Sample 81 completed
---- Sample 82 completed
---- Sample 83 completed
---- Sample 84 completed
---- Sample 85 completed
---- Sample 86 completed
---- Sample 87 completed
---- Sample 88 completed
---- Sample 89 completed
---- Sample 90 completed
---- Sample 91 completed
---- Sample 92 completed
---- Sample 93 completed
---- Sample 94 completed
---- Sample 95 completed
---- Sample 96 completed
---- Sample 97 completed
---- Sample 98 completed
---- Sample 99 completed
---- Sample 100 completed
Shutting down workers...
7.2 Distributing workloads among different machines#
Employing this federated setup, one can launch workers on one or more machines and just pass the network adresses to the Estimator object that runs the optimization task. Assume on both machines the model and data from above are already defined:
7.1.1 Parameter estimation#
# code run on machine A: worker nodes
print(" \n Machine A:")
estimator_A = Estimator(
model = SimpleBatchModel,
data=data,
bounds = {"X0": [0.01, 0.5], "mu_max": [0.2, 0.8]},
t=[0,10, 0.1]
)
estimator_A.launch_workers(
host= "localhost", # use the real host adress of machine A her
ports=list(range(9500,9506)) # port adresses for workers
)
print("-------------------------------------------------------")
# code run on machine B: parameter estimation
print(" \n Machine B:")
estimator_B = Estimator(
model = SimpleBatchModel,
data=data,
bounds = {"X0": [0.01, 0.5], "mu_max": [0.2, 0.8]},
t=[0,10, 0.1]
)
hosts_and_ports = [("localhost", port) for port in list(range(9500,9506))]
# set the hosts_and_ports attribute
estimator_B.hosts_and_ports = hosts_and_ports
estimator_B.estimate(method='de', max_iter=1000, n_jobs=2, federated_workers=True)
Machine A:
Launching 6 workers...
c:\Users\Latour\projects\estim8\examples\..\estim8\estimator.py:487: UserWarning: n_workers argument overriden by ports.
warn(f"n_workers argument overriden by ports.")
-------------------------------------------------------
Machine B:
testing workers..
2025-07-08 15:59:52,022 [INFO] Client 2637475702128-13036-20432 connected to localhost:9500
2025-07-08 15:59:52,095 [INFO] Closing evaluation stream
2025-07-08 15:59:52,097 [INFO] Client 2637475702128-13036-20432 connected to localhost:9501
2025-07-08 15:59:52,116 [INFO] Closing evaluation stream
2025-07-08 15:59:52,117 [INFO] Client 2637475702128-13036-20432 connected to localhost:9502
2025-07-08 15:59:52,138 [INFO] Closing evaluation stream
2025-07-08 15:59:52,140 [INFO] Client 2637475702128-13036-20432 connected to localhost:9503
2025-07-08 15:59:52,160 [INFO] Closing evaluation stream
2025-07-08 15:59:52,162 [INFO] Client 2637475702128-13036-20432 connected to localhost:9504
2025-07-08 15:59:52,180 [INFO] Closing evaluation stream
2025-07-08 15:59:52,184 [INFO] Client 2637475702128-13036-20432 connected to localhost:9505
2025-07-08 15:59:52,203 [INFO] Closing evaluation stream
Worker test succesfull.
c:\Users\Latour\AppData\Local\miniforge3\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,
Shutting down workers...
({'X0': 0.2845093044738398, 'mu_max': 0.3308911608324544},
message: Optimization terminated successfully.
success: True
fun: 56.06457476197551
x: [ 2.845e-01 3.309e-01]
nit: 21
nfev: 723
population: [[ 2.845e-01 3.309e-01]
[ 2.780e-01 3.345e-01]
...
[ 2.873e-01 3.295e-01]
[ 2.911e-01 3.272e-01]]
population_energies: [ 5.606e+01 5.607e+01 ... 5.607e+01 5.607e+01])