Пример #1
0
def test_dependencies():
    """Test that dependency lists make sense."""
    prop = PropertyTemplate(name="name", bounds=IntegerBounds(0, 1))
    cond = ConditionTemplate(name="name", bounds=IntegerBounds(0, 1))
    param = ParameterTemplate(name="name", bounds=IntegerBounds(0, 1))

    msr_template = MeasurementTemplate("a process template",
                                       conditions=[cond],
                                       properties=[prop],
                                       parameters=[param])
    assert prop in msr_template.all_dependencies()
    assert cond in msr_template.all_dependencies()
    assert param in msr_template.all_dependencies()
def test_passthrough_bounds():
    """Test that unspecified Bounds are accepted and set to None."""
    template = ProcessTemplate('foo',
                               conditions=[
                                   (LinkByUID('1', '2'), None),
                                   [LinkByUID('3', '4'), None],
                                   LinkByUID('5', '6'),
                                   ConditionTemplate('foo',
                                                     bounds=IntegerBounds(
                                                         0, 10)),
                               ])
    assert len(template.conditions) == 4
    for _, bounds in template.conditions:
        assert bounds is None
    copied = loads(dumps(template))
    assert len(copied.conditions) == 4
    for _, bounds in copied.conditions:
        assert bounds is None
    from_dict = ProcessTemplate.build({
        'type':
        'process_template',
        'name':
        'foo',
        'conditions': [[
            {
                'scope': 'foo',
                'id': 'bar',
                'type': 'link_by_uid',
            },
            None,
        ]],
    })
    assert len(from_dict.conditions) == 1
def test_attributes():
    """Exercise permutations of attributes, bounds and values."""
    prop_tmpl = PropertyTemplate(name="Property",
                                 bounds=RealBounds(0, 10, "m"))
    cond_tmpl = ConditionTemplate(name="Condition",
                                  bounds=CategoricalBounds(["a", "b", "c"]))
    param_tmpl = ParameterTemplate(name="Parameter",
                                   bounds=CompositionBounds(
                                       EmpiricalFormula.all_elements()))
    mol_tmpl = PropertyTemplate(name="Molecule",
                                bounds=MolecularStructureBounds())
    int_tmpl = ConditionTemplate(name="Integer", bounds=IntegerBounds(0, 10))

    msr = add_measurement(make_node('Material'),
                          name='Measurement',
                          attributes=[
                              make_attribute(prop_tmpl, 5),
                              make_attribute(cond_tmpl, 'a'),
                              make_attribute(param_tmpl, 'SiC'),
                              make_attribute(mol_tmpl, 'InChI=1S/CSi/c1-2'),
                              make_attribute(mol_tmpl, '[C-]#[Si+]'),
                              make_attribute(int_tmpl, 5)
                          ])
    assert msr.properties[0].value.nominal == 5
    assert msr.conditions[0].value.category == 'a'
    assert msr.parameters[0].value.formula == 'SiC'
    assert msr.properties[1].value.inchi == 'InChI=1S/CSi/c1-2'
    assert msr.properties[2].value.smiles == '[C-]#[Si+]'
    assert msr.conditions[1].value.nominal == 5
def test_dependencies():
    """Test that dependency lists make sense."""
    prop = PropertyTemplate(name="name", bounds=IntegerBounds(0, 1))
    cond = ConditionTemplate(name="name", bounds=IntegerBounds(0, 1))

    template = MaterialTemplate("measurement template")
    spec = MaterialSpec("A spec", template=template,
                        properties=[PropertyAndConditions(
                            property=Property("name", template=prop, value=NominalInteger(1)),
                            conditions=[
                                Condition("name", template=cond, value=NominalInteger(1))
                            ]
                        )])

    assert template in spec.all_dependencies()
    assert cond in spec.all_dependencies()
    assert prop in spec.all_dependencies()
Пример #5
0
    def _to_bounds(self) -> IntegerBounds:
        """
        Return the smallest bounds object that is consistent with the Value.

        Returns
        -------
        IntegerBounds
            The minimally consistent
            :class:`bounds <gemd.entity.bounds.integer_bounds.IntegerBounds>`.

        """
        return IntegerBounds(lower_bound=self.lower_bound,
                             upper_bound=self.upper_bound)
