Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/source/workflows/design_spaces.rst
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,11 @@ Labels provide a way to map a string to a set of ingredient names.
For example, salt can be labeled as a solute by specifying the mapping ``{"solute": {"salt"}}``.
An ingredient may be given multiple labels, and an ingredient will always be given all applicable labels when present in a formulation.

Every ingredient that can appear in a formulation is listed in ``ingredients``.
The optional ``untested_ingredients`` set is a subset of ``ingredients``: it names those ingredients that are absent from the predictor's training data.
Leave ``untested_ingredients`` empty, or omit it, when there are no such novel ingredients.
When you build the design space with :meth:`~citrine.resources.design_space.DesignSpaceCollection.create_default`, it is populated for you.

Constraints restrict the total number or fractional amount of ingredients in formulations sampled from the design space.
There are three types of constraint that can be specified as part of a formulation design space:

Expand Down
2 changes: 1 addition & 1 deletion src/citrine/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "4.1.1"
__version__ = "4.2.0"
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ class FormulationDesignSpace(Resource['FormulationDesignSpace'], DesignSubspace)
labels: Mapping[str, set[str]] | None
map from a label to each ingredient that should given that label
when it's included in a formulation, e.g., ``{'solvent': {'water', 'alcohol'}}``
untested_ingredients: set[str] | None
names of ingredients to explore that are absent from the predictor's training
data. This should be a subset of ``ingredients`` and will be empty if there are
no novel input materials.
resolution: float, optional
Minimum increment used to specify ingredient quantities.
Default is 0.0001.
Expand All @@ -42,6 +46,8 @@ class FormulationDesignSpace(Resource['FormulationDesignSpace'], DesignSubspace)
properties.String,
properties.Set(properties.String)
), 'labels')
untested_ingredients = properties.Optional(
properties.Set(properties.String), 'untested_ingredients')
constraints = properties.Set(properties.Object(Constraint), 'constraints')
resolution = properties.Float('resolution')

Expand All @@ -55,13 +61,15 @@ def __init__(self,
ingredients: set[str],
constraints: set[Constraint],
labels: Mapping[str, set[str]] | None = None,
untested_ingredients: set[str] | None = None,
resolution: float = 0.0001):
self.name: str = name
self.description: str = description
self.formulation_descriptor: FormulationDescriptor = formulation_descriptor
self.ingredients: set[str] = ingredients
self.constraints: set[Constraint] = constraints
self.labels: Mapping[str, set[str]] | None = labels
self.untested_ingredients: set[str] | None = untested_ingredients
self.resolution: float = resolution

def __str__(self):
Expand Down
3 changes: 3 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def valid_product_design_space_data():
formulation_descriptor=FormulationDescriptor.hierarchical().dump(),
ingredients=['foo'],
labels={'bar': ['foo']},
untested_ingredients=['qux'],
constraints=[],
resolution=0.1
),
Expand All @@ -70,6 +71,7 @@ def valid_product_design_space_data():
formulation_descriptor=FormulationDescriptor.hierarchical().dump(),
ingredients=['baz'],
labels={},
untested_ingredients=None,
constraints=[],
resolution=0.1
)
Expand Down Expand Up @@ -130,6 +132,7 @@ def valid_formulation_design_space_data():
formulation_descriptor=descriptor.dump(),
ingredients=['foo'],
labels={'bar': ['foo']},
untested_ingredients=['qux'],
constraints=[constraint.dump()],
resolution=0.1
)
Expand Down
24 changes: 24 additions & 0 deletions tests/informatics/test_design_spaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def formulation_design_space() -> FormulationDesignSpace:
formulation_descriptor=desc,
ingredients={"dog", "cat", "bird"},
labels={"canine": {"dog"}, "feline": {"cat"}},
untested_ingredients={"fish", "hamster"},
constraints={
IngredientCountConstraint(formulation_descriptor=desc, min=1, max=2)
}
Expand Down Expand Up @@ -79,6 +80,29 @@ def material_node_definition(formulation_design_space) -> MaterialNodeDefinition
)


def test_formulation_initialization(formulation_design_space):
"""Make sure the correct fields go to the correct places."""
assert formulation_design_space.name == 'Formulation DS'
assert formulation_design_space.ingredients == {"dog", "cat", "bird"}
assert formulation_design_space.untested_ingredients == {"fish", "hamster"}
# The untested split survives serialization.
assert set(formulation_design_space.dump()["untested_ingredients"]) \
== {"fish", "hamster"}


def test_formulation_untested_ingredients_default():
"""untested_ingredients is optional and defaults to None."""
desc = FormulationDescriptor.hierarchical()
ds = FormulationDesignSpace(
name="No untested",
description="Does formulations",
formulation_descriptor=desc,
ingredients={"dog"},
constraints={IngredientCountConstraint(formulation_descriptor=desc, min=1, max=1)}
)
assert ds.untested_ingredients is None


def test_product_initialization(product_design_space):
"""Make sure the correct fields go to the correct places."""
assert product_design_space.name == 'my design space'
Expand Down
1 change: 1 addition & 0 deletions tests/resources/test_design_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def test_formulation_build(valid_formulation_design_space_data):
assert design_space.formulation_descriptor.key == FormulationKey.HIERARCHICAL.value
assert design_space.ingredients == {'foo'}
assert design_space.labels == {'bar': {'foo'}}
assert design_space.untested_ingredients == {'qux'}
assert len(design_space.constraints) == 1
assert design_space.resolution == 0.1

Expand Down
13 changes: 13 additions & 0 deletions tests/serialization/test_design_spaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def test_formulation_deserialization(valid_formulation_design_space_data):
assert design_space.formulation_descriptor.key == expected_descriptor.key
assert design_space.ingredients == {'foo'}
assert design_space.labels == {'bar': {'foo'}}
assert design_space.untested_ingredients == {'qux'}
assert len(design_space.constraints) == 1
actual_constraint: IngredientCountConstraint = next(iter(design_space.constraints))
assert actual_constraint.formulation_descriptor == expected_descriptor
Expand All @@ -69,6 +70,18 @@ def test_formulation_serialization(valid_formulation_design_space_data):
design_space_serialization_check(valid_formulation_design_space_data, FormulationDesignSpace)


def test_formulation_without_untested_ingredients(valid_formulation_design_space_data):
"""A payload lacking untested_ingredients still deserializes (field is optional).

Pins backward compatibility with design spaces that predate the field: the
field must stay optional, so older payloads without the key don't fail to build.
"""
data = deepcopy(valid_formulation_design_space_data)
del data['untested_ingredients']
design_space: FormulationDesignSpace = FormulationDesignSpace.build(data)
assert design_space.untested_ingredients is None


def test_invalid_design_subspace_type(invalid_design_subspace_data):
"""Ensures we raise proper exception when an invalid type is used."""
with pytest.raises(ValueError):
Expand Down
Loading