Boqa blended exomiser integration - #45
Open
hansenp wants to merge 6 commits into
Open
Conversation
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.
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
force-pushed
the
BoqaBlendedExomiserIntegration
branch
from
July 17, 2026 11:45
a6e0549 to
a2f42d0
Compare
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 andwrites JSON — none of which Exomiser wants.
What it does
BoqaBlendedExomiserAnalyzer(inorg.p2gx.boqa.core.analysis) mirrorsone
ANCHOR_VS_ANCHORiteration of the blended benchmark: the callersupplies 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
(Ontology, HpoDiseases, Map<String, Set<String>> genesByDisease, AlgorithmParameters). Exomiser already holds these; the wrapperconverts
HpoDiseasesinto ourDiseaseDatainternally viaDiseaseDataPhenolIngest.of.the caller's concern;
analyzetakes disease IDs directly.ANCHOR_VS_ANCHORonly, as it is Exomiser's only use case.Fewer than two anchors throws
IllegalArgumentException, sincepairing anchors against each other needs at least two.
analyzeper patient. Instances are immutableand the analysis is read-only, so one instance serves many patients.
if no single gene explains both is now
BlendedDiseaseData.geneDisjointBlend, used by both the wrapper andthe 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.