Пример #6
0
def test_dependencies():
    """Test that dependency lists make sense."""
    prop = PropertyTemplate(name="name", bounds=IntegerBounds(0, 1))
    cond = ConditionTemplate(name="name", bounds=IntegerBounds(0, 1))
    param = ParameterTemplate(name="name", bounds=IntegerBounds(0, 1))

    template = MeasurementTemplate("measurement template",
                                   parameters=[param],
                                   conditions=[cond],
                                   properties=[prop])
    spec = MeasurementSpec("A spec", template=template)
    mat = MaterialRun(name="mr")
    meas = MeasurementRun("A run",
                          spec=spec,
                          material=mat,
                          properties=[
                              Property(prop.name,
                                       template=prop,
                                       value=NominalInteger(1))
                          ],
                          conditions=[
                              Condition(cond.name,
                                        template=cond,
                                        value=NominalInteger(1))
                          ],
                          parameters=[
                              Parameter(param.name,
                                        template=param,
                                        value=NominalInteger(1))
                          ])

    assert template not in meas.all_dependencies()
    assert spec in meas.all_dependencies()
    assert mat in meas.all_dependencies()
    assert prop in meas.all_dependencies()
    assert cond in meas.all_dependencies()
    assert param in meas.all_dependencies()
def test_mat_spec_properties(caplog):
    """Make sure template validations and level controls behave as expected."""
    prop_tmpl = PropertyTemplate("Name", bounds=IntegerBounds(0, 2))
    cond_tmpl = ConditionTemplate("Name", bounds=IntegerBounds(0, 2))
    mat_tmpl = MaterialTemplate("Material Template", properties=[[prop_tmpl, IntegerBounds(0, 1)]])
    mat_spec = MaterialSpec("Material Spec", template=mat_tmpl)
    good_prop = PropertyAndConditions(
        property=Property("Name", value=NominalInteger(1), template=prop_tmpl),
        conditions=[Condition("Name", value=NominalInteger(1), template=cond_tmpl)]
    )
    bad_prop = PropertyAndConditions(
        property=Property("Name", value=NominalInteger(2), template=prop_tmpl),
        conditions=[Condition("Name", value=NominalInteger(1), template=cond_tmpl)]
    )
    bad_cond = PropertyAndConditions(  # This will pass since we don't have a condition constraint
        property=Property("Name", value=NominalInteger(1), template=prop_tmpl),
        conditions=[Condition("Name", value=NominalInteger(2), template=cond_tmpl)]
    )
    with validation_level(WarningLevel.IGNORE):
        mat_spec.properties.append(good_prop)
        assert len(caplog.records) == 0, "Warning encountered on IGNORE"
        mat_spec.properties.append(bad_prop)
        assert len(caplog.records) == 0, "Warning encountered on IGNORE"
        mat_spec.properties.append(bad_cond)
        assert len(caplog.records) == 0, "Warning encountered on IGNORE"
    with validation_level(WarningLevel.WARNING):
        mat_spec.properties.append(good_prop)
        assert len(caplog.records) == 0, "Warning encountered on Good value"
        mat_spec.properties.append(bad_prop)
        assert len(caplog.records) == 1, "No warning encountered on Bad Value"
        mat_spec.properties.append(bad_cond)
        assert len(caplog.records) == 1, "Warning encountered on Bad condition"
    with validation_level(WarningLevel.FATAL):
        mat_spec.properties.append(good_prop)  # This is fine
        with pytest.raises(ValueError):
            mat_spec.properties.append(bad_prop)
        mat_spec.properties.append(bad_cond)  # This should probably not be fine
def test_invalid_template_conversions():
    with pytest.raises(NoEquivalentDescriptorError):
        template_to_descriptor(
            ConditionTemplate("foo",
                              bounds=IntegerBounds(lower_bound=0,
                                                   upper_bound=1)))

    with pytest.raises(NoEquivalentDescriptorError):
        template_to_descriptor(
            PropertyTemplate(
                "mixture",
                bounds=CompositionBounds(components=["sugar", "spice"])))

    class DummyBounds(BaseBounds):
        """Fake bounds to test unrecognized bounds."""
        def contains(self, bounds):
            return False

    with pytest.raises(ValueError):
        template_to_descriptor(ParameterTemplate("dummy",
                                                 bounds=DummyBounds()))
def test_contains():
    """Test that bounds know if a Value is contained within it."""
    bounds = IntegerBounds(1, 3)
    assert bounds.contains(NominalInteger(2)._to_bounds())
    assert not bounds.contains(NominalInteger(5)._to_bounds())
