Compute and save ensemble averages and Jacobians for each job - #173
mattwthompson wants to merge 21 commits into
Conversation
for more information, see https://pre-commit.ci
This comment was marked as outdated.
This comment was marked as outdated.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #173 +/- ##
==========================================
- Coverage 81.40% 80.03% -1.37%
==========================================
Files 56 58 +2
Lines 5070 5204 +134
==========================================
+ Hits 4127 4165 +38
- Misses 943 1039 +96 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
lilyminium
left a comment
There was a problem hiding this comment.
Thanks Matt, this is a good start. I have several blocking comments; could you please also add a test?
| interchanges = [] | ||
| for path_index, interchange_path in enumerate(glob.glob(f"{job_dir}/single_molecule_interchange_*.json")): | ||
| unique_molecule_index = int(pathlib.Path(interchange_path).stem.split("single_molecule_interchange_")[-1]) | ||
|
|
||
| # hope we're loading up the single-molecule interchanges in the same order as we have unique molecules | ||
| assert unique_molecule_index == path_index | ||
|
|
||
| with open(interchange_path) as f: | ||
| interchanges.append(Interchange.model_validate_json(f.read())) | ||
|
|
||
| assert len(interchanges) > 0, "Did not find single-molecule `Interchange`s as expected" | ||
|
|
||
| tensor_force_field, tensor_topologies = tyff.converters.convert_interchange(interchanges) |
There was a problem hiding this comment.
I don't think this will work generally; the tensor_force_field is a series of tensors masquerading as a force field, and it builds a dense matrix of only the subset of parameters used in the interchanges passed to convert_interchange. Rebuilding the tensor_force_field for each property means the shapes of the jacobians returned will be different and un-alignable. I'd recommend passing in TensorSystem and TensorForceField as originally laid out in the function signature.
| worker_ff, | ||
| frames_path, | ||
| temperature * openmm.unit.kelvin, | ||
| None if pressure is None else pressure * openmm.unit.atmosphere, |
There was a problem hiding this comment.
Could we please standardise the pressure unit across tyff? I think kPa is used elsewhere (e.g.
Line 138 in 4db3654
There was a problem hiding this comment.
Oddly this is not standardized across the framework, I will open a separate issue
$ grep --include="*.py" -r "\.atm" tyff | wc -l jacobian-app ✱
19
$ grep --include="*.py" -r "\.*pascal" tyff | wc -l jacobian-app ✱
4|
Running into an issue with the design here: If we want the We could potentially separate the compute (from system prep to production MD runs, inclusive) from the Jacobian step such that the first Jacobian calculation is done after all production runs are done. This should work fine ... unless more targets are submitted. So I'm thinking the Jacobians2 should be computed in a step between the "compute" and before the prediction steps. I'll have to come back to this later as it's clunky and I'd like something smoother. Footnotes
|
|
And now I'm getting hung up on how then should the In other words, as I currently understand it, which is incongruous with the Put a slightly different way (if repetitive) here's a collection of the unique SMILES in the (canned) dataset (n=3) vs. the compute config for a single job (n=1) Maybe I just need to massage the data to make it, using the above values, look like |
You can construct a TensorSystem from just the tensor topologies in the simulation, this is usually dense not sparse.
Let's discuss this synchronously tomorrow, but I thought about and tossed up some scenarios where: a) we just pass in the TensorForceField / tensor topologies to workflow submit_target to pass into this method, as originally laid out in the issue code (but this probably makes tyff difficult to use for benchmarking) At the end of the day I think we have to simply require that the user specifies the entire dataset up front. This is already kind of essential since we need a TensorForceField created up front to be fit (see the MVP issue). The driver could serialize that with the Inside the function what we could do instead is still create a local TensorForceField but have it remap from the global reference, and still take the Jacobian wrt the packed reference force field so autograd stil gets the right reference column. def _gather_from_reference(
local: tyff.TensorForceField, reference: tyff.TensorForceField
) -> tyff.TensorForceField:
"""Rebuild ``local`` with values indexed out of ``reference``, keeping local row order so the
topologies' parameter maps stay valid."""
reference_by_type = reference.potentials_by_type
potentials = []
for potential in local.potentials:
ref = reference_by_type[potential.type] # KeyError: potential type missing from reference
assert potential.parameter_cols == ref.parameter_cols, potential.type
ref_idx = {key: i for i, key in enumerate(ref.parameter_keys)}
idx = torch.tensor([ref_idx[key] for key in potential.parameter_keys]) # KeyError: unseen parameter
attributes = potential.attributes
if potential.attribute_cols is not None:
attr_idx = torch.tensor([ref.attribute_cols.index(col) for col in potential.attribute_cols])
attributes = ref.attributes[attr_idx]
potentials.append(dataclasses.replace(potential, parameters=ref.parameters[idx], attributes=attributes))
v_sites = local.v_sites
if v_sites is not None:
ref_idx = {key: i for i, key in enumerate(reference.v_sites.keys)}
idx = torch.tensor([ref_idx[key] for key in v_sites.keys])
v_sites = dataclasses.replace(v_sites, parameters=reference.v_sites.parameters[idx])
return tyff.TensorForceField(potentials, v_sites)In the existing function, then: local_ff, tensor_topologies = tyff.converters.convert_interchange(interchanges) # of just this system
reference_ff = ... # deserialize global reference TensorForceField
tensors, parameter_lookup, attribute_lookup, has_v_sites = _pack_force_field(reference_ff) # was local
parameters = torch.cat([...]).requires_grad_(True) # unchanged
...
reference_worker_ff = _unpack_force_field(tensors, parameter_lookup, attribute_lookup, has_v_sites, reference_ff)
worker_ff = _gather_from_reference(local_ff, reference_worker_ff)
means, _ = tyff.mm.compute_ensemble_averages(system, worker_ff, ...)Lastly I also considered that we don't really need the Jacobian for benchmarking and that part can stay using a locally generated FF -- this is probably overengineered from that case, but also maybe this is not the right PR to address that. |
Closes #145