def test_complex_substitutions(): """Make sure accounting works for realistic objects.""" root = MaterialRun("root", process=ProcessRun("root", spec=ProcessSpec("root")), spec=MaterialSpec("root")) root.spec.process = root.process.spec input = MaterialRun("input", process=ProcessRun("input", spec=ProcessSpec("input")), spec=MaterialSpec("input")) input.spec.process = input.process.spec IngredientRun(process=root.process, material=input, spec=IngredientSpec("ingredient", process=root.process.spec, material=input.spec)) param = ParameterTemplate("Param", bounds=RealBounds(-1, 1, "m")) root.process.spec.template = ProcessTemplate("Proc", parameters=[param]) root.process.parameters.append( Parameter("Param", value=NormalReal(0, 1, 'm'), template=param)) links = flatten(root, scope="test-scope") index = make_index(links) rebuild = substitute_objects(links, index, inplace=True) rebuilt_root = next(x for x in rebuild if x.name == root.name and x.typ == root.typ) all_objs = recursive_flatmap(rebuilt_root, func=lambda x: [x], unidirectional=False) unique = [x for i, x in enumerate(all_objs) if i == all_objs.index(x)] assert not any(isinstance(x, LinkByUID) for x in unique), "All are objects" assert len(links) == len(unique), "Objects are missing"
def test_sub_inplace_objects(): """Verify consistency for GEMD objects.""" run = IngredientRun(spec=IngredientSpec("string"), notes="note") run.spec = None _substitute_inplace(run, applies=lambda x: isinstance(x, str), sub=lambda x: f"{x}s") assert run.name == "strings" assert run.notes == "notes"
def test_name_persistance(): """Verify that a serialized IngredientRun doesn't lose its name.""" from gemd.entity.object import IngredientSpec from gemd.entity.link_by_uid import LinkByUID from gemd.json import GEMDJson je = GEMDJson() ms_link = LinkByUID(scope='local', id='mat_spec') mr_link = LinkByUID(scope='local', id='mat_run') ps_link = LinkByUID(scope='local', id='pro_spec') pr_link = LinkByUID(scope='local', id='pro_run') spec = IngredientSpec(name='Ingred', labels=['some', 'words'], process=ps_link, material=ms_link) run = IngredientRun(spec=spec, process=pr_link, material=mr_link) assert run.name == spec.name assert run.labels == spec.labels # Try changing them and make sure they change spec.name = 'Frank' spec.labels = ['other', 'words'] assert run.name == spec.name assert run.labels == spec.labels run.spec = LinkByUID(scope='local', id='ing_spec') # Name and labels are now stashed but not stored assert run == je.copy(run) assert run.name == spec.name assert run.labels == spec.labels # Test that serialization doesn't get confused after a deser and set spec_too = IngredientSpec(name='Jorge', labels=[], process=ps_link, material=ms_link) run.spec = spec_too assert run == je.copy(run) assert run.name == spec_too.name assert run.labels == spec_too.labels
def _make_ingredient(*, material, process, **kwargs): """Convenience method to utilize material fields in creating an ingredient's arguments.""" return IngredientSpec(name=material.name.lower(), tags=list(material.tags), material=material, process=process, uids={ DEMO_SCOPE: "{}--{}".format(material.uids[DEMO_SCOPE], process.uids[DEMO_SCOPE]) }, **kwargs)
def test_ingredient_spec(): """Tests that a process can house an ingredient, and that pairing survives serialization.""" # Create a ProcessSpec proc_spec = ProcessSpec(name="a process spec", tags=["tag1", "tag2"]) IngredientSpec(name='Input', material=MaterialSpec(name='Raw'), process=proc_spec) # Make copies of both specs proc_spec_copy = loads(dumps(proc_spec)) assert proc_spec_copy == proc_spec, "Full structure wasn't preserved across serialization"
def test_equality(): """Test that __eq__ and _cached_equals behave as expected.""" from gemd.entity.object import ProcessSpec, IngredientSpec, MaterialSpec from gemd.entity.link_by_uid import LinkByUID one = ProcessSpec("Object", tags=["tags!"], uids={"scope": "id"}) assert one == LinkByUID(scope="scope", id="id"), "Objects equal their links" assert one == ("scope", "id"), "Objects equal their equivalent tuples" assert one != ("scope", "id", "extra"), "But not if they are too long" assert one != ("epocs", "id"), "Or have the wrong scope" assert one != ("scope", "di"), "Or have the wrong id" junk = MaterialSpec("Object", tags=["tags!"], uids={"scope": "id"}) assert one != junk, "Objects don't match across types" two = ProcessSpec("Object", tags=["tags!"], uids={"scope": "id"}, notes="Notes!") assert one != two, "Objects don't match unless the fields do" one.notes = "Notes!" assert one == two, "And then they will" # It ignores the ingredient length mismatch if the uids matched one_ing = IngredientSpec("Ingredient", process=one) assert one == two assert two == one # And a cycle is not a problem one_mat = MaterialSpec("Material", tags=["other tags!"], process=one) two_mat = MaterialSpec("Material", tags=["other tags!"], process=two) one_ing.material = two_mat IngredientSpec("Ingredient", process=two, material=one_mat) # two_ing assert one == two assert two == one # Order doesn't matter for tags one.tags = ["One", "Two", "Three", "Four"] two.tags = ["Four", "One", "Three", "Two"] assert one == two assert two == one
def test_equality(): """Test that equality check works as expected.""" spec1 = ProcessSpec("A spec") spec2 = ProcessSpec("A spec", tags=["a tag"]) assert spec1 != spec2 spec3 = deepcopy(spec1) assert spec1 == spec3, "Copy somehow failed" IngredientSpec("An ingredient", process=spec3) assert spec1 != spec3 spec4 = deepcopy(spec3) assert spec4 == spec3, "Copy somehow failed" spec4.ingredients[0].tags.append('A tag') assert spec4 != spec3 spec5 = next(x for x in flatten(spec4, 'test-scope') if isinstance(x, ProcessSpec)) assert spec5 == spec4, "Flattening removes measurement references, but that's okay"
def test_flatten_empty_history(): """Test that flatten works when the objects are empty and go through a whole history.""" procured = ProcessSpec(name="procured") input = MaterialSpec(name="foo", process=procured) transform = ProcessSpec(name="transformed") ingredient = IngredientSpec(name="input", material=input, process=transform) procured_run = ProcessRun(name="procured", spec=procured) input_run = MaterialRun(name="foo", process=procured_run, spec=input) transform_run = ProcessRun(name="transformed", spec=transform) ingredient_run = IngredientRun(material=input_run, process=transform_run, spec=ingredient) assert len(flatten(procured)) == 1 assert len(flatten(input)) == 1 assert len(flatten(ingredient)) == 3 assert len(flatten(transform)) == 3 assert len(flatten(procured_run)) == 3 assert len(flatten(input_run)) == 3 assert len(flatten(ingredient_run)) == 7 assert len(flatten(transform_run)) == 7
def add_edge(input_material: MaterialRun, output_material: MaterialRun, *, name: str = None, mass_fraction: Union[float, ContinuousValue] = None, number_fraction: Union[float, ContinuousValue] = None, volume_fraction: Union[float, ContinuousValue] = None, absolute_quantity: Union[int, float, ContinuousValue] = None, absolute_units: str = None, ) -> IngredientRun: """ Connect two material-process spec-run quadruples with ingredients. Parameters ---------- input_material: MaterialRun The `material` for the returned IngredientRun output_material: MaterialRun The `process` for the returned IngredientRun will be `output_material.process` name: str The ingredient name. Defaults to `input_material.name`. mass_fraction: float or ContinuousValue The mass fraction of the Ingredient Run. 0 <= x <= 1 number_fraction: float or ContinuousValue The number fraction of the Ingredient Run. 0 <= x <= 1 volume_fraction: float or ContinuousValue The volume fraction of the Ingredient Run. 0 <= x <= 1 absolute_quantity: float or ContinuousValue The absolute quantity. 0 <= x absolute_units: str The absolute units. Required if absolute_quantity is provided as a float Returns -------- IngredientRun A IngredientRun with linked processes, specs and materials """ output_spec = output_material.spec if not isinstance(output_spec, MaterialSpec) \ or output_spec.process is None \ or output_material.process is None: raise ValueError("Output Material must be a MaterialRun with connected " "Specs and Processes.") if input_material.spec is None: raise ValueError("Input Material must be a MaterialRun with connected Spec.") if name is None: name = input_material.name my_ingredient_spec = IngredientSpec(name=name, process=output_spec.process, material=input_material.spec ) my_ingredient_run = IngredientRun(spec=my_ingredient_spec, process=output_material.process, material=input_material ) if mass_fraction is not None: if isinstance(mass_fraction, float): mass_fraction = NominalReal(nominal=mass_fraction, units='') my_ingredient_run.mass_fraction = mass_fraction if number_fraction is not None: if isinstance(number_fraction, float): number_fraction = NominalReal(nominal=number_fraction, units='') my_ingredient_run.number_fraction = number_fraction if volume_fraction is not None: if isinstance(volume_fraction, float): volume_fraction = NominalReal(nominal=volume_fraction, units='') my_ingredient_run.volume_fraction = volume_fraction if absolute_quantity is not None: if isinstance(absolute_quantity, float): if absolute_units is None: raise ValueError("Absolute Units are required if Absolute Quantity is not a Value") absolute_quantity = NominalReal(nominal=absolute_quantity, units=absolute_units) absolute_units = None my_ingredient_run.absolute_quantity = absolute_quantity if absolute_units is not None: raise ValueError("Absolute Units are only used if " "Absolute Quantity is given as is a float.") return my_ingredient_run
def make_cake_spec(tmpl=None): """Define a recipe for making a cake.""" ############################################################################################### # Templates if tmpl is None: tmpl = make_cake_templates() count = dict() def ingredient_kwargs(material): # Pulls the elements of a material that all ingredients consume out count[material.name] = count.get(material.name, 0) + 1 return { "name": "{} input{}".format(material.name.replace('Abstract ', ''), " (Again)" * (count[material.name] - 1)), "tags": list(material.tags), "material": material } ############################################################################################### # Objects cake = MaterialSpec( name="Abstract Cake", template=tmpl["Dessert"], process=ProcessSpec( name='Icing Cake, in General', template=tmpl["Icing"], tags=['spreading'], notes='The act of covering a baked output with frosting'), properties=[ PropertyAndConditions( Property(name="Tastiness", value=NominalInteger(5), template=tmpl["Tastiness"], origin="specified")) ], file_links=FileLink( filename="Becky's Butter Cake", url='https://www.landolakes.com/recipe/16730/becky-s-butter-cake/' ), tags=['cake::butter cake', 'dessert::baked::cake', 'iced::chocolate'], notes= 'Butter cake recipe reminiscent of the 1-2-3-4 cake that Grandma may have baked.' ) ######################## frosting = MaterialSpec( name="Abstract Frosting", template=tmpl["Dessert"], process=ProcessSpec( name='Mixing Frosting, in General', template=tmpl["Mixing"], tags=['mixing'], notes='Combining ingredients to make a sweet frosting'), tags=['frosting::chocolate', 'topping::chocolate'], notes='Chocolate frosting') IngredientSpec(**ingredient_kwargs(frosting), notes='Seems like a lot of frosting', labels=['coating'], process=cake.process, absolute_quantity=NominalReal(nominal=0.751, units='kg')) baked_cake = MaterialSpec( name="Abstract Baked Cake", template=tmpl["Generic Material"], process=ProcessSpec( name='Baking, in General', template=tmpl["Baking in an oven"], tags=['oven::baking'], notes='Using heat to convert batter into a solid matrix'), tags=[], notes='The cakey part of the cake') IngredientSpec(**ingredient_kwargs(baked_cake), labels=['substrate'], process=cake.process) ######################## batter = MaterialSpec( name="Abstract Batter", template=tmpl["Generic Material"], process=ProcessSpec( name='Mixing Batter, in General', template=tmpl["Mixing"], tags=['mixing'], notes='Combining ingredients to make a baking feedstock'), tags=[], notes='The fluid that converts to cake with heat') IngredientSpec(**ingredient_kwargs(batter), labels=['precursor'], process=baked_cake.process) ######################## wetmix = MaterialSpec( name="Abstract Wet Mix", template=tmpl["Generic Material"], process=ProcessSpec( name='Mixing Wet, in General', template=tmpl["Mixing"], tags=['mixing'], notes='Combining wet ingredients to make a baking feedstock'), tags=[], notes='The wet fraction of a batter') IngredientSpec(**ingredient_kwargs(wetmix), labels=['wet'], process=batter.process) drymix = MaterialSpec( name="Abstract Dry Mix", template=tmpl["Generic Material"], process=ProcessSpec( name='Mixing Dry, in General', template=tmpl["Mixing"], tags=['mixing'], notes='Combining dry ingredients to make a baking feedstock'), tags=[], notes='The dry fraction of a batter') IngredientSpec(**ingredient_kwargs(drymix), labels=['dry'], process=batter.process, absolute_quantity=NominalReal(nominal=3.052, units='cups')) ######################## flour = MaterialSpec( name="Abstract Flour", template=tmpl["Nutritional Material"], properties=[ PropertyAndConditions( property=Property(name="Nutritional Information", value=NominalComposition({ "dietary-fiber": 1, "sugars": 1, "other-carbohydrate": 20, "protein": 4, "other": 4 }), template=tmpl["Nutritional Information"], origin="specified"), conditions=Condition(name="Serving Size", value=NominalReal(30, 'g'), template=tmpl["Sample Mass"], origin="specified")) ], process=ProcessSpec(name='Buying Flour, in General', template=tmpl["Procurement"], tags=['purchase::dry-goods'], notes='Purchasing all purpose flour'), tags=[], notes='All-purpose flour') IngredientSpec( **ingredient_kwargs(flour), labels=['dry'], process=drymix.process, volume_fraction=NominalReal(nominal=0.9829, units='') # 3 cups ) baking_powder = MaterialSpec(name="Abstract Baking Powder", template=tmpl["Generic Material"], process=ProcessSpec( name='Buying Baking Powder, in General', template=tmpl["Procurement"], tags=['purchase::dry-goods'], notes='Purchasing baking powder'), tags=[], notes='Leavening agent for cake') IngredientSpec( **ingredient_kwargs(baking_powder), labels=['leavening', 'dry'], process=drymix.process, volume_fraction=NominalReal(nominal=0.0137, units='') # 2 teaspoons ) salt = MaterialSpec(name="Abstract Salt", template=tmpl["Formulaic Material"], process=ProcessSpec(name='Buying Salt, in General', template=tmpl["Procurement"], tags=['purchase::dry-goods'], notes='Purchasing salt'), tags=[], notes='Plain old NaCl', properties=[ PropertyAndConditions( Property(name='Formula', value=EmpiricalFormula("NaCl"))) ]) IngredientSpec( **ingredient_kwargs(salt), labels=['dry', 'seasoning'], process=drymix.process, volume_fraction=NominalReal(nominal=0.0034, units='') # 1/2 teaspoon ) sugar = MaterialSpec( name="Abstract Sugar", template=tmpl["Formulaic Material"], process=ProcessSpec(name='Buying Sugar, in General', template=tmpl["Procurement"], tags=['purchase::dry-goods'], notes='Purchasing all purpose flour'), tags=[], notes='Sugar', properties=[ PropertyAndConditions( Property(name="Formula", value=EmpiricalFormula("C12H22O11"))), PropertyAndConditions( Property(name='SMILES', value=Smiles( "C(C1C(C(C(C(O1)OC2(C(C(C(O2)CO)O)O)CO)O)O)O)O"), template=tmpl["Molecular Structure"])) ]) IngredientSpec(**ingredient_kwargs(sugar), labels=['wet', 'sweetener'], process=wetmix.process, absolute_quantity=NominalReal(nominal=2, units='cups')) butter = MaterialSpec( name="Abstract Butter", template=tmpl["Generic Material"], process=ProcessSpec(name='Buying Butter, in General', template=tmpl["Procurement"], tags=['purchase::produce'], notes='Purchasing butter'), tags=[], notes='Shortening for making rich, buttery baked goods') IngredientSpec(**ingredient_kwargs(butter), labels=['wet', 'shortening'], process=wetmix.process, absolute_quantity=NominalReal(nominal=1, units='cups')) IngredientSpec( **ingredient_kwargs(butter), labels=['shortening'], process=frosting.process, mass_fraction=NominalReal(nominal=0.1434, units='') # 1/2 c @ 0.911 g/cc ) eggs = MaterialSpec(name="Abstract Eggs", template=tmpl["Generic Material"], process=ProcessSpec(name='Buying Eggs, in General', template=tmpl["Procurement"], tags=['purchase::produce'], notes='Purchasing eggs'), tags=[], notes='') IngredientSpec(**ingredient_kwargs(eggs), labels=['wet'], absolute_quantity=NominalReal(nominal=4, units='')) vanilla = MaterialSpec( name="Abstract Vanilla", template=tmpl["Generic Material"], process=ProcessSpec(name='Buying Vanilla, in General', template=tmpl["Procurement"], tags=['purchase::dry-goods'], notes='Purchasing vanilla'), tags=[], notes= 'Vanilla Extract is mostly alcohol but the most important component ' 'is vanillin (see attached structure)', properties=[ PropertyAndConditions( Property( name='Component Structure', value=InChI( "InChI=1S/C8H8O3/c1-11-8-4-6(5-9)2-3-7(8)10/h2-5,10H,1H3" ), template=tmpl["Molecular Structure"])) ]) IngredientSpec(**ingredient_kwargs(vanilla), labels=['wet', 'flavoring'], process=wetmix.process, absolute_quantity=NominalReal(nominal=2, units='teaspoons')) IngredientSpec( **ingredient_kwargs(vanilla), labels=['flavoring'], process=frosting.process, mass_fraction=NominalReal(nominal=0.0231, units='') # 2 tsp @ 0.879 g/cc ) milk = MaterialSpec(name="Abstract Milk", template=tmpl["Generic Material"], process=ProcessSpec(name='Buying Milk, in General', template=tmpl["Procurement"], tags=['purchase::produce'], notes='Purchasing milk'), tags=[], notes='') IngredientSpec(**ingredient_kwargs(milk), labels=['wet'], process=batter.process, absolute_quantity=NominalReal(nominal=1, units='cup')) IngredientSpec( **ingredient_kwargs(milk), labels=[], process=frosting.process, mass_fraction=NominalReal(nominal=0.0816, units='') # 1/4 c @ 1.037 g/cc ) chocolate = MaterialSpec(name="Abstract Chocolate", template=tmpl["Generic Material"], process=ProcessSpec( name='Buying Chocolate, in General', template=tmpl["Procurement"], tags=['purchase::dry-goods'], notes='Purchasing chocolate'), tags=[], notes='') IngredientSpec( **ingredient_kwargs(chocolate), labels=['flavoring'], process=frosting.process, mass_fraction=NominalReal(nominal=0.1132, units='') # 3 oz. ) powder_sugar = MaterialSpec( name="Abstract Powdered Sugar", template=tmpl["Generic Material"], process=ProcessSpec(name='Buying Powdered Sugar, in General', template=tmpl["Procurement"], tags=['purchase::dry-goods'], notes='Purchasing powdered sugar'), tags=[], notes='Granulated sugar mixed with corn starch') IngredientSpec( **ingredient_kwargs(powder_sugar), labels=['flavoring'], process=frosting.process, mass_fraction=NominalReal(nominal=0.6387, units='') # 4 c @ 30 g/ 0.25 cups ) # Crawl tree and annotate with uids; only add ids if there's nothing there recursive_foreach( cake, lambda obj: obj.uids or obj.add_uid(DEMO_SCOPE, obj.name)) return cake