KGX Export
just export-kgx projects kb/disorders/*.yaml into a Biolink-typed KGX graph
via koza:
just export-kgx # -> output/kgx/kgx_export_{nodes,edges,sepio}.jsonl
The transform lives in
src/dismech/export/kgx_export.py.
The _sepio sidecar is a separate model documented in
SEPIO Evidence Export; it joins to the edge file on id.
Edge qualifiers
Several dismech slots carry a modifier that the Biolink predicate alone
cannot express. Biolink declares the qualifiers slot as:
qualifiers:
deprecated: true
description: connects an association to qualifiers that modify or qualify the meaning of that association
range: ontology class
multivalued: true
range: ontology class — so every entry is a CURIE naming a class. A
free-text entry like direction:increased is not one, and its prefix claims a
namespace (direction:) that nothing can resolve. Both forms are gone; see
9131.
Two honest caveats about this slot. Biolink marks it deprecated: true in the
same block, and it is used anyway because it is the only qualifier slot the
pinned bindings expose on these association classes — the typed direction
qualifiers are the eventual destination (#9132). And the dismech: CURIEs below
satisfy range: ontology class in form but not in substance: nothing publishes a
class at those IRIs. Both are conscious positions, not oversights.
| Source slot | Emitted on | Qualifies |
|---|---|---|
Descriptor.modifier |
Disease→process edges (biological process, molecular function, pathway) | the object |
exposure_term.modifier |
Exposure→Disease edges | the subject |
downstream[].causal_link_type |
pathophysiology causal edges (SEPIO sidecar) | the edge |
Note pathway_to_edge carries object_category: biolink:Pathway rather than a
GO category, even though the term is a GO biological process — a consumer
filtering qualifier-carrying edges by category has to include biolink:Pathway
alongside biolink:BiologicalProcess and biolink:MolecularActivity.
The vocabulary comes from the schema
Values are not invented by the exporter. ModifierEnum already binds four of
its seven permissible values to PATO, and those export as the bound term:
ModifierEnum |
Emitted CURIE | Label |
|---|---|---|
INCREASED |
PATO:0002300 |
increased quality |
DECREASED |
PATO:0002301 |
decreased quality |
ABNORMAL |
PATO:0000460 |
abnormal |
ABSENT |
PATO:0000462 |
absent |
DYSREGULATED |
dismech:ModifierEnum#DYSREGULATED |
— |
GAIN_OF_FUNCTION |
dismech:ModifierEnum#GAIN_OF_FUNCTION |
— |
LOSS_OF_FUNCTION |
dismech:ModifierEnum#LOSS_OF_FUNCTION |
— |
The last three carry no meaning: in the schema, so they fall back to the
dismech namespace, declared in the schema prefix map as
https://w3id.org/monarch-initiative/dismech/ and set as default_prefix. These
are resolvable CURIEs pointing at our own model, which is the right answer when
dismech and Biolink do not align: refer to the dismech model by its own prefix
rather than minting a fictional one.
The scope of that "no term" finding matters, so it is recorded precisely. The
schema carries two different notes here, with different scopes: DYSREGULATED is
"No PATO term exists — verified via OAK 2026-06-26", while GAIN_OF_FUNCTION
and LOSS_OF_FUNCTION are "No suitable ontology term found across
PATO/GENO/GO/SO (verified 2026-06-26)". An OLS-wide recheck on 2026-08-20
confirmed the first and partly overturned the second:
DYSREGULATED— confirmed unbound. Every "dysregulation" hit across PATO, GO, NCIT, OGMS and MPATH is a disease entity (IPEX, DMDD), not a quality. The nearest quality isPATO:0000460abnormal, whichABNORMALalready uses and which is a distinct curator choice.GAIN_OF_FUNCTION/LOSS_OF_FUNCTION— candidates exist that the original four-ontology search did not surface:PATO:0001625"increased functionality" andPATO:0001624"decreased functionality", in a PATO branch separate from theincreased quality/decreased qualityused forINCREASED/DECREASED. Not adopted: it is a schema question (they belong on the enum'smeaning:, which this exporter only reads), and the fit is imperfect —ModifierEnum'sGAIN_OF_FUNCTIONmeans escaping regulatory control rather than increased ability to perform a function. Note this contradicts the schema note, which says nothing suitable was found across PATO. Tracked in #9136; the export follows whatever the schema binds.
CausalLinkTypeEnum binds none of its four values, so all four take the same
route: dismech:CausalLinkTypeEnum#DIRECT, #INDIRECT_KNOWN_INTERMEDIATES,
#INDIRECT_UNKNOWN_INTERMEDIATES, #UNKNOWN.
The fallback is qualified by its enum, not flat. 18 permissible-value names
in this schema belong to more than one enum. GAIN_OF_FUNCTION and
LOSS_OF_FUNCTION are among them — they are FunctionalImpactEnum values too,
where they describe a variant's consequence rather than a pathway's activity
state, a distinction the schema keeps deliberately and which can co-occur on one
node. A flat dismech:GAIN_OF_FUNCTION would give both concepts one IRI. The
dismech:{Enum}#{VALUE} form matches SchemaView.get_uri(ModifierEnum) and the
fragment convention already used for pathophysiology node IDs.
test_modifier_curies_match_schema_meanings pins the map against
src/dismech/schema/dismech.yaml, so the exporter and the schema cannot drift;
test_no_qualifier_is_a_bare_string pins that every emitted value is a CURIE
under a declared prefix.
Which end a qualifier applies to
The CURIE does not say whether it qualifies the subject or the object — it is a
quality, not a statement about edge geometry. It is recoverable from the edge:
a disease is never the end a decreased quality describes, so on an exposure
edge the qualifier can only be about the exposure, and on a Disease→process edge
only about the process.
Biolink's typed subject_direction_qualifier / object_direction_qualifier
would state it outright, but neither is present on Association or
ExposureEventToOutcomeAssociation in the pinned bindings (their only qualifier
slots are qualifier, qualifiers, and — on the exposure class —
population_context_qualifier and temporal_context_qualifier). Tracked in
9132.
Why exposure polarity matters
The exposure edge's subject is the ECTO term itself, so the qualifier is what
separates a deficiency from an exposure. Both of these curate ECTO:9000123
(exposure to folic acid) as the subject:
Anencephaly ECTO:9000123 --contributes_to--> MONDO:0000819
qualifiers: [PATO:0002301] # decreased quality
Ventricular_Septal_Defect ECTO:9000123 --associated_with_decreased_likelihood_of--> MONDO:0002070
qualifiers: [PATO:0002300] # increased quality
Without the qualifier the first triple asserts that exposure to folate causes anencephaly, the opposite of the curated claim (#8468). The qualifier is carried independently of the predicate because the two axes are orthogonal: the second entry is an increased exposure that is protective.
The causal graph (#2101)
Each pathophysiology node is exported as a node of category
biolink:PathologicalProcess, with the id the SEPIO sidecar mints for it:
dismech:<Disease_Name>#<Node_Name>. Three kinds of edge come out of the graph:
| Source slot | Edge | Predicate |
|---|---|---|
pathophysiology[] |
Disease → node | biolink:disease_has_basis_in |
pathophysiology[].downstream[] |
node → node, or node → phenotype term | biolink:causes |
phenotypes[].sequelae[] |
phenotype term → phenotype term (or node) | biolink:causes |
A bare target resolves to the pathophysiology node of that name, else to the
bound term of the phenotype of that name, the same lookup the pathograph uses.
A downstream edge carries its causal_link_type in qualifiers. An HP → HP
sequelae edge is a PhenotypicFeatureToPhenotypicFeatureAssociation with the
disease as disease_context_qualifier, because the two terms alone say nothing
about which disease the progression was curated for.
Node and downstream edges use the SEPIO statements' deterministic UUIDv5 ids
instead of random ones, so they are stable across releases and join to the
sidecar. The SEPIO statements keep their own predicates
(dismech:has_pathophysiology, dismech:causally_upstream_of) and the
hypothesis groups, which a KGX edge has no slot for.
The per-slot edges from a node's cell types, processes and locations to the disease are unchanged. They now exist alongside the graph rather than instead of it; routing them through the node is a separate decision with consumer impact.
Diagnosis, datasets and trials (#2100, #2103)
| Source slot | Edge | Predicate |
|---|---|---|
differential_diagnoses[] |
Disease → Disease | biolink:similar_to |
diagnosis[].diagnosis_term |
Disease → biolink:Procedure |
biolink:is_diagnosed_by |
datasets[] |
biolink:Dataset → Disease |
biolink:mentions |
clinical_trials[] |
biolink:ClinicalTrial → Disease |
biolink:related_to_at_instance_level |
clinical_trials[].target_phenotypes[] |
biolink:ClinicalTrial → PhenotypicFeature |
biolink:related_to_at_instance_level |
None of these has an exact Biolink 4.4 predicate, so each is the closest canonical one:
- Differentials. There is no differential-diagnosis predicate (the
disease_has_differential_diagnosisproposed in #2100 does not exist).similar_tois symmetric and says the two diseases share features, which is why a differential is listed, and claims nothing more. - Diagnosis.
is_diagnosed_bydeclares the rangediagnostic aid, "a device or substance".diagnosis_termbinds NCIT procedures such as computed tomography, so the object is typedbiolink:Procedurefor what it is, outside the predicate's declared range. - Datasets.
mentionsis Biolink's predicate for an information content entity, whichdatasetis, referring to a named thing when the context is not modelled. The node id is theaccessionCURIE with its prefix lowercased (GEO:GSE1andgeo:GSE1are one node); the dataset'spublicationis added to the edge'spublications. An accession that is a publication (PMID:,PMC:,DOI:), a URL, or aclinicaltrials:id is not exported as a dataset: a publication is already cited in edges'publications, a DOI does not say whether it names a paper or a dataset, a URL is not a CURIE, and a trial is exported fromclinical_trials. - Trials. Biolink models a trial's conditions as a node property, and its
trial predicates take a chemical as subject, so the edge is the generic
instance-level one. An NCT id becomes
clinicaltrials:NCT…. A trial on another registry is keyed on theICTRP:record the entry cites. A trial with neither gets no node, because the exporter does not extract registry ids from free text.
One id, one category
extract_nodes writes a node the first time a record names its id, with that
section's category. An NCIT term curated as both a treatment and a diagnostic
procedure in one entry (NCIT:C15240 Genetic Counseling, NCIT:C15329 Surgical
Procedure) is therefore written as biolink:Treatment, while its
is_diagnosed_by edge says object_category: biolink:Procedure. Across entries
the node file carries both. This is the same first-section-wins behaviour that
already gives some CHEBI ids both ChemicalEntity and MolecularEntity, and is
not specific to the new sections.
What is not exported
cellular_components[].modifier—cellular_component_to_edgenever readsmodifier, so 61 curated values acrosskb/are dropped. Pre-existing, and left alone here rather than quietly widening this change; wiring it would add qualifiers to edges that have never carried them.Pathophysiology.triggers— the other route an ECTO exposure term reaches a mechanism node. It has no KGX edge at all, so the exposure-polarity handling above does not apply to it.- Model links (
experimental_models,animal_models,computational_models) and their readouts.kgx_export.pyhas no model handling; these render in the HTML card only. diagnosis[].measurements(LOINC). Only the NCITdiagnosis_termis exported.datasets[].findings. A dataset exports as one node and one edge.- A causal
targetthat resolves to nothing (just check-causal-targets), or to a phenotype with no bound term. The edge is skipped; it is never given an invented id. Its SEPIO statement is still written, so these are the sidecar statements with no matching edge (490 at the time of writing, allpathophysiology.downstream).