コード例 #1
0
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
コード例 #2
0
 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)
コード例 #3
0
    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
コード例 #4
0
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))
コード例 #5
0
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))