Modeling a Mixture in Traditional Representation

When modeling mixtures, we are often faced with a large set of ingredients to choose from. A common way to formalize this type of selection problem is to assign each ingredient its own numerical parameter representing the amount of the ingredient in the mixture. A sum constraint imposed on all parameters then ensures that the total amount of ingredients in the mix is always 100%. In addition, there could be other constraints, for instance, to impose further restrictions on individual subgroups of ingredients. In BayBE’s language, we call this the traditional mixture representation.

In this example, we demonstrate how to create a search space in this representation, using a simple mixture of up to six components, which are divided into three subgroups: solvents, bases and phase agents.

Slot-based Representation

For an alternative way to describe mixtures, see our slot-based representation.

Imports

import numpy as np
import pandas as pd
from baybe.constraints import ContinuousLinearConstraint
from baybe.parameters import NumericalContinuousParameter
from baybe.recommenders import RandomRecommender
from baybe.searchspace import SearchSpace

Parameter Setup

We start by creating lists containing our substance labels according to their subgroups:

g1 = ["Solvent1", "Solvent2"]
g2 = ["Base1", "Base2"]
g3 = ["PhaseAgent1", "PhaseAgent2"]

Next, we create continuous parameters describing the substance amounts for each group. Here, the maximum amount for each substance depends on its group, i.e. we allow adding more of a solvent compared to a base or a phase agent:

p_g1_amounts = [
    NumericalContinuousParameter(name=f"{name}", bounds=(0, 80)) for name in g1
]
p_g2_amounts = [
    NumericalContinuousParameter(name=f"{name}", bounds=(0, 20)) for name in g2
]
p_g3_amounts = [
    NumericalContinuousParameter(name=f"{name}", bounds=(0, 5)) for name in g3
]

Constraints Setup

Now, we set up our constraints. We start with the overall mixture constraint, ensuring the total of all ingredients is 100%:

c_total_sum = ContinuousLinearConstraint(
    parameters=g1 + g2 + g3,
    operator="=",
    coefficients=(1,) * len(g1 + g2 + g3),
    rhs=100,
)

Additionally, we require bases make up at least 10% of the mixture:

c_g2_min = ContinuousLinearConstraint(
    parameters=g2,
    operator=">=",
    coefficients=(1,) * len(g2),
    rhs=10,
)

By contrast, phase agents should make up no more than 5%:

c_g3_max = ContinuousLinearConstraint(
    parameters=g3,
    operator="<=",
    coefficients=(1,) * len(g3),
    rhs=5,
)

Search Space Creation

Having both parameter and constraint definitions at hand, we can create our search space:

searchspace = SearchSpace.from_product(
    parameters=[*p_g1_amounts, *p_g2_amounts, *p_g3_amounts],
    constraints=[c_total_sum, c_g2_min, c_g3_max],
)

Verification of Constraints

To verify that the constraints imposed above are fulfilled, let us draw some random points from the search space:

recommendations = RandomRecommender().recommend(batch_size=10, searchspace=searchspace)
print(recommendations)
       Base1      Base2  PhaseAgent1  PhaseAgent2   Solvent1   Solvent2
0  12.826127   7.768990     3.140897     1.233782  26.857876  48.172328
1   7.522812  10.990066     0.711404     1.140649  24.900679  54.734389
2  13.713002   5.477481     1.877529     1.374739  31.918398  45.638851
3  19.953180  10.904170     1.098517     0.722834  25.515296  41.806002
4  17.544601   6.174396     0.283602     3.510043   9.503841  62.983517
5  12.582160   6.548887     0.934146     0.783106  55.888722  23.262979
6  18.762971  14.234325     1.865397     2.873378  15.564030  46.699899
7  17.669412   9.921733     0.496171     0.431005   9.019242  62.462438
8   4.353371  12.709048     1.555586     2.555608  68.578126  10.248260
9  18.443451   3.040533     1.278727     2.606403  67.687040   6.943845

Computing the respective row sums reveals the expected result:

stats = pd.DataFrame(
    {
        "Total": recommendations.sum(axis=1),
        "Total_Bases": recommendations[g2].sum(axis=1),
        "Total_Phase_Agents": recommendations[g3].sum(axis=1),
    }
)
print(stats)
   Total  Total_Bases  Total_Phase_Agents
0  100.0    20.595117            4.374679
1  100.0    18.512878            1.852054
2  100.0    19.190483            3.252268
3  100.0    30.857350            1.821351
4  100.0    23.718997            3.793644
5  100.0    19.131047            1.717252
6  100.0    32.997296            4.738774
7  100.0    27.591145            0.927176
8  100.0    17.062419            4.111195
9  100.0    21.483985            3.885130
assert np.allclose(stats["Total"], 100)
assert (stats["Total_Bases"] >= 10).all()
assert (stats["Total_Phase_Agents"] <= 5).all()