def fake_project():
    """Fake project that serves templates from template collection's list_all method."""
    templates = [
        PropertyTemplate("density",
                         bounds=RealBounds(lower_bound=0,
                                           upper_bound=100,
                                           default_units="g / cm^3"),
                         uids={"my_scope": "density"}),
        ConditionTemplate("volume",
                          bounds=IntegerBounds(lower_bound=0, upper_bound=11),
                          uids={"my_scope": "volume"}),
        ParameterTemplate(
            "speed",
            bounds=CategoricalBounds(categories=["slow", "fast"]),
            uids={})
    ]

    class FakePropertyTemplateCollection(PropertyTemplateCollection):
        def __init__(self):
            pass

        def list_all(self,
                     forward: bool = True,
                     per_page: int = 100) -> Iterator[PropertyTemplate]:
            return iter(
                [x for x in templates if isinstance(x, PropertyTemplate)])

    class FakeConditionTemplateCollection(ConditionTemplateCollection):
        def __init__(self):
            pass

        def list_all(self,
                     forward: bool = True,
                     per_page: int = 100) -> Iterator[ConditionTemplate]:
            return iter(
                [x for x in templates if isinstance(x, ConditionTemplate)])

    class FakeParameterTemplateCollection(ParameterTemplateCollection):
        def __init__(self):
            pass

        def list_all(self,
                     forward: bool = True,
                     per_page: int = 100) -> Iterator[ParameterTemplate]:
            return iter(
                [x for x in templates if isinstance(x, ParameterTemplate)])

    class FakeProject(Project):
        def __init__(self):
            pass

        @property
        def property_templates(self) -> PropertyTemplateCollection:
            return FakePropertyTemplateCollection()

        @property
        def condition_templates(self) -> ConditionTemplateCollection:
            return FakeConditionTemplateCollection()

        @property
        def parameter_templates(self) -> ParameterTemplateCollection:
            return FakeParameterTemplateCollection()

    return FakeProject()
Пример #11
0
def test_links_as_templates():
    """Verify that LinkByUIDs don't break anything we could have otherwise done."""
    prop_tmpl = PropertyTemplate("Name",
                                 uids={"scope": "prop"},
                                 bounds=IntegerBounds(1, 5))
    cond_tmpl = ConditionTemplate("Name",
                                  uids={"scope": "cond"},
                                  bounds=IntegerBounds(1, 5))
    param_tmpl = ParameterTemplate("Name",
                                   uids={"scope": "param"},
                                   bounds=IntegerBounds(1, 5))
    no_bounds = MeasurementTemplate(
        "Name",
        properties=[prop_tmpl],
        conditions=[cond_tmpl],
        parameters=[param_tmpl],
    )

    just_right = NominalInteger(2)
    middling = NominalInteger(4)
    too_high = NominalInteger(7)

    scenarios = [
        ("property", Property, MeasurementTemplate.validate_property,
         prop_tmpl),
        ("condition", Condition, MeasurementTemplate.validate_condition,
         cond_tmpl),
        ("parameter", Parameter, MeasurementTemplate.validate_parameter,
         param_tmpl),
    ]

    # Check that Attributes are checked against the template when the attributes links
    for scenario in scenarios:
        name, attr, validate, tmpl = scenario

        assert validate(no_bounds,
                        attr("Other name", template=tmpl.to_link(), value=middling)), \
            f"{name} didn't validate with {name}.template as LinkByUID."
        assert not validate(no_bounds,
                            attr("Other name", template=tmpl.to_link(), value=too_high)), \
            f"{name} DID validate with {name}.template as LinkByUID and bad value."

    with_bounds = MeasurementTemplate(
        "Name",
        properties=[(prop_tmpl.to_link(), IntegerBounds(1, 3))],
        conditions=[(cond_tmpl.to_link(), IntegerBounds(1, 3))],
        parameters=[(param_tmpl.to_link(), IntegerBounds(1, 3))],
    )

    # Check that Attributes are checked against the bounds when the attributes links
    for scenario in scenarios:
        name, attr, validate, tmpl = scenario

        assert validate(with_bounds,
                        attr("Other name", template=tmpl.to_link(), value=just_right)), \
            f"{name} didn't validate with {name}.template as LinkByUID and bounds."
        assert not validate(with_bounds,
                            attr("Other name", template=tmpl.to_link(), value=middling)), \
            f"{name} DID validate with {name}.template as LinkByUID, bad value, and bounds."

    with_links = MeasurementTemplate(
        "Name",
        properties=[(prop_tmpl.to_link())],
        conditions=[(cond_tmpl.to_link())],
        parameters=[(param_tmpl.to_link())],
    )

    # Check that tests pass when there's no way to test
    for scenario in scenarios:
        name, attr, validate, tmpl = scenario

        assert validate(with_links,
                        attr("Other name", template=tmpl.to_link(), value=too_high)), \
            f"{name} didn't validate with LinkByUID for everything."
