def test_material_run(): """ Test the ability to create a MaterialRun that is linked to a MaterialSpec. Make sure all enumerated values are respected, and check consistency after serializing and deserializing. """ # Define a property, and make sure that an inappropriate value for origin throws ValueError with pytest.raises(ValueError): prop = Property(name="A property", origin="bad origin", value=NominalReal(17, units='')) # Create a MaterialSpec with a property prop = Property(name="A property", origin="specified", value=NominalReal(17, units='')) mat_spec = MaterialSpec( name="a specification for a material", properties=PropertyAndConditions(prop), notes="Funny lookin'" ) # Make sure that when property is serialized, origin (an enumeration) is serialized as a string copy_prop = json.loads(dumps(mat_spec)) copy_origin = copy_prop["context"][0]["properties"][0]['property']['origin'] assert isinstance(copy_origin, str) # Create a MaterialRun, and make sure an inappropriate value for sample_type throws ValueError with pytest.raises(ValueError): mat = MaterialRun("name", spec=mat_spec, sample_type="imaginary") mat = MaterialRun("name", spec=mat_spec, sample_type="virtual") # ensure that serialization does not change the MaterialRun copy = loads(dumps(mat)) assert dumps(copy) == dumps(mat), \ "Material run is modified by serialization or deserialization"
def test_contains(): """Make sure unit conversions are applied to bounds for contains.""" dim = RealBounds(lower_bound=0, upper_bound=100, default_units="degC") dim2 = RealBounds(lower_bound=33, upper_bound=200, default_units="degF") assert dim.contains(dim2) from gemd.entity.value import NominalReal assert dim.contains(NominalReal(5, 'degC')) assert not dim.contains(NominalReal(5, 'K'))
def test_template_validations(caplog): """Make sure template validations and level controls behave as expected.""" msr_tmpl = MeasurementTemplate( name="Measurement Template", properties=[PropertyTemplate("Name", bounds=RealBounds(0, 1, ""))], conditions=[ConditionTemplate("Name", bounds=RealBounds(0, 1, ""))], parameters=[ParameterTemplate("Name", bounds=RealBounds(0, 1, ""))], ) msr_spec = MeasurementSpec("Measurement Spec", template=msr_tmpl) msr_run = MeasurementRun("MeasurementRun", spec=msr_spec) with validation_level(WarningLevel.IGNORE): msr_run.properties.append(Property("Name", value=NominalReal(-1, ""))) msr_run.conditions.append(Condition("Name", value=NominalReal(-1, ""))) msr_run.parameters.append(Parameter("Name", value=NominalReal(-1, ""))) assert len(caplog.records) == 0, "Logging records wasn't empty" with validation_level(WarningLevel.WARNING): msr_run.properties.append(Property("Name", value=NominalReal(-1, ""))) assert len( caplog.records) == 1, "WARNING didn't warn on invalid Property." msr_run.conditions.append(Condition("Name", value=NominalReal(-1, ""))) assert len( caplog.records) == 2, "WARNING didn't warn on invalid Condition." msr_run.parameters.append(Parameter("Name", value=NominalReal(-1, ""))) assert len( caplog.records) == 3, "WARNING didn't warn on invalid Parameter." with validation_level(WarningLevel.FATAL): with pytest.raises(ValueError): msr_run.properties.append( Property("Name", value=NominalReal(-1, ""))) with pytest.raises(ValueError): msr_run.conditions.append( Condition("Name", value=NominalReal(-1, ""))) with pytest.raises(ValueError): msr_run.parameters.append( Parameter("Name", value=NominalReal(-1, "")))
def make_value(value: Union[str, float, int], bounds: BaseBounds) -> BaseValue: """ Generate a Value object based upon a number or string and a particular bounds. Parameters ---------- value: Union[str, float, int] The primitive type to wrap in a Value bounds: BaseBounds The bounds type to determine which value type we want to coerce the value into Returns -------- BaseValue The generated value """ if isinstance(bounds, RealBounds): result = NominalReal(value, units=bounds.default_units) elif isinstance(bounds, IntegerBounds): result = NominalInteger(value) elif isinstance(bounds, CategoricalBounds): result = NominalCategorical(value) elif isinstance(bounds, CompositionBounds): result = EmpiricalFormula(value) elif isinstance(bounds, MolecularStructureBounds): if str(value).startswith("InChI="): result = InChI(value) else: result = Smiles(value) else: raise ValueError(f"Unrecognized bound type in template {type(bounds)}") return result
def test_material_soft_link(): """Test that a measurement run can link to a material run, and that it survives serde.""" dye = MaterialRun("rhodamine", file_links=FileLink(filename='a.csv', url='/a/path')) assert dye.measurements == [], "default value of .measurements should be an empty list" # The .measurements member should not be settable with pytest.raises(AttributeError): dye.measurements = [MeasurementRun("Dummy")] absorbance = MeasurementRun(name="Absorbance", uids={'id': str(uuid4())}, properties=[ Property(name='Abs at 500 nm', value=NominalReal(0.1, '')) ]) assert absorbance.material is None, "Measurements should have None as the material by default" absorbance.material = dye assert absorbance.material == dye, "Material not set correctly for measurement" assert dye.measurements == [ absorbance ], "Soft-link from material to measurement not created" fluorescence = MeasurementRun(name="Fluorescence", uids={'id': str(uuid4())}, properties=[ Property(name='PL counts at 550 nm', value=NominalReal(30000, '')) ], material=dye) assert fluorescence.material == dye, "Material not set correctly for measurement" assert dye.measurements == [absorbance, fluorescence], \ "Soft-link from material to measurements not created" assert loads(dumps(absorbance)) == absorbance, \ "Measurement should remain unchanged when serialized" assert loads(dumps(fluorescence)) == fluorescence, \ "Measurement should remain unchanged when serialized" assert 'measurements' in repr(dye) assert 'material' in repr(fluorescence) assert 'material' in repr(absorbance) subbed = substitute_links(dye) assert 'measurements' in repr(subbed)
def test_invalid_assignment(): """Invalid assignments to `material` or `spec` throw a TypeError.""" with pytest.raises(TypeError): MeasurementRun("name", spec=Condition("value of pi", value=NominalReal(3.14159, ''))) with pytest.raises(TypeError): MeasurementRun("name", material=FileLink("filename", "url")) with pytest.raises(TypeError): MeasurementRun() # Name is required
def test_build(): """Test that build recreates the material.""" spec = MaterialSpec("A spec", properties=PropertyAndConditions( property=Property("a property", value=NominalReal(3, ''))), tags=["a tag"]) mat = MaterialRun(name="a material", spec=spec) mat_dict = mat.as_dict() mat_dict['spec'] = mat.spec.as_dict() assert MaterialRun.build(mat_dict) == mat
def test_quantities(): """Exercise the expressions on quantity in add_edge.""" ing_one = add_edge(make_node("Input"), make_node("Output"), mass_fraction=0.1, number_fraction=0.2, volume_fraction=0.3, absolute_quantity=0.4, absolute_units='kg') assert ing_one.mass_fraction.nominal == 0.1, "Mass fraction got set." assert ing_one.number_fraction.nominal == 0.2, "Number fraction got set." assert ing_one.volume_fraction.nominal == 0.3, "Volume fraction got set." assert ing_one.absolute_quantity.nominal == 0.4, "Absolute quantity got set." assert ing_one.absolute_quantity.units == parse_units( 'kg'), "Absolute units got set." ing_two = add_edge(make_node("Input"), make_node("Output"), mass_fraction=NominalReal(0.5, ''), number_fraction=NominalReal(0.6, ''), volume_fraction=NominalReal(0.7, ''), absolute_quantity=NominalReal(0.8, 'liters')) assert ing_two.mass_fraction.nominal == 0.5, "Mass fraction got set." assert ing_two.number_fraction.nominal == 0.6, "Number fraction got set." assert ing_two.volume_fraction.nominal == 0.7, "Volume fraction got set." assert ing_two.absolute_quantity.nominal == 0.8, "Absolute quantity got set." assert ing_two.absolute_quantity.units == parse_units( 'liters'), "Absolute units got set." with pytest.raises(ValueError): add_edge(make_node("Input"), make_node("Output"), absolute_quantity=0.4) with pytest.raises(ValueError): add_edge(make_node("Input"), make_node("Output"), absolute_quantity=NominalReal(0.8, 'liters'), absolute_units='liters')
def test_measurement_spec(): """Test the measurement spec/run connection survives ser/de.""" condition = Condition(name="Temp condition", value=NominalReal(nominal=298, units='kelvin')) parameter = Parameter(name="Important parameter") spec = MeasurementSpec(name="Precise way to do a measurement", parameters=parameter, conditions=condition) # Create a measurement run from this measurement spec measurement = MeasurementRun("The Measurement", conditions=condition, spec=spec) copy = loads(dumps(measurement)) assert dumps(copy.spec) == dumps(measurement.spec), \ "Measurement spec should be preserved if measurement run is serialized"
def test_equality(): """Test that equality check works as expected.""" spec = MaterialSpec("A spec", properties=PropertyAndConditions( property=Property("a property", value=NominalReal(3, ''))), tags=["a tag"]) mat1 = MaterialRun("A material", spec=spec) mat2 = MaterialRun("A material", spec=spec, tags=["A tag"]) assert mat1 == deepcopy(mat1) assert mat1 != mat2 assert mat1 != "A material" mat3 = deepcopy(mat1) assert mat1 == mat3, "Copy somehow failed" MeasurementRun("A measurement", material=mat3) assert mat1 != mat3 mat4 = deepcopy(mat3) assert mat4 == mat3, "Copy somehow failed" mat4.measurements[0].tags.append('A tag') assert mat4 != mat3 mat5 = next(x for x in flatten(mat4, 'test-scope') if isinstance(x, MaterialRun)) assert mat5 == mat4, "Flattening removes measurement references, but that's okay"
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(seed=None, tmpl=None, cake_spec=None, toothpick_img=None): """Define all objects that go into making a demo cake.""" import struct import hashlib if seed is not None: random.seed(seed) # Code to generate quasi-repeatable run annotations # Note there are potential machine dependencies md5 = hashlib.md5() for x in random.getstate()[1]: md5.update(struct.pack(">I", x)) run_key = md5.hexdigest() ###################################################################### # Parent Objects if tmpl is None: tmpl = make_cake_templates() if cake_spec is None: cake_spec = make_cake_spec(tmpl) ###################################################################### # Objects cake_obj = make_instance(cake_spec) operators = ['gwash', 'jadams', 'thomasj', 'jmadison', 'jmonroe'] producers = ['Fresh Farm', 'Sunnydale', 'Greenbrook'] drygoods = ['Acme', 'A1', 'Reliable', "Big Box"] cake_obj.process.source = PerformedSource( performed_by=random.choice(operators), performed_date='2015-03-14') def _randomize_object(item): # Add in the randomized particular values if not isinstance(item, (MaterialRun, ProcessRun, IngredientRun)): return item.add_uid(DEMO_SCOPE, '{}-{}'.format(item.spec.uids[DEMO_SCOPE], run_key)) if item.spec.tags is not None: item.tags = list(item.spec.tags) if item.spec.notes: # Neither None or empty string item.notes = 'The spec says "{}"'.format(item.spec.notes) if isinstance(item, MaterialRun): if 'raw material' in item.tags: if 'produce' in item.tags: supplier = random.choice(producers) else: supplier = random.choice(drygoods) item.name = "{} {}".format(supplier, item.spec.name) if isinstance(item, ProcessRun): if item.template.name == "Procuring": item.source = PerformedSource(performed_by='hamilton', performed_date='2015-02-17') item.name = "{} {}".format(item.template.name, item.output_material.name) else: item.source = cake_obj.process.source if isinstance(item, IngredientRun): fuzz = 0.95 + 0.1 * random.random() if item.spec.absolute_quantity is not None: item.absolute_quantity = \ NormalReal(mean=fuzz * item.spec.absolute_quantity.nominal, std=0.05 * item.spec.absolute_quantity.nominal, units=item.spec.absolute_quantity.units) if item.spec.volume_fraction is not None: # The only element here is dry mix, and it's almost entirely flour item.volume_fraction = \ NormalReal(mean=0.01 * (fuzz - 0.5) + item.spec.volume_fraction.nominal, std=0.005, units=item.spec.volume_fraction.units) if item.spec.mass_fraction is not None: item.mass_fraction = \ UniformReal(lower_bound=(fuzz - 0.05) * item.spec.mass_fraction.nominal, upper_bound=(fuzz + 0.05) * item.spec.mass_fraction.nominal, units=item.spec.mass_fraction.units) if item.spec.number_fraction is not None: item.number_fraction = \ NormalReal(mean=fuzz * item.spec.number_fraction.nominal, std=0.05 * item.spec.number_fraction.nominal, units=item.spec.number_fraction.units) recursive_foreach(cake_obj, _randomize_object) frosting = \ next(x.material for x in cake_obj.process.ingredients if 'rosting' in x.name) baked = \ next(x.material for x in cake_obj.process.ingredients if 'aked' in x.name) def _find_name(name, material): """Recursively search for the right material.""" if name in material.name: return material for ingredient in material.process.ingredients: result = _find_name(name, ingredient.material) if result: return result return flour = _find_name('Flour', cake_obj) salt = _find_name('Salt', cake_obj) sugar = _find_name('Sugar', cake_obj) # Add measurements cake_taste = MeasurementRun(name='Final Taste', material=cake_obj) cake_appearance = MeasurementRun(name='Final Appearance', material=cake_obj) frosting_taste = MeasurementRun(name='Frosting Taste', material=frosting) frosting_sweetness = MeasurementRun(name='Frosting Sweetness', material=frosting) baked_doneness = MeasurementRun(name='Baking doneness', material=baked) flour_content = MeasurementRun(name='Flour nutritional analysis', material=flour) salt_content = MeasurementRun(name='Salt elemental analysis', material=salt) sugar_content = MeasurementRun(name='Sugar elemental analysis', material=sugar) if toothpick_img is not None: baked_doneness.file_links.append(toothpick_img) # and spec out the measurements cake_taste.spec = MeasurementSpec(name='Taste', template=tmpl['Taste test']) cake_appearance.spec = MeasurementSpec(name='Appearance') frosting_taste.spec = cake_taste.spec # Taste frosting_sweetness.spec = MeasurementSpec(name='Sweetness') baked_doneness.spec = MeasurementSpec(name='Doneness', template=tmpl["Doneness"]) flour_content.spec = MeasurementSpec(name='Nutritional analysis', template=tmpl["Nutritional Analysis"]) salt_content.spec = MeasurementSpec(name='Elemental analysis', template=tmpl["Elemental Analysis"]) sugar_content.spec = salt_content.spec # Note that while specs are regenerated each make_cake invocation, they are all identical for msr in (cake_taste, cake_appearance, frosting_taste, frosting_sweetness, baked_doneness, flour_content, salt_content, sugar_content): msr.spec.add_uid(DEMO_SCOPE, msr.spec.name.lower()) msr.add_uid( DEMO_SCOPE, '{}--{}-{}'.format(msr.spec.uids[DEMO_SCOPE], msr.material.spec.uids[DEMO_SCOPE], run_key)) ###################################################################### # Let's add some attributes baked.process.conditions.append( Condition(name='Cooking time', template=tmpl['Cooking time'], origin=Origin.MEASURED, value=NominalReal(nominal=48, units='min'))) baked.process.conditions.append( Condition(name='Oven temperature', origin="measured", value=NominalReal(nominal=362, units='degF'))) cake_taste.properties.append( Property(name='Tastiness', origin=Origin.MEASURED, template=tmpl['Tastiness'], value=UniformInteger(4, 5))) cake_appearance.properties.append( Property(name='Visual Appeal', origin=Origin.MEASURED, value=NominalInteger(nominal=5))) frosting_taste.properties.append( Property(name='Tastiness', origin=Origin.MEASURED, template=tmpl['Tastiness'], value=NominalInteger(nominal=4))) frosting_sweetness.properties.append( Property(name='Sweetness (Sucrose-basis)', origin=Origin.MEASURED, value=NominalReal(nominal=1.7, units=''))) baked_doneness.properties.append( Property(name='Toothpick test', origin="measured", template=tmpl["Toothpick test"], value=NominalCategorical("crumbs"))) baked_doneness.properties.append( Property(name='Color', origin="measured", template=tmpl["Color"], value=DiscreteCategorical({ "Pale": 0.05, "Golden brown": 0.65, "Deep brown": 0.3 }))) flour_content.properties.append( Property(name='Nutritional Information', value=NominalComposition({ "dietary-fiber": 1 * (0.99 + 0.02 * random.random()), "sugars": 1 * (0.99 + 0.02 * random.random()), "other-carbohydrate": 20 * (0.99 + 0.02 * random.random()), "protein": 4 * (0.99 + 0.02 * random.random()), "other": 4 * (0.99 + 0.02 * random.random()) }), template=tmpl["Nutritional Information"], origin="measured")) flour_content.conditions.append( Condition(name='Sample Mass', value=NormalReal(mean=99 + 2 * random.random(), std=1.5, units='mg'), template=tmpl["Sample Mass"], origin="measured")) flour_content.parameters.append( Parameter(name='Expected Sample Mass', value=NominalReal(nominal=0.1, units='g'), template=tmpl["Expected Sample Mass"], origin="specified")) flour_content.spec.conditions.append( Condition(name='Sample Mass', value=NominalReal(nominal=100, units='mg'), template=tmpl["Sample Mass"], origin="specified")) flour_content.spec.parameters.append( Parameter(name='Expected Sample Mass', value=NominalReal(nominal=0.1, units='g'), template=tmpl["Expected Sample Mass"], origin="specified")) salt_content.properties.append( Property(name="Composition", value=EmpiricalFormula( formula="NaClCa0.006Si0.006O0.018K0.000015I0.000015"), template=tmpl["Chemical Formula"], origin="measured")) salt_content.conditions.append( Condition(name='Sample Mass', value=NormalReal(mean=99 + 2 * random.random(), std=1.5, units='mg'), template=tmpl["Sample Mass"], origin="measured")) salt_content.parameters.append( Parameter(name='Expected Sample Mass', value=NominalReal(nominal=0.1, units='g'), template=tmpl["Expected Sample Mass"], origin="specified")) salt_content.spec.conditions.append( Condition(name='Sample Mass', value=NominalReal(nominal=100, units='mg'), template=tmpl["Sample Mass"], origin="specified")) sugar_content.properties.append( Property( name="Composition", value=EmpiricalFormula(formula='C11.996H21.995O10.997S0.00015'), template=tmpl["Chemical Formula"], origin="measured")) sugar_content.conditions.append( Condition(name='Sample Mass', value=NormalReal(mean=99 + 2 * random.random(), std=1.5, units='mg'), template=tmpl["Sample Mass"], origin="measured")) sugar_content.spec.parameters.append( Parameter(name='Expected Sample Mass', value=NominalReal(nominal=0.1, units='g'), template=tmpl["Expected Sample Mass"], origin="specified")) cake_obj.notes = cake_obj.notes + "; Très délicieux! 😀" cake_obj.file_links = [ FileLink( filename="Photo", url='https://storcpdkenticomedia.blob.core.windows.net/media/' 'recipemanagementsystem/media/recipe-media-files/recipes/retail/x17/' '16730-beckys-butter-cake-600x600.jpg?ext=.jpg') ] return cake_obj
def make_cake_spec(tmpl=None): """Define a recipe for making a cake.""" ############################################################################################### # Templates if tmpl is None: tmpl = make_cake_templates() 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 _make_material(*, material_name, template, process_tmpl_name, process_kwargs, **material_kwargs): """Convenience method to reuse material name in creating a material's arguments.""" process_name = "{} {}".format(process_tmpl_name, material_name) return MaterialSpec( name=material_name, uids={DEMO_SCOPE: material_name.lower().replace(' ', '-')}, template=template, process=ProcessSpec( name=process_name, uids={DEMO_SCOPE: process_name.lower().replace(' ', '-')}, template=tmpl[process_tmpl_name], **process_kwargs), **material_kwargs) ############################################################################################### # Objects cake_obj = _make_material( material_name="Cake", process_tmpl_name="Icing", process_kwargs={ "tags": ['spreading'], "notes": 'The act of covering a baked output with frosting' }, template=tmpl["Dessert"], 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 = _make_material( material_name="Frosting", process_tmpl_name="Mixing", process_kwargs={ "tags": ['mixing'], "parameters": [ Parameter(name='Mixer speed setting', template=tmpl['Mixer speed setting'], origin='specified', value=NominalInteger(2)) ], "notes": 'Combining ingredients to make a sweet frosting' }, template=tmpl["Dessert"], tags=['frosting::chocolate', 'topping::chocolate'], notes='Chocolate frosting') _make_ingredient(material=frosting, notes='Seems like a lot of frosting', labels=['coating'], process=cake_obj.process, absolute_quantity=NominalReal(nominal=0.751, units='kg')) baked_cake = _make_material( material_name="Baked Cake", process_tmpl_name="Baking", process_kwargs={ "tags": ['oven::baking'], "conditions": [ Condition(name='Cooking time', template=tmpl['Cooking time'], origin=Origin.SPECIFIED, value=NormalReal(mean=50, std=5, units='min')) ], "parameters": [ Parameter(name='Oven temperature setting', template=tmpl['Oven temperature setting'], origin="specified", value=NominalReal(nominal=350, units='degF')) ], "notes": 'Using heat to convert batter into a solid matrix' }, template=tmpl["Baked Good"], properties=[ PropertyAndConditions( property=Property(name="Toothpick test", value=NominalCategorical("completely clean"), template=tmpl["Toothpick test"])), PropertyAndConditions( property=Property(name="Color", value=NominalCategorical("Golden brown"), template=tmpl["Color"], origin="specified")) ], tags=['substrate'], notes='The cakey part of the cake') _make_ingredient(material=baked_cake, labels=['substrate'], process=cake_obj.process) ######################## batter = _make_material( material_name="Batter", process_tmpl_name="Mixing", process_kwargs={ "tags": ['mixing'], "parameters": [ Parameter(name='Mixer speed setting', template=tmpl['Mixer speed setting'], origin='specified', value=NominalInteger(2)) ], "notes": 'Combining ingredients to make a baking feedstock' }, template=tmpl["Generic Material"], tags=['mixture'], notes='The fluid that converts to cake with heat') _make_ingredient(material=batter, labels=['precursor'], process=baked_cake.process) ######################## wetmix = _make_material( material_name="Wet Ingredients", process_tmpl_name="Mixing", process_kwargs={ "tags": ['mixing'], "parameters": [ Parameter(name='Mixer speed setting', template=tmpl['Mixer speed setting'], origin='specified', value=NominalInteger(2)) ], "notes": 'Combining wet ingredients to make a baking feedstock' }, template=tmpl["Generic Material"], tags=["mixture"], notes='The wet fraction of a batter') _make_ingredient(material=wetmix, labels=['wet'], process=batter.process) drymix = _make_material( material_name="Dry Ingredients", process_tmpl_name="Mixing", process_kwargs={ "tags": ['mixing'], "notes": 'Combining dry ingredients to make a baking feedstock' }, template=tmpl["Generic Material"], tags=["mixture"], notes='The dry fraction of a batter') _make_ingredient(material=drymix, labels=['dry'], process=batter.process, absolute_quantity=NominalReal(nominal=3.052, units='cups')) ######################## flour = _make_material( material_name="Flour", process_tmpl_name="Procuring", process_kwargs={ "tags": ['purchase::dry-goods'], "notes": 'Purchasing all purpose 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")) ], tags=['raw material', 'flour', 'dry-goods'], notes='All-purpose flour') _make_ingredient( material=flour, labels=['dry'], process=drymix.process, volume_fraction=NominalReal(nominal=0.9829, units='') # 3 cups ) baking_powder = _make_material( material_name="Baking Powder", process_tmpl_name="Procuring", process_kwargs={ "tags": ['purchase::dry-goods'], "notes": 'Purchasing baking powder' }, template=tmpl["Generic Material"], tags=['raw material', 'leavening', 'dry-goods'], notes='Leavening agent for cake') _make_ingredient( material=baking_powder, labels=['leavening', 'dry'], process=drymix.process, volume_fraction=NominalReal(nominal=0.0137, units='') # 2 teaspoons ) salt = _make_material(material_name="Salt", process_tmpl_name="Procuring", process_kwargs={ "tags": ['purchase::dry-goods'], "notes": 'Purchasing salt' }, template=tmpl["Formulaic Material"], tags=['raw material', 'seasoning', 'dry-goods'], notes='Plain old NaCl', properties=[ PropertyAndConditions( Property(name='Formula', value=EmpiricalFormula("NaCl"))) ]) _make_ingredient( material=salt, labels=['dry', 'seasoning'], process=drymix.process, volume_fraction=NominalReal(nominal=0.0034, units='') # 1/2 teaspoon ) sugar = _make_material( material_name="Sugar", process_tmpl_name="Procuring", process_kwargs={ "tags": ['purchase::dry-goods'], "notes": 'Purchasing granulated sugar' }, template=tmpl["Formulaic Material"], tags=['raw material', 'sweetener', 'dry-goods'], 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"])) ]) _make_ingredient(material=sugar, labels=['wet', 'sweetener'], process=wetmix.process, absolute_quantity=NominalReal(nominal=2, units='cups')) butter = _make_material( material_name="Butter", process_tmpl_name="Procuring", process_kwargs={ "tags": ['purchase::produce'], "notes": 'Purchasing butter' }, template=tmpl["Generic Material"], tags=['raw material', 'produce', 'shortening', 'dairy'], notes='Shortening for making rich, buttery baked goods') _make_ingredient(material=butter, labels=['wet', 'shortening'], process=wetmix.process, absolute_quantity=NominalReal(nominal=1, units='cups')) _make_ingredient( material=butter, labels=['shortening'], process=frosting.process, mass_fraction=NominalReal(nominal=0.1434, units='') # 1/2 c @ 0.911 g/cc ) eggs = _make_material(material_name="Eggs", process_tmpl_name="Procuring", process_kwargs={ "tags": ['purchase::produce'], "notes": 'Purchasing eggs' }, template=tmpl["Generic Material"], tags=[ 'raw material', 'produce', ], notes='A custard waiting to happen') _make_ingredient(material=eggs, labels=['wet'], process=wetmix.process, absolute_quantity=NominalReal(nominal=4, units='')) vanilla = _make_material( material_name="Vanilla", process_tmpl_name="Procuring", process_kwargs={ "tags": ['purchase::solution'], "notes": 'Purchasing vanilla' }, template=tmpl["Generic Material"], tags=['raw material', 'seasoning'], 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"])) ]) _make_ingredient(material=vanilla, labels=['wet', 'flavoring'], process=wetmix.process, absolute_quantity=NominalReal(nominal=2, units='teaspoons')) _make_ingredient( material=vanilla, labels=['flavoring'], process=frosting.process, mass_fraction=NominalReal(nominal=0.0231, units='') # 2 tsp @ 0.879 g/cc ) milk = _make_material(material_name="Milk", process_tmpl_name="Procuring", process_kwargs={ "tags": ['purchase::produce'], "notes": 'Purchasing milk' }, template=tmpl["Generic Material"], tags=['raw material', 'produce', 'dairy'], notes='') _make_ingredient(material=milk, labels=['wet'], process=batter.process, absolute_quantity=NominalReal(nominal=1, units='cup')) _make_ingredient( material=milk, labels=[], process=frosting.process, mass_fraction=NominalReal(nominal=0.0816, units='') # 1/4 c @ 1.037 g/cc ) chocolate = _make_material(material_name="Chocolate", process_tmpl_name="Procuring", process_kwargs={ "tags": ['purchase::dry-goods'], "notes": 'Purchasing chocolate' }, template=tmpl["Generic Material"], tags=['raw material'], notes='') _make_ingredient( material=chocolate, labels=['flavoring'], process=frosting.process, mass_fraction=NominalReal(nominal=0.1132, units='') # 3 oz. ) powder_sugar = _make_material( material_name="Powdered Sugar", process_tmpl_name="Procuring", process_kwargs={ "tags": ['purchase::dry-goods'], "notes": 'Purchasing powdered sugar' }, template=tmpl["Generic Material"], tags=['raw material', 'sweetener', 'dry-goods'], notes='Granulated sugar mixed with corn starch') _make_ingredient( material=powder_sugar, labels=['flavoring'], process=frosting.process, mass_fraction=NominalReal(nominal=0.6387, units='') # 4 c @ 30 g/ 0.25 cups ) return cake_obj
def make_cake(seed=None, tmpl=None, cake_spec=None): """Define all objects that go into making a demo cake.""" import struct import hashlib if seed is not None: random.seed(seed) ###################################################################### # Parent Objects if tmpl is None: tmpl = make_cake_templates() if cake_spec is None: cake_spec = make_cake_spec(tmpl) ###################################################################### # Objects cake = make_instance(cake_spec) operators = ['gwash', 'jadams', 'thomasj', 'jmadison', 'jmonroe'] cake.process.source = PerformedSource( performed_by=random.choice(operators), performed_date='2015-03-14') # Replace Abstract/In General queue = [cake] while queue: item = queue.pop(0) if item.spec.tags is not None: item.tags = list(item.spec.tags) if item.spec.notes: # None or empty string item.notes = 'The spec says "{}"'.format(item.spec.notes) if isinstance(item, MaterialRun): item.name = item.name.replace('Abstract ', '') queue.append(item.process) elif isinstance(item, ProcessRun): item.name = item.name.replace(', in General', '') queue.extend(item.ingredients) if item.template.name == "Procurement": item.source = PerformedSource(performed_by='hamilton', performed_date='2015-02-17') else: item.source = cake.process.source elif isinstance(item, IngredientRun): queue.append(item.material) fuzz = 0.95 + 0.1 * random.random() if item.spec.absolute_quantity is not None: item.absolute_quantity = \ NormalReal(mean=fuzz * item.spec.absolute_quantity.nominal, std=0.05 * item.spec.absolute_quantity.nominal, units=item.spec.absolute_quantity.units) if item.spec.volume_fraction is not None: # The only element here is dry mix, and it's almost entirely flour item.volume_fraction = \ NormalReal(mean=0.01 * (fuzz - 0.5) + item.spec.volume_fraction.nominal, std=0.005, units=item.spec.volume_fraction.units) if item.spec.mass_fraction is not None: item.mass_fraction = \ UniformReal(lower_bound=(fuzz - 0.05) * item.spec.mass_fraction.nominal, upper_bound=(fuzz + 0.05) * item.spec.mass_fraction.nominal, units=item.spec.mass_fraction.units) if item.spec.number_fraction is not None: item.number_fraction = \ NormalReal(mean=fuzz * item.spec.number_fraction.nominal, std=0.05 * item.spec.number_fraction.nominal, units=item.spec.number_fraction.units) else: raise TypeError("Unexpected object in the queue") frosting = \ next(x.material for x in cake.process.ingredients if 'rosting' in x.name) baked = \ next(x.material for x in cake.process.ingredients if 'aked' in x.name) def find_name(name, material): # Recursively search for the right material if name == material.name: return material for ingredient in material.process.ingredients: result = find_name(name, ingredient.material) if result: return result return flour = find_name('Flour', cake) salt = find_name('Salt', cake) sugar = find_name('Sugar', cake) # Add measurements cake_taste = MeasurementRun(name='Final Taste', material=cake) cake_appearance = MeasurementRun(name='Final Appearance', material=cake) frosting_taste = MeasurementRun(name='Frosting Taste', material=frosting) frosting_sweetness = MeasurementRun(name='Frosting Sweetness', material=frosting) baked_doneness = MeasurementRun(name='Baking doneness', material=baked) flour_content = MeasurementRun(name='Flour nutritional analysis', material=flour) salt_content = MeasurementRun(name='Salt elemental analysis', material=salt) sugar_content = MeasurementRun(name='Sugar elemental analysis', material=sugar) # and spec out the measurements cake_taste.spec = MeasurementSpec(name='Taste', template=tmpl['Taste test']) cake_appearance.spec = MeasurementSpec(name='Appearance') frosting_taste.spec = cake_taste.spec # Taste frosting_sweetness.spec = MeasurementSpec(name='Sweetness') baked_doneness.spec = MeasurementSpec(name='Doneness', template=tmpl["Doneness"]) flour_content.spec = MeasurementSpec(name='Nutritional analysis', template=tmpl["Nutritional Analysis"]) salt_content.spec = MeasurementSpec(name='Elemental analysis', template=tmpl["Elemental Analysis"]) sugar_content.spec = salt_content.spec for msr in (cake_taste, cake_appearance, frosting_taste, frosting_sweetness, baked_doneness, flour_content, salt_content, sugar_content): msr.spec.add_uid(DEMO_SCOPE, msr.spec.name) ###################################################################### # Let's add some attributes baked.process.conditions.append( Condition(name='Cooking time', template=tmpl['Cooking time'], origin=Origin.MEASURED, value=NominalReal(nominal=48, units='min'))) baked.spec.process.conditions.append( Condition(name='Cooking time', template=tmpl['Cooking time'], origin=Origin.SPECIFIED, value=NormalReal(mean=50, std=5, units='min'))) baked.process.conditions.append( Condition(name='Oven temperature', origin="measured", value=NominalReal(nominal=362, units='degF'))) baked.spec.process.parameters.append( Parameter(name='Oven temperature setting', template=tmpl['Oven temperature setting'], origin="specified", value=NominalReal(nominal=350, units='degF'))) cake_taste.properties.append( Property(name='Tastiness', origin=Origin.MEASURED, template=tmpl['Tastiness'], value=UniformInteger(4, 5))) cake_appearance.properties.append( Property(name='Visual Appeal', origin=Origin.MEASURED, value=NominalInteger(nominal=5))) frosting_taste.properties.append( Property(name='Tastiness', origin=Origin.MEASURED, template=tmpl['Tastiness'], value=NominalInteger(nominal=4))) frosting_sweetness.properties.append( Property(name='Sweetness (Sucrose-basis)', origin=Origin.MEASURED, value=NominalReal(nominal=1.7, units=''))) baked_doneness.properties.append( Property(name='Toothpick test', origin="measured", template=tmpl["Toothpick test"], value=NominalCategorical("crumbs"))) baked_doneness.properties.append( Property(name='Color', origin="measured", template=tmpl["Color"], value=DiscreteCategorical({ "Pale": 0.05, "Golden brown": 0.65, "Deep brown": 0.3 }))) flour_content.properties.append( Property(name='Nutritional Information', value=NominalComposition({ "dietary-fiber": 1 * (0.99 + 0.02 * random.random()), "sugars": 1 * (0.99 + 0.02 * random.random()), "other-carbohydrate": 20 * (0.99 + 0.02 * random.random()), "protein": 4 * (0.99 + 0.02 * random.random()), "other": 4 * (0.99 + 0.02 * random.random()) }), template=tmpl["Nutritional Information"], origin="measured")) flour_content.conditions.append( Condition(name='Sample Mass', value=NormalReal(mean=99 + 2 * random.random(), std=1.5, units='mg'), template=tmpl["Sample Mass"], origin="measured")) flour_content.parameters.append( Parameter(name='Expected Sample Mass', value=NominalReal(nominal=0.1, units='g'), template=tmpl["Expected Sample Mass"], origin="specified")) flour_content.spec.conditions.append( Condition(name='Sample Mass', value=NominalReal(nominal=100, units='mg'), template=tmpl["Sample Mass"], origin="specified")) flour_content.spec.parameters.append( Parameter(name='Expected Sample Mass', value=NominalReal(nominal=0.1, units='g'), template=tmpl["Expected Sample Mass"], origin="specified")) salt_content.properties.append( Property(name="Composition", value=EmpiricalFormula( formula="NaClCa0.006Si0.006O0.018K0.000015I0.000015"), template=tmpl["Chemical Formula"], origin="measured")) salt_content.conditions.append( Condition(name='Sample Mass', value=NormalReal(mean=99 + 2 * random.random(), std=1.5, units='mg'), template=tmpl["Sample Mass"], origin="measured")) salt_content.parameters.append( Parameter(name='Expected Sample Mass', value=NominalReal(nominal=0.1, units='g'), template=tmpl["Expected Sample Mass"], origin="specified")) salt_content.spec.conditions.append( Condition(name='Sample Mass', value=NominalReal(nominal=100, units='mg'), template=tmpl["Sample Mass"], origin="specified")) sugar_content.properties.append( Property( name="Composition", value=EmpiricalFormula(formula='C11.996H21.995O10.997S0.00015'), template=tmpl["Chemical Formula"], origin="measured")) sugar_content.conditions.append( Condition(name='Sample Mass', value=NormalReal(mean=99 + 2 * random.random(), std=1.5, units='mg'), template=tmpl["Sample Mass"], origin="measured")) sugar_content.spec.parameters.append( Parameter(name='Expected Sample Mass', value=NominalReal(nominal=0.1, units='g'), template=tmpl["Expected Sample Mass"], origin="specified")) # Code to generate quasi-repeatable run annotations # Note there are potential machine dependencies md5 = hashlib.md5() for x in random.getstate()[1]: md5.update(struct.pack(">I", x)) run_key = md5.hexdigest() # 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 + run_key)) cake.notes = cake.notes + "; Très délicieux! 😀" cake.file_links = [ FileLink( filename="Photo", url='https://www.landolakes.com/RecipeManagementSystem/media/' 'Recipe-Media-Files/Recipes/Retail/x17/16730-beckys-butter-cake-600x600.jpg?ext=.jpg' ) ] return cake
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