Skip to content

Advanced operations

The following tutorial will show you how to:

  • optimize multiple objectives simultaneously
  • use multiple workstations in a single template
  • set parameter constraints in your template

Pre-requisites

You should already be familiar with SDLabs and have followed the Getting started and the API-connected workstations or the Nexus custom workstations tutorials.

Multi-objective optimization

Atinary supports different algorithms able to combine multiple objectives into a single one, including weighted sum and Chimera. The main steps to create a multi-objective function are:

Tasks

  1. Create at least two individual objectives
  2. Define the individual objectives configurations, where you will set attributes to your objectives such as relative weight, hierarchy, relative tolerance, etc. depending on the selected algorithm (Chimera or Weighted Sum)
  3. Create a multi-objective function, where you will add the objectives and select the algorithm

The weighted sum simply defines weights applied to each objective, ranging from 0 to 10. Weights are normalized internally, meaning that setting the weights A = 0.3, B = 0.3, C = 0.9 is strictly equivalent to setting the weights A = 1, B = 1, C = 3.

The rest of this section will focus on the Chimera multi-objective function.

Import additional dependencies

Note that these dependencies are the same as for previous tutorials, except for the MultiObjectiveFunctionApi that is used to handle multi-objective functions.

# Multi objective function api
mof_api = sct.MultiObjectiveFunctionApi(api_client)

Create individual objectives

Let us assume that your workstation produces two measurements: yield and selectivity (as in the API-connected workstations and the Nexus custom workstations tutorials), which you want to maximize simultaneously. Let us create the objectives:

Warning

Objective names must match the names of your workstation measurements.

obj_1 = obj_api.objective_create(
    objective_obj=sct.ObjectiveObj(
        name='selectivity', 
        description='Maximize selectivity', 
        goal=sct.Goal.MAX,
    )
).object

obj_2 = obj_api.objective_create(
    objective_obj=sct.ObjectiveObj(
        name='yield', 
        description='Maximize yield', 
        goal=sct.Goal.MAX,
    )
).object 

Create individual objective configurations

Let us assume we want to give priority to the selectivity over the yield. Then we can assign the following parameters:

Chimera hierarchy

In Chimera, the hierarchy starts at 0 (the most important objective), and every following objective must have its hierarchy set incrementally (1, 2, etc. - the lower, the more important).

Chimera tolerance

In Chimera, the relative and absolute tolerances represent the proportion of objective the optimizer should accept as valid before starting to optimize the next objective.

  • relative (from 0 to 1) is the ratio relative to the best result seen so far
  • absolute is a positive numerical value that matches the unit of your objective's measurement (e.g. if the measurement is byproduct (in mL), an absolute tolerance of 10 would be 10 mL)
  • it is safer to set the tolerance to 0 for the last objective to be optimized (the one with the highest hierarchy)
cfg_1 = sct.MofObjConfig(
    objective_id=obj_1.id,
    hierarchy=0,    # add set to highest hierarchy of 0 
    weight=0.7,     # relative weight
    relative=0.1    # relative tolerance Chimera (proportion of the result you are willing to sacrifice before starting to optimize the next objective)
)

cfg_2 = sct.MofObjConfig(
    objective_id=obj_2.id,
    hierarchy=1,
    weight=0.3,
    relative=0
)

Create multi-objective object

multi_obj = mof_api.mof_create(
    mof_obj=sct.MofObj(
        configuration=[cfg_1, cfg_2],
        description="Maximize selectivity and yield",
        name="Yield and Selectivity objectives",
        function=sct.MofType.CHIMERA
    )
).object

Create template referencing Multi Objective Function

Requirements

  • It is assumed that you already have chosen your optimizer algorithm as my_opt, and your workstation as my_wst, following previous tutorials.
  • When creating a TemplateObj, we should provide either a single objective or a single multi_objective_function. In this case we will be passing a multi_objective_function.
tpl_name = 'My Multi-Objective Template' # enter name here. 
tpl = tpl_api.template_create(
    template_obj=sct.TemplateObj(
        # budget: total number of objective function measurements allowed
        budget=5,
        # define optimizer
        optimizer=my_opt.id,
        # name of the optimization template
        name=tpl_name,
        # define the objectives, here a multi-objective function
        multi_objective_function=multi_obj.id,
        # define the parameters for each workstation
        parameters=[
            sct.StepObj(
                level=1,
                parameters=[
                    sct.ParameterCpgObj(
                        parameter_id=param_a.id,  # Copy of the workstation's parameter / parameters names should match!
                        workstation_id=my_wst.id,
                    ),
                    sct.ParameterCpgObj(
                        parameter_id=param_b.id,  # Copy of the workstation's parameter / parameters names should match!
                        workstation_id=my_wst.id,
                    ),
                ],
            )
        ],
    )
).object

You can now run this template with a multi-objective function!

Using multiple workstations in the same template