Пример #12
0
def test_mixins():
    """Measurement templates have all 3 mixin traits."""
    obj = MeasurementTemplate(
        "Name",
        properties=[PropertyTemplate("Name", bounds=IntegerBounds(0, 1))],
        conditions=[ConditionTemplate("Name", bounds=IntegerBounds(0, 1))],
        parameters=[ParameterTemplate("Name", bounds=IntegerBounds(0, 1))],
    )
    with pytest.raises(TypeError):
        obj.properties.append(
            ConditionTemplate("3", bounds=IntegerBounds(0, 5)))
    with pytest.raises(TypeError):
        obj.properties.append(
            ParameterTemplate("3", bounds=IntegerBounds(0, 5)))

    with pytest.raises(TypeError):
        obj.conditions.append(PropertyTemplate("3", bounds=IntegerBounds(0,
                                                                         5)))
    with pytest.raises(TypeError):
        obj.conditions.append(
            ParameterTemplate("3", bounds=IntegerBounds(0, 5)))

    with pytest.raises(TypeError):
        obj.parameters.append(
            ConditionTemplate("3", bounds=IntegerBounds(0, 5)))
    with pytest.raises(TypeError):
        obj.parameters.append(PropertyTemplate("3", bounds=IntegerBounds(0,
                                                                         5)))

    with pytest.raises(TypeError):  # You passed a `scalar` to extend
        obj.properties.extend((PropertyTemplate("3",
                                                bounds=IntegerBounds(0, 5)),
                               IntegerBounds(1, 3)))

    with pytest.raises(ValueError):  # You passed a `scalar` to extend
        obj.properties = (PropertyTemplate("3", bounds=IntegerBounds(1, 3)),
                          IntegerBounds(0, 5))

    obj.properties.append(
        (PropertyTemplate("2", bounds=IntegerBounds(0,
                                                    5)), IntegerBounds(1, 3)))
    obj.properties.extend([
        PropertyTemplate("3", bounds=IntegerBounds(0, 5)),
        (PropertyTemplate("4", bounds=IntegerBounds(0,
                                                    5)), IntegerBounds(1, 3))
    ])
    obj.conditions.insert(1, ConditionTemplate("2", bounds=IntegerBounds(0,
                                                                         1)))
    obj.parameters[0] = ParameterTemplate("Name", bounds=IntegerBounds(0, 1))

    for x in (obj.properties, obj.conditions, obj.parameters):
        assert isinstance(x, ValidList)
        for y in x:
            assert isinstance(y, list)
            assert len(y) == 2
            assert isinstance(y[0], AttributeTemplate)
            if y[1] is not None:
                assert isinstance(y[1], BaseBounds)

    second = MeasurementTemplate(
        "Name",
        properties=[
            PropertyTemplate("Name", bounds=IntegerBounds(0, 1)),
            IntegerBounds(0, 1)
        ],
        conditions=[
            ConditionTemplate("Name", bounds=IntegerBounds(0, 1)),
            IntegerBounds(0, 1)
        ],
        parameters=[
            ParameterTemplate("Name", bounds=IntegerBounds(0, 1)),
            IntegerBounds(0, 1)
        ],
    )
    assert len(second.properties) == 1
    assert len(second.conditions) == 1
    assert len(second.parameters) == 1

    good_val = NominalInteger(1)
    bad_val = NominalInteger(2)

    assert second.validate_condition(Condition("Other name",
                                               value=good_val,
                                               template=second.conditions[0][0])), \
        "Condition with template and good value didn't validate."
    assert not second.validate_condition(Condition("Other name",
                                                   value=bad_val,
                                                   template=second.conditions[0][0])), \
        "Condition with template and bad value DID validate."
    assert second.validate_parameter(Parameter("Other name",
                                               value=good_val,
                                               template=second.parameters[0][0])), \
        "Parameter with template and good value didn't validate."
    assert not second.validate_parameter(Parameter("Other name",
                                                   value=bad_val,
                                                   template=second.parameters[0][0])), \
        "Parameter with template and bad value DID validate."
    assert second.validate_property(Property("Other name",
                                             value=good_val,
                                             template=second.properties[0][0])), \
        "Property with template and good value didn't validate."
    assert not second.validate_property(Property("Other name",
                                                 value=bad_val,
                                                 template=second.properties[0][0])), \
        "Property with template and bad value DID validate."

    assert second.validate_condition(Condition("Name", value=good_val)), \
        "Condition without template and good value didn't validate."
    assert not second.validate_condition(Condition("Name", value=bad_val)), \
        "Condition without template and bad value DID validate."
    assert second.validate_parameter(Parameter("Name", value=good_val)), \
        "Parameter without template and good value didn't validate."
    assert not second.validate_parameter(Parameter("Name", value=bad_val)), \
        "Parameter without template and bad value DID validate."
    assert second.validate_property(Property("Name", value=good_val)), \
        "Property without template and good value didn't validate."
    assert not second.validate_property(Property("Name", value=bad_val)), \
        "Property without template and bad value DID validate."

    assert second.validate_condition(Condition("Other name", value=bad_val)), \
        "Unmatched condition and bad value didn't validate."
    assert second.validate_parameter(Parameter("Other name", value=bad_val)), \
        "Unmatched parameter and bad value didn't validate."
    assert second.validate_property(Property("Other name", value=bad_val)), \
        "Unmatched property and bad value didn't validate."

    second.conditions[0][1] = None
    second.parameters[0][1] = None
    second.properties[0][1] = None
    assert second.validate_condition(Condition("Name", value=good_val)), \
        "Condition and good value with passthrough didn't validate."
    assert second.validate_parameter(Parameter("Name", value=good_val)), \
        "Parameter and good value with passthrough didn't validate."
    assert second.validate_property(Property("Name", value=good_val)), \
        "Property and good value with passthrough didn't validate."
    assert second.validate_property(
        PropertyAndConditions(property=Property("Name", value=good_val))), \
        "PropertyAndConditions didn't fall back to Property."
