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'))
Example #3
0
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, "")))
Example #4
0
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
Example #5
0
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)
Example #6
0
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')
Example #9
0
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"
Example #11
0
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
Example #12
0
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
Example #13
0
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
Example #14
0
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
Example #15
0
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