So far we have only seen examples of using a single workstation, with one set of parameters and one set of measurements, but using multiple workstations in the same template is also possible! You just need to add more steps (StepObj) in the parameters property:

Create template with the sequential steps wst_1, wst_2, wst_3 (we assume that these workstations, the given parameters, the optimizer and multi-objective function have all been defined).

tpl_name = 'My Multi-Workstation Template'  # enter name here
tpl = tpl_api.template_create(
    template_obj=sct.TemplateObj(
        # template properties
        budget=5,
        optimizer=my_opt.id,
        name=tpl_name,
        multi_objective_function=my_multi_obj.id,  # Objective names must match the workstations measurements!
        # steps with several workstations
        parameters=[
            # 1st step workstation has 2 parameters
            sct.StepObj(
                level=1,
                parameters=[
                    sct.ParameterCpgObj(
                        parameter_id=param_1_a_copy.id,
                        workstation_id=wst_1.id,
                    ),
                    sct.ParameterCpgObj(
                        parameter_id=param_1_b_copy.id,
                        workstation_id=wst_1.id,
                    )
                ]
            ),
            # 2nd step workstation has only one parameter
            sct.StepObj(
                level=2,
                parameters=[
                    sct.ParameterCpgObj(
                        parameter_id=param_2_a_copy.id,
                        workstation_id=wst_2.id,
                    )
                ]
            ),
            # 3rd step workstation has NO parameter (for instance, it only measures the output of the 2nd workstation)
            sct.StepObj(
                level=3,
                parameters=[
                    sct.ParameterCpgObj(
                        workstation_id=wst_3.id,
                    ),
                ],
            )
        ],
    )
).object

You can now run this multi-steps template!

Setting constraints on template parameters

Info

Available constraints types as of today are:

  • exclusion
  • conditional_exclusion
  • linear_eq
  • linear_lte
  • linear_gte
  • linear_between

Warning

  • keep in mind that you need to provide IDs of the Template Parameters, not the Workstation Parameters
  • at the moment, providing a name to any new Constraint object is mandatory
  • when choosing a linear constraint, you should always provide weights in the definition of all the involved parameters
  • when choosing an exclusion (or conditional exclusion) constraint, you should always provide bounds, which is a list of lists, where each internal list corresponds to an excluded region
  • if you are creating a simple exclusion constraint on a categorical parameter, you should actually consider simply removing the excluded categories from the Template Parameter definition directly

Import additional dependencies

Note that these dependencies are the same as for previous tutorials, except for the ConstraintApi that is used to create and manage constraints.

# manage constraints
cstr_api = sct.ConstraintApi(sdlabs_api_client)

Create an equality constraint

Adding an equality constraint on continuous parameters in your template will mean that in your next campaign, whenever the optimizer suggests new parameters, the constrained continuous parameters will add up to a certain value (e.g. if you have 3 parameters A, B, C all ranging from 0 to 1 and a target equality constraint of 2.0, then optimizer may suggest something like A = 1.2, B = 0.1, C = 0.7).

# If you do NOT yet have your template parameters objects listed in `tpl_params` you can retrieve them from `my_template`as follows:
# otherwise, go to the next code cell
tpl_params = []
for step in my_template.parameters:
    for prm in step.parameters:
        tpl_params.append(prm.parameter)

print(tpl_params)
# All numerical parameters in the template (i.e. that are not Categorical)
#  will add up to the target (here, `5`) when suggested by the optimizer:
eq_cstr_obj = sct.ConstraintObj(
    name="linear equality",
    type=sct.ConstraintType.LINEAR_EQ,
    targets=[5],
    definitions=[
        # Weight is mandatory here (must range from 1 to 10, and is normalized internally)
        sct.ConstraintDefinition(parameter=prm.id, weight=1.0)
        for prm in tpl_params
        if prm.type != sct.ParameterType.CATEGORICAL
    ]
)

# Please note that all linear_* constraints work the same way as 'linear_eq',
#  except Between, which requires 2 different targets (low and high bounds);
#  also, they all apply only to numerical parameters.

my_constraints: List[sct.ConstraintObjServer] = cstr_api.constraint_create_many(
    constraint_obj=[eq_cstr_obj]
).objects

We can now create a template with these constraints as follows:

tpl_name = 'My Constrained Template'  # enter name here
 # define template object
my_template = tpl_api.template_create(
    template_obj=sct.TemplateObj(
        budget=tpl_budget,
        optimizer=my_opt_id,
        name=tpl_name,
        multi_objective_function=my_multi_obj.id,  # Objective names must match the workstations measurements!
        parameters=[
            sct.StepObj(
                level=1,
                parameters=[
                    sct.ParameterCpgObj(
                        parameter_id=prm.id,  # Copy of the workstation's parameter / parameters names should match!
                        workstation_id=my_workstation.id,
                    )
                    for prm in tpl_params
                ],
            )
        ],
        constraints=[cstr.id for cstr in my_constraints],
    )
).object

print(my_template)

You can now run this template with constraints!