Пример #13
0
def test_contains():
    """Test that bounds know if a Value is contained within it."""
    bounds = IntegerBounds(1, 3)
    assert bounds.contains(UniformInteger(1, 2)._to_bounds())
    assert not bounds.contains(UniformInteger(3, 5)._to_bounds())
Пример #14
0
def make_cake_templates():
    """Define all templates independently, as in the wild this will be an independent operation."""
    tmpl = dict()

    # Attributes
    tmpl["Mixer speed setting"] = ParameterTemplate(
        name="Mixer speed setting",
        description="What speed setting to use on the mixer",
        bounds=IntegerBounds(0, 10))

    tmpl['Cooking time'] = ConditionTemplate(
        name="Cooking time",
        description="The time elapsed during a cooking process",
        bounds=RealBounds(0, 7 * 24.0, "hr"))
    tmpl["Oven temperature setting"] = ParameterTemplate(
        name="Oven temperature setting",
        description="Where the knob points",
        bounds=RealBounds(0, 2000.0, "K"))
    tmpl["Oven temperature"] = ConditionTemplate(
        name="Oven temperature",
        description="Actual temperature measured by the thermocouple",
        bounds=RealBounds(0, 2000.0, "K"))

    tmpl["Toothpick test"] = PropertyTemplate(
        name="Toothpick test",
        description="Results of inserting a toothpick to check doneness",
        bounds=CategoricalBounds(["wet", "crumbs", "completely clean"]))
    tmpl["Color"] = PropertyTemplate(
        name="Baked color",
        description="Visual observation of the color of a baked good",
        bounds=CategoricalBounds(
            ["Pale", "Golden brown", "Deep brown", "Black"]))

    tmpl["Tastiness"] = PropertyTemplate(
        name="Tastiness",
        description="Yumminess on a fairly arbitrary scale",
        bounds=IntegerBounds(lower_bound=1, upper_bound=10))

    tmpl["Nutritional Information"] = PropertyTemplate(
        name="Nutritional Information",
        description=
        "FDA Nutrition Facts, mass basis.  Please be attentive to g vs. mg.  "
        "`other-carbohydrate` and `other-fat` are the total values minus the "
        "broken-out quantities. Other is the difference between the total and the "
        "serving size.",
        bounds=CompositionBounds(components=[
            'other', 'saturated-fat', 'trans-fat', 'other-fat', 'cholesterol',
            'sodium', 'dietary-fiber', 'sugars', 'other-carbohydrate',
            'protein', 'vitamin-d', 'calcium', 'iron', 'potassium'
        ]))
    tmpl["Sample Mass"] = ConditionTemplate(
        name="Sample Mass",
        description=
        "Sample size in mass units, to go along with FDA Nutrition Facts",
        bounds=RealBounds(1.e-3, 1.e4, "g"))
    tmpl["Expected Sample Mass"] = ParameterTemplate(
        name="Expected Sample Mass",
        description=
        "Specified sample size in mass units, to go along with FDA Nutrition Facts",
        bounds=RealBounds(1.e-3, 1.e4, "g"))
    tmpl["Chemical Formula"] = PropertyTemplate(
        name="Chemical Formula",
        description="The chemical formula of a material",
        bounds=CompositionBounds(components=EmpiricalFormula.all_elements()))
    tmpl["Molecular Structure"] = PropertyTemplate(
        name="Molecular Structure",
        description="The molecular structure of the material",
        bounds=MolecularStructureBounds())

    # Objects
    tmpl["Procuring"] = ProcessTemplate(
        name="Procuring",
        description="Buyin' stuff",
        allowed_names=[]  # Takes no ingredients by definition
    )
    tmpl["Baking"] = ProcessTemplate(
        name="Baking",
        description='Using heat to promote chemical reactions in a material',
        allowed_names=['batter'],
        allowed_labels=['precursor'],
        conditions=[(tmpl["Oven temperature"], RealBounds(0, 700, "degF"))],
        parameters=[(tmpl["Oven temperature setting"],
                     RealBounds(100, 550, "degF"))])
    tmpl["Icing"] = ProcessTemplate(
        name="Icing",
        description='Applying a coating to a substrate',
        allowed_labels=['coating', 'substrate'])
    tmpl["Mixing"] = ProcessTemplate(
        name="Mixing",
        description='Physically combining ingredients',
        allowed_labels=[
            'wet', 'dry', 'leavening', 'seasoning', 'sweetener', 'shortening',
            'flavoring'
        ],
        parameters=[tmpl["Mixer speed setting"]])

    tmpl["Generic Material"] = MaterialTemplate(name="Generic")
    tmpl["Nutritional Material"] = MaterialTemplate(
        name="Nutritional Material",
        description="A material with FDA Nutrition Facts attached",
        properties=[tmpl["Nutritional Information"]])
    tmpl["Formulaic Material"] = MaterialTemplate(
        name="Formulaic Material",
        description="A material with chemical characterization",
        properties=[tmpl["Chemical Formula"], tmpl["Molecular Structure"]])
    tmpl["Baked Good"] = MaterialTemplate(
        name="Baked Good", properties=[tmpl["Toothpick test"], tmpl["Color"]])
    tmpl["Dessert"] = MaterialTemplate(name="Dessert",
                                       properties=[tmpl["Tastiness"]])

    tmpl["Doneness"] = MeasurementTemplate(
        name="Doneness test",
        description=
        "An ensemble of tests to determine the doneness of a baked good",
        properties=[tmpl["Toothpick test"], tmpl["Color"]])
    tmpl["Taste test"] = MeasurementTemplate(name="Taste test",
                                             properties=[tmpl["Tastiness"]])
    tmpl["Nutritional Analysis"] = MeasurementTemplate(
        name="Nutritional Analysis",
        properties=[tmpl["Nutritional Information"]],
        conditions=[tmpl["Sample Mass"]],
        parameters=[tmpl["Expected Sample Mass"]])
    tmpl["Elemental Analysis"] = MeasurementTemplate(
        name="Elemental Analysis",
        properties=[tmpl["Chemical Formula"]],
        conditions=[tmpl["Sample Mass"]],
        parameters=[tmpl["Expected Sample Mass"]])

    for key in tmpl:
        tmpl[key].add_uid(TEMPLATE_SCOPE, key.lower().replace(' ', '-'))
    return tmpl
