def __init__(self, ff_file_name, debug=False): from foyer.forcefield import preprocess_forcefield_files try: preprocessed_ff_file_name = preprocess_forcefield_files( [ff_file_name]) ff_tree = etree.parse(preprocessed_ff_file_name[0]) self.validate_xsd(ff_tree) self.atom_type_names = ff_tree.xpath( "/ForceField/AtomTypes/Type/@name") self.atom_types = ff_tree.xpath("/ForceField/AtomTypes/Type") self.validate_class_type_exclusivity(ff_tree) # Loading forcefield should succeed, because XML can be parsed and # basics have been validated. from foyer.forcefield import Forcefield self.smarts_parser = Forcefield(preprocessed_ff_file_name, validation=False).parser finally: for ff_file_name in preprocessed_ff_file_name: os.remove(ff_file_name) self.validate_smarts(debug=debug) self.validate_overrides()
def test_structure_to_hoomdsimulation(self, ethane): import hoomd from foyer.forcefield import Forcefield from mbuild.formats.hoomd_simulation import create_hoomd_simulation ff = Forcefield(name="oplsaa") structure = ff.apply(ethane) sim = hoomd.context.SimulationContext() with sim: create_hoomd_simulation(structure, 2.5) sim_forces = hoomd.context.current.forces pair_force = import_("hoomd.md.pair") charge_force = import_("hoomd.md.charge") special_pair_force = import_("hoomd.md.special_pair") bond_force = import_("hoomd.md.bond") angle_force = import_("hoomd.md.angle") dihedral_force = import_("hoomd.md.dihedral") assert isinstance(sim_forces[0], pair_force.lj) assert isinstance(sim_forces[1], charge_force.pppm) assert isinstance(sim_forces[2], pair_force.ewald) assert isinstance(sim_forces[3], special_pair_force.lj) assert isinstance(sim_forces[4], special_pair_force.coulomb) assert isinstance(sim_forces[5], bond_force.harmonic) assert isinstance(sim_forces[6], angle_force.harmonic) assert isinstance(sim_forces[7], dihedral_force.opls)
def test_hoomdsimulation_restart(self): import gsd.hoomd import hoomd from foyer.forcefield import Forcefield from mbuild.formats.hoomd_simulation import create_hoomd_simulation box = mb.Compound() box.add(mb.Compound(name="Ar", pos=[1, 1, 1])) box.add(mb.Compound(name="Ar", pos=[1, 1, 1])) ff = Forcefield(forcefield_files=get_fn("lj.xml")) structure = ff.apply(box) structure.box = [10, 10, 10, 90, 90, 90] sim = hoomd.context.SimulationContext() with sim: hoomd_obj, ref_vals = create_hoomd_simulation( structure, 2.5, restart=get_fn("restart.gsd")) sim_forces = hoomd.context.current.forces pair_force = import_("hoomd.md.pair") assert isinstance(sim_forces[0], pair_force.lj) snap = hoomd_obj[0] with gsd.hoomd.open(get_fn("restart.gsd")) as f: rsnap = f[0] assert np.array_equal(snap.particles.position, rsnap.particles.position)
def test_param_structure_to_snapshot(self, ethane): from foyer.forcefield import Forcefield from mbuild.formats.hoomd_snapshot import to_hoomdsnapshot ff = Forcefield(name="oplsaa") structure = ff.apply(ethane) snap, _ = to_hoomdsnapshot(structure) assert snap.particles.N == 8 assert snap.bonds.N == 7 assert snap.angles.N == 12 assert snap.dihedrals.N == 9 assert snap.pairs.N == 9
def __init__(self, ff_file_name): from foyer.forcefield import preprocess_forcefield_files preprocessed_ff_file_name = preprocess_forcefield_files([ff_file_name]) ff_tree = etree.parse(preprocessed_ff_file_name[0]) self.validate_xsd(ff_tree) # Loading forcefield should succeed, because XML can be parsed and # basics have been validated. from foyer.forcefield import Forcefield self.smarts_parser = Forcefield(preprocessed_ff_file_name, validation=False).parser self.validate_smarts(ff_tree)
def test_lj_to_hoomd_forcefield(self): import hoomd from foyer.forcefield import Forcefield from mbuild.formats.hoomd_forcefield import create_hoomd_forcefield box = mb.Compound() box.add(mb.Compound(name="Ar", pos=[1, 1, 1])) box.add(mb.Compound(name="Ar", pos=[1, 1, 1])) ff = Forcefield(forcefield_files=get_fn("lj.xml")) structure = ff.apply(box) structure.box = [10, 10, 10, 90, 90, 90] snapshot, forces, ref_values = create_hoomd_forcefield(structure, 2.5) assert isinstance(forces[0], hoomd.md.pair.LJ)
def test_hexa_coordinated(): ff = Forcefield(forcefield_files=get_fn('pf6.xml')) mol2 = pmd.load_file(get_fn('pf6.mol2'), structure=True) pf6 = ff.apply(mol2) types = [a.type for a in pf6.atoms] assert types.count('P') == 1 assert types.count('F1') == 2 assert types.count('F2') == 2 assert types.count('F3') == 2 assert len(pf6.bonds) == 6 assert all(bond.type for bond in pf6.bonds) assert len(pf6.angles) == 15 assert all(angle.type for angle in pf6.angles)
def test_hexa_coordinated(self): ff = Forcefield(forcefield_files=get_fn("pf6.xml")) mol2 = pmd.load_file(get_fn("pf6.mol2"), structure=True) pf6 = ff.apply(mol2) types = [a.type for a in pf6.atoms] assert types.count("P") == 1 assert types.count("F1") == 2 assert types.count("F2") == 2 assert types.count("F3") == 2 assert len(pf6.bonds) == 6 assert all(bond.type for bond in pf6.bonds) assert len(pf6.angles) == 15 assert all(angle.type for angle in pf6.angles)
def test_lj_to_hoomdsimulation(self): import hoomd from foyer.forcefield import Forcefield from mbuild.formats.hoomd_simulation import create_hoomd_simulation box = mb.Compound() box.add(mb.Compound(name="Ar", pos=[1, 1, 1])) box.add(mb.Compound(name="Ar", pos=[1, 1, 1])) ff = Forcefield(forcefield_files=get_fn("lj.xml")) structure = ff.apply(box) structure.box = [10, 10, 10, 90, 90, 90] sim = hoomd.context.SimulationContext() with sim: create_hoomd_simulation(structure, 2.5) sim_forces = hoomd.context.current.forces pair_force = import_("hoomd.md.pair") assert isinstance(sim_forces[0], pair_force.lj)
def test_structure_to_hoomd_forcefield(self, ethane): import hoomd from foyer.forcefield import Forcefield from mbuild.formats.hoomd_forcefield import create_hoomd_forcefield ff = Forcefield(name="oplsaa") structure = ff.apply(ethane) snapshot, forces, ref_values = create_hoomd_forcefield(structure, 2.5) assert isinstance(forces[0], hoomd.md.pair.LJ) assert isinstance(forces[1], hoomd.md.pair.Ewald) assert isinstance(forces[2], hoomd.md.long_range.pppm.Coulomb) assert isinstance(forces[3], hoomd.md.special_pair.LJ) assert isinstance(forces[4], hoomd.md.special_pair.Coulomb) assert isinstance(forces[5], hoomd.md.bond.Harmonic) assert isinstance(forces[6], hoomd.md.angle.Harmonic) assert isinstance(forces[7], hoomd.md.dihedral.OPLS)
def test_atom_typing( self, openff_topology_graph, gmso_topology_graph, parmed_topology_graph ): # ToDo: More robust testing for atomtyping opls = Forcefield(name="oplsaa") openff_typemap = find_atomtypes(openff_topology_graph, forcefield=opls) gmso_typemap = find_atomtypes(gmso_topology_graph, forcefield=opls) parmed_typemap = find_atomtypes(parmed_topology_graph, forcefield=opls) assert openff_typemap assert gmso_typemap assert parmed_typemap
def __init__(self, ff_file_name, debug=False): from foyer.forcefield import preprocess_forcefield_files preprocessed_ff_file_name = preprocess_forcefield_files([ff_file_name]) ff_tree = etree.parse(preprocessed_ff_file_name[0]) self.validate_xsd(ff_tree) self.atom_type_names = ff_tree.xpath('/ForceField/AtomTypes/Type/@name') self.atom_types = ff_tree.xpath('/ForceField/AtomTypes/Type') self.validate_class_type_exclusivity(ff_tree) # Loading forcefield should succeed, because XML can be parsed and # basics have been validated. from foyer.forcefield import Forcefield self.smarts_parser = Forcefield(preprocessed_ff_file_name, validation=False).parser self.validate_smarts(debug=debug) self.validate_overrides()
class Validator(object): def __init__(self, ff_file_name): from foyer.forcefield import preprocess_forcefield_files preprocessed_ff_file_name = preprocess_forcefield_files([ff_file_name]) ff_tree = etree.parse(preprocessed_ff_file_name[0]) self.validate_xsd(ff_tree) # Loading forcefield should succeed, because XML can be parsed and # basics have been validated. from foyer.forcefield import Forcefield self.smarts_parser = Forcefield(preprocessed_ff_file_name, validation=False).parser self.validate_smarts(ff_tree) # TODO: figure out what exceptions are raised, and provide good error messages def validate_xsd(self, ff_tree, xsd_file=None): if xsd_file is None: xsd_file = join(split(abspath(__file__))[0], 'forcefields', 'ff.xsd') xmlschema_doc = etree.parse(xsd_file) xmlschema = etree.XMLSchema(xmlschema_doc) try: xmlschema.assertValid(ff_tree) except DocumentInvalid as ex: message = ex.error_log.last_error.message line = ex.error_log.last_error.line # rewrite error message for constraint violation if ex.error_log.last_error.type_name == "SCHEMAV_CVC_IDC": if "missing_atom_type_in_nonbonded" in message: atomtype = message[message.find("[") + 1:message.find("]")] raise ValidationError( "Atom type {} is found in NonbondedForce at line {} but" " undefined in AtomTypes".format(atomtype, line), ex, line) elif "nonunique_atomtype_name" in message: atomtype = message[message.find("[") + 1:message.find("]")] raise ValidationError( "Atom type {} is defined a second time at line {}".format( atomtype, line), ex, line) raise def validate_smarts(self, ff_tree): results = ff_tree.xpath('/ForceField/AtomTypes/Type') atom_types = ff_tree.xpath('/ForceField/AtomTypes/Type/@name') missing_smarts = [] for r in results: smarts_string = r.attrib.get('def') name = r.attrib['name'] if smarts_string is None: missing_smarts.append(name) continue # make sure smarts string can be parsed try: self.smarts_parser.parse(smarts_string) except ParseError as ex: if " col " in ex.args[0]: column = ex.args[0][ex.args[0].find(" col ") + 5:].strip() column = " at character {} of {}".format(column, smarts_string) else: column = "" raise ValidationError("Malformed SMARTS string{} on line {}".format( column, r.sourceline), ex, r.sourceline) # make sure referenced labels exist smarts_graph = SMARTSGraph(smarts_string, parser=self.smarts_parser, name=name, overrides=r.attrib.get('overrides')) for atom_expr in nx.get_node_attributes(smarts_graph, 'atom').values(): labels = atom_expr.select('has_label') for label in labels: atom_type = label.tail[0][1:] if atom_type not in atom_types: raise ValidationError( "Reference to undefined atomtype {} in SMARTS string" " '{}' at line {}".format( atom_type, r.attrib['def'], r.sourceline), None, r.sourceline) warn("The following atom types do not have smarts definitions: {}".format(', '.join(missing_smarts)), ValidationWarning) def validate_overrides(self, ff_tree): results = ff_tree.xpath('/ForceField/AtomTypes/Type[@name]') for r in results: smarts_string = r.attrib['def'] print(smarts_string) print(r.sourceline) # make sure smarts string can be parsed self.smarts.parse(smarts_string)
class Validator(object): def __init__(self, ff_file_name, debug=False): from foyer.forcefield import preprocess_forcefield_files preprocessed_ff_file_name = preprocess_forcefield_files([ff_file_name]) ff_tree = etree.parse(preprocessed_ff_file_name[0]) self.validate_xsd(ff_tree) self.atom_type_names = ff_tree.xpath('/ForceField/AtomTypes/Type/@name') self.atom_types = ff_tree.xpath('/ForceField/AtomTypes/Type') self.validate_class_type_exclusivity(ff_tree) # Loading forcefield should succeed, because XML can be parsed and # basics have been validated. from foyer.forcefield import Forcefield self.smarts_parser = Forcefield(preprocessed_ff_file_name, validation=False).parser self.validate_smarts(debug=debug) self.validate_overrides() @staticmethod def validate_xsd(ff_tree, xsd_file=None): if xsd_file is None: xsd_file = join(split(abspath(__file__))[0], 'forcefields', 'ff.xsd') xmlschema_doc = etree.parse(xsd_file) xmlschema = etree.XMLSchema(xmlschema_doc) error_texts = {'missing_atom_type_in_nonbonded': ("Atom type {} is found in NonbondedForce at line {}" " but undefined in AtomTypes"), 'nonunique_atomtype_name': "Atom type {} is defined a second time at line {}", 'atomtype_name_key': "Atom type {} is defined a second time at line {}" } def create_error(keyword, message, line): atomtype = message[message.find("[") + 1:message.find("]")] error_text = error_texts[keyword].format(atomtype, line) return ValidationError(error_text, ex, line) try: xmlschema.assertValid(ff_tree) except DocumentInvalid as ex: message = ex.error_log.last_error.message line = ex.error_log.last_error.line # rewrite error message for constraint violation if ex.error_log.last_error.type_name == "SCHEMAV_CVC_IDC": for keyword in error_texts: if keyword in message: raise create_error(keyword, message, line) else: raise ValidationError('Unhandled XML validation error. ' 'Please consider submitting a bug report.', ex, line) raise def validate_class_type_exclusivity(self, ff_tree): sections = {'HarmonicBondForce/Bond': 2, 'HarmonicAngleForce/Angle': 3, 'RBTorsionForce/Proper': 4} errors = [] for element, num_atoms in sections.items(): valid_attribs = set() for n in range(1, num_atoms + 1): valid_attribs.add('class{}'.format(n)) valid_attribs.add('type{}'.format(n)) for entry in ff_tree.xpath('/ForceField/{}'.format(element)): attribs = [valid for valid in valid_attribs if entry.attrib.get(valid) is not None] if num_atoms != len(attribs): error = ValidationError( 'Invalid number of "class" and/or "type" attributes for' ' {} at line {}'.format(element, entry.sourceline), None, entry.sourceline) errors.append(error) number_endings = Counter([a[-1] for a in attribs]) if not all(1 == x for x in number_endings.values()): error = ValidationError( 'Only one "class" or "type" attribute may be defined' ' for each atom in a bonded force. See line {}'.format(entry.sourceline), None, entry.sourceline) errors.append(error) referenced_types = [] for valid in valid_attribs: if valid.startswith('type'): atomtype = entry.attrib.get(valid) if atomtype: referenced_types.append(atomtype) for atomtype in referenced_types: if atomtype not in self.atom_type_names: error = ValidationError( 'Atom type {} is found in {} at line {} but' ' undefined in AtomTypes'.format(atomtype, element.split('/')[0], entry.sourceline), None, entry.sourceline) errors.append(error) raise_collected(errors) def validate_smarts(self, debug): missing_smarts = [] errors = [] for entry in self.atom_types: smarts_string = entry.attrib.get('def') name = entry.attrib['name'] if smarts_string is None: missing_smarts.append(name) continue # make sure smarts string can be parsed try: self.smarts_parser.parse(smarts_string) except lark.ParseError as ex: if " col " in ex.args[0]: column = ex.args[0][ex.args[0].find(" col ") + 5:].strip() column = " at character {} of {}".format(column, smarts_string) else: column = "" malformed = ValidationError( "Malformed SMARTS string{} on line {}".format(column, entry.sourceline), ex, entry.sourceline) errors.append(malformed) continue # make sure referenced labels exist smarts_graph = SMARTSGraph(smarts_string, parser=self.smarts_parser, name=name, overrides=entry.attrib.get('overrides')) for atom_expr in nx.get_node_attributes(smarts_graph, name='atom').values(): labels = atom_expr.find_data('has_label') for label in labels: atom_type = label.children[0][1:] if atom_type not in self.atom_type_names: undefined = ValidationError( "Reference to undefined atomtype '{}' in SMARTS " "string '{}' at line {}".format(atom_type, entry.attrib['def'], entry.sourceline), None, entry.sourceline) errors.append(undefined) raise_collected(errors) if missing_smarts and debug: warn("The following atom types do not have smarts definitions: {}".format( ', '.join(missing_smarts)), ValidationWarning) if missing_smarts and not debug: warn("There are {} atom types that are missing a smarts definition. " "To view the missing atom types, re-run with debug=True when " "applying the forcefield.".format(len(missing_smarts)), ValidationWarning) def validate_overrides(self): errors = [] for entry in self.atom_types: overrides = entry.attrib.get('overrides') if not overrides: continue overridden_types = [at.strip() for at in overrides.split(',') if at] for atom_type in overridden_types: if atom_type not in self.atom_type_names: undefined = ValidationError( "Reference to undefined atomtype '{}' in 'overrides' " "'{}' at line {}".format(atom_type, entry.attrib['overrides'], entry.sourceline), None, entry.sourceline) errors.append(undefined) raise_collected(errors)
class Validator(object): """Verifies formatting of force field XML.""" def __init__(self, ff_file_name, debug=False): from foyer.forcefield import preprocess_forcefield_files try: preprocessed_ff_file_name = preprocess_forcefield_files( [ff_file_name]) ff_tree = etree.parse(preprocessed_ff_file_name[0]) self.validate_xsd(ff_tree) self.atom_type_names = ff_tree.xpath( "/ForceField/AtomTypes/Type/@name") self.atom_types = ff_tree.xpath("/ForceField/AtomTypes/Type") self.validate_class_type_exclusivity(ff_tree) # Loading forcefield should succeed, because XML can be parsed and # basics have been validated. from foyer.forcefield import Forcefield self.smarts_parser = Forcefield(preprocessed_ff_file_name, validation=False).parser finally: for ff_file_name in preprocessed_ff_file_name: os.remove(ff_file_name) self.validate_smarts(debug=debug) self.validate_overrides() @staticmethod def validate_xsd(ff_tree, xsd_file=None): """Check consistency with forcefields/ff.xsd.""" if xsd_file is None: xsd_file = join( split(abspath(__file__))[0], "forcefields", "ff.xsd") xmlschema_doc = etree.parse(xsd_file) xmlschema = etree.XMLSchema(xmlschema_doc) error_texts = { "missing_atom_type_in_nonbonded": ("Atom type {} is found in NonbondedForce at line {}" " but undefined in AtomTypes"), "nonunique_atomtype_name": "Atom type {} is defined a second time at line {}", "atomtype_name_key": "Atom type {} is defined a second time at line {}", } def create_error(keyword, message, line): atomtype = message[message.find("[") + 1:message.find("]")] error_text = error_texts[keyword].format(atomtype, line) return ValidationError(error_text, ex, line) try: xmlschema.assertValid(ff_tree) except DocumentInvalid as ex: message = ex.error_log.last_error.message line = ex.error_log.last_error.line # rewrite error message for constraint violation if ex.error_log.last_error.type_name == "SCHEMAV_CVC_IDC": for keyword in error_texts: if keyword in message: raise create_error(keyword, message, line) else: raise ValidationError( "Unhandled XML validation error. " "Please consider submitting a bug report.", ex, line, ) raise def validate_class_type_exclusivity(self, ff_tree): """Assert unique bond/angle/dihedral definitions and class lengths.""" sections = { "HarmonicBondForce/Bond": 2, "HarmonicAngleForce/Angle": 3, "RBTorsionForce/Proper": 4, } errors = [] for element, num_atoms in sections.items(): valid_attribs = set() for n in range(1, num_atoms + 1): valid_attribs.add("class{}".format(n)) valid_attribs.add("type{}".format(n)) for entry in ff_tree.xpath("/ForceField/{}".format(element)): attribs = [ valid for valid in valid_attribs if entry.attrib.get(valid) is not None ] if num_atoms != len(attribs): error = ValidationError( 'Invalid number of "class" and/or "type" attributes for' " {} at line {}".format(element, entry.sourceline), None, entry.sourceline, ) errors.append(error) number_endings = Counter([a[-1] for a in attribs]) if not all(1 == x for x in number_endings.values()): error = ValidationError( 'Only one "class" or "type" attribute may be defined' " for each atom in a bonded force. See line {}".format( entry.sourceline), None, entry.sourceline, ) errors.append(error) referenced_types = [] for valid in valid_attribs: if valid.startswith("type"): atomtype = entry.attrib.get(valid) if atomtype: referenced_types.append(atomtype) for atomtype in referenced_types: if atomtype not in self.atom_type_names: error = ValidationError( "Atom type {} is found in {} at line {} but" " undefined in AtomTypes".format( atomtype, element.split("/")[0], entry.sourceline, ), None, entry.sourceline, ) errors.append(error) raise_collected(errors) def validate_smarts(self, debug): """Check SMARTS definitions for missing or non-parseable.""" missing_smarts = [] errors = [] for entry in self.atom_types: smarts_string = entry.attrib.get("def") if not smarts_string: warn("You have empty smart definition(s)", ValidationWarning) continue name = entry.attrib["name"] if smarts_string is None: missing_smarts.append(name) continue # make sure smarts string can be parsed try: self.smarts_parser.parse(smarts_string) except lark.ParseError as ex: if " col " in ex.args[0]: column = ex.args[0][ex.args[0].find(" col ") + 5:].strip() column = " at character {} of {}".format( column, smarts_string) else: column = "" malformed = ValidationError( "Malformed SMARTS string{} on line {}".format( column, entry.sourceline), ex, entry.sourceline, ) errors.append(malformed) continue # make sure referenced labels exist smarts_graph = SMARTSGraph( smarts_string, parser=self.smarts_parser, name=name, overrides=entry.attrib.get("overrides"), ) for atom_expr in nx.get_node_attributes(smarts_graph, name="atom").values(): labels = atom_expr.find_data("has_label") for label in labels: atom_type = label.children[0][1:] if atom_type not in self.atom_type_names: undefined = ValidationError( "Reference to undefined atomtype '{}' in SMARTS " "string '{}' at line {}".format( atom_type, entry.attrib["def"], entry.sourceline), None, entry.sourceline, ) errors.append(undefined) raise_collected(errors) if missing_smarts and debug: warn( "The following atom types do not have smarts definitions: {}". format(", ".join(missing_smarts)), ValidationWarning, ) if missing_smarts and not debug: warn( "There are {} atom types that are missing a smarts definition. " "To view the missing atom types, re-run with debug=True when " "applying the forcefield.".format(len(missing_smarts)), ValidationWarning, ) def validate_overrides(self): """Assert all overrides are defined elsewhere in force field.""" errors = [] for entry in self.atom_types: overrides = entry.attrib.get("overrides") if not overrides: continue overridden_types = [ at.strip() for at in overrides.split(",") if at ] for atom_type in overridden_types: if atom_type not in self.atom_type_names: undefined = ValidationError( "Reference to undefined atomtype '{}' in 'overrides' " "'{}' at line {}".format( atom_type, entry.attrib["overrides"], entry.sourceline, ), None, entry.sourceline, ) errors.append(undefined) raise_collected(errors)
class Validator(object): def __init__(self, ff_file_name): from foyer.forcefield import preprocess_forcefield_files preprocessed_ff_file_name = preprocess_forcefield_files([ff_file_name]) ff_tree = etree.parse(preprocessed_ff_file_name[0]) self.validate_xsd(ff_tree) self.atom_type_names = ff_tree.xpath('/ForceField/AtomTypes/Type/@name') self.atom_types = ff_tree.xpath('/ForceField/AtomTypes/Type') self.validate_class_type_exclusivity(ff_tree) # Loading forcefield should succeed, because XML can be parsed and # basics have been validated. from foyer.forcefield import Forcefield self.smarts_parser = Forcefield(preprocessed_ff_file_name, validation=False).parser self.validate_smarts() self.validate_overrides() @staticmethod def validate_xsd(ff_tree, xsd_file=None): if xsd_file is None: xsd_file = join(split(abspath(__file__))[0], 'forcefields', 'ff.xsd') xmlschema_doc = etree.parse(xsd_file) xmlschema = etree.XMLSchema(xmlschema_doc) error_texts = {'missing_atom_type_in_nonbonded': ("Atom type {} is found in NonbondedForce at line {}" " but undefined in AtomTypes"), 'nonunique_atomtype_name': "Atom type {} is defined a second time at line {}", 'atomtype_name_key': "Atom type {} is defined a second time at line {}" } def create_error(keyword, message, line): atomtype = message[message.find("[") + 1:message.find("]")] error_text = error_texts[keyword].format(atomtype, line) return ValidationError(error_text, ex, line) try: xmlschema.assertValid(ff_tree) except DocumentInvalid as ex: message = ex.error_log.last_error.message line = ex.error_log.last_error.line # rewrite error message for constraint violation if ex.error_log.last_error.type_name == "SCHEMAV_CVC_IDC": for keyword in error_texts: if keyword in message: raise create_error(keyword, message, line) else: raise ValidationError('Unhandled XML validation error. ' 'Please consider submitting a bug report.', ex, line) raise def validate_class_type_exclusivity(self, ff_tree): sections = {'HarmonicBondForce/Bond': 2, 'HarmonicAngleForce/Angle': 3, 'RBTorsionForce/Proper': 4} errors = [] for element, num_atoms in sections.items(): valid_attribs = set() for n in range(1, num_atoms + 1): valid_attribs.add('class{}'.format(n)) valid_attribs.add('type{}'.format(n)) for entry in ff_tree.xpath('/ForceField/{}'.format(element)): attribs = [valid for valid in valid_attribs if entry.attrib.get(valid) is not None] if num_atoms != len(attribs): error = ValidationError( 'Invalid number of "class" and/or "type" attributes for' ' {} at line {}'.format(element, entry.sourceline), None, entry.sourceline) errors.append(error) number_endings = Counter([a[-1] for a in attribs]) if not all(1 == x for x in number_endings.values()): error = ValidationError( 'Only one "class" or "type" attribute may be defined' ' for each atom in a bonded force. See line {}'.format(entry.sourceline), None, entry.sourceline) errors.append(error) referenced_types = [] for valid in valid_attribs: if valid.startswith('type'): atomtype = entry.attrib.get(valid) if atomtype: referenced_types.append(atomtype) for atomtype in referenced_types: if atomtype not in self.atom_type_names: error = ValidationError( 'Atom type {} is found in {} at line {} but' ' undefined in AtomTypes'.format(atomtype, element.split('/')[0], entry.sourceline), None, entry.sourceline) errors.append(error) raise_collected(errors) def validate_smarts(self): missing_smarts = [] errors = [] for entry in self.atom_types: smarts_string = entry.attrib.get('def') name = entry.attrib['name'] if smarts_string is None: missing_smarts.append(name) continue # make sure smarts string can be parsed try: self.smarts_parser.parse(smarts_string) except ParseError as ex: if " col " in ex.args[0]: column = ex.args[0][ex.args[0].find(" col ") + 5:].strip() column = " at character {} of {}".format(column, smarts_string) else: column = "" malformed = ValidationError( "Malformed SMARTS string{} on line {}".format(column, entry.sourceline), ex, entry.sourceline) errors.append(malformed) continue # make sure referenced labels exist smarts_graph = SMARTSGraph(smarts_string, parser=self.smarts_parser, name=name, overrides=entry.attrib.get('overrides')) for atom_expr in nx.get_node_attributes(smarts_graph, 'atom').values(): labels = atom_expr.select('has_label') for label in labels: atom_type = label.tail[0][1:] if atom_type not in self.atom_type_names: undefined = ValidationError( "Reference to undefined atomtype '{}' in SMARTS " "string '{}' at line {}".format(atom_type, entry.attrib['def'], entry.sourceline), None, entry.sourceline) errors.append(undefined) raise_collected(errors) if missing_smarts: warn("The following atom types do not have smarts definitions: {}".format( ', '.join(missing_smarts)), ValidationWarning) def validate_overrides(self): errors = [] for entry in self.atom_types: overrides = entry.attrib.get('overrides') if not overrides: continue overridden_types = [at.strip() for at in overrides.split(',') if at] for atom_type in overridden_types: if atom_type not in self.atom_type_names: undefined = ValidationError( "Reference to undefined atomtype '{}' in 'overrides' " "'{}' at line {}".format(atom_type, entry.attrib['overrides'], entry.sourceline), None, entry.sourceline) errors.append(undefined) raise_collected(errors)
class Validator(object): def __init__(self, ff_file_name): from foyer.forcefield import preprocess_forcefield_files preprocessed_ff_file_name = preprocess_forcefield_files([ff_file_name]) ff_tree = etree.parse(preprocessed_ff_file_name[0]) self.validate_xsd(ff_tree) # Loading forcefield should succeed, because XML can be parsed and # basics have been validated. from foyer.forcefield import Forcefield self.smarts_parser = Forcefield(preprocessed_ff_file_name, validation=False).parser self.validate_smarts(ff_tree) # TODO: figure out what exceptions are raised, and provide good error messages def validate_xsd(self, ff_tree, xsd_file=None): if xsd_file is None: xsd_file = join( split(abspath(__file__))[0], 'forcefields', 'ff.xsd') xmlschema_doc = etree.parse(xsd_file) xmlschema = etree.XMLSchema(xmlschema_doc) try: xmlschema.assertValid(ff_tree) except DocumentInvalid as ex: message = ex.error_log.last_error.message line = ex.error_log.last_error.line # rewrite error message for constraint violation if ex.error_log.last_error.type_name == "SCHEMAV_CVC_IDC": if "missing_atom_type_in_nonbonded" in message: atomtype = message[message.find("[") + 1:message.find("]")] raise ValidationError( "Atom type {} is found in NonbondedForce at line {} but" " undefined in AtomTypes".format(atomtype, line), ex, line) elif "nonunique_atomtype_name" in message: atomtype = message[message.find("[") + 1:message.find("]")] raise ValidationError( "Atom type {} is defined a second time at line {}". format(atomtype, line), ex, line) raise def validate_smarts(self, ff_tree): results = ff_tree.xpath('/ForceField/AtomTypes/Type') atom_types = ff_tree.xpath('/ForceField/AtomTypes/Type/@name') missing_smarts = [] for r in results: smarts_string = r.attrib.get('def') name = r.attrib['name'] if smarts_string is None: missing_smarts.append(name) continue # make sure smarts string can be parsed try: self.smarts_parser.parse(smarts_string) except ParseError as ex: if " col " in ex.args[0]: column = ex.args[0][ex.args[0].find(" col ") + 5:].strip() column = " at character {} of {}".format( column, smarts_string) else: column = "" raise ValidationError( "Malformed SMARTS string{} on line {}".format( column, r.sourceline), ex, r.sourceline) # make sure referenced labels exist smarts_graph = SMARTSGraph(smarts_string, parser=self.smarts_parser, name=name, overrides=r.attrib.get('overrides')) for atom_expr in nx.get_node_attributes(smarts_graph, 'atom').values(): labels = atom_expr.select('has_label') for label in labels: atom_type = label.tail[0][1:] if atom_type not in atom_types: raise ValidationError( "Reference to undefined atomtype {} in SMARTS string" " '{}' at line {}".format(atom_type, r.attrib['def'], r.sourceline), None, r.sourceline) warn( "The following atom types do not have smarts definitions: {}". format(', '.join(missing_smarts)), ValidationWarning) def validate_overrides(self, ff_tree): results = ff_tree.xpath('/ForceField/AtomTypes/Type[@name]') for r in results: smarts_string = r.attrib['def'] print(smarts_string) print(r.sourceline) # make sure smarts string can be parsed self.smarts.parse(smarts_string)