Issue with evaluate_callbacks (_population)

Hi Cadet-Devs,

I’m running into an issue when trying to evaluate a list of parameters for variables. evaluate_objectives works fine, but when I include evaluate_callbacks I get a value error regarding the output bound. Same for both functions when using populations which is the goal here.

I don’t get what the output bounds are doing and how to influence them.

I’m using the add_fractionation_metric branch atm with Python 3.10.13 on VS Code. See my MinExample:

#Process setup
import numpy as np
import CADETProcess
CADETProcess.settings.working_directory = 'C:/CADET_working_directory'

# Example import
from examples.batch_elution.process import process as process1
components = ['A', 'B']
process1.flow_sheet.column.binding_model.adsorption_rate=[0.011, 0.02]
process1.flow_sheet.column.binding_model.desorption_rate=[1, 1]
process1.flow_sheet.column.binding_model.is_kinetic = True

# simulate processes
from CADETProcess.simulator import Cadet
simulator = Cadet(install_path = 'C:\\ProgramData\\Anaconda3\\envs\\cadet_feat_FracMetric\\')
simulation_result = simulator.simulate(process1)
# Reference data
def gaussian(x, mu, sigma, amplitude):
    return amplitude * np.exp(-((x - mu) ** 2) / (2 * sigma ** 2))

timepoints = np.arange(0, 601)
y_values_1A = gaussian(timepoints, 5 * 60, 80, 9)
y_values_1B = gaussian(timepoints, 8 * 60, 50, 3)
ref_data1 =np.column_stack((y_values_1A, y_values_1B))
# Comparator
from CADETProcess.comparison import Comparator
from CADETProcess.reference import ReferenceIO
reference = ReferenceIO('ref_' + process1.name, timepoints, ref_data1, component_system= process1.component_system)

# add references to comparator and define difference_metric
comparator = Comparator()
comparator.name =  'comparator'
comparator.add_reference(reference)
comparator.add_difference_metric(
                                'SSE', reference, 'column.outlet',
                                components = components
                                )

# Setup optimization problem
from CADETProcess.optimization import OptimizationProblem
optimization_problem = OptimizationProblem('Min_Example')
optimization_problem.add_evaluation_object(process1, name = process1.name)
optimization_problem.add_evaluator(simulator)
    
optimization_problem.add_objective(
    comparator,
    evaluation_objects = process1,
    name = 'obj_' + process1.name,
    n_objectives = comparator.n_metrics,
    requires = [simulator]
    )

# Adding variables
optimization_problem.add_variable( 
    parameter_path = 'flow_sheet.column.binding_model.adsorption_rate',
    name = ('adsorption_rate'),
    lb = 1e-10, ub=1e15,
    transform = 'log',
)

# Callbacks
def callback(results, individual, callbacks_dir = './'):
    x_values = ', '.join(f"{num:.5g}" for num in individual.x)
    comparator.plot_comparison(
                        results,
                        show=0, 
                        file_name=f'''{callbacks_dir}/{comparator.name}_x=[{x_values}]_{individual.id}.png''',
                        )

optimization_problem.add_callback(
                            callback,
                            requires=[simulator], 
                            keep_progress = True
                            )

should_evaluate = 1

from CADETProcess.optimization import Individual

if should_evaluate == True:
    
    def log_distributed_values(start, end, num_values):
        res = np.logspace(np.log10(start), np.log10(end), num_values)
        res = res.reshape(-1,1)
        return res
    
    def lin_distributed_values(start, end, num_values):
        return np.linspace(start, end, num_values)
    
    if optimization_problem.callbacks[0].callbacks_dir == None:
        optimization_problem.callbacks[0].callbacks_dir = CADETProcess.settings.working_directory/'callbacks' # must be set for testing callback

    values_log = values_lin = []
    values_log = log_distributed_values(1e-5, 1e3, 10)
    # values_lin = lin_distributed_values(1, 9, 20)
    values = np.append(values_log, values_lin)
    
    pop = []
    for i, val in enumerate(values):
        pop.append(Individual(x = np.array([val])))
        
    optimization_problem.delete_cache()
    optimization_problem.setup_cache()
        
    # print(optimization_problem.evaluate_objectives(values[6], force = True)) # works
    # print(optimization_problem.evaluate_objectives_population(np.column_stack([values]), force = True))
    
    optimization_problem.evaluate_callbacks(pop[6]) # does not work
    # optimization_problem.evaluate_callbacks_population(pop, force = True)

The error message:

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[37], line 35
     32 print(optimization_problem.evaluate_objectives(1, force = True)) # works
     33 # print(optimization_problem.evaluate_objectives_population(np.column_stack([values]), force = True))