def test_build():
    """Test builder routines."""
    mix_tmpl = ProcessTemplate(name="Mixing")
    term_tmpl = MaterialTemplate(name="Terminal")
    procure_tmpl = ProcessTemplate(name="Procure")
    raw_tmpl = MaterialTemplate(name="Raw")

    prop_tmpl = PropertyTemplate(name="Property",
                                 bounds=RealBounds(0, 10, "m"))
    cond_tmpl = ConditionTemplate(name="Condition",
                                  bounds=CategoricalBounds(["a", "b", "c"]))
    param_tmpl = ParameterTemplate(name="Parameter",
                                   bounds=CompositionBounds(
                                       EmpiricalFormula.all_elements()))
    mol_tmpl = PropertyTemplate(name="Molecule",
                                bounds=MolecularStructureBounds())
    int_tmpl = ConditionTemplate(name="Integer", bounds=IntegerBounds(0, 10))
    bad_tmpl = PropertyTemplate(name="Bad", bounds=UnsupportedBounds())

    root = make_node(name="Root",
                     material_template=term_tmpl,
                     process_template=mix_tmpl)
    assert root.template == term_tmpl, "Object didn't link correctly."
    assert root.process.template == mix_tmpl, "Object didn't link correctly."
    one = make_node("One",
                    material_template=raw_tmpl,
                    process_template=procure_tmpl)
    add_edge(output_material=root, input_material=one)
    add_edge(output_material=root, input_material=make_node("Two"))
    assert len(
        root.process.ingredients) == 2, "Ingredient count didn't line up."

    # Attribute tests
    with pytest.raises(ValueError):
        add_attribute(one.spec, cond_tmpl, "b")  # No property yet

    # Create a property-and-condition on the mat spec
    add_attribute(one.spec, prop_tmpl, 1)
    assert len(
        one.spec.properties) == 1, "Material spec didn't get a property."
    assert one.spec.properties[
        0].property.template == prop_tmpl, "Wrong linking on property."
    assert one.spec.properties[
        0].property.value.units == "meter", "Wrong units on property."
    assert one.spec.properties[
        0].property.value.nominal == 1, "Wrong value on property."
    add_attribute(one.spec, cond_tmpl, "b")
    assert len(
        one.spec.properties[0].conditions) == 1, "Wrong location on condition."
    assert one.spec.properties[0].conditions[0].template == cond_tmpl, \
        "Wrong linking on condition."
    assert one.spec.properties[0].conditions[
        0].value.category == "b", "Wrong value on condition."
    with pytest.raises(ValueError):
        add_attribute(one.spec, param_tmpl,
                      "H2O")  # Mat Specs don't support parameters

    # Create a second property-and-condition on the mat spec
    add_attribute(one.spec, mol_tmpl, "C")
    assert len(one.spec.properties) == 2, "Second property added."
    add_attribute(one.spec, int_tmpl, 5)
    assert len(one.spec.properties[-1].conditions
               ) == 1, "Second property has a condition."

    # Attach a measurement
    msr_tmpl = MeasurementTemplate(name="Measure!", properties=[prop_tmpl])
    msr = add_measurement(material=root, template=msr_tmpl)
    assert len(root.measurements) == 1, "Measurement was added to root."
    add_attribute(msr, cond_tmpl, "c")  # Order shouldn't matter anymore
    assert len(msr.conditions) == 1, "Condition wasn't added to measurement."
    add_attribute(msr, prop_tmpl, 5)
    assert len(msr.properties) == 1, "Property wasn't added to measurement."
    add_attribute(msr, param_tmpl, "CH4")
    assert len(msr.parameters) == 1, "Parameter wasn't added to measurement."
    assert msr.parameters[
        0].template == param_tmpl, "Wrong linking on parameter."
    assert msr.parameters[
        0].value.formula == "CH4", "Wrong value on parameter."

    # Test failed builds
    with pytest.raises(ValueError):
        add_measurement(material=root)
    assert len(root.measurements
               ) == 1, "Failed measurement build still added an object."
    with pytest.raises(ValueError):
        add_attribute(msr, bad_tmpl, "Word")
    assert len(
        msr.properties) == 1, "Failed attribute build still added an object."
    with pytest.raises(ValueError):
        add_attribute(root.process, prop_tmpl, 9)
    assert len(root.process.conditions
               ) == 0, "Failed attribute build still added an object."
    assert len(root.process.parameters
               ) == 0, "Failed attribute build still added an object."