[WIP] transformations#26
Conversation
03f8080 to
4d23e35
Compare
|
another side-track (sorry): while running TYK2 through the pipeline again, I ran into a pose that RDKit could not restore bond orders for because the connectivity was too wonky. I added a validator that catches this in a7324a3 |
|
@ianmkenney ready for a first look, in the meantime I'm just running TYK2 with this newest pipeline to see if it all works out. |
|
note that given a ligand series, there might be multiple net charges. Traditionally you would solve this by:
But this is a bit finnicky to set up robustly. OFE supports charge-changing transformations now, so we should just allow these for now -- it saves us from having to do the split/merge. We will need to 1) warn the user 2) make sure that in |
| forcefield: str = "AMBER" | ||
|
|
||
|
|
||
| class MaxVolumeSiteTransformation(Transformation): |
There was a problem hiding this comment.
There is a lot going on in this transformation. I would intuitively expect that this only alters the structures based on ligand volumes but it also alters charge state. We are able to stack transformations.
There was a problem hiding this comment.
addressed by the split + explicit single-Structure helpers in ef4f9f5
| out = Path(tmp) / "merged.pdb" | ||
| merged.atoms.write(str(out)) | ||
| data = base64.b64encode(out.read_bytes()).decode() | ||
| return Structure( |
There was a problem hiding this comment.
enforces that the structure is always PDB after transformation even if the original structure was something else.
| def _setup(self) -> None: | ||
| pass | ||
|
|
||
| def _transform(self, structures: list[Structure]) -> list[Structure]: |
There was a problem hiding this comment.
Tracing the execution path here is tricky and make futures maintainability difficult. The function names don't capture their actual behavior and they share too much responsibility.
Specifically, _prepare and _protonate silently rely on the correct ordering of the input sequence, and _prepare only operates on a single structure despite accepting a list.
We should rewrite this to be more explicit and decoupled. Something like this would be cleaner:
def _transform(self, structures) -> list[Structure]:
chosen_complex = self._select_complex(structures)
other_structures = [s for s in structures if s != chosen_complex]
fixed_protein = self._fix_protein_heavy_atoms(chosen_complex)
protonated_protein = self._protonate_protein_context(fixed_protein)
...There was a problem hiding this comment.
Additionally, we should try to minimize when we write to disk. Aside from performance benefits, using the io module will narrow the classes of exceptions we might run into, like running out of space in /tmp/. I think the only place we fully need disk IO would be with pdb2pqr.
| ) | ||
|
|
||
|
|
||
| class TestMaxVolumeSiteTransformation(TestCase): |
There was a problem hiding this comment.
Good start but will need more
|
propka turns out not to be a suitable direction because it only predicts pkas and there is no turnkey solution to actually using those predictions to protonate the whole complex in one go. Instead we'll go with what's been built in |
|
@ianmkenney can you have another look? this PR has become too inflated at this point imo |
There was a problem hiding this comment.
I think we need to look again at how these transformations fit into the overall pipeline and how they should be stacked.
Given a pre-validated set of structures, we first apply MaxVolumeSiteSubstitutionTransformation (or whatever we would call it), where the "canonical structure" is immediately substituted into all other structures. From there we can apply the ProteinPreparationTransformation. By caching, we can avoid redoing the extra N-1 calculations. We then apply the ComplexProtonationTransformation. This completely removes the need to keep track of an implicit structure ordering dependency.
| structures | ||
| ) | ||
|
|
||
| self.assertEqual(len(structures), 1) |
There was a problem hiding this comment.
I'm not sure how much this is adding over individual transformation tests. You could remove the MaxVolumeSiteSelectionTransformation.transform call everything would still pass. Both of your final asserts are targeted to a single transformation but the way it's written doesn't attribute the change to either of the function calls.
|
|
||
| selected = transformation.transform([small, large]) | ||
|
|
||
| self.assertEqual(selected[0].ligand_name, "large") |
There was a problem hiding this comment.
why do we expect a reordering of structures after a transformation?
| atom.name, | ||
| ) | ||
| continue | ||
| clashing_h = min(p_hs, key=lambda i: float(np.linalg.norm(p_xyz[i] - l_xyz[l_heavy]))) |
There was a problem hiding this comment.
It seems to me that this could strip a neutral lysine down to a single hydrogen by mistake.
There was a problem hiding this comment.
These should probably be in the ComplexProtonationTransformation class as methods instead of individual classes.
There was a problem hiding this comment.
I'd say hold off on doing this type of abstraction until we know we need it. As some of the other comments go over, most of this behavior should probably be packaged into the ComplexProtonationTransformation class. There may come a day when we add more implementations and something like this will be helpful.
There was a problem hiding this comment.
As stated in other comments, not sure this subpackage is needed.
There was a problem hiding this comment.
I feel like this should all be part of the ComplexProtonationTransformation class.
There was a problem hiding this comment.
Let's go over some of the logic here in our next sync
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def _perceive_ligand(structure: Structure) -> Chem.Mol | None: |
There was a problem hiding this comment.
This is called in multiple validators. In a new PR I'll have this called in the parent class and cache the results.
| opt into only the stages they want -- e.g. prepare without protonating, or | ||
| protonate a set that was selected elsewhere. | ||
|
|
||
| Ordering contract: **index 0 is the canonical protein context.** The selection |
There was a problem hiding this comment.
We should not use order dependence here.
| out_file = pdb_io.PDBFile() | ||
| out_file.set_structure(structure) | ||
| out_pdb = Path(tmp) / "protein_h.pdb" | ||
| out_file.write(str(out_pdb)) |
There was a problem hiding this comment.
Can this be done in-memory?
There was a problem hiding this comment.
Don't do ordering, stick to duplication of effort. We can handle it in a later PR with caching.
Given selected
Structures (one per ligand query), do the following transformations:side-track: also adding the full n=16 TYK2 set from the OFE industry benchmarks