Skip to content

Boqa blended exomiser integration - #45

Open
hansenp wants to merge 6 commits into
developfrom
BoqaBlendedExomiserIntegration
Open

Boqa blended exomiser integration#45
hansenp wants to merge 6 commits into
developfrom
BoqaBlendedExomiserIntegration

Conversation

@hansenp

@hansenp hansenp commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Lets Exomiser run BOQA-blended scoring directly, without going through
the CLI. Until now the blended analysis existed only inside
BlendedBenchmarkCommand, which reads files, loops over iterations and
writes JSON — none of which Exomiser wants.

What it does

BoqaBlendedExomiserAnalyzer (in org.p2gx.boqa.core.analysis) mirrors
one ANCHOR_VS_ANCHOR iteration of the blended benchmark: the caller
supplies anchor diseases, they are paired with each other, each pair's
HPO annotations are blended into one entry, and the patient is scored
against the anchors and their blends. A single deterministic pass,
results returned in memory.

Decisions

  • Pre-built inputs, not file paths. The constructor takes
    (Ontology, HpoDiseases, Map<String, Set<String>> genesByDisease, AlgorithmParameters). Exomiser already holds these; the wrapper
    converts HpoDiseases into our DiseaseData internally via
    DiseaseDataPhenolIngest.of.
  • Anchors are diseases, not genes. Gene-to-disease resolution stays
    the caller's concern; analyze takes disease IDs directly.
  • ANCHOR_VS_ANCHOR only, as it is Exomiser's only use case.
    Fewer than two anchors throws IllegalArgumentException, since
    pairing anchors against each other needs at least two.
  • Heavy inputs once, analyze per patient. Instances are immutable
    and the analysis is read-only, so one instance serves many patients.
  • Shared-gene guard extracted. The rule that two diseases blend only
    if no single gene explains both is now
    BlendedDiseaseData.geneDisjointBlend, used by both the wrapper and
    the CLI instead of being duplicated inline.

Notes

Anchor diseases without phenotype annotations are filtered out before
pairing. No behavior change to the CLI: its predicate was already
semantically identical to the extracted factory.

hansenp added 3 commits July 14, 2026 18:02
Disease-gene associations are domain data, like DiseaseData, and
belong in boqa-core rather than boqa-cli. Moving the parser there
lets the upcoming BOQA-blended Exomiser wrapper, and any other core
consumer, share it instead of core depending on a cli type.

Repackage the class and its test to org.p2gx.boqa.core.genes and
point the test at the genes_to_disease fixture already present in
boqa-core's test resources; drop the now-duplicate cli copy. Update
BlendedBenchmarkCommand's import accordingly; behavior is unchanged.
Provide a boqa-core facade that runs one BOQA-blended analysis for a
single patient, so Exomiser can invoke blended scoring directly. It
mirrors the ANCHOR_VS_ANCHOR path of BlendedBenchmarkCommand: the caller
supplies the anchor diseases, these are paired with each other via
BlendedDiseaseData, and the patient is scored against the anchor
diseases and their blends with BoqaSetCounter and BoqaPatientAnalyzer.

The reusable inputs (ontology, phenol HpoDiseases, disease-gene map and
algorithm parameters) are passed once at construction; the wrapper
converts HpoDiseases into the plain DiseaseData internally via
DiseaseDataPhenolIngest. analyze() is then called per patient with the
anchor diseases and a results limit, and requires at least two anchor
diseases (pairing anchors against each other is meaningless with fewer).
The run is deterministic and results are returned in memory.

The shared-gene blend rule (two anchor diseases are combined only when
their gene sets are disjoint, so diseases explained by the same gene are
never blended) is provided by a new BlendedDiseaseData.geneDisjointBlend
factory that builds the predicate from the disease-gene map; the wrapper
uses it instead of rebuilding the lambda inline.

Add a unit test covering blended-pair formation for gene-disjoint
anchors, the guard that leaves shared-gene anchors unblended, ranking of
a patient that matches one anchor, the results limit, and the
at-least-two-anchors precondition.
BlendedBenchmarkCommand built its own disjoint-gene predicate inline,
duplicating the logic that BlendedDiseaseData.geneDisjointBlend already
provides for the Exomiser wrapper. Both treat a disease without known
genes as blendable with anything, so this is a pure deduplication with
no behavior change.

