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[0][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(spec=mat_spec, sample_type="imaginary") mat = MaterialRun(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 _parse_value(val): """Example field-parsing logic.""" # If the string is complicated, split it up and try to get uncertainty and/or units if isinstance(val, str) and len(val.split()) > 1: toks = val.split() mean = float(toks[0]) std = -1 if toks[1] in {"+-", "+/-"}: std = float(toks[2]) try: unit = units.parse_units(toks[-1]) except (ValueError, units.UndefinedUnitError): print("Couldn't find {}".format(toks[-1])) unit = '' if std >= 0: return NormalReal(mean=mean, std=std, units=unit) else: return NominalReal(mean, units=unit) # if it is just a number wrap it in a nominal value elif isinstance(val, (float, int)): return NominalReal(val, '') # if it is a single string, its either a single number of a category elif isinstance(val, str): try: num = float(val) return NominalReal(num, '') except ValueError: return DiscreteCategorical(val) else: raise ValueError("Couldn't parse {}".format(val))
def test_serialized_history(): """Test the serialization of a complete material history.""" # Create several runs and specs linked together buy_spec = LinkByUID("id", "pr723") cookie_dough_spec = MaterialSpec("cookie dough spec", process=buy_spec) buy_cookie_dough = ProcessRun("Buy cookie dough", uids={'id': '32283'}, spec=buy_spec) cookie_dough = MaterialRun("cookie dough", process=buy_cookie_dough, spec=cookie_dough_spec) bake = ProcessRun("bake cookie dough", conditions=[ Condition("oven temp", origin='measured', value=NominalReal(357, 'degF'))]) IngredientRun(material=cookie_dough, process=bake, number_fraction=NominalReal(1, '')) cookie = MaterialRun("cookie", process=bake, tags=["chocolate chip", "drop"]) MeasurementRun("taste", material=cookie, properties=[ Property("taste", value=DiscreteCategorical("scrumptious"))]) cookie_history = complete_material_history(cookie) # There are 7 entities in the serialized list: cookie dough (spec & run), buy cookie dough, # cookie dough ingredient, bake cookie dough, cookie, taste assert len(cookie_history) == 7 for entity in cookie_history: assert len(entity['uids']) > 0, "Serializing material history should assign uids." # Check that the measurement points to the material taste_dict = next(x for x in cookie_history if x.get('type') == 'measurement_run') cookie_dict = next(x for x in cookie_history if x.get('name') == 'cookie') scope = taste_dict.get('material').get('scope') assert taste_dict.get('material').get('id') == cookie_dict.get('uids').get(scope) # Check that both the material spec and the process run point to the same process spec. # Because that spec was initially a LinkByUID, this also tests the methods ability to # serialize a LinkByUID. cookie_dough_spec_dict = next(x for x in cookie_history if x.get('type') == 'material_spec') buy_cookie_dough_dict = next(x for x in cookie_history if x.get('name') == 'Buy cookie dough') assert cookie_dough_spec_dict.get('process') == buy_spec.as_dict() assert buy_cookie_dough_dict.get('spec') == buy_spec.as_dict()
def test_unit_normalization(): """Make sure units are internally represented in a reasonable way.""" assert NominalReal(2.7, "newton / m^2").units == NominalReal( 2.7, "m^-1 newton / meter").units val = NominalReal(2.7, "cm") assert val.units == "centimeter" assert val.dump()["units"] == "centimeter"
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()] 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" # Serializing the material breaks the material-->measurement link. assert loads(dumps(dye)).measurements == [], \ "Measurement information should be removed when material is serialized" assert 'measurements' in repr(dye) assert 'material' in repr(fluorescence) assert 'material' in repr(absorbance) substitute_links(dye.measurements) assert 'measurements' in repr(dye)
def make_flexural_test_measurement(my_id, deflection, extra_tags=frozenset()): """ Compute the stree, strain, and modulus. According to https://en.wikipedia.org/wiki/Three-point_flexural_test """ stress = 3 * applied_force * span / (2 * thickness * thickness * width) strain = 6 * deflection * thickness / (span * span) modulus = stress / strain measurement = MeasurementRun( uids={"my_id": my_id}, tags=["3_pt_bend", "mechanical", "flex"] + list(extra_tags), properties=[ Property(name="flexural stress", value=NormalReal(stress, std=(0.01 * stress), units="MPa"), origin=Origin.MEASURED), Property(name="flexural strain", value=NormalReal(strain, std=(0.01 * strain), units=""), origin=Origin.MEASURED), Property(name="flexural modulus", value=NormalReal(modulus, std=(0.01 * modulus), units="MPa"), origin=Origin.MEASURED), Property(name="deflection", value=NominalReal(deflection, units="mm"), origin=Origin.MEASURED) ]) return measurement
def test_access_data(): """Demonstrate and test access patterns within the data island.""" binders = { "Polyethylene Glycol 100M": 0.02, "Sodium lignosulfonate": 0.004, "Polyvinyl Acetate": 0.0001 } powders = {"Al2O3": 0.96} island = make_data_island( density=1.0, bulk_modulus=300.0, firing_temperature=750.0, binders=binders, powders=powders, tag="Me" ) # read the density value assert(island.measurements[0].properties[0].value == NominalReal(1.0, '')) # read the bulk modulus value assert(island.measurements[0].properties[1].value == NormalReal(300.0, 3.0, '')) # read the firing temperature assert(island.process.conditions[0].value == UniformReal(749.5, 750.5, 'degC')) assert(island.process.parameters[0].value == DiscreteCategorical({"hot": 1.0})) # read the quantity of alumina quantities = island.process.ingredients[0].material.process.conditions[0].value.quantities assert(list( keyfilter(lambda x: x == "Al2O3", quantities).values() )[0] == 0.96) # check that the serialization results in the correct number of objects in the preface # (note that neither measurements nor ingredients are serialized) assert(len(json.loads(dumps(island))["context"]) == 26)
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"))
def ingest_table(material_run, table): """Ingest a material run into an existing table.""" for _, row in table.iterrows(): exp = MeasurementRun() for prop_name in known_properties: if prop_name in row: exp.properties.append( Property(name=prop_name, value=NominalReal(row[prop_name], ''))) for cond_name in known_conditions: if cond_name in row: exp.conditions.append( Condition(name=cond_name, value=NominalReal(row[cond_name], ''))) exp.material = material_run return material_run
def test_deserialize(): """Round-trip serde should leave the object unchanged.""" condition = Condition(name="A condition", value=NominalReal(7, '')) parameter = Parameter(name="A parameter", value=NormalReal(mean=17, std=1, units='')) measurement = MeasurementRun(tags="A tag on a measurement", conditions=condition, parameters=parameter) copy_meas = copy(measurement) assert(copy_meas.conditions[0].value == measurement.conditions[0].value) assert(copy_meas.parameters[0].value == measurement.parameters[0].value) assert(copy_meas.uids["auto"] == measurement.uids["auto"])
def real_mapper(prop): """Mapping methods for RealBounds.""" if 'uncertainty' in prop['scalars'][0]: 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
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_invalid_assignment(): """Test that invalid assignments throw the appropriate errors.""" with pytest.raises(ValueError): Property(value=NominalReal(10, '')) with pytest.raises(TypeError): Property(name="property", value=10) with pytest.raises(TypeError): Property(name="property", template=ProcessTemplate("wrong kind of template")) with pytest.raises(ValueError): Property(name="property", origin=None)
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"
def test_material_attachment(): """ Attach a material run to an ingredient run. Check that the ingredient can be built, and that the connection survives ser/de. """ flour = MaterialRun("flour", sample_type='unknown') flour_ingredient = IngredientRun(material=flour, absolute_quantity=NominalReal(500, 'g'), name='500 g flour') flour_ingredient_copy = IngredientRun.build(flour_ingredient.dump()) assert flour_ingredient_copy == flour_ingredient
def test_template_assignment(): """Test that an object and its attributes can both be assigned templates.""" humidity_template = ConditionTemplate("Humidity", RealBounds(0.5, 0.75, "")) template = ProcessTemplate( "Dry", conditions=[[humidity_template, RealBounds(0.5, 0.65, "")]]) ProcessSpec("Dry a polymer", template=template, conditions=[ Condition("Humidity", value=NominalReal(0.6, ""), template=humidity_template) ])
def test_attribute_serde(): """An attribute with a link to an attribute template should be copy-able.""" prop_tmpl = PropertyTemplate(name='prop_tmpl', bounds=RealBounds(0, 2, 'm') ) prop = Property(name='prop', template=prop_tmpl, value=NominalReal(1, 'm') ) meas_spec = MeasurementSpec("a spec") meas = MeasurementRun("a measurement", spec=meas_spec, properties=[prop]) assert loads(dumps(prop)) == prop assert loads(dumps(meas)) == meas assert isinstance(prop.template, PropertyTemplate)
def test_simple_deserialization(valid_data, attribute_list): for (key, Attribute, AttributeTemplate) in attribute_list: valid_data['type'] = key valid_data['template']['type'] = key + '_template' attribute: Attribute = Attribute.build(valid_data) assert attribute.name == 'mass' assert attribute.notes == 'This is a note' assert attribute.value == NominalReal(5.0, units='gram') assert attribute.template == \ AttributeTemplate('mass', uids={'id': valid_data['template']['uids']['id']}, description='mass of object', bounds=RealBounds(0.0, 20.0, 'gram') ) assert attribute.origin == 'measured' assert attribute.file_links == [] assert attribute.typ == key
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(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_simple_deserialization(valid_data): """Ensure that a deserialized Material Spec looks sane.""" material_spec: MaterialSpec = MaterialSpec.build(valid_data) assert material_spec.uids == {'id': valid_data['uids']['id']} assert material_spec.name == 'spec of material' assert material_spec.tags == [] assert material_spec.notes is None assert material_spec.process is None assert material_spec.properties[0] == \ PropertyAndConditions(Property("color", origin='specified', value=NominalCategorical("tan")), conditions=[Condition('temperature', origin='specified', value=NominalReal(300, units='kelvin'))]) assert material_spec.template is None assert material_spec.file_links == [] assert material_spec.typ == 'material_spec'
def test_recursive_foreach(): """Test that recursive foreach will actually walk through a material history.""" mat_run = MaterialRun("foo") process_run = ProcessRun("bar") IngredientRun(process=process_run, material=mat_run) output = MaterialRun(process=process_run) # property templates are trickier than templates because they are referenced in attributes template = PropertyTemplate("prop", bounds=RealBounds(0, 1, "")) prop = Property("prop", value=NominalReal(1.0, ""), template=template) MeasurementRun("check", material=output, properties=prop) types = [] recursive_foreach(output, lambda x: types.append(x.typ)) expected = [ "ingredient_run", "material_run", "material_run", "process_run", "measurement_run", "property_template" ] assert sorted(types) == sorted(expected)
def test_simple_deserialization(valid_data): """Ensure that a deserialized Process Run looks sane.""" process_run: ProcessRun = ProcessRun.build(valid_data) assert process_run.uids == {'id': valid_data['uids']['id'], 'my_id': 'process1-v1'} assert process_run.tags == ['baking::cakes', 'danger::low'] assert process_run.conditions[0] == Condition(name='oven temp', value=NominalReal(203.0, ''), origin='measured') assert process_run.parameters == [] assert process_run.file_links == [] assert process_run.template is None assert process_run.output_material is None assert process_run.spec == \ ProcessSpec(name="Spec for proc 1", uids={'id': valid_data['spec']['uids']['id']}, conditions=[Condition(name='oven temp', value=UniformReal(175, 225, ''), origin='specified')] ) assert process_run.name == 'Process 1' assert process_run.notes == 'make sure to use oven mitts' assert process_run.typ == 'process_run'
def test_serialize(): """Serializing a nested object should be identical to individually serializing each piece.""" condition = Condition(name="A condition", value=NominalReal(7, '')) parameter = Parameter(name="A parameter", value=NormalReal(mean=17, std=1, units='')) input_material = MaterialRun(tags="input") process = ProcessRun(tags="A tag on a process run") ingredient = IngredientRun(material=input_material, process=process) material = MaterialRun(tags=["A tag on a material"], process=process) measurement = MeasurementRun(tags="A tag on a measurement", conditions=condition, parameters=parameter, material=material) # serialize the root of the tree native_object = json.loads(dumps(measurement)) # ingredients don't get serialized on the process assert(len(native_object[0]) == 3) assert(native_object[1]["type"] == LinkByUID.typ) # serialize all of the nodes native_batch = json.loads(dumps([material, process, measurement, ingredient])) assert(len(native_batch[0]) == 5) assert(len(native_batch[1]) == 4) assert(all(x["type"] == LinkByUID.typ for x in native_batch[1]))
def test_default_units(): """An empty string should turn into the 'dimensionless' unit.""" assert NominalReal(2.7, "") == NominalReal(2.7, "dimensionless")
def test_invalid_assignment(): """Invalid assignments to `process` or `material` throw a TypeError.""" with pytest.raises(TypeError): IngredientSpec(name="name", material=NominalReal(3, '')) with pytest.raises(TypeError): IngredientSpec(name="name", process="process")
assert set(boiling.ingredients) == {oil, potatoes} assert frying.ingredients == [] oil.process = frying assert oil.process == frying assert boiling.ingredients == [potatoes] assert frying.ingredients == [oil] potatoes.process = frying assert potatoes.process == frying assert boiling.ingredients == [] assert set(frying.ingredients) == {oil, potatoes} VALID_QUANTITIES = [ NominalReal(14.0, ''), UniformReal(0.5, 0.6, 'm'), NormalReal(-0.3, 0.6, "kg") ] INVALID_QUANTITIES = [ NominalCategorical("blue"), NominalInteger(5), EmpiricalFormula("CH4"), 0.33, "0.5" ] @pytest.mark.parametrize("valid_quantity", VALID_QUANTITIES) def test_valid_quantities(valid_quantity):
def make_data_island(density, bulk_modulus, firing_temperature, binders, powders, tag=None): """Helper function to create a relatively involved data island.""" binder_specs = keymap(lambda x: MaterialSpec(name=x), binders) powder_specs = keymap(lambda x: MaterialSpec(name=x), powders) binder_runs = keymap(lambda x: MaterialRun(spec=x), binder_specs) powder_runs = keymap(lambda x: MaterialRun(spec=x), powder_specs) all_input_materials = keymap(lambda x: x.spec.name, merge(binder_runs, powder_runs)) mixing_composition = Condition( name="composition", value=NominalComposition(all_input_materials) ) mixing_process = ProcessRun( tags=["mixing"], conditions=[mixing_composition] ) binder_ingredients = [] for run in binder_runs: binder_ingredients.append( IngredientRun( material=run, process=mixing_process, mass_fraction=NominalReal(binders[run.spec.name], ''), ) ) powder_ingredients = [] for run in powder_runs: powder_ingredients.append( IngredientRun( material=run, process=mixing_process, mass_fraction=NominalReal(powders[run.spec.name], ''), ) ) green_sample = MaterialRun(process=mixing_process) measured_firing_temperature = Condition( name="Firing Temperature", value=UniformReal(firing_temperature - 0.5, firing_temperature + 0.5, 'degC'), template=firing_temperature_template ) specified_firing_setting = Parameter( name="Firing setting", value=DiscreteCategorical("hot") ) firing_spec = ProcessSpec(template=firing_template) firing_process = ProcessRun( conditions=[measured_firing_temperature], parameters=[specified_firing_setting], spec=firing_spec ) IngredientRun( green_sample, process=firing_process, mass_fraction=NormalReal(1.0, 0.0, ''), volume_fraction=NormalReal(1.0, 0.0, ''), number_fraction=NormalReal(1.0, 0.0, '') ) measured_density = Property( name="Density", value=NominalReal(density, ''), template=density_template ) measured_modulus = Property( name="Bulk modulus", value=NormalReal(bulk_modulus, bulk_modulus / 100.0, '') ) measurement_spec = MeasurementSpec(template=measurement_template) measurement = MeasurementRun( properties=[measured_density, measured_modulus], spec=measurement_spec ) tags = [tag] if tag else [] material_spec = MaterialSpec(template=material_template) material_run = MaterialRun(process=firing_process, tags=tags, spec=material_spec) measurement.material = material_run return material_run
def test_units(): """Make sure units can be set without error.""" NominalReal(2.7, "") NominalReal(2.7, "cm") NominalReal(2.7, "cm^2/joule") NominalReal(2.7, "")
def test_taurus_object_serde(): """Test that an unspecified taurus object can be serialized and deserialized.""" good_obj = SampleClass("Can be serialized", NominalReal(17, '')) copy = SampleClass.build(good_obj.dump()) assert copy.prop_value == good_obj.prop_value assert copy.prop_string == good_obj.prop_string
def test_bad_object_serde(): """Test that a 'mystery' object cannot be serialized.""" bad_obj = SampleClass("Cannot be serialized", NominalReal(34, ''), UnserializableClass(1)) with pytest.raises(AttributeError): bad_obj.dump()