Skip to content

Replace the hardcoded OXT hydrophobicity with a value derived from the selected scale #34

Open

Description

@teese

User Story

As someone building a dataset of Prodes features from structures that came from more than one source
I want the C-terminal OXT atom scored with a hydrophobicity appropriate to a carboxylate oxygen
So that the surface hydrophobicity features describe the protein rather than which program wrote the file

Description

Why this change matters:

  • Problem: structure sources disagree about whether to write the C-terminal OXT atom, and Prodes gives that atom an anomalous hydrophobicity. A dataset assembled from more than one source therefore carries a systematic difference between its sources that has nothing to do with the proteins.
  • Current behaviour: Property_point.set_lipo in src/prodes/core/point.py gives every atom named OXT a hydrophobicity of exactly 1.0. Every other heavy atom takes the value of the residue it belongs to. On the default mj_scaled scale, 1.0 is the maximum, the same value as phenylalanine.
  • Desired behaviour: OXT is scored with a value appropriate to a carboxylate oxygen, which is strongly hydrophilic, and that value is derived from the selected scale rather than hardcoded.

Measured over 855 structures assembled for an ion-exchange retention study, which source writes an OXT:

source structures with OXT
AlphaFold database 200 of 200 sampled
Boltz2 predicted monomers 0 of 200 sampled
crystal structures 21 of 29

So the anomalous value is currently applied to every AlphaFold structure, no Boltz2 structure, and most but not all crystal structures.

This is a hydrophobicity problem, not a charge problem, so ion exchange work is largely unaffected. OXT carries no charge in Prodes: Atom.charge assigns charge only to the side-chain atoms listed for a residue type and to the backbone atom named N or C at a terminus, and OXT is neither. Checked directly: across five test structures, all four OXT atoms return a charge of zero at pH 7.4. Measured on 40 Boltz2 structures, adding an OXT left Formal charge and Isoelectric point unchanged on all 40. The electrostatic surface features move very slightly, by a median of 0.03 per cent on NSurfPosEpFormal, but that is the ordinary consequence of one more heavy atom in the solvent-accessible surface and would happen whatever hydrophobicity the atom carried.

The hydrophobicity features do move, and in one direction: NSurfPosMhp rises on 33 of the 39 structures where it moves at all, and SurfMhpMean rises on 12 of the 13.

Technical Context

The line is src/prodes/core/point.py:

contributions = np.array([1.0 if a.name == "OXT" else scale_dict[a.residue_name] for a in atoms_masked])

Three things are worth knowing before changing it:

  • The lookup the special case bypasses would work. An OXT atom carries the residue name of its own residue, which is always present in the scale, so the special case is not preventing a KeyError.
  • The constant is not scale-aware. It is 1.0 whatever --hydro is set to, so it is the maximum on mj_scaled, roughly mid-range on kd where the maximum is 4.5, and something else again on cw.
  • The behaviour is inherited from the original tneijenhuis/prodes repository and this fork preserves it. Changing it breaks output compatibility with the original for any structure that has an OXT, which is most of them.

Implementation Suggestion

Two candidates, and the choice should be made deliberately rather than by whichever is easier:

  • Remove the special case, so OXT takes its residue's value like every other heavy atom of that residue. Minimal, internally consistent with how the rest of the function works, and scale-aware for free.
  • Give OXT an explicit low value derived from the selected scale, for example the scale's minimum or the value of the most hydrophilic residue on it. More chemically defensible for a carboxylate oxygen, but it introduces an atom-level concept into a residue-level function.

Whichever is chosen, the value must come from the scale rather than be a literal, so that --hydro continues to mean something.

Acceptance Criteria

  • OXT no longer receives a hardcoded value, and the value it does receive is derived from the selected hydrophobicity scale.
  • Running Prodes on the same structure with --hydro mj_scaled, --hydro kd and --hydro cw gives OXT a value on each scale that sits at the hydrophilic end of that scale.
  • The change is released under a major version bump, and the README's Output compatibility section records that hydrophobicity feature values move for any structure containing an OXT.
  • docs/structure_preparation.md, which currently documents the present behaviour, is updated to describe the new behaviour.
  • The size of the change to each affected feature is measured on a set of structures and recorded, in the way the existing analyses under docs/ are.

Out of Scope

  • Changing how hydrophobicity is assigned to any atom other than OXT.
  • Moving from residue-level to atom-level hydrophobicity generally. That is a larger question and would supersede this ticket rather than be part of it.
  • Any change to charge assignment or to the electrostatic features.
  • Adding OXT to structures that lack one. That is the job of the structure preparation step already documented in the README.

Testing Considerations

  • A unit test that a C-terminal OXT receives a hydrophilic value, on each of the shipped scales, so that a future hardcoded constant cannot reappear unnoticed.
  • The existing regression test will fail and that is expected. tests/test_sasa.py compares this fork's output against tests/data/ARH96693_prodes_orig_output.csv, which was generated by the original code. That structure contains one OXT, on SER 60 of chain A, so its hydrophobicity columns will move. Decide whether to regenerate the reference file or to exclude the hydrophobicity columns from that comparison, and say which in the commit.
  • A test on a structure with more than one chain, which has one OXT per chain rather than one per structure.

Dependencies

  • None blocking. The change is self-contained within Property_point.set_lipo.
  • It should land before any dataset is regenerated with the structure preparation step now documented in the README, because that step adds an OXT to every structure that lacks one and so applies the current behaviour uniformly.

Resources

Estimation

Story Points: 3

Priority

Medium

Nothing is broken for ion exchange elution prediction, which is charge-driven. It matters for hydrophobicity-driven work and for any dataset that mixes structure sources.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions