def reaction() -> reaction_pb2.Reaction: resolver = units.UnitResolver() reaction = reaction_pb2.Reaction() reaction.setup.is_automated = True reaction.inputs["dummy_input"].components.add().CopyFrom( message_helpers.build_compound( name="n-hexane", smiles="CCCCCC", role="reactant", amount="1 milliliters", ) ) reaction.inputs["dummy_input"].components.add().CopyFrom( message_helpers.build_compound( name="THF", smiles="C1OCCC1", role="solvent", amount="40 liters", ) ) reaction.inputs["dummy_input2"].components.add().CopyFrom( message_helpers.build_compound( name="Pd", smiles="[Pd]", role="catalyst", amount="catalytic", ) ) reaction.conditions.pressure.atmosphere.type = reaction_pb2.PressureConditions.Atmosphere.OXYGEN reaction.conditions.stirring.rate.rpm = 100 reaction.conditions.temperature.control.type = reaction_pb2.TemperatureConditions.TemperatureControl.OIL_BATH reaction.conditions.temperature.setpoint.CopyFrom( reaction_pb2.Temperature(value=100, units=reaction_pb2.Temperature.CELSIUS) ) outcome = reaction.outcomes.add() outcome.reaction_time.CopyFrom(resolver.resolve("40 minutes")) outcome.products.add().identifiers.extend( message_helpers.build_compound(name="hexanone", smiles="CCCCC(=O)C").identifiers ) yield reaction
def setUp(self): super().setUp() self.test_subdirectory = tempfile.mkdtemp(dir=flags.FLAGS.test_tmpdir) self._resolver = units.UnitResolver() reaction = reaction_pb2.Reaction() reaction.setup.is_automated = True reaction.inputs['dummy_input'].components.add().CopyFrom( message_helpers.build_compound( name='n-hexane', smiles='CCCCCC', role='reactant', amount='1 milliliters', )) reaction.inputs['dummy_input'].components.add().CopyFrom( message_helpers.build_compound( name='THF', smiles='C1OCCC1', role='solvent', amount='40 liters', )) reaction.conditions.pressure.atmosphere.type = ( reaction_pb2.PressureConditions.Atmosphere.OXYGEN) reaction.conditions.stirring.rate.rpm = 100 reaction.conditions.temperature.control.type = ( reaction_pb2.TemperatureConditions.TemperatureControl.OIL_BATH) reaction.conditions.temperature.setpoint.CopyFrom( reaction_pb2.Temperature(value=100, units=reaction_pb2.Temperature.CELSIUS)) outcome = reaction.outcomes.add() outcome.reaction_time.CopyFrom(self._resolver.resolve('40 minutes')) outcome.products.add().identifiers.extend( message_helpers.build_compound( name='hexanone', smiles='CCCCC(=O)C', ).identifiers) reaction.reaction_id = 'dummy_reaction_id' self._reaction = reaction self._input = os.path.join(self.test_subdirectory, 'reaction.pbtxt') message_helpers.write_message(self._reaction, self._input)
def setUp(self): super().setUp() self._resolver = units.UnitResolver() reaction = reaction_pb2.Reaction() reaction.setup.is_automated = reaction_pb2.Boolean.TRUE reaction.inputs['dummy_input'].components.add().CopyFrom( message_helpers.build_compound( name='n-hexane', smiles='CCCCCC', role='reactant', amount='1 milliliters', )) reaction.inputs['dummy_input'].components.add().CopyFrom( message_helpers.build_compound( name='C1OCCC1', smiles='THF', role='solvent', amount='40 liters', )) reaction.conditions.pressure.atmosphere.type = ( reaction_pb2.PressureConditions.Atmosphere.OXYGEN) reaction.conditions.stirring.rate.rpm = 100 reaction.conditions.temperature.control.type = ( reaction_pb2.TemperatureConditions.TemperatureControl.OIL_BATH) reaction.conditions.temperature.setpoint.CopyFrom( reaction_pb2.Temperature(value=100, units=reaction_pb2.Temperature.CELSIUS)) outcome = reaction.outcomes.add() outcome.reaction_time.CopyFrom(self._resolver.resolve('40 minutes')) outcome.products.add().compound.CopyFrom( message_helpers.build_compound( name='hexanone', smiles='CCCCC(=O)C', role='product', )) reaction.reaction_id = 'dummy_reaction_id' self._reaction = reaction
class ValidationsTest(parameterized.TestCase, absltest.TestCase): def setUp(self): super().setUp() # Redirect warning messages to stdout so they can be filtered from the # other test output. self._showwarning = warnings.showwarning # pylint: disable=too-many-arguments def _showwarning(message, category, filename, lineno, file=None, line=None): del file # Unused. self._showwarning(message=message, category=category, filename=filename, lineno=lineno, file=sys.stdout, line=line) # pylint: enable=too-many-arguments warnings.showwarning = _showwarning def tearDown(self): super().tearDown() # Restore the original showwarning. warnings.showwarning = self._showwarning def _run_validation(self, message, **kwargs): original = type(message)() original.CopyFrom(message) output = validations.validate_message(message, **kwargs) # Verify that `message` is unchanged by the validation process. self.assertEqual(original, message) return output @parameterized.named_parameters( ('volume', reaction_pb2.Volume(value=15.0, units=reaction_pb2.Volume.MILLILITER)), ('time', reaction_pb2.Time(value=24, units=reaction_pb2.Time.HOUR)), ('mass', reaction_pb2.Mass(value=32.1, units=reaction_pb2.Mass.GRAM)), ) def test_units(self, message): self.assertEmpty(self._run_validation(message)) @parameterized.named_parameters( ('neg volume', reaction_pb2.Volume( value=-15.0, units=reaction_pb2.Volume.MILLILITER), 'non-negative'), ('neg time', reaction_pb2.Time( value=-24, units=reaction_pb2.Time.HOUR), 'non-negative'), ('neg mass', reaction_pb2.Mass(value=-32.1, units=reaction_pb2.Mass.GRAM), 'non-negative'), ('no units', reaction_pb2.FlowRate(value=5), 'units'), ('percentage out of range', reaction_pb2.Percentage(value=200), 'between'), ('low temperature', reaction_pb2.Temperature( value=-5, units='KELVIN'), 'between'), ('low temperature 2', reaction_pb2.Temperature(value=-500, units='CELSIUS'), 'between'), ) def test_units_should_fail(self, message, expected_error): with self.assertRaisesRegex(validations.ValidationError, expected_error): self._run_validation(message) def test_orcid(self): message = reaction_pb2.Person(orcid='0000-0001-2345-678X') self.assertEmpty(self._run_validation(message)) def test_orcid_should_fail(self): message = reaction_pb2.Person(orcid='abcd-0001-2345-678X') with self.assertRaisesRegex(validations.ValidationError, 'Invalid'): self._run_validation(message) def test_reaction(self): message = reaction_pb2.Reaction() with self.assertRaisesRegex(validations.ValidationError, 'reaction input'): self._run_validation(message) def test_reaction_recursive(self): message = reaction_pb2.Reaction() # Reactions must have at least one input with self.assertRaisesRegex(validations.ValidationError, 'reaction input'): self._run_validation(message, recurse=False) dummy_input = message.inputs['dummy_input'] # Reactions must have at least one outcome with self.assertRaisesRegex(validations.ValidationError, 'reaction outcome'): self._run_validation(message, recurse=False) outcome = message.outcomes.add() self.assertEmpty(self._run_validation(message, recurse=False)) # Inputs must have at least one component with self.assertRaisesRegex(validations.ValidationError, 'component'): self._run_validation(message) dummy_component = dummy_input.components.add() # Components must have at least one identifier with self.assertRaisesRegex(validations.ValidationError, 'identifier'): self._run_validation(message) dummy_component.identifiers.add(type='CUSTOM') # Custom identifiers must have details specified with self.assertRaisesRegex(validations.ValidationError, 'details'): self._run_validation(message) dummy_component.identifiers[0].details = 'custom_identifier' dummy_component.identifiers[0].value = 'custom_value' # Components of reaction inputs must have a defined amount with self.assertRaisesRegex(validations.ValidationError, 'require an amount'): self._run_validation(message) dummy_component.mass.value = 1 dummy_component.mass.units = reaction_pb2.Mass.GRAM # Reactions must have defined products or conversion with self.assertRaisesRegex(validations.ValidationError, 'products or conversion'): self._run_validation(message) outcome.conversion.value = 75 # If converseions are defined, must have limiting reagent flag with self.assertRaisesRegex(validations.ValidationError, 'is_limiting'): self._run_validation(message) dummy_component.is_limiting = True self.assertEmpty(self._run_validation(message)) # If an analysis uses an internal standard, a component must have # an INTERNAL_STANDARD reaction role outcome.analyses['dummy_analysis'].CopyFrom( reaction_pb2.ReactionAnalysis(type='CUSTOM', details='test', uses_internal_standard=True)) with self.assertRaisesRegex(validations.ValidationError, 'INTERNAL_STANDARD'): self._run_validation(message) # Assigning internal standard role to input should resolve the error message_input_istd = reaction_pb2.Reaction() message_input_istd.CopyFrom(message) message_input_istd.inputs['dummy_input'].components[ 0].reaction_role = ( reaction_pb2.Compound.ReactionRole.INTERNAL_STANDARD) self.assertEmpty(self._run_validation(message_input_istd)) # Assigning internal standard role to workup should resolve the error message_workup_istd = reaction_pb2.Reaction() message_workup_istd.CopyFrom(message) workup = message_workup_istd.workup.add(type='CUSTOM', details='test') istd = workup.input.components.add() istd.identifiers.add(type='SMILES', value='CCO') istd.mass.value = 1 istd.mass.units = reaction_pb2.Mass.GRAM istd.reaction_role = istd.ReactionRole.INTERNAL_STANDARD self.assertEmpty(self._run_validation(message_workup_istd)) def test_reaction_recursive_noraise_on_error(self): message = reaction_pb2.Reaction() message.inputs['dummy_input'].components.add() errors = self._run_validation(message, raise_on_error=False) expected = [ 'Compounds must have at least one identifier', "Reaction input's components require an amount", 'Reactions should have at least 1 reaction outcome', ] self.assertEqual(errors, expected) def test_datetimes(self): message = reaction_pb2.ReactionProvenance() message.experiment_start.value = '11 am' message.record_created.time.value = '10 am' with self.assertRaisesRegex(validations.ValidationError, 'after'): self._run_validation(message) message.record_created.time.value = '11:15 am' self.assertEmpty(self._run_validation(message)) def test_reaction_id(self): message = reaction_pb2.Reaction() _ = message.inputs['test'] message.outcomes.add() message.reaction_id = 'ord-c0bbd41f095a44a78b6221135961d809' self.assertEmpty(self._run_validation(message, recurse=False)) @parameterized.named_parameters( ('too short', 'ord-c0bbd41f095a4'), ('too long', 'ord-c0bbd41f095a4c0bbd41f095a4c0bbd41f095a4'), ('bad prefix', 'foo-c0bbd41f095a44a78b6221135961d809'), ('bad capitalization', 'ord-C0BBD41F095A44A78B6221135961D809'), ('bad characters', 'ord-h0bbd41f095a44a78b6221135961d809'), ('bad characters 2', 'ord-notARealId'), ) def test_bad_reaction_id(self, reaction_id): message = reaction_pb2.Reaction(reaction_id=reaction_id) _ = message.inputs['test'] message.outcomes.add() with self.assertRaisesRegex(validations.ValidationError, 'malformed'): self._run_validation(message, recurse=False) def test_data(self): message = reaction_pb2.Data() with self.assertRaisesRegex(validations.ValidationError, 'requires one of'): self._run_validation(message) message.bytes_value = b'test data' with self.assertRaisesRegex(validations.ValidationError, 'format is required'): self._run_validation(message) message.string_value = 'test data' self.assertEmpty(self._run_validation(message)) def test_dataset_bad_reaction_id(self): message = dataset_pb2.Dataset(reaction_ids=['foo']) with self.assertRaisesRegex(validations.ValidationError, 'malformed'): self._run_validation(message) def test_dataset_records_and_ids(self): message = dataset_pb2.Dataset( reactions=[reaction_pb2.Reaction()], reaction_ids=['ord-c0bbd41f095a44a78b6221135961d809']) with self.assertRaisesRegex(validations.ValidationError, 'not both'): self._run_validation(message, recurse=False) def test_dataset_bad_id(self): message = dataset_pb2.Dataset(reactions=[reaction_pb2.Reaction()], dataset_id='foo') with self.assertRaisesRegex(validations.ValidationError, 'malformed'): self._run_validation(message, recurse=False) def test_dataset_example(self): message = dataset_pb2.DatasetExample() with self.assertRaisesRegex(validations.ValidationError, 'description is required'): self._run_validation(message) message.description = 'test example' with self.assertRaisesRegex(validations.ValidationError, 'url is required'): self._run_validation(message) message.url = 'example.com' with self.assertRaisesRegex(validations.ValidationError, 'created is required'): self._run_validation(message) message.created.time.value = '11 am' self.assertEmpty(self._run_validation(message))
class ValidationsTest(parameterized.TestCase, absltest.TestCase): @parameterized.named_parameters( ('volume', reaction_pb2.Volume(value=15.0, units=reaction_pb2.Volume.MILLILITER)), ('time', reaction_pb2.Time(value=24, units=reaction_pb2.Time.HOUR)), ('mass', reaction_pb2.Mass(value=32.1, units=reaction_pb2.Mass.GRAM)), ) def test_units(self, message): self.assertEmpty(validations.validate_message(message)) @parameterized.named_parameters( ('neg volume', reaction_pb2.Volume( value=-15.0, units=reaction_pb2.Volume.MILLILITER), 'non-negative'), ('neg time', reaction_pb2.Time( value=-24, units=reaction_pb2.Time.HOUR), 'non-negative'), ('neg mass', reaction_pb2.Mass(value=-32.1, units=reaction_pb2.Mass.GRAM), 'non-negative'), ('no units', reaction_pb2.FlowRate(value=5), 'units'), ('percentage out of range', reaction_pb2.Percentage(value=200), 'between'), ('low temperature', reaction_pb2.Temperature( value=-5, units='KELVIN'), 'between'), ('low temperature 2', reaction_pb2.Temperature(value=-500, units='CELSIUS'), 'between'), ) def test_units_should_fail(self, message, expected_error): with self.assertRaisesRegex(validations.ValidationError, expected_error): validations.validate_message(message) def test_orcid(self): message = reaction_pb2.Person(orcid='0000-0001-2345-678X') self.assertEmpty(validations.validate_message(message)) def test_orcid_should_fail(self): message = reaction_pb2.Person(orcid='abcd-0001-2345-678X') with self.assertRaisesRegex(validations.ValidationError, 'Invalid'): validations.validate_message(message) def test_reaction(self): message = reaction_pb2.Reaction() with self.assertRaisesRegex(validations.ValidationError, 'reaction input'): validations.validate_message(message) def test_reaction_recursive(self): message = reaction_pb2.Reaction() # Reactions must have at least one input with self.assertRaisesRegex(validations.ValidationError, 'reaction input'): validations.validate_message(message, recurse=False) dummy_input = message.inputs['dummy_input'] # Reactions must have at least one outcome with self.assertRaisesRegex(validations.ValidationError, 'reaction outcome'): validations.validate_message(message, recurse=False) outcome = message.outcomes.add() self.assertEmpty(validations.validate_message(message, recurse=False)) # Inputs must have at least one component with self.assertRaisesRegex(validations.ValidationError, 'component'): validations.validate_message(message) dummy_component = dummy_input.components.add() # Components must have at least one identifier with self.assertRaisesRegex(validations.ValidationError, 'identifier'): validations.validate_message(message) dummy_component.identifiers.add(type='CUSTOM') # Custom identifiers must have details specified with self.assertRaisesRegex(validations.ValidationError, 'details'): validations.validate_message(message) dummy_component.identifiers[0].details = 'custom_identifier' dummy_component.identifiers[0].value = 'custom_value' # Components of reaction inputs must have a defined amount with self.assertRaisesRegex(validations.ValidationError, 'require an amount'): validations.validate_message(message) dummy_component.mass.value = 1 dummy_component.mass.units = reaction_pb2.Mass.GRAM # Reactions must have defined products or conversion with self.assertRaisesRegex(validations.ValidationError, 'products or conversion'): validations.validate_message(message) outcome.conversion.value = 75 # If converseions are defined, must have limiting reagent flag with self.assertRaisesRegex(validations.ValidationError, 'is_limiting'): validations.validate_message(message) dummy_component.is_limiting = True self.assertEmpty(validations.validate_message(message)) # If an analysis uses an internal standard, a component must have # an INTERNAL_STANDARD reaction role outcome.analyses['dummy_analysis'].uses_internal_standard = True with self.assertRaisesRegex(validations.ValidationError, 'INTERNAL_STANDARD'): validations.validate_message(message) # Assigning internal standard role to input should resolve the error message_input_istd = reaction_pb2.Reaction() message_input_istd.CopyFrom(message) message_input_istd.inputs['dummy_input'].components[ 0].reaction_role = ( reaction_pb2.Compound.ReactionRole.INTERNAL_STANDARD) self.assertEmpty(validations.validate_message(message_input_istd)) # Assigning internal standard role to workup should resolve the error message_workup_istd = reaction_pb2.Reaction() message_workup_istd.CopyFrom(message) workup = message_workup_istd.workup.add() istd = workup.components.add() istd.identifiers.add(type='SMILES', value='CCO') istd.mass.value = 1 istd.mass.units = reaction_pb2.Mass.GRAM istd.reaction_role = istd.ReactionRole.INTERNAL_STANDARD self.assertEmpty(validations.validate_message(message_workup_istd)) def test_reaction_recursive_noraise_on_error(self): message = reaction_pb2.Reaction() message.inputs['dummy_input'].components.add() errors = validations.validate_message(message, raise_on_error=False) expected = [ 'Compounds must have at least one identifier', "Reaction input's components require an amount", 'Reactions should have at least 1 reaction outcome', ] self.assertEqual(errors, expected) def test_datetimes(self): message = reaction_pb2.ReactionProvenance() message.experiment_start.value = '11 am' message.record_created.time.value = '10 am' with self.assertRaisesRegex(validations.ValidationError, 'after'): validations.validate_message(message) message.record_created.time.value = '11:15 am' self.assertEmpty(validations.validate_message(message)) def test_record_id(self): message = reaction_pb2.ReactionProvenance() message.record_created.time.value = '10 am' message.record_id = 'ord-c0bbd41f095a44a78b6221135961d809' self.assertEmpty(validations.validate_message(message)) @parameterized.named_parameters( ('too short', 'ord-c0bbd41f095a4'), ('too long', 'ord-c0bbd41f095a4c0bbd41f095a4c0bbd41f095a4'), ('bad prefix', 'foo-c0bbd41f095a44a78b6221135961d809'), ('bad capitalization', 'ord-C0BBD41F095A44A78B6221135961D809'), ('bad characters', 'ord-h0bbd41f095a44a78b6221135961d809'), ('bad characters 2', 'ord-notARealId'), ) def test_bad_record_id(self, record_id): message = reaction_pb2.ReactionProvenance() message.record_created.time.value = '10 am' message.record_id = record_id with self.assertRaisesRegex(validations.ValidationError, 'malformed'): validations.validate_message(message) def test_compound_name_resolver(self): message = reaction_pb2.Compound() identifier = message.identifiers.add() identifier.type = identifier.NAME identifier.value = 'aspirin' validations.validate_message(message) # Message is modified in place. self.assertEqual( message.identifiers[1], reaction_pb2.CompoundIdentifier( type='SMILES', value='CC(=O)OC1=CC=CC=C1C(=O)O', details='NAME resolved by PubChem')) @absltest.skipIf(Chem is None, 'no rdkit') def test_compound_rdkit_binary(self): mol = Chem.MolFromSmiles('CC(=O)OC1=CC=CC=C1C(=O)O') message = reaction_pb2.Compound() identifier = message.identifiers.add() identifier.type = identifier.SMILES identifier.value = Chem.MolToSmiles(mol) validations.validate_message(message) # Message is modified in place. self.assertEqual( message.identifiers[1], reaction_pb2.CompoundIdentifier(type='RDKIT_BINARY', bytes_value=mol.ToBinary())) def test_data(self): message = reaction_pb2.Data() with self.assertRaisesRegex(validations.ValidationError, 'requires one of'): validations.validate_message(message) message.bytes_value = b'test data' with self.assertRaisesRegex(validations.ValidationError, 'format is required'): validations.validate_message(message) message.value = 'test data' self.assertEmpty(validations.validate_message(message))