Expose the associations as a disease ID to gene IDs map from
DiseaseGeneAssociations so the CLI can hand them to the factory, which
takes a plain map because Exomiser supplies one directly.

The predicate closes over nothing but those associations, which are
loaded once and never mutated, so it is now built once above the
iteration loop instead of being rebuilt on every iteration.
@hansenp
hansenp requested a review from pnrobinson July 16, 2026 09:22
BlendedDiseaseData knew which two diseases a blend was made of while
forming the pairs, encoded them into the "d1-d2" entry ID, and then
immediately split that ID apart again to look their features up. The
composition was available and thrown away.

Have formDiseasePairs return DiseasePair records instead of
concatenated strings, and record the components of every entry as it
is built: the disease itself for a singleton anchor, both diseases for
a blended pair. The split("-", 2) round trip is gone; entry IDs are
still formed as "d1-d2", since DiseaseData and Counter are keyed by
String.

Expose the composition via componentsOf(), which the upcoming Exomiser
result records need to report per-component BOQA counts alongside the
blended counts without parsing IDs. The existing pairing tests use it
instead of splitting IDs themselves; add tests for the composition of
both entry kinds and for the unknown-ID case.

No behavior change: all 3266 tests pass, including the CSV reference
scores.
@hansenp
hansenp force-pushed the BoqaBlendedExomiserIntegration branch from a6e0549 to a2f42d0 Compare July 17, 2026 11:45
hansenp added 2 commits July 17, 2026 14:07
The wrapper currently returns BoqaAnalysisResult, which carries no gene
information and cannot say which diseases a blended entry is made of.
Exomiser needs both to build a PriorityResult: a gene ID and symbol per
result, and the counts of each component next to those of the blend.

Add TargetDisease and BlendedResult, following the prototype design in
PR #43. TargetDisease pairs a candidate disease with the gene that made
it a candidate; the caller supplies gene identity, since it already
knows which gene put the disease on the list, and core would otherwise
have to load gene data it does not need. BlendedResult reports the
components with their genes, each component's counts, the counts the
entry was scored on and its score, with isBlended() distinguishing the
two cases. Single diseases use the same record with one component, so
callers need no second code path.

A blend spans two genes by construction, so both components stay
visible and the caller decides how to map them onto its per-gene
results; the prototype's single finalDisease field is left out, as it
cannot express that.

Neither record refers to Exomiser types, so boqa-core stays free of the
dependency. Nothing constructs them yet; the wrapper switches over in
the next commit.
The wrapper returned BoqaAnalysisResult, which carries no gene identity
and cannot say which diseases a blended entry is made of, so Exomiser
could not build a PriorityResult from it.

Take the candidates as TargetDiseases and return BlendedResults. Each
entry is joined to the diseases it is made of, via the composition
BlendedDiseaseData now records, and to each of their counts. The
entries are assembled before the results limit is applied: a blend
reports its components' counts, and those components are separate
entries that the limit may well cut, so limiting first would leave a
surviving blend without them.

The scored entries already arrive ranked by score, so assembling them
in order keeps that ranking, including its handling of NaN scores.

Since the caller now supplies gene identity, the wrapper no longer
needs the disease-gene map and it is dropped from the constructor. Two
candidates are consequently blended when the genes that nominated them
differ, rather than when their full gene sets are disjoint as the CLI
requires.

Which of the two rules belongs here? They part when two diseases are
nominated by different genes but share some third gene: this wrapper
blends them, the CLI does not. Should the CLI's rule win, the map
returns to the constructor and the existing geneDisjointBlend of
BlendedDiseaseData replaces the comparison, leaving the result records
untouched.

Reject a disease anchored on two genes, which BlendedDiseaseData cannot
represent, as it keys entries by disease ID and one anchor would
silently win.

Rework the wrapper tests onto the new types, and cover the single
disease case and the duplicate anchor guard. The results limit test now
caps below the number of entries while keeping the blend, so that it
fails if the entries are limited before being assembled.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant