Skip to content

[BUG] Hydrogen Bond Metrics Not Getting Calculated #374

Description

@rclune

Describe the bug
When select_hbond_donor or select_hbond_acceptor are included in the input and HBPLUS is properly installed, hydrogen bond metrics are still not getting calculated. Setting verbose to True gives this error:
WARNING rfd3.metrics.hbonds_hbplus_metrics: [rank: 0] Could not calculate hbond metrics: 'AtomArray' object has no attribute 'is_motif_atom'

To Reproduce
HBPLUS must be installed and the environment variable must be set as is described in the README

The input PDB, JSON, and command line arguments I used for the run are attached along with a logfile and the warnings/error messages seen during the run.

I am using the version of RFD3 available through the foundry module on PyPI.

theozyme.pdb
metalloprotease_rfd3_input_hbond.json
logfile.txt
error.txt

Expected behavior
When the hydrogen bond constraints are added to an RFD3 input file additional metrics should appear in the output JSON file (such as num_hbonds).

Additional context
Without setting verbose=True no warning messages are generated and the calculation of the hydrogen-bonding metrics fails silently.

Claude's summary of the issue:

Summary

When running RFD3 inference with select_hbond_donor/select_hbond_acceptor set in the input spec (and HBPLUS_PATH correctly configured), hydrogen-bond metrics are silently never produced in the output. With verbose logging enabled, the actual failure surfaces as:

WARNING rfd3.metrics.hbonds_hbplus_metrics: [rank: 0] Could not calculate hbond metrics: 'AtomArray' object has no attribute 'is_motif_atom'

Without verbose logging, this failure is completely silent — the run completes cleanly with no errors or warnings, and the output <example>_model_<idx>.json simply has no hbond-related keys (num_hbonds, correct_donor_percent, correct_acceptor_percent, etc.) in its metrics dict.

Root cause

calculate_hbonds() in src/rfd3/transforms/hbonds_hbplus.py:159-160,209-210 directly accesses atom_array.is_motif_atom as if it's a guaranteed annotation:

current_donor_is_motif = atom_array.is_motif_atom[current_donor_mask][0]
current_acceptor_is_motif = atom_array.is_motif_atom[current_acceptor_mask][0]

is_motif_atom is not part of the guaranteed atom-array schema. REQUIRED_CONDITIONING_ANNOTATION_VALUES (src/rfd3/constants.py:8-13) only guarantees:
- is_motif_atom_with_fixed_seq
- is_motif_atom_with_fixed_coord
- is_motif_atom_unindexed
- is_motif_atom_unindexed_motif_breakpoint

is_motif_atom is meant to be derived from the first three, on demand, via get_motif_features() (src/rfd3/transforms/conditioning_base.py:55-66):

is_motif_atom = is_fixed | is_sequence_fixed | is_unindexed

Worse, the inference input-parsing path explicitly deletes is_motif_atom if present, precisely because it's meant to always be recomputed rather than reused (src/rfd3/inference/input_parsing.py:904-910):

# Ensure motif annotations are removed
atom_array.del_annotation("is_motif_token") if "is_motif_token" in atom_array.get_annotation_categories() else None
atom_array.del_annotation("is_motif_atom") if "is_motif_atom" in atom_array.get_annotation_categories() else None

So by design, no atom array flowing through inference ever carries a real is_motif_atom annotation. hbonds_hbplus_metrics.py (the metrics entry point used at inference, calculate_hbond_statsget_hbond_metrics) correctly recomputes it downstream via get_motif_features():

# src/rfd3/metrics/hbonds_hbplus_metrics.py:156
motif_mask = np.array(get_motif_features(output_atom_array)["is_motif_atom"])

...but calculate_hbonds() is called before that, inside calculate_hbond_stats (hbonds_hbplus_metrics.py:148-150), and it never recomputes the annotationit assumes it already exists, so it raises AttributeError. This is caught by a blanket try/except in get_hbond_metrics() (hbonds_hbplus_metrics.py:268-270) and silently downgraded to a warning + empty {}, so the failure is invisible unless verbose/warning-level logging is enabled.

This bug does not affect the training pathduring training, an earlier transform in the full conditioning pipeline happens to set is_motif_atom before CalculateHbondsPlus runs, masking the issue. It only manifests at inference time, where that upstream annotation is guaranteed absent.

Impact

Hydrogen-bond metrics (num_hbonds, correct_donor_percent, correct_acceptor_percent, donor_atom_names, acceptor_atom_names, hbond_connections) can never be computed during inference in the current codebase, regardless of correct HBPLUS installation/configuration or correct select_hbond_donor/select_hbond_acceptor input specification. The failure is silent by default (no warning at normal log levelsonly visible with verbose mode), making it easy to believe hbond conditioning "isn't working" when it actually is; only the metrics reporting is broken.

Suggested fix

In calculate_hbonds() (src/rfd3/transforms/hbonds_hbplus.py), derive is_motif_atom the same way get_motif_features() does, rather than assuming the annotation existse.g. at the top of the function (or immediately before first use at line 159):

from rfd3.transforms.conditioning_base import get_motif_features
...
is_motif_atom = get_motif_features(atom_array)["is_motif_atom"]
...
current_donor_is_motif = is_motif_atom[current_donor_mask][0]
current_acceptor_is_motif = is_motif_atom[current_acceptor_mask][0]

and similarly at the two other direct-access sites in CalculateHbondsPlus.forward (lines 209-210), or by having calculate_hbonds() set the annotation on the array itself before use so downstream callers stay unchanged.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions