Data Model¶
Semantic primitives¶
A semantic primitive describes what kind of OMOP thing something is, without saying anything about which CDM table it goes in.
There are four:
OmopConcept
A single OMOP concept_id. Use this when exactly one concept is valid for a slot.
OmopGroup
A named set of OMOP concepts defined by their relationship to one or more anchor (parent_concepts) concepts. The group's meaning is: "any standard descendant of these anchors." The library itself does not expand descendants — that belongs in a downstream database-aware layer. What the library stores and resolves are the anchor ids.
OmopEnum
An explicitly listed, static set of concepts. Use this for short, stable lists that should not change dynamically with vocabulary updates (e.g. the T/N/M staging axes, episode type codes).
OmopValueSet
A composite of the above. Use this when a template slot accepts concepts from multiple groups or enums — for example, a staging template whose entity could be T-stage, N-stage, or group-stage concepts.
At runtime, all four resolve to set[int] via OmopSemanticResolver.resolve().
Templates¶
A template binds a semantic primitive to a CDM row shape. It has:
- a role (e.g.
"demographic","staging","modifier") - an entity_concept — the semantic primitive defining valid values for the CDM concept slot
- an optional value_concept — the semantic primitive defining valid values for the value slot
- a cdm_profile — the CDM row shape (see below)
Templates are the primary unit of semantic convention. A template says: "the 'Country of birth' demographic observation lives in the observation table, uses observation_concept_id for the concept, and the valid concept is {4155450}."
CDM profiles¶
A profile describes the shape of a CDM row without making any semantic claim. It names:
- the target CDM table
- the concept slot (e.g.
observation_concept_id) - optionally, the value slot (e.g.
value_as_concept_id,value_as_string,value_as_number) - optionally, richer structural slots such as:
unit_slotoperator_slotmodifier_slotextra_concept_slotsnumeric_slotsstring_slotsreference_slots
Shipped profiles include observation_simple, observation_coded, observation_string, observation_numeric, observation_numeric_with_unit, observation_with_qualifier, measurement_numeric, measurement_numeric_with_unit, measurement_numeric_with_operator, measurement_coded, measurement_simple, procedure_simple, procedure_with_inline_modifier, condition_simple, condition_with_status, drug_exposure_simple, drug_exposure_dose, drug_exposure_with_quantity_and_days_supply, drug_exposure_with_route, device_simple, visit_simple, death_simple, specimen_simple, and specimen_with_site_status_quantity.
A profile is structural. A template gives it semantic meaning.
Output definitions¶
An output definition sits above profiles and templates. It does not replace either:
- profiles still describe valid CDM row shapes
- templates still describe semantic admissibility for grounded concepts
- output definitions describe how one deterministic semantic result becomes one or more projected rows and links
The current implementation slice provides this as a programmatic runtime surface:
OutputDefinitionOutputRowProjectionOutputLinkRuleOutputDefinitionRuntime
This is intentionally additive and runtime-only for now. A dedicated schema-backed YAML authoring surface can be added later once the execution model is stable.
Use the Visualization runtime helpers to inspect what a compiled definition produces for one input and why rows were kept, suppressed, or left unresolved.
Cross-field derivation and row suppression¶
Most of the time a row's fields come from field_bindings — context reachable from
the same grounded fact the row represents. Two situations don't fit that:
DerivationRule populates a row's slot from a different source field than
the one that grounded the row — the canonical case is a diagnosis paired with a
separately-collected role/status field (Primary/Contributing/Non-contributing).
The raw code is looked up in code_map to get a concept id; codes in
suppress_codes drop the row entirely instead.
SpecialValuePolicy suppresses a row based on its own source value, with no
second field involved — a "meets criteria for X" Yes/No field is the canonical
case, where the negative answer means nothing should be written.
Both report through ProjectedOutputBundle.suppressed_rows rather than silently
omitting the row: suppression is a deterministic outcome, not a lost fact.
Profile groups¶
A profile group is a named family of admissible profiles. For example, ObservationProfiles groups observation_simple, observation_coded, and observation_string. Profile groups are used for documentation and routing logic — they describe which shapes are valid within a broad CDM family.
Registry organisation¶
RegistryGroup is an organisational container for templates within a registry file. It has a name, a role, and a list of templates. Registry groups exist for readability and navigation; they carry no semantic meaning.
RegistryFragment is the top-level structure of a registry YAML file. It contains a list of RegistryGroup instances. Multiple fragments can be merged at load time.
Naming disambiguation¶
Three things in the codebase are called "group" and they are not the same:
| Name | What it is |
|---|---|
OmopGroup |
A semantic set of OMOP concepts defined by ancestry anchors |
RegistryGroup |
An organisational grouping of templates in a registry file |
| Profile group | A named family of admissible CDM profiles |
Anchor ids versus descendant expansion¶
The runtime resolves OmopGroup to its parent_concepts anchor ids — not to a database-expanded descendant set. This is intentional: the library is portable and requires no live vocabulary connection.
If your downstream logic needs the full descendant set, use the anchor ids from entity_concept_ids as the input to a vocabulary query in your database layer.