Pizza example
In this example, we’ll import the raw ontology constructor, update it with our own additional pizza-themed entries, and see how a few features of the pyiron ontology play out when building workflows.
[1]:
import owlready2 as owl
from pyiron_ontology import Constructor
* Owlready2 * Warning: optimized Cython parser module 'owlready2_optimized' is not available, defaulting to slower Python implementation
First, we instantiate a new constructor, then use owlready’s with syntax to add new entities.
Very often, we want to describe features of some entity that are mutually exclusive. In materials science, we might thing about a material being “bulk”, i.e. “defect-free”, or “defected”, i.e. containing one or more grain boundaries, phase boundaries, surfaces, vacancies, etc. In our pizza example we could think of a pizza being “vegetarian” or containing any nonzero number of “meat” toppings.
Handling this mutual exclusion is a little bit tricky in the OWL paradigm. In pyiron, we use a subclass based approach where features are represented using subclasses and these subclasses are registered as disjoint – i.e. no individual can inherit from both of them. We can then have multiple non-exclusionary features by including them all in the is_a field when instantiating our classes.
For exactly this “vegetarian” example, you’ll also see below that we can leverage owlready’s equivalent_to field and some good class structure to very succinctly define these mutually-exclusive features.
[2]:
c = Constructor('pizza')
with c.onto:
class Flour(c.onto.Generic): pass
class Wheat(Flour): pass
class GlutenFree(Flour): pass
owl.AllDisjoint([GlutenFree, Wheat])
class Crust(c.onto.Generic): pass
class Thin(Crust): pass
class Regular(Crust): pass
owl.AllDisjoint([Thin, Regular])
class Stuffed(Regular): pass
class Ingredients(c.onto.Generic): pass
class HasVegetables(Ingredients): pass
class HasMushrooms(HasVegetables): pass
class HasPeppers(HasVegetables): pass
class HasMeat(Ingredients): pass
class HasSalami(HasMeat): pass
class HasBacon(HasMeat): pass
class Vegetarian(Ingredients):
equivalent_to = [Ingredients & owl.Not(HasMeat)]
owl.AllDisjoint([Vegetarian, HasMeat])
class RawPizza(c.onto.Generic): pass
class CookedPizza(c.onto.Generic): pass
owl.AllDisjoint([Flour, Crust, Ingredients, RawPizza, CookedPizza])
Now that we’ve defined the universe of things our workflows will operate in, we’ll define different workflow elements as individuals.
Here, we’ll have a workflow where we buy flour and make a crust, then combine the crust with toppings to make a raw pizza, and finally bake that pizza.
What type of pizza we wind up with is controlled by the ontological classes for our inputs and outputs, as well as the extra requirements that we insist be satisfied in the workflow tree.
In particular, at the stage of assembling the raw pizza, we indicate that the flour type is a “transitive requirement”. This is a requirement that is not necessary at this stage, but may (or may not) be necessary farther upstream – in this case when we’re making our crust. Marking it as “transitive” ensures that it gets passed along to these upstream workflow steps.
[3]:
buy_wheat_flour = c.onto.Function("buy_wheat_flour")
buy_wheat_flour_out = c.onto.Output(
"buy_wheat_flour_out",
output_of=buy_wheat_flour,
generic=Wheat()
)
buy_corn_flour = c.onto.Function("buy_corn_flour")
buy_corn_flour_out = c.onto.Output(
"buy_corn_flour_out",
output_of=buy_corn_flour,
generic=GlutenFree()
)
make_crust = c.onto.Function("make_crust")
make_crust_inp_flour = c.onto.Input(
name="make_crust_inp_flour",
mandatory_input_of=make_crust,
generic=Flour(),
)
make_crust_out = c.onto.Output(
name="make_crust_out",
output_of=make_crust,
generic=Crust(),
)
make_thin_crust = c.onto.Function("make_thin_crust")
make_thin_crust_inp_flour = c.onto.Input(
name="make_thin_crust_inp_flour",
mandatory_input_of=make_thin_crust,
generic=Flour(),
)
make_thin_crust_out = c.onto.Output(
name="make_thin_crust_out",
output_of=make_thin_crust,
generic=Thin(),
)
make_gluten_free_crust = c.onto.Function("make_gluten_free_crust")
make_gluten_free_crust_inp_flour = c.onto.Input(
name="make_gluten_free_crust_inp_flour",
mandatory_input_of=make_gluten_free_crust,
generic=GlutenFree(),
)
make_gluten_free_crust_out = c.onto.Output(
name="make_gluten_free_crust_out",
output_of=make_gluten_free_crust,
generic=Crust(),
)
add_meat = c.onto.Function("add_meat")
add_meat_inp_ingredients = c.onto.Input(
name="add_meat_inp_ingredients",
mandatory_input_of=add_meat,
generic=HasMeat(),
)
add_meat_inp_crust = c.onto.Input(
name="add_meat_inp_crust",
mandatory_input_of=add_meat,
generic=Crust(),
transitive_requirements=[Flour()]
)
add_meat_out = c.onto.Output(
name="add_meat_out",
output_of=add_meat,
generic=RawPizza()
)
add_vegetables = c.onto.Function("add_vegetables")
add_vegetables_inp_ingredients = c.onto.Input(
name="add_vegetables_inp_ingredients",
mandatory_input_of=add_vegetables,
generic=HasVegetables(),
)
add_vegetables_inp_crust = c.onto.Input(
name="add_vegetables_inp_crust",
mandatory_input_of=add_vegetables,
generic=Crust(),
transitive_requirements=[Flour()]
)
add_vegetables_out = c.onto.Output(
name="add_vegetables_out",
output_of=add_vegetables,
generic=RawPizza()
)
canadian = c.onto.Function("canadian")
canadian_inp_ingredients = c.onto.Input(
name="canadian_inp_ingredients",
mandatory_input_of=canadian,
generic=Ingredients(is_a=[HasBacon, HasMushrooms]),
)
canadian_inp_crust = c.onto.Input(
name="canadian_inp_crust",
mandatory_input_of=canadian,
generic=Crust(),
transitive_requirements=[Flour()]
)
canadian_out = c.onto.Output(
name="canadian_out",
output_of=canadian,
generic=RawPizza()
)
bake_for_omnivor = c.onto.Function("bake_for_omnivor")
bake_for_omnivor_inp = c.onto.Input(
name="bake_for_omnivor_inp",
mandatory_input_of=bake_for_omnivor,
generic=RawPizza(),
)
bake_for_omnivor_out = c.onto.Output(
name="bake_for_omnivor_out",
output_of=bake_for_omnivor,
generic=CookedPizza()
)
bake_for_vegetarian = c.onto.Function("bake_for_vegetarian")
bake_for_vegetarian_inp = c.onto.Input(
name="bake_for_vegetarian_inp",
mandatory_input_of=bake_for_vegetarian,
generic=RawPizza(),
requirements=[Vegetarian()]
)
bake_for_vegetarian_out = c.onto.Output(
name="bake_for_vegetarian_out",
output_of=bake_for_vegetarian,
generic=CookedPizza()
)
bake_stuffed_crust = c.onto.Function("bake_stuffed_crust")
bake_stuffed_crust_inp = c.onto.Input(
name="bake_stuffed_crust_inp",
mandatory_input_of=bake_stuffed_crust,
generic=RawPizza(),
requirements=[Stuffed(), Wheat()]
)
bake_stuffed_crust_out = c.onto.Output(
name="bake_stuffed_crust_out",
output_of=bake_stuffed_crust,
generic=CookedPizza()
)
bake_dietary_restrictions = c.onto.Function("bake_dietary_restrictions")
bake_dietary_restrictions_inp = c.onto.Input(
name="bake_dietary_restrictions_inp",
mandatory_input_of=bake_dietary_restrictions,
generic=RawPizza(),
requirements=[GlutenFree(), Vegetarian()]
)
bake_dietary_restrictions_out = c.onto.Output(
name="bake_dietary_restrictions_out",
output_of=bake_dietary_restrictions,
generic=CookedPizza()
)
Finally, we’ll re-synchronize our reasoner, to make sure it leverages the ontology to find all the equivalencies, make sure we haven’t declared any forbidden individuals (i.e. violating AllDisjoint), etc.
[4]:
c.sync()
Alright, let’s see the available workflow paths to build some of these pizzas! This is easy with the get_source_tree method available on workflow individuals.
[5]:
bake_for_omnivor_out.get_source_tree().render()
bake_for_omnivor_out
bake_for_omnivor
bake_for_omnivor_inp
add_meat_out
add_meat
add_meat_inp_ingredients
add_meat_inp_crust
make_crust_out
make_crust
make_crust_inp_flour
buy_corn_flour_out
buy_corn_flour
buy_wheat_flour_out
buy_wheat_flour
make_gluten_free_crust_out
make_gluten_free_crust
make_gluten_free_crust_inp_flour
buy_corn_flour_out
buy_corn_flour
make_thin_crust_out
make_thin_crust
make_thin_crust_inp_flour
buy_corn_flour_out
buy_corn_flour
buy_wheat_flour_out
buy_wheat_flour
canadian_out
canadian
canadian_inp_ingredients
canadian_inp_crust
make_crust_out
make_crust
make_crust_inp_flour
buy_corn_flour_out
buy_corn_flour
buy_wheat_flour_out
buy_wheat_flour
make_gluten_free_crust_out
make_gluten_free_crust
make_gluten_free_crust_inp_flour
buy_corn_flour_out
buy_corn_flour
make_thin_crust_out
make_thin_crust
make_thin_crust_inp_flour
buy_corn_flour_out
buy_corn_flour
buy_wheat_flour_out
buy_wheat_flour
add_vegetables_out
add_vegetables
add_vegetables_inp_ingredients
add_vegetables_inp_crust
make_crust_out
make_crust
make_crust_inp_flour
buy_corn_flour_out
buy_corn_flour
buy_wheat_flour_out
buy_wheat_flour
make_gluten_free_crust_out
make_gluten_free_crust
make_gluten_free_crust_inp_flour
buy_corn_flour_out
buy_corn_flour
make_thin_crust_out
make_thin_crust
make_thin_crust_inp_flour
buy_corn_flour_out
buy_corn_flour
buy_wheat_flour_out
buy_wheat_flour
We didn’t place any restrictions on that pizza, so we see all the possible pizza-making workflows available in our ontology.
Note how the canadian ingredients demonstrate using is_a to declare a class with multiple non-exclusive properties.
[6]:
bake_for_vegetarian_out.get_source_tree().render()
bake_for_vegetarian_out
bake_for_vegetarian
bake_for_vegetarian_inp
add_vegetables_out
add_vegetables
add_vegetables_inp_ingredients
add_vegetables_inp_crust
make_crust_out
make_crust
make_crust_inp_flour
buy_corn_flour_out
buy_corn_flour
buy_wheat_flour_out
buy_wheat_flour
make_gluten_free_crust_out
make_gluten_free_crust
make_gluten_free_crust_inp_flour
buy_corn_flour_out
buy_corn_flour
make_thin_crust_out
make_thin_crust
make_thin_crust_inp_flour
buy_corn_flour_out
buy_corn_flour
buy_wheat_flour_out
buy_wheat_flour
The ontology can tell that add_vegetables gives an output that is consistent with the equivalent_to definition of Vegetarian()! The add_meat and canadian functions both return ingredients with meat in them, so these are excluded from our workflow tree.
[7]:
bake_stuffed_crust_out.get_source_tree().render()
bake_stuffed_crust_out
bake_stuffed_crust
bake_stuffed_crust_inp
add_meat_out
add_meat
add_meat_inp_ingredients
add_meat_inp_crust
make_crust_out
make_crust
make_crust_inp_flour
buy_wheat_flour_out
buy_wheat_flour
canadian_out
canadian
canadian_inp_ingredients
canadian_inp_crust
make_crust_out
make_crust
make_crust_inp_flour
buy_wheat_flour_out
buy_wheat_flour
add_vegetables_out
add_vegetables
add_vegetables_inp_ingredients
add_vegetables_inp_crust
make_crust_out
make_crust
make_crust_inp_flour
buy_wheat_flour_out
buy_wheat_flour
Here we demonstrate how mutual exclusion is inherited: Stuffed crust is not directly exclusive to Thin crust, but Regular and Thin are exclusive and Stuffed inherits from Regular.
We have additionally required that the crust be made of Wheat, which precludes the use of make_gluten_free_crust.
Thus, there’s lots of choices for topping ingredients on this pizza, but only the generic make_crust function has the necessary flexibility to satisfy our crust demands.
[8]:
bake_dietary_restrictions_out.get_source_tree().render()
bake_dietary_restrictions_out
bake_dietary_restrictions
bake_dietary_restrictions_inp
add_vegetables_out
add_vegetables
add_vegetables_inp_ingredients
add_vegetables_inp_crust
make_crust_out
make_crust
make_crust_inp_flour
buy_corn_flour_out
buy_corn_flour
make_gluten_free_crust_out
make_gluten_free_crust
make_gluten_free_crust_inp_flour
buy_corn_flour_out
buy_corn_flour
make_thin_crust_out
make_thin_crust
make_thin_crust_inp_flour
buy_corn_flour_out
buy_corn_flour
Finally, we show how transitive_requirements pass requirements upstream. In this case add_vegetables – our only option for ingredients since we’ve also required Vegetarian – has no use for a Flour requirement, but we know it is possible to use this upstream so we have registered it as transtive. Later, we see that we must buy only Corn flour to bake this pizza!
As an experiment, you can go back and remove the transitive requirement and re-run the notebook. You should then see that this workflow terminates much earlier, because no workflow elements can be found that satisfy all the requirements!
In the last two examples, we see that we get any workflow element who is flexible enough that it could satisfy our requirements. Alternatively, we could have written the path building to demand that upstream elements are only suggested if they are more specific so they are guaranteed to satisft requirements. It’s possible we will switch to this second paradigm in future versions of pyiron_ontology, but knowing which is more useful will require building up more experience.
[ ]: