diff --git a/openff/bespokefit/cli/executor/submit.py b/openff/bespokefit/cli/executor/submit.py index 361c9323..e3284cd0 100644 --- a/openff/bespokefit/cli/executor/submit.py +++ b/openff/bespokefit/cli/executor/submit.py @@ -82,7 +82,7 @@ def _to_input_schema( "openff.bespokefit", ) - workflow_factory = BespokeWorkflowFactory.parse_file(spec_file_name) + workflow_factory = BespokeWorkflowFactory.from_file(spec_file_name) workflow_factory.initial_force_field = force_field_path return workflow_factory.optimization_schema_from_molecule(molecule) diff --git a/openff/bespokefit/executor/services/optimizer/worker.py b/openff/bespokefit/executor/services/optimizer/worker.py index ac039f29..2298fab2 100644 --- a/openff/bespokefit/executor/services/optimizer/worker.py +++ b/openff/bespokefit/executor/services/optimizer/worker.py @@ -26,6 +26,6 @@ def optimize(self, optimization_input_json: str) -> str: input_schema.id = self.request.id optimizer = get_optimizer(input_schema.optimizer.type) - result = optimizer.optimize(input_schema, keep_files=False) + result = optimizer.optimize(input_schema, keep_files=settings.BEFLOW_KEEP_FILES) return serialize(result, encoding="json") diff --git a/openff/bespokefit/executor/services/qcgenerator/worker.py b/openff/bespokefit/executor/services/qcgenerator/worker.py index c8fc9255..fdaba8d3 100644 --- a/openff/bespokefit/executor/services/qcgenerator/worker.py +++ b/openff/bespokefit/executor/services/qcgenerator/worker.py @@ -2,7 +2,7 @@ import qcengine import redis -from openff.toolkit.topology import Molecule +from openff.toolkit.topology import Atom, Molecule from qcelemental.models import AtomicResult from qcelemental.models.common_models import DriverEnum from qcelemental.models.procedures import ( @@ -28,6 +28,17 @@ celery_app = configure_celery_app("qcgenerator", redis_connection) +def _select_atom(atoms: List[Atom]) -> int: + """ + For a list of atoms chose the heaviest atom. + """ + candidate = atoms[0] + for atom in atoms: + if atom.atomic_number > candidate.atomic_number: + candidate = atom + return candidate.molecule_atom_index + + @celery_app.task(acks_late=True) def compute_torsion_drive(task_json: str) -> TorsionDriveResult: """Runs a torsion drive using QCEngine.""" @@ -46,24 +57,31 @@ def compute_torsion_drive(task_json: str) -> TorsionDriveResult: index_2 = map_to_atom_index[task.central_bond[0]] index_3 = map_to_atom_index[task.central_bond[1]] - index_1 = [ - atom.molecule_atom_index + index_1_atoms = [ + atom for atom in molecule.atoms[index_2].bonded_atoms if atom.molecule_atom_index != index_3 - ][0] - index_4 = [ - atom.molecule_atom_index + ] + index_4_atoms = [ + atom for atom in molecule.atoms[index_3].bonded_atoms if atom.molecule_atom_index != index_2 - ][0] + ] del molecule.properties["atom_map"] input_schema = TorsionDriveInput( keywords=TDKeywords( - dihedrals=[(index_1, index_2, index_3, index_4)], + dihedrals=[ + ( + _select_atom(index_1_atoms), + index_2, + index_3, + _select_atom(index_4_atoms), + ) + ], grid_spacing=[task.grid_spacing], - dihedral_ranges=[task.scan_range], + dihedral_ranges=[task.scan_range] if task.scan_range is not None else None, ), extras={ "canonical_isomeric_explicit_hydrogen_mapped_smiles": molecule.to_smiles( @@ -72,13 +90,13 @@ def compute_torsion_drive(task_json: str) -> TorsionDriveResult: }, initial_molecule=molecule.to_qcschema(), input_specification=QCInputSpecification( - model=task.model, driver=DriverEnum.gradient + model=task.model, + driver=DriverEnum.gradient, ), optimization_spec=OptimizationSpecification( - procedure=task.optimization_spec.procedure, + procedure=task.optimization_spec.program, keywords={ - "coordsys": "dlc", - "maxiter": task.optimization_spec.max_iterations, + **task.optimization_spec.dict(exclude={"program", "constraints"}), "program": task.program, }, ), @@ -104,6 +122,8 @@ def compute_optimization( task_json: str, ) -> List[OptimizationResult]: """Runs a set of geometry optimizations using QCEngine.""" + # TODO: should we only return the lowest energy optimization? + # or the first optimisation to work? task = OptimizationTask.parse_raw(task_json) @@ -113,8 +133,7 @@ def compute_optimization( input_schemas = [ OptimizationInput( keywords={ - "coordsys": "dlc", - "maxiter": task.optimization_spec.max_iterations, + **task.optimization_spec.dict(exclude={"program", "constraints"}), "program": task.program, }, extras={ @@ -123,7 +142,8 @@ def compute_optimization( ) }, input_specification=QCInputSpecification( - model=task.model, driver=DriverEnum.gradient + model=task.model, + driver=DriverEnum.gradient, ), initial_molecule=molecule.to_qcschema(conformer=i), ) @@ -135,7 +155,7 @@ def compute_optimization( for input_schema in input_schemas: return_value = qcengine.compute_procedure( - input_schema, task.optimization_spec.procedure, raise_error=True + input_schema, task.optimization_spec.program, raise_error=True ) if isinstance(return_value, OptimizationResult): diff --git a/openff/bespokefit/executor/services/settings.py b/openff/bespokefit/executor/services/settings.py index 664235e1..7235be62 100644 --- a/openff/bespokefit/executor/services/settings.py +++ b/openff/bespokefit/executor/services/settings.py @@ -35,3 +35,4 @@ class Settings(BaseSettings): BEFLOW_OPTIMIZER_PREFIX = "optimizations" BEFLOW_OPTIMIZER_ROUTER = "openff.bespokefit.executor.services.optimizer.app:router" BEFLOW_OPTIMIZER_WORKER = "openff.bespokefit.executor.services.optimizer.worker" + BEFLOW_KEEP_FILES = False diff --git a/openff/bespokefit/optimizers/forcebalance/forcebalance.py b/openff/bespokefit/optimizers/forcebalance/forcebalance.py index bccb449f..d4674d67 100644 --- a/openff/bespokefit/optimizers/forcebalance/forcebalance.py +++ b/openff/bespokefit/optimizers/forcebalance/forcebalance.py @@ -103,14 +103,11 @@ def _optimize(cls, schema: BaseOptimizationSchema) -> OptimizerResultsType: _logger.debug("Launching Forcebalance") - current_env = os.environ - current_env["ENABLE_FB_SMIRNOFF_CACHING"] = "false" subprocess.run( "ForceBalance optimize.in", shell=True, stdout=log, stderr=log, - env=current_env, ) results = cls._collect_results("", schema=schema) diff --git a/openff/bespokefit/schema/fitting.py b/openff/bespokefit/schema/fitting.py index 62d6e672..877333a4 100644 --- a/openff/bespokefit/schema/fitting.py +++ b/openff/bespokefit/schema/fitting.py @@ -66,18 +66,27 @@ def n_targets(self) -> int: return len(self.targets) @property - def initial_parameter_values(self) -> Dict[BaseSMIRKSParameter, unit.Quantity]: + def initial_parameter_values( + self, + ) -> Dict[BaseSMIRKSParameter, Dict[str, unit.Quantity]]: """A list of the refit force field parameters.""" initial_force_field = ForceField(self.initial_force_field) return { - parameter: getattr( - initial_force_field[parameter.type].parameters[parameter.smirks], - attribute, + parameter: dict( + ( + attribute, + getattr( + initial_force_field[parameter.type].parameters[ + parameter.smirks + ], + attribute, + ), + ) + for attribute in parameter.attributes ) for parameter in self.parameters - for attribute in parameter.attributes } def get_fitting_force_field(self) -> ForceField: diff --git a/openff/bespokefit/schema/results.py b/openff/bespokefit/schema/results.py index 2c43f5b9..52c95f60 100644 --- a/openff/bespokefit/schema/results.py +++ b/openff/bespokefit/schema/results.py @@ -44,7 +44,9 @@ class BaseOptimizationResults(SchemaBase, abc.ABC): # ) @property - def initial_parameter_values(self) -> Dict[BaseSMIRKSParameter, unit.Quantity]: + def initial_parameter_values( + self, + ) -> Dict[BaseSMIRKSParameter, Dict[str, unit.Quantity]]: """A list of the refit force field parameters.""" return self.input_schema.initial_parameter_values @@ -52,7 +54,7 @@ def initial_parameter_values(self) -> Dict[BaseSMIRKSParameter, unit.Quantity]: @property def refit_parameter_values( self, - ) -> Optional[Dict[BaseSMIRKSParameter, unit.Quantity]]: + ) -> Optional[Dict[BaseSMIRKSParameter, Dict[str, unit.Quantity]]]: """A list of the refit force field parameters.""" if self.refit_force_field is None: @@ -61,12 +63,17 @@ def refit_parameter_values( refit_force_field = ForceField(self.refit_force_field) return { - parameter: getattr( - refit_force_field[parameter.type].parameters[parameter.smirks], - attribute, + parameter: dict( + ( + attribute, + getattr( + refit_force_field[parameter.type].parameters[parameter.smirks], + attribute, + ), + ) + for attribute in parameter.attributes ) for parameter in self.input_schema.parameters - for attribute in parameter.attributes } diff --git a/openff/bespokefit/schema/tasks.py b/openff/bespokefit/schema/tasks.py index b75f11b7..808f8e73 100644 --- a/openff/bespokefit/schema/tasks.py +++ b/openff/bespokefit/schema/tasks.py @@ -1,6 +1,7 @@ import abc -from typing import Tuple +from typing import Optional, Tuple +from openff.qcsubmit.procedures import GeometricProcedure from pydantic import Field, conint from qcelemental.models.common_models import Model from typing_extensions import Literal @@ -8,21 +9,13 @@ from openff.bespokefit.utilities.pydantic import BaseModel -class OptimizationSpec(BaseModel): - - procedure: Literal["geometric"] = "geometric" - - max_iterations: conint(gt=0) = Field( - 300, - description="The maximum number of iterations to perform before raising a " - "convergence failure exception.", - ) - - class QCGenerationTask(BaseModel, abc.ABC): type: Literal["base-task"] + program: str = Field(..., description="The program to use to evaluate the model.") + model: Model = Field(..., description=str(Model.__doc__)) + class HessianTaskSpec(QCGenerationTask): @@ -34,12 +27,8 @@ class HessianTaskSpec(QCGenerationTask): "hessian. Each conformer will be minimized and the one with the lowest energy " "will have its hessian computed.", ) - - program: str = Field(..., description="The program to use to evaluate the model.") - model: Model = Field(..., description=str(Model.__doc__)) - - optimization_spec: OptimizationSpec = Field( - OptimizationSpec(), + optimization_spec: GeometricProcedure = Field( + GeometricProcedure(), description="The specification for how to optimize each conformer before " "computing the hessian.", ) @@ -54,22 +43,9 @@ class HessianTask(HessianTaskSpec): ) -class OptimizationTaskSpec(QCGenerationTask): +class OptimizationTaskSpec(HessianTaskSpec): type: Literal["optimization"] = "optimization" - n_conformers: conint(gt=0) = Field( - ..., - description="The maximum number of conformers to begin the optimization from.", - ) - - program: str = Field(..., description="The program to use to evaluate the model.") - model: Model = Field(..., description=str(Model.__doc__)) - - optimization_spec: OptimizationSpec = Field( - OptimizationSpec(), - description="The specification for how to optimize each conformer.", - ) - class OptimizationTask(OptimizationTaskSpec): @@ -84,15 +60,12 @@ class Torsion1DTaskSpec(QCGenerationTask): type: Literal["torsion1d"] = "torsion1d" grid_spacing: int = Field(15, description="The spacing between grid angles.") - scan_range: Tuple[int, int] = Field( - (-180, 165), description="The range of grid angles to scan." + scan_range: Optional[Tuple[int, int]] = Field( + None, description="The range of grid angles to scan." ) - program: str = Field(..., description="The program to use to evaluate the model.") - model: Model = Field(..., description=str(Model.__doc__)) - - optimization_spec: OptimizationSpec = Field( - OptimizationSpec(), + optimization_spec: GeometricProcedure = Field( + GeometricProcedure(enforce=0.1, reset=True, qccnv=True, epsilon=0.0), description="The specification for how to optimize the structure at each angle " "in the scan.", ) diff --git a/openff/bespokefit/tests/executor/services/qcgenerator/test_app.py b/openff/bespokefit/tests/executor/services/qcgenerator/test_app.py index 9f7e77f7..b0aabdfb 100644 --- a/openff/bespokefit/tests/executor/services/qcgenerator/test_app.py +++ b/openff/bespokefit/tests/executor/services/qcgenerator/test_app.py @@ -34,7 +34,7 @@ def mock_atomic_result() -> AtomicResult: return AtomicResult( molecule=molecule.to_qcschema(), driver=DriverEnum.hessian, - model=Model(method="rdkit", basis=None), + model=Model(method="uff", basis=None), return_result=5.2, success=True, provenance=Provenance(creator="pytest"), @@ -51,7 +51,7 @@ def mock_torsion_drive_result() -> TorsionDriveResult: return TorsionDriveResult( keywords=TDKeywords(dihedrals=[(0, 1, 2, 3)], grid_spacing=[15]), input_specification=QCInputSpecification( - model=Model(method="rdkit", basis=None), driver=DriverEnum.gradient + model=Model(method="uff", basis=None), driver=DriverEnum.gradient ), initial_molecule=molecule.to_qcschema(), optimization_spec=OptimizationSpecification(procedure="geometric"), diff --git a/openff/bespokefit/tests/executor/services/qcgenerator/test_worker.py b/openff/bespokefit/tests/executor/services/qcgenerator/test_worker.py index 61a4f801..92609eba 100644 --- a/openff/bespokefit/tests/executor/services/qcgenerator/test_worker.py +++ b/openff/bespokefit/tests/executor/services/qcgenerator/test_worker.py @@ -1,6 +1,5 @@ import json -import pytest from openff.toolkit.topology import Molecule from qcelemental.models.common_models import Model from qcelemental.models.procedures import OptimizationResult, TorsionDriveResult @@ -12,7 +11,7 @@ def test_compute_torsion_drive(): task = Torsion1DTask( - smiles="[CH3:1][CH3:2]", + smiles="[F][CH2:1][CH2:2][F]", central_bond=(1, 2), grid_spacing=180, scan_range=(-180, 180), @@ -35,7 +34,11 @@ def test_compute_torsion_drive(): # Make sure a molecule can be created from CMILES final_molecule = Molecule.from_mapped_smiles(cmiles) - assert Molecule.are_isomorphic(final_molecule, Molecule.from_smiles("CC"))[0] + assert Molecule.are_isomorphic(final_molecule, Molecule.from_smiles("FCCF"))[0] + dihedral = result.keywords.dihedrals[0] + # make sure heavy atoms are targeted + atoms = [final_molecule.atoms[i] for i in dihedral] + assert all([atom.atomic_number != 1 for atom in atoms]) def test_compute_optimization(): @@ -67,34 +70,3 @@ def test_compute_optimization(): # Make sure a molecule can be created from CMILES final_molecule = Molecule.from_mapped_smiles(cmiles) assert Molecule.are_isomorphic(final_molecule, Molecule.from_smiles("CCCCC")) - - -@pytest.mark.parametrize( - "compute_function, task", - [ - ( - worker.compute_torsion_drive, - Torsion1DTask( - smiles="[CH2:1]=[CH2:2]", - central_bond=(1, 2), - grid_spacing=180, - scan_range=(-180, 180), - program="non-existent-program", - model=Model(method="uff", basis=None), - ), - ), - ( - worker.compute_optimization, - OptimizationTask( - smiles="CCCCC", - n_conformers=1, - program="non-existent-program", - model=Model(method="uff", basis=None), - ), - ), - ], -) -def test_compute_failure(compute_function, task, celery_worker): - - with pytest.raises(ValueError, match="non-existent-program"): - compute_function(task.json()) diff --git a/openff/bespokefit/tests/optimizers/forcebalance/test_forcebalance.py b/openff/bespokefit/tests/optimizers/forcebalance/test_forcebalance.py index b464ac58..09210d69 100644 --- a/openff/bespokefit/tests/optimizers/forcebalance/test_forcebalance.py +++ b/openff/bespokefit/tests/optimizers/forcebalance/test_forcebalance.py @@ -148,14 +148,17 @@ def test_forcebalance_collect_general_results( refit_values = results.refit_parameter_values for parameter_smirks in initial_values: + initial_parameters = initial_values[parameter_smirks] + refit_parameters = refit_values[parameter_smirks] - initial_value = initial_values[parameter_smirks] - refit_value = refit_values[parameter_smirks] + for attribute in initial_parameters: + initial_parameter = initial_parameters[attribute] + refit_parameter = refit_parameters[attribute] - refit_value = refit_value.value_in_unit(initial_value.unit) - initial_value = initial_value.value_in_unit(initial_value.unit) + refit_value = refit_parameter.value_in_unit(initial_parameter.unit) + initial_value = initial_parameter.value_in_unit(initial_parameter.unit) - assert not np.isclose(initial_value, refit_value) + assert not np.isclose(initial_value, refit_value) def test_forcebalance_optimize( diff --git a/openff/bespokefit/tests/schema/test_results.py b/openff/bespokefit/tests/schema/test_results.py index 366d839f..6f6bb9e4 100644 --- a/openff/bespokefit/tests/schema/test_results.py +++ b/openff/bespokefit/tests/schema/test_results.py @@ -36,25 +36,41 @@ def bespoke_optimization_results( def test_initial_parameter_values(bespoke_optimization_results): - parameter_values = bespoke_optimization_results.initial_parameter_values + parameter_values = bespoke_optimization_results.initial_parameter_values.values() assert len(parameter_values) == len( bespoke_optimization_results.input_schema.parameters ) - assert all(isinstance(x, unit.Quantity) for x in parameter_values.values()) + assert all( + isinstance(parameter, unit.Quantity) + for x in parameter_values + for parameter in x.values() + ) - assert all(x != 2 * unit.kilojoules_per_mole for x in parameter_values.values()) + assert all( + parameter != 2 * unit.kilojoules_per_mole + for x in parameter_values + for parameter in x.values() + ) def test_refit_parameter_values(bespoke_optimization_results): - refit_parameter_values = bespoke_optimization_results.refit_parameter_values + refit_parameter_values = ( + bespoke_optimization_results.refit_parameter_values.values() + ) assert len(refit_parameter_values) == len( bespoke_optimization_results.input_schema.parameters ) - assert all(isinstance(x, unit.Quantity) for x in refit_parameter_values.values()) + assert all( + isinstance(parameter, unit.Quantity) + for x in refit_parameter_values + for parameter in x.values() + ) assert all( - x == 2 * unit.kilocalories_per_mole for x in refit_parameter_values.values() + parameter == 2 * unit.kilocalories_per_mole + for x in refit_parameter_values + for parameter in x.values() ) diff --git a/openff/bespokefit/tests/schema/test_targets.py b/openff/bespokefit/tests/schema/test_targets.py index a67b7f12..e7de18b4 100644 --- a/openff/bespokefit/tests/schema/test_targets.py +++ b/openff/bespokefit/tests/schema/test_targets.py @@ -16,7 +16,7 @@ def test_check_reference_data(qc_torsion_drive_results): TorsionProfileTargetSchema( reference_data=BespokeQCData( spec=Torsion1DTaskSpec( - model=Model(method="uff", basis=None), program="rdkit" + program="rdkit", model=Model(method="uff", basis=None) ) ) ) @@ -27,7 +27,7 @@ def test_check_reference_data(qc_torsion_drive_results): TorsionProfileTargetSchema( reference_data=BespokeQCData( spec=HessianTaskSpec( - model=Model(method="uff", basis=None), program="rdkit" + program="rdkit", model=Model(method="uff", basis=None) ) ) ) diff --git a/openff/bespokefit/tests/utilities/test_smirks.py b/openff/bespokefit/tests/utilities/test_smirks.py index da8d4598..de99a948 100644 --- a/openff/bespokefit/tests/utilities/test_smirks.py +++ b/openff/bespokefit/tests/utilities/test_smirks.py @@ -344,5 +344,24 @@ def test_expand_torsion_terms(bespoke_smirks, expand_torsions): if expand_torsions: assert len(parameter.k) == 4 + assert parameter.periodicity == [1, 2, 3, 4] else: assert len(parameter.k) < 4 + if bespoke_smirks: + assert "-BF" in parameter.id + + +def test_fit_interpolated_torsion(): + """ + Make sure an error is raised if we try and fit an interpolated torsion. + """ + ff = ForceFieldEditor(force_field_name="openff_unconstrained-1.3.0.offxml") + # add an interploated general parameter + parameter = ff.force_field["ProperTorsions"].parameters[0] + parameter._k_bondorder = [1, 2] + ff.force_field["ProperTorsions"]._parameters = [parameter] + # run the generation + gen = SMIRKSGenerator(initial_force_field=ff) + mol = Molecule.from_smiles("CC") + with pytest.raises(NotImplementedError): + _ = gen.generate_smirks_from_molecule(molecule=mol) diff --git a/openff/bespokefit/tests/workflows/test_bespoke.py b/openff/bespokefit/tests/workflows/test_bespoke.py index 8fa5e1d9..6b0c08ec 100644 --- a/openff/bespokefit/tests/workflows/test_bespoke.py +++ b/openff/bespokefit/tests/workflows/test_bespoke.py @@ -109,10 +109,6 @@ def test_check_target_torsion_smirks(): dict(parameter_hyperparameters=[]), pytest.raises(TargetNotSetError, match="There are no parameter settings"), ), - ( - dict(target_smirks=[]), - pytest.raises(TargetNotSetError, match="No forcefield groups have"), - ), ], ) def test_pre_run_check(input_kwargs, expected_raises): @@ -125,7 +121,11 @@ def test_pre_run_check(input_kwargs, expected_raises): factory._pre_run_check() -def test_export_factory(): +@pytest.mark.parametrize( + "filename", + [pytest.param("test.yaml", id="yaml"), pytest.param("test.json", id="json")], +) +def test_export_factory(filename): """Test exporting and importing a workflow.""" factory = BespokeWorkflowFactory( @@ -135,10 +135,10 @@ def test_export_factory(): with temporary_cd(): - factory.export_factory(file_name="test.json") + factory.export_factory(file_name=filename) # now read it back in - recreated = BespokeWorkflowFactory.parse_file("test.json") + recreated = BespokeWorkflowFactory.from_file(file_name=filename) assert factory.dict() == recreated.dict() diff --git a/openff/bespokefit/utilities/smirks.py b/openff/bespokefit/utilities/smirks.py index eb50e3cf..031521e8 100644 --- a/openff/bespokefit/utilities/smirks.py +++ b/openff/bespokefit/utilities/smirks.py @@ -256,15 +256,31 @@ def generate_smirks_from_fragment( if not isinstance(parameter, ProperTorsionHandler.ProperTorsionType): continue - for i in range(len(parameter.k), 4): - - parameter.k.append(0.0 * unit.kilocalories_per_mole) - parameter.phase.append(parameter.phase[-1]) - parameter.periodicity.append(parameter.periodicity[-1]) - parameter.idivf.append(parameter.idivf[-1]) - - if parameter.k_bondorder is not None: - parameter.k_bondorder.append(parameter.k_bondorder[i - 1]) + # parameters have been split so set all to 1.0 as idivf may have changed + parameter.idivf = [1.0] * 4 + default_k = [0 * unit.kilocalories_per_mole] * 4 + default_phase = [ + 0 * unit.degree, + 180 * unit.degree, + 0 * unit.degree, + 180 * unit.degree, + ] + default_p = [1, 2, 3, 4] + + # update the existing k values for the correct phase and p + for i, p in enumerate(parameter.periodicity): + default_k[p - 1] = parameter.k[i] + + # update with new parameters + parameter.k = default_k + parameter.phase = default_phase + parameter.periodicity = default_p + + # TODO make sure we do not fit interpolated parameters + if parameter.k_bondorder is not None: + raise NotImplementedError( + "Bespokefit can not fit interpolated parameters!" + ) return new_parameters diff --git a/openff/bespokefit/utilities/smirnoff.py b/openff/bespokefit/utilities/smirnoff.py index 3db01b2c..f6a1c965 100644 --- a/openff/bespokefit/utilities/smirnoff.py +++ b/openff/bespokefit/utilities/smirnoff.py @@ -190,6 +190,9 @@ def get_initial_parameters( initial_parameter = copy.deepcopy(parameters[match]) initial_parameter.smirks = smirks_pattern + # mark the parameter as being bespokefit + if not initial_parameter.id.endswith("-BF"): + initial_parameter.id += "-BF" initial_parameters.append(initial_parameter) diff --git a/openff/bespokefit/workflows/bespoke.py b/openff/bespokefit/workflows/bespoke.py index 3eb9359f..0c355de9 100644 --- a/openff/bespokefit/workflows/bespoke.py +++ b/openff/bespokefit/workflows/bespoke.py @@ -27,7 +27,6 @@ vdWHandler, ) from pydantic import Field, validator -from qcelemental.models.common_models import Model from qcportal.models import OptimizationRecord, ResultRecord, TorsionDriveRecord from openff.bespokefit.exceptions import ( @@ -122,12 +121,6 @@ class BespokeWorkflowFactory(ClassBase): "By default bespoke torsion parameters (if requested) will be constructed for " "all non-terminal 'rotatable bonds'", ) - target_smirks: List[SMIRKSType] = Field( - [ - SMIRKSType.ProperTorsions, - ], - description="The list of parameters the new smirks patterns should be made for.", - ) expand_torsion_terms: bool = Field( True, @@ -209,14 +202,16 @@ def _pre_run_check(self) -> None: "There are no parameter settings specified which will mean that the " "optimiser has no parameters to optimize." ) - elif len(self.target_smirks) == 0: - raise TargetNotSetError( - "No forcefield groups have been supplied, which means no smirks were " - "selected to be optimized." - ) else: return + @property + def target_smirks(self) -> List[SMIRKSType]: + """Returns a list of the target smirks types based on the selected hyper parameters.""" + return list( + {SMIRKSType(parameter.type) for parameter in self.parameter_hyperparameters} + ) + def export_factory(self, file_name: str) -> None: """ Export the factory to yaml or json file. @@ -568,10 +563,7 @@ def _build_optimization_schema( target_schema.reference_data = BespokeQCData( spec=task_type_to_spec[task_type]( - model=Model( - method=default_qc_spec.method, basis=default_qc_spec.basis - ), - program=default_qc_spec.program, + program=default_qc_spec.program, model=default_qc_spec.qc_model ) )