def test_object_template_serde(): """Test serde of an object template.""" length_template = PropertyTemplate("Length", bounds=RealBounds(2.0, 3.5, 'cm')) sub_bounds = RealBounds(2.5, 3.0, 'cm') color_template = PropertyTemplate("Color", bounds=CategoricalBounds(["red", "green", "blue"])) # Properties are a mixture of property templates and [template, bounds], pairs block_template = MaterialTemplate("Block", properties=[[length_template, sub_bounds], color_template]) copy_template = MaterialTemplate.build(block_template.dump()) assert copy_template == block_template # Tests below exercise similar code, but for measurement and process templates pressure_template = ConditionTemplate("pressure", bounds=RealBounds(0.1, 0.11, 'MPa')) index_template = ParameterTemplate("index", bounds=IntegerBounds(2, 10)) meas_template = MeasurementTemplate("A measurement of length", properties=[length_template], conditions=[pressure_template], description="Description", parameters=[index_template], tags=["foo"]) assert MeasurementTemplate.build(meas_template.dump()) == meas_template proc_template = ProcessTemplate("Make an object", parameters=[index_template], conditions=[pressure_template], allowed_labels=["Label"], allowed_names=["first sample", "second sample"]) assert ProcessTemplate.build(proc_template.dump()) == proc_template # Check that serde still works if the template is a LinkByUID pressure_template.uids['id'] = '12345' # uids['id'] not populated by default proc_template.conditions[0][0] = LinkByUID('id', pressure_template.uids['id']) assert ProcessTemplate.build(proc_template.dump()) == proc_template
def test_contains_incompatible_units(): """Make sure contains returns false when the units don't match.""" dim = RealBounds(lower_bound=0, upper_bound=100, default_units="m") dim2 = RealBounds(lower_bound=0, upper_bound=100, default_units="kJ") dim3 = RealBounds(lower_bound=0, upper_bound=100, default_units='') assert not dim.contains(dim2) assert not dim.contains(dim3)
def test_simple_deserialization(valid_data): """Ensure that a deserialized Process Spec looks sane.""" process_spec: ProcessSpec = ProcessSpec.build(valid_data) assert process_spec.uids == {'id': valid_data['uids']['id']} assert process_spec.tags == ['baking::cakes', 'danger::low'] assert process_spec.parameters[0] == Parameter(name='oven temp', value=UniformReal( 195, 205, ''), origin='specified') assert process_spec.conditions == [] assert process_spec.template == \ ProcessTemplate('the template', uids={'id': valid_data['template']['uids']['id']}, parameters=[ [ParameterTemplate('oven temp template', bounds=RealBounds(175, 225, ''), uids={'id': valid_data['template']['parameters'][0][0]['uids']['id']}), RealBounds(175, 225, '')] ], description='a long description', allowed_labels=['a', 'b'], allowed_names=['a name']) assert process_spec.name == 'Process 1' assert process_spec.notes == 'make sure to use oven mitts' assert process_spec.file_links == [ FileLink('cake_recipe.txt', 'www.baking.com') ] assert process_spec.typ == 'process_spec' assert process_spec.audit_info == AuditInfo(**valid_data['audit_info'])
def test_bounds_mismatch(): """Test that a mismatch between the attribute and given bounds throws a ValueError.""" attribute_bounds = RealBounds(0, 100, '') object_bounds = RealBounds(200, 300, '') cond_template = ConditionTemplate("a condition", bounds=attribute_bounds) with pytest.raises(ValueError): ProcessTemplate("a process template", conditions=[[cond_template, object_bounds]])
def test_dependencies(): """Test that dependency lists make sense.""" targets = [ PropertyTemplate(name="name", bounds=RealBounds(0, 1, '')), ConditionTemplate(name="name", bounds=RealBounds(0, 1, '')), ParameterTemplate(name="name", bounds=RealBounds(0, 1, '')), ] for target in targets: assert len(target.all_dependencies()) == 0, f"{type(target)} had dependencies"
def _check(value: BaseValue): fraction_bounds = RealBounds(lower_bound=0.0, upper_bound=1.0, default_units='') level = get_validation_level() accept = level == WarningLevel.IGNORE or fraction_bounds.contains( value) if not accept: message = f"Value {value} is not between 0 and 1." if level == WarningLevel.WARNING: logger.warning(message) else: raise ValueError(message)
def test_dependencies(): """Test that dependency lists make sense.""" attribute_bounds = RealBounds(0, 100, '') cond_template = ConditionTemplate("a condition", bounds=attribute_bounds) proc_template = ProcessTemplate("a process template", conditions=[cond_template]) assert cond_template in proc_template.all_dependencies()
def test_constructor_error(): """Test that invalid real bounds cannot be constructed.""" with pytest.raises(ValueError): RealBounds() with pytest.raises(ValueError): RealBounds(0, float("inf"), "meter") with pytest.raises(ValueError): RealBounds(None, 10, '') with pytest.raises(ValueError): RealBounds(0, 100) with pytest.raises(ValueError): RealBounds(100, 0, "m")
def test_complex_substitutions(): """Make sure accounting works for realistic objects.""" root = MaterialRun("root", process=ProcessRun("root", spec=ProcessSpec("root")), spec=MaterialSpec("root")) root.spec.process = root.process.spec input = MaterialRun("input", process=ProcessRun("input", spec=ProcessSpec("input")), spec=MaterialSpec("input")) input.spec.process = input.process.spec IngredientRun(process=root.process, material=input, spec=IngredientSpec("ingredient", process=root.process.spec, material=input.spec)) param = ParameterTemplate("Param", bounds=RealBounds(-1, 1, "m")) root.process.spec.template = ProcessTemplate("Proc", parameters=[param]) root.process.parameters.append( Parameter("Param", value=NormalReal(0, 1, 'm'), template=param)) links = flatten(root, scope="test-scope") index = make_index(links) rebuild = substitute_objects(links, index, inplace=True) rebuilt_root = next(x for x in rebuild if x.name == root.name and x.typ == root.typ) all_objs = recursive_flatmap(rebuilt_root, func=lambda x: [x], unidirectional=False) unique = [x for i, x in enumerate(all_objs) if i == all_objs.index(x)] assert not any(isinstance(x, LinkByUID) for x in unique), "All are objects" assert len(links) == len(unique), "Objects are missing"
def test_invalid_assignment(caplog): """Test that invalid assignments throw the appropriate errors.""" with pytest.raises(TypeError): 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) valid_prop = Property(name="property", value=NominalReal(10, ''), template=PropertyTemplate("template", bounds=RealBounds( 0, 100, ''))) good_val = valid_prop.value bad_val = NominalReal(-10.0, '') assert len(caplog.records ) == 0, "Warning caught before logging tests were reached." with validation_level(WarningLevel.IGNORE): valid_prop.value = bad_val assert len(caplog.records ) == 0, "Validation warned even though level is IGNORE." assert valid_prop.value == bad_val, "IGNORE allowed the bad value to be set." valid_prop.value = good_val assert len(caplog.records ) == 0, "Validation warned even though level is IGNORE." with validation_level(WarningLevel.WARNING): valid_prop.value = bad_val assert len(caplog.records ) == 1, "Validation didn't warn on out of bounds value." assert valid_prop.value == bad_val, "WARNING allowed the bad value to be set." valid_prop.value = good_val assert len( caplog.records) == 1, "Validation DID warn on a valid value." with validation_level(WarningLevel.FATAL): with pytest.raises(ValueError): valid_prop.value = bad_val assert valid_prop.value == good_val, "FATAL didn't allow the bad value to be set." with validation_level(WarningLevel.FATAL): with pytest.raises(ValueError): valid_prop.template = PropertyTemplate("template", bounds=RealBounds(0, 1, '')) assert valid_prop.value == good_val, "FATAL didn't allow the bad value to be set."
def test_invalid_assignment(): """Invalid assignments to `process` or `material` throw a TypeError.""" with pytest.raises(TypeError): IngredientRun(material=RealBounds(0, 5.0, '')) with pytest.raises(TypeError): IngredientRun(process="process") with pytest.raises(TypeError): IngredientRun(spec=5)
def test_object_template_validation(): """Test that attribute templates are validated against given bounds.""" length_template = PropertyTemplate("Length", bounds=RealBounds(2.0, 3.5, 'cm')) dial_template = ConditionTemplate("dial", bounds=IntegerBounds(0, 5)) color_template = ParameterTemplate("Color", bounds=CategoricalBounds(["red", "green", "blue"])) with pytest.raises(TypeError): MaterialTemplate() with pytest.raises(ValueError): MaterialTemplate("Block", properties=[[length_template, RealBounds(3.0, 4.0, 'cm')]]) with pytest.raises(ValueError): ProcessTemplate("a process", conditions=[[color_template, CategoricalBounds(["zz"])]]) with pytest.raises(ValueError): MeasurementTemplate("A measurement", parameters=[[dial_template, IntegerBounds(-3, -1)]])
def test_contains(): """Test basic contains logic.""" bounds = CategoricalBounds(categories={"spam", "eggs"}) assert bounds.contains(CategoricalBounds(categories={"spam"})) assert not bounds.contains(CategoricalBounds(categories={"spam", "foo"})) assert not bounds.contains(RealBounds(0.0, 2.0, '')) assert not bounds.contains(None) with pytest.raises(TypeError): bounds.contains({"spam", "eggs"})
def test_contains(): """Test basic contains logic.""" bounds = CompositionBounds(components={"spam", "eggs"}) assert bounds.contains(CompositionBounds(components={"spam"})) assert not bounds.contains(CompositionBounds(components={"foo"})) assert not bounds.contains(RealBounds(0.0, 2.0, '')) assert not bounds.contains(None) with pytest.raises(TypeError): bounds.contains({"spam"})
def test_condition_template(): """Test creation and serde of condition templates.""" bounds = RealBounds(2.5, 10.0, default_units='cm') template = ConditionTemplate("Chamber width", bounds=bounds, description="width of chamber") assert template.uids is not None # uids should be added automatically # Take template through a serde cycle and ensure that it is unchanged assert ConditionTemplate.build(template.dump()) == template # A more complicated cycle that goes through both gemd-python and citrine-python serde. assert ConditionTemplate.build(loads(dumps(template.dump())).as_dict()) == 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_contains(): """Test basic contains logic.""" bounds = MolecularStructureBounds() assert bounds.contains(MolecularStructureBounds()) assert not bounds.contains(RealBounds(0.0, 2.0, '')) assert not bounds.contains(None) with pytest.raises(TypeError): bounds.contains('c1(C=O)cc(OC)c(O)cc1') with pytest.raises(TypeError): bounds.contains( 'InChI=1/C8H8O3/c1-11-8-4-6(5-9)2-3-7(8)10/h2-5,10H,1H3')
def test_type_mismatch(): """Test that incompatible types cannot be matched against RealBounds.""" bounds = RealBounds(0, 1, default_units="meters") assert not bounds.contains(IntegerBounds(0, 1)) assert not bounds.contains(None) with pytest.raises(TypeError): bounds.contains([.33, .66])
def test_contains(): """Test basic contains logic.""" bounds = CategoricalBounds(categories={"spam", "eggs"}) assert bounds.contains(CategoricalBounds(categories={"spam"})) assert not bounds.contains(CategoricalBounds(categories={"spam", "foo"})) assert not bounds.contains(RealBounds(0.0, 2.0, '')) assert not bounds.contains(None) with pytest.raises(TypeError): bounds.contains({"spam", "eggs"}) from gemd.entity.value import NominalCategorical assert bounds.contains(NominalCategorical("spam")) assert not bounds.contains(NominalCategorical("foo"))
def test_contains(): """Test basic contains logic.""" bounds = CompositionBounds(components={"spam", "eggs"}) assert bounds.contains(CompositionBounds(components={"spam"})) assert not bounds.contains(CompositionBounds(components={"foo"})) assert not bounds.contains(RealBounds(0.0, 2.0, '')) assert not bounds.contains(None) with pytest.raises(TypeError): bounds.contains({"spam"}) from gemd.entity.value import NominalComposition assert bounds.contains(NominalComposition({"spam": 0.2, "eggs": 0.8})) assert not bounds.contains(NominalComposition({"foo": 1.0}))
def test_contains(): """Make sure unit conversions are applied to bounds for contains.""" dim = RealBounds(lower_bound=0, upper_bound=100, default_units="degC") dim2 = RealBounds(lower_bound=33, upper_bound=200, default_units="degF") assert dim.contains(dim2) from gemd.entity.value import NominalReal assert dim.contains(NominalReal(5, 'degC')) assert not dim.contains(NominalReal(5, 'K'))
def test_contains(): """Test basic contains logic.""" bounds = MolecularStructureBounds() assert bounds.contains(MolecularStructureBounds()) assert not bounds.contains(RealBounds(0.0, 2.0, '')) assert not bounds.contains(None) with pytest.raises(TypeError): bounds.contains('c1(C=O)cc(OC)c(O)cc1') with pytest.raises(TypeError): bounds.contains( 'InChI=1/C8H8O3/c1-11-8-4-6(5-9)2-3-7(8)10/h2-5,10H,1H3') from gemd.entity.value import Smiles, NominalInteger assert bounds.contains(Smiles('c1(C=O)cc(OC)c(O)cc1')) assert not bounds.contains(NominalInteger(5))
def test_recursive_foreach(): """Test that recursive_foreach() applies a method to every object.""" new_tag = "Extra tag" def func(base_ent): """Adds a specific tag to the object.""" base_ent.tags.extend([new_tag]) return param_template = ParameterTemplate("a param template", bounds=RealBounds(0, 100, '')) meas_template = MeasurementTemplate("Measurement template", parameters=[param_template]) parameter = Parameter(name="A parameter", value=NormalReal(mean=17, std=1, units='')) measurement = MeasurementSpec(name="name", parameters=parameter, template=meas_template) test_dict = {"foo": measurement} recursive_foreach(test_dict, func, apply_first=True) for ent in [param_template, meas_template, measurement]: assert new_tag in ent.tags
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("material", 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 make_templates(template_scope=DEMO_TEMPLATE_SCOPE): """Build all templates needed for the table.""" tmpl = dict() # Attribute Templates attribute_feed = { "Formula": [PropertyTemplate, CompositionBounds(components=EmpiricalFormula.all_elements())], "Crystallinity": [ConditionTemplate, CategoricalBounds( ['Amorphous', 'Polycrystalline', 'Single crystalline'] )], "Color": [PropertyTemplate, CategoricalBounds( ['Amber', 'Black', 'Blue', 'Bluish', 'Bronze', 'Brown', 'Brown-Black', 'Copper-Red', 'Dark Brown', 'Dark Gray', 'Dark Green', 'Dark Red', 'Gray', 'Light Gray', 'Ocher', 'Orange', 'Orange-Red', 'Pale Yellow', 'Red', 'Red-Yellow', 'Violet', 'White', 'Yellow', 'Yellow-Orange', 'Yellow-White'] )], "Band gap": [PropertyTemplate, RealBounds(lower_bound=0.001, upper_bound=100, default_units='eV')], "Temperature": [ConditionTemplate, RealBounds(lower_bound=1, upper_bound=1000, default_units='K')], "Temperature derivative of band gap": [PropertyTemplate, RealBounds(lower_bound=-0.01, upper_bound=0.01, default_units='eV/K')], "Lasing": [PropertyTemplate, CategoricalBounds(['True', 'False'])], "Cathodoluminescence": [PropertyTemplate, CategoricalBounds(['True', 'False'])], "Mechanical luminescence": [PropertyTemplate, CategoricalBounds(['True', 'False'])], "Photoluminescence": [PropertyTemplate, CategoricalBounds(['True', 'False'])], "Electroluminescence": [PropertyTemplate, CategoricalBounds(['True', 'False'])], "Thermoluminescence": [PropertyTemplate, CategoricalBounds(['True', 'False'])], "Morphology": [ConditionTemplate, CategoricalBounds(['Thin film', 'Bulk'])], "Electric field polarization": [ConditionTemplate, CategoricalBounds(['Parallel to A axis', 'Parallel to B axis', 'Parallel to C axis', 'Perpendicular to B axis', 'Perpendicular to C axis'])], "Phase": [ConditionTemplate, CategoricalBounds(['A', 'B', 'B1', 'B2', 'Fused quartz', 'Natural diamond', 'Rutile', 'Sapphire', 'Synthetic quartz'])], "Crystal system": [ConditionTemplate, CategoricalBounds(['Cubic', 'Hexagonal', 'Orthorhombic', 'Tetragonal', 'Trigonal'])], "Transition": [ConditionTemplate, CategoricalBounds(['Direct', 'Excitonic', 'Indirect'])], "Bands": [ConditionTemplate, CategoricalBounds(['G1 to X1', 'G15 to G1', 'G15 to X1', 'G25 to G1', 'G25 to G12', 'G25 to G15', 'G6 to G8', 'G8 to G6+', 'L6+ to L6-'])] } for (name, (typ, bounds)) in attribute_feed.items(): assert name not in tmpl tmpl[name] = typ(name=name, bounds=bounds, uids={template_scope: name}, tags=['citrine::demo::template::attribute'] ) # Object Templates object_feed = { "Sample preparation": [ ProcessTemplate, dict() ], "Chemical": [ MaterialTemplate, {"properties": [tmpl["Formula"]]} ], "Band gap measurement": [ MeasurementTemplate, {"properties": [tmpl["Band gap"], tmpl["Temperature derivative of band gap"], tmpl["Color"], tmpl["Lasing"], tmpl["Cathodoluminescence"], tmpl["Mechanical luminescence"], tmpl["Photoluminescence"], tmpl["Electroluminescence"], tmpl["Thermoluminescence"] ], "conditions": [tmpl["Temperature"], tmpl["Crystallinity"], tmpl["Morphology"], tmpl["Electric field polarization"], tmpl["Phase"], tmpl["Crystal system"], tmpl["Transition"], tmpl["Bands"] ] } ], } for (name, (typ, kw_args)) in object_feed.items(): assert name not in tmpl tmpl[name] = typ(name=name, uids={template_scope: name}, tags=['citrine::demo::template::object'], **kw_args) return tmpl
def test_incompatible_types(): """Make sure that incompatible types aren't contained or validated.""" int_bounds = IntegerBounds(0, 1) assert not int_bounds.contains(RealBounds(0.0, 1.0, ''))
XOR(name="terminal name or sample_type", headers=["Root", "Info"], variables=[ RootInfo(name="terminal name", headers=["Root", "Name"], field="name"), RootInfo(name="terminal name", headers=["Root", "Sample Type"], field="sample_type") ]), AttributeByTemplate(name="density", headers=["density"], template=LinkByUID(scope="templates", id="density"), attribute_constraints=[[ LinkByUID(scope="templates", id="density"), RealBounds(0, 100, "g/cm**3") ]]), AttributeByTemplateAfterProcessTemplate( name="density", headers=["density"], attribute_template=LinkByUID(scope="template", id="density"), process_template=LinkByUID(scope="template", id="process")), AttributeByTemplateAndObjectTemplate( name="density", headers=["density"], attribute_template=LinkByUID(scope="template", id="density"), object_template=LinkByUID(scope="template", id="object")), AttributeInOutput( name="density", headers=["density"], attribute_template=LinkByUID(scope="template", id="density"),
def test_get_object_id_from_data_concepts_id_is_none(): template = ConditionTemplate(name='test', bounds=RealBounds(0.0, 1.0, '')) template.uids = {'id': None} with pytest.raises(ValueError): get_object_id(template)
def test_get_object_id_from_data_concepts(): uid = str(uuid.uuid4()) template = ConditionTemplate(name='test', bounds=RealBounds(0.0, 1.0, ''), uids={'id': uid}) assert uid == get_object_id(template)
from citrine.resources.process_template import ProcessTemplate, ProcessTemplateCollection from citrine.resources.property_template import PropertyTemplate, PropertyTemplateCollection from citrine.resources.response import Response from citrine.resources.workflow_executions import WorkflowExecution, WorkflowExecutionStatus arbitrary_uuid = uuid.uuid4() resource_string_data = [ (IngredientRun, {}, "<Ingredient run None>"), (IngredientSpec, {'name': 'foo'}, "<Ingredient spec 'foo'>"), (MaterialSpec, {'name': 'foo'}, "<Material spec 'foo'>"), (MaterialTemplate, {'name': 'foo'}, "<Material template 'foo'>"), (MeasurementRun, {'name': 'foo'}, "<Measurement run 'foo'>"), (MeasurementSpec, {'name': 'foo'}, "<Measurement spec 'foo'>"), (MeasurementTemplate, {'name': 'foo'}, "<Measurement template 'foo'>"), (ParameterTemplate, {'name': 'foo', 'bounds': RealBounds(0, 1, '')}, "<Parameter template 'foo'>"), (ProcessRun, {'name': 'foo'}, "<Process run 'foo'>"), (ProcessSpec, {'name': 'foo'}, "<Process spec 'foo'>"), (ProcessTemplate, {'name': 'foo'}, "<Process template 'foo'>"), (PropertyTemplate, {'name': 'foo', 'bounds': RealBounds(0, 1, '')}, "<Property template 'foo'>"), (ConditionTemplate, {'name': 'foo', 'bounds': RealBounds(0, 1, '')}, "<Condition template 'foo'>"), (Response, {'status_code': 200}, "<Response '200'>"), (WorkflowExecution, {'uid': arbitrary_uuid, 'project_id': arbitrary_uuid, 'workflow_id': arbitrary_uuid, 'version_number': 1}, "<WorkflowExecution '{}'>".format(arbitrary_uuid)), (WorkflowExecutionStatus, {'status': 'Failed', 'session': None}, "<WorkflowExecutionStatus 'Failed'>"), ] resource_type_data = [ (IngredientRunCollection, IngredientRun), (IngredientSpecCollection, IngredientSpec),