Esempio n. 1
0
def test_circular_crawl():
    """Test that make_instance can handle a circular set of linked objects."""
    proc = ProcessSpec("process name")
    mat = MaterialSpec("material name", process=proc)
    IngredientSpec(name="ingredient name", material=mat, process=proc)
    mat_run = make_instance(mat)
    assert mat_run == mat_run.process.ingredients[0].material
Esempio n. 2
0
def test_make_instance():
    """Build up several linked objects and test their properties."""
    msr_spec = MeasurementSpec()
    assert isinstance(make_instance(msr_spec), MeasurementRun)

    mat_spec = MaterialSpec(name='Mat name')
    mat_spec.process = ProcessSpec(name='Pro name')
    IngredientSpec(name='Ing label', process=mat_spec.process)
    mat_spec.process.ingredients[0].material = MaterialSpec(name='Baby mat name')

    mat_run = make_instance(mat_spec)
    assert isinstance(mat_run, MaterialRun)
    assert isinstance(mat_run.process, ProcessRun)
    assert isinstance(mat_run.process.ingredients[0], IngredientRun)
    assert isinstance(mat_run.process.ingredients[0].material, MaterialRun)

    assert mat_run.process.spec == mat_run.spec.process
    ingredient = mat_run.process.ingredients[0]
    assert ingredient.spec == mat_run.spec.process.ingredients[0]
    assert ingredient.material.spec == mat_run.spec.process.ingredients[0].material
Esempio n. 3
0
def make_strehlow_objects(table=None, template_scope=DEMO_TEMPLATE_SCOPE):
    """Make a table with Strehlow & Cook data."""
    tmpl = make_templates(template_scope)

    if table is None:
        table = import_table()

    # Specs
    msr_spec = MeasurementSpec(name='Band gap',
                               template=tmpl["Band gap measurement"]
                               )

    def real_mapper(prop):
        """Mapping methods for RealBounds."""
        if 'uncertainty' in prop['scalars'][0]:
            if prop['units'] == 'eV':  # Arbitrarily convert to attojoules
                mean = convert_units(value=float(prop['scalars'][0]['value']),
                                     starting_unit=prop['units'],
                                     final_unit='aJ'
                                     )
                std = convert_units(value=float(prop['scalars'][0]['value']),
                                    starting_unit=prop['units'],
                                    final_unit='aJ'
                                    )
                val = NormalReal(mean=mean,
                                 units='aJ',
                                 std=std
                                 )
            else:
                val = NormalReal(mean=float(prop['scalars'][0]['value']),
                                 units=prop['units'],
                                 std=float(prop['scalars'][0]['uncertainty'])
                                 )
        else:
            val = NominalReal(nominal=float(prop['scalars'][0]['value']),
                              units=prop['units']
                              )
        return val

    content_map = {
        RealBounds: real_mapper,
        CategoricalBounds: lambda prop: NominalCategorical(category=prop['scalars'][0]['value']),
        type(None): lambda bnd: 'Label'
    }

    datapoints = []
    compounds = dict()
    for row in table:
        formula = formula_clean(row['chemicalFormula'])
        if formula not in compounds:
            compounds[formula] = MaterialSpec(
                name=formula_latex(formula),
                template=tmpl["Chemical"],
                process=ProcessSpec(name="Sample preparation",
                                    template=tmpl["Sample preparation"]
                                    ))
        spec = compounds[formula]
        run = make_instance(spec)
        datapoints.append(run)

        if not spec.properties:
            spec.properties.append(
                PropertyAndConditions(
                    property=Property(name=spec.template.properties[0][0].name,
                                      value=EmpiricalFormula(formula=formula),
                                      template=spec.template.properties[0][0])
                ))

        msr = make_instance(msr_spec)
        msr.material = run

        # 2 categories in the PIF need to be split to avoid repeat Attribute Templates in a Run
        name_map = {
            'Phase': 'Crystal system',
            'Transition': 'Bands'
        }
        origin_map = {
            'EXPERIMENTAL': Origin.MEASURED,
            'COMPUTATIONAL': Origin.COMPUTED
        }
        seen = set()  # Some conditions come in from multiple properties on the same object
        for prop in row['properties']:
            origin = origin_map.get(prop.get('dataType', None), Origin.UNKNOWN)
            if 'method' in prop:
                method = 'Method: ' + prop['method']['name']
            else:
                method = 'Method: unreported'
            for attr in [prop] + prop.get('conditions', []):
                if attr['name'] in seen:
                    # Early return if it's a repeat
                    continue
                seen.add(attr['name'])

                template = tmpl[attr['name']]
                # Figure out if we need to split this column
                if attr['name'] in name_map:
                    value = attr['scalars'][0]['value']
                    if value not in template.bounds.categories:
                        template = tmpl[name_map[attr['name']]]

                # Move into GEMD structure
                if type(template) == PropertyTemplate:
                    msr.properties.append(
                        Property(name=template.name,
                                 template=template,
                                 value=content_map[type(template.bounds)](attr),
                                 origin=origin,
                                 notes=method
                                 ))
                elif type(template) == ConditionTemplate:
                    msr.conditions.append(
                        Condition(name=template.name,
                                  template=template,
                                  value=content_map[type(template.bounds)](attr),
                                  origin=origin,
                                  notes=method
                                  ))

    return datapoints
Esempio n. 4
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
Esempio n. 5
0
def test_invalid_instance():
    """Calling make_instance on a non-spec should throw a TypeError."""
    not_specs = [MeasurementRun("meas"), Condition("cond"), UniformReal(0, 1, ''), 'foo', 10]
    for not_spec in not_specs:
        with pytest.raises(TypeError):
            make_instance(not_spec)
Esempio n. 6
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