---> 35 optimization_problem.evaluate_callbacks(pop[6]) # does not work
     36 # optimization_problem.evaluate_callbacks_population(pop, current_iteration = 0, force = True)

File c:\ProgramData\Anaconda3\envs\cadet_feat_FracMetric\lib\site-packages\CADETProcess\optimization\optimizationProblem.py:1588, in OptimizationProblem.evaluate_callbacks(self, ind, current_iteration, force)
   1585 callback._current_iteration = current_iteration
   1587 try:
-> 1588     self._evaluate(ind.x_transformed, callback, force, untransform=True)
   1589 except CADETProcessError:
   1590     self.logger.warning(
   1591         f'Evaluation of {callback} failed at {ind.x}.'
   1592     )

File c:\ProgramData\Anaconda3\envs\cadet_feat_FracMetric\lib\site-packages\CADETProcess\optimization\optimizationProblem.py:136, in OptimizationProblem.untransforms.<locals>.wrapper(self, x, untransform, *args, **kwargs)
    134 x = np.array(x, ndmin=1)
    135 if untransform:
--> 136     x = self.untransform(x)
    138 return func(self, x, *args, **kwargs)

File c:\ProgramData\Anaconda3\envs\cadet_feat_FracMetric\lib\site-packages\CADETProcess\optimization\optimizationProblem.py:2601, in OptimizationProblem.untransform(self, x_transformed)
   2598 untransform = np.zeros(x_transformed_2d.shape)
   2600 for i, ind in enumerate(x_transformed_2d):
-> 2601     untransform[i, :] = [
   2602         var.untransform_fun(value)
   2603         for value, var in zip(ind, self.independent_variables)
   2604     ]
   2606 return untransform.reshape(x_transformed.shape).tolist()

File c:\ProgramData\Anaconda3\envs\cadet_feat_FracMetric\lib\site-packages\CADETProcess\optimization\optimizationProblem.py:2602, in <listcomp>(.0)
   2598 untransform = np.zeros(x_transformed_2d.shape)
   2600 for i, ind in enumerate(x_transformed_2d):
   2601     untransform[i, :] = [
-> 2602         var.untransform_fun(value)
   2603         for value, var in zip(ind, self.independent_variables)
   2604     ]
   2606 return untransform.reshape(x_transformed.shape).tolist()

File c:\ProgramData\Anaconda3\envs\cadet_feat_FracMetric\lib\site-packages\CADETProcess\optimization\optimizationProblem.py:3298, in OptimizationVariable.untransform_fun(self, x)
   3297 def untransform_fun(self, x):
-> 3298     return self._transform.untransform(x)

File c:\ProgramData\Anaconda3\envs\cadet_feat_FracMetric\lib\site-packages\CADETProcess\transform.py:205, in TransformBase.untransform(self, x)
    186 """Transform the output parameter space to the input parameter space.
    187 
    188 Applies the transformation function _untransform to x after performing output
   (...)
    200     Transformed parameter values.
    201 """
    202 if (
    203         not self.allow_extended_output and
    204         not np.all((self.lb <= x) * (x <= self.ub))):
--> 205     raise ValueError("Value exceeds output bounds.")
    206 x = self._untransform(x)
    207 if (
    208         not self.allow_extended_input and
    209         not np.all((self.lb_input <= x) * (x <= self.ub_input))):

ValueError: Value exceeds output bounds.

Hey Ortler,

we’ve got a fix. You can install it with

pip install -e git+https://github.com/fau-advanced-separations/CADET-Process.git@fix/callbacks_with_un_untransformable_x#egg=CADET-Process

For context: .evaluate_callbacks expects to receive individuals that are created by the OptimizationProblem class and that contain information about the parameter transformations. It tries to use the x_transformed value (so, the parameter in the transformed space (0,1) usually) and then tries to un-transform it. When you create individuals individually, they don’t get a transformation, so x_transformed is just x (~2.5 in your case). Therefore the un-transform function fails, because 2.5 is outside of 0-1. We’ve now changed it to expect untransformed x coordinates and not un-transform them. So this should be more robust and work for your use-case

1 Like

Nice, that fix came with speed of light! :zap:

It’s great to sample through a set of parameters and see results right away.
Thank you!

2 Likes

Hey Ronald,

unfortunately this fix leads to similar behavior than described here:
Callbacks while having dependent variables - CADET Troubleshooting - CADET (cadet-web.de)

While optimizing with dependent variables, I don’t get any callbacks. When I use the current dev-branch or without dependent variables it works fine.

import numpy as np
import CADETProcess
CADETProcess.settings.working_directory = 'C:/CADET_working_directory'

# Example import
from examples.batch_elution.process import process as process1
components = ['A', 'B']
process1.flow_sheet.column.binding_model.adsorption_rate=[0.011, 0.02]
process1.flow_sheet.column.binding_model.desorption_rate=[1, 1]
process1.flow_sheet.column.binding_model.is_kinetic = True

# simulate processes
from CADETProcess.simulator import Cadet
simulator = Cadet(install_path = 'C:\\ProgramData\\Anaconda3\\envs\\cadet\\')
simulation_result = simulator.simulate(process1)
# Reference data
def gaussian(x, mu, sigma, amplitude):
    return amplitude * np.exp(-((x - mu) ** 2) / (2 * sigma ** 2))

timepoints = np.arange(0, 601)
y_values_1A = gaussian(timepoints, 5 * 60, 80, 9)
y_values_1B = gaussian(timepoints, 8 * 60, 50, 3)
ref_data1 =np.column_stack((y_values_1A, y_values_1B))
# Comparator
from CADETProcess.comparison import Comparator
from CADETProcess.reference import ReferenceIO
reference = ReferenceIO('ref_' + process1.name, timepoints, ref_data1, component_system= process1.component_system)

# add references to comparator and define difference_metric
comparator = Comparator()
comparator.name =  'comparator'
comparator.add_reference(reference)
comparator.add_difference_metric(
                                'SSE', reference, 'column.outlet',
                                components = components
                                )

# Setup optimization problem
from CADETProcess.optimization import OptimizationProblem
optimization_problem = OptimizationProblem('Min_Example')
optimization_problem.add_evaluation_object(process1, name = process1.name)
optimization_problem.add_evaluator(simulator)
    
optimization_problem.add_objective(
    comparator,
    evaluation_objects = process1,
    name = 'obj_' + process1.name,
    n_objectives = comparator.n_metrics,
    requires = [simulator]
    )

k_eq = 0.02 # k_eq for all species

# Adding variables
k_des_lb = 0.1
k_des_ub = 100

#adsorption rate
optimization_problem.add_variable( 
    parameter_path = 'flow_sheet.column.binding_model.adsorption_rate',
    name = ('adsorption_rate'),
    lb = k_des_lb*k_eq, ub=k_des_ub*k_eq,
    transform = 'log',
)
# desorption rate
optimization_problem.add_variable(
    parameter_path = 'flow_sheet.column.binding_model.desorption_rate',
    name = ('desorption_rate'),
    lb = k_des_lb, ub=k_des_ub,
    transform = 'log',
)

# Callbacks
def callback(results, individual, callbacks_dir = './'):
    x_values = ', '.join(f"{num:.5g}" for num in individual.x)
    comparator.plot_comparison(
                        results,
                        show=0, 
                        file_name=f'''{callbacks_dir}/{comparator.name}_x=[{x_values}]_{individual.id}.png''',
                        )

optimization_problem.add_callback(
                            callback,
                            requires=[simulator], 
                            keep_progress = True
                            )

# Dependencies
make_dependencies = 1

if make_dependencies == True:
    
    def k_ads(k_des):
        return k_des*k_eq
    
    optimization_problem.add_variable_dependency('adsorption_rate', 'desorption_rate', 
                                                    transform = k_ads)

# Optimizer
should_optimize = 1

from CADETProcess.optimization import U_NSGA3
optimizer = U_NSGA3()
optimizer.n_cores = 8
optimizer.n_max_gen = 2
optimizer.pop_size = 2
optimizer.progress_frequency = 1

if should_optimize == True:
    
    if optimization_problem.callbacks[0].callbacks_dir != None:
        optimization_problem.callbacks[0].callbacks_dir = None # must be None for callback in optimization, otherwise error appears
    
    optimization_results = optimizer.optimize(
        optimization_problem,
        save_results = True,
        )




Curses. We’ll think about something! :wink:

Hey @ortler

we’ve added a new method to the OptimiationProblem that automatically creates an individual.

    pop = []
    for i, val in enumerate(values):
        ind = optimization_problem.create_individual(val)
        pop.append(ind)

Could you please check again if that works for you? Eventually, I would also like to use that method for wrapping the evaluation methods s.t. it is no longer required to pass individuals at all. Moreover, I’d like to also add a create_population method.

1 Like

Hey Jo,

that works for me, thank you! :ok_hand:

1 Like

Before merging, this now works for both cases, right?

Yes, both cases work for me now.