Skip to content

Commit be658a2

Browse files
authored
Merge pull request #12 from ecrum19/fix/vcf45-structured-accessors
Resolve local alleles correctly and emit the missing VCF 4.5 accessors
2 parents e4de9d0 + ad4c6c6 commit be658a2

22 files changed

Lines changed: 1492 additions & 89 deletions

‎CITATION.cff‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ cff-version: 1.2.0
22
message: "If you use VCF-RDFizer in your research, please cite it using the metadata below."
33
title: "VCF-RDFizer"
44
type: software
5-
version: "3.0.2"
5+
version: "3.0.3"
66
authors:
77
- name: "VCF-RDFizer maintainers"
88
repository-code: "https://github.com/ecrum19/VCF-RDFizer"

‎README.md‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -347,9 +347,12 @@ preserves the per-sample vocabulary model:
347347
- every represented FORMAT slot is a `vcfc:FormatFieldValue`, linked to its
348348
declaration with `vcfc:declaredBy`;
349349
- `GT` is parsed into a `vcfc:Genotype` with an explicit `vcfc:phasingStatus`
350-
and one `vcfc:GenotypeAlleleCall` per position, and `FT`, `PS`/`PSL`, `LAA`,
351-
the copy-number and haplotype keys, and the `M`/`DPM`/`ADM` base-modification
352-
families get their own resources;
350+
— `vcfc:MixedPhasing` when its positions disagree — and one
351+
`vcfc:GenotypeAlleleCall` per position carrying its own `vcfc:phaseIndicator`,
352+
and `FT`, `PS`/`PSL`, `LAA` (with per-sample `vcfc:LocalAlleleMembership`
353+
ordinals), the copy-number and haplotype keys, and the `M`/`DPM`/`ADM`
354+
base-modification families in both their numeric and aliased spellings get
355+
their own resources;
353356
- the VCF file declares `vcfc:representationProfile vcfc:ExpandedRepresentation`.
354357

355358
With the default rules, these triples are appended directly from `records.tsv`;
@@ -1080,7 +1083,7 @@ Safe termination:
10801083

10811084
If you use VCF-RDFizer in a publication, please cite:
10821085

1083-
VCF-RDFizer maintainers. (2026). *VCF-RDFizer* (Version 3.0.2) [Computer software]. GitHub. https://github.com/ecrum19/VCF-RDFizer
1086+
VCF-RDFizer maintainers. (2026). *VCF-RDFizer* (Version 3.0.3) [Computer software]. GitHub. https://github.com/ecrum19/VCF-RDFizer
10841087

10851088
BibTeX:
10861089

@@ -1089,7 +1092,7 @@ BibTeX:
10891092
author = {{VCF-RDFizer maintainers}},
10901093
title = {VCF-RDFizer},
10911094
year = {2026},
1092-
version = {3.0.2},
1095+
version = {3.0.3},
10931096
url = {https://github.com/ecrum19/VCF-RDFizer},
10941097
note = {Computer software}
10951098
}

‎conda-recipe/README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ do not submit this package to `staged-recipes`.
77

88
## Before submitting to conda-forge
99

10-
1. Commit the version bump, then create and push a Git tag (for example `v3.0.2`).
10+
1. Commit the version bump, then create and push a Git tag (for example `v3.0.3`).
1111
2. Download the source tarball and compute sha256:
1212
```bash
1313
curl -L -o vcf-rdfizer.tar.gz \
14-
https://github.com/ecrum19/VCF-RDFizer/archive/refs/tags/v3.0.2.tar.gz
14+
https://github.com/ecrum19/VCF-RDFizer/archive/refs/tags/v3.0.3.tar.gz
1515
shasum -a 256 vcf-rdfizer.tar.gz
1616
```
1717
3. Replace `version` and `sha256` in the feedstock's `recipe/meta.yaml`.

‎conda-recipe/meta.yaml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{% set name = "vcf-rdfizer" %}
2-
{% set version = "3.0.2" %}
2+
{% set version = "3.0.3" %}
33

44
package:
55
name: {{ name|lower }}
66
version: {{ version }}
77

88
source:
99
url: https://github.com/ecrum19/VCF-RDFizer/archive/refs/tags/v{{ version }}.tar.gz
10-
sha256: 46c08df157269ec1e3b1c4666a20e37a7b1709b9aa8366265a2bae91d10ae637
10+
sha256: REPLACE_WITH_GITHUB_TARBALL_SHA256
1111

1212
build:
1313
noarch: python

‎docs/architecture.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ whose size is the product of its dimensions.
8989

9090
| Emitter | Emits | Why not RML |
9191
| --- | --- | --- |
92-
| `emit_sample_representation` | Expanded: `SampleCall`/`FormatFieldValue`/`Genotype`/`PhaseSet`/`LocalAlleleSet`/`BaseModification`. Condensed: `SampleSet`/`CohortCallMatrix`/`FormatValueVector`. `SampleSet` and `VCFSample` in both. | RML would first have to materialize a helper table of variants × samples (× FORMAT keys) — the largest intermediate the pipeline can produce |
92+
| `emit_sample_representation` | Expanded: `SampleCall`/`FormatFieldValue`/`Genotype`/`PhaseSet`/`LocalAlleleSet`/`LocalAlleleMembership`/`BaseModification`. Condensed: `SampleSet`/`CohortCallMatrix`/`FormatValueVector`. `SampleSet` and `VCFSample` in both. | RML would first have to materialize a helper table of variants × samples (× FORMAT keys) — the largest intermediate the pipeline can produce |
9393
| `append_header_representation_rdf` | Header-line subclasses, the ordered `HeaderAttribute` resources every structured line needs, and the INFO/FORMAT/FILTER/ALT/contig/META/SAMPLE/PEDIGREE declaration properties | RML cannot choose a class per row, nor decompose one angle-bracketed cell into an ordered set of resources |
94-
| `emit_record_detail` | `ID`, `ALT`, `QUAL`, `FILTER`, `infoRaw` (each may be the missing token); the allele layer (`ReferenceAllele`/`AltAllele`, breakends, symbolic types); structured `InfoFieldValue` with its `FieldValueItem` decomposition; and the SV carriers | RML cannot type the missing token per row, cannot switch a literal's datatype on a declared `Type`, and the allele and value-item layers would need variants × alleles and variants × INFO-keys helper tables |
94+
| `emit_record_detail` | `ID`, `ALT`, `QUAL`, `FILTER`, `infoRaw` (each may be the missing token); their decompositions (`RecordIdentifier`, `filterStatus` with its `FilterCode` resources, the ordered `FormatKey` list); the allele layer (`ReferenceAllele`/`AltAllele`, breakends, symbolic types); structured `InfoFieldValue` with its `FieldValueItem` decomposition; and the SV carriers | RML cannot type the missing token per row, cannot switch a literal's datatype on a declared `Type`, and the allele, decomposition and value-item layers would need variants × alleles, variants × components and variants × INFO-keys helper tables |
9595

9696
The exception count did not change with the move to the VCF Core vocabulary, but
9797
what each emitter covers grew considerably: the vocabulary models the VCF 4.5

‎docs/conversion.md‎

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,23 @@ Float lexical space:
129129
| `.` or empty | `"."^^vcfc:Null` |
130130
| anything else | a plain string literal, deliberately kept rather than dropped, and reported by the SHACL layer |
131131

132+
Three of those cells are also **decomposed**, in both INFO representations,
133+
because a single cell carries a list the vocabulary models as resources:
134+
135+
- **ID.** Each semicolon-separated component becomes a
136+
`vcfc:RecordIdentifier` at `…#record/{ROW_ID}/id/{N}`, linked with
137+
`vcfc:hasIdentifier` and carrying `identifierValue` and `componentIndex`.
138+
The raw `vcfc:recordId` stays beside it.
139+
- **FILTER.** The call gets a `vcfc:filterStatus` of `vcfc:FiltersPassed`
140+
(`PASS`), `vcfc:FiltersNotApplied` (`.`) or `vcfc:FiltersFailed`. In the
141+
failed case each code also becomes a `vcfc:FilterCode` with
142+
`filterCodeValue` and `componentIndex`, joined to its `##FILTER` declaration
143+
with `vcfc:declaredByFilter` so a consumer can read the description without
144+
re-reading the header.
145+
- **FORMAT.** The key list becomes ordered `vcfc:FormatKey` resources carrying
146+
`vcfc:fieldIndex` and `vcfc:declaredBy`, beside the raw `vcfc:formatRaw`.
147+
The IRI shape follows the class's own `iriTemplate`.
148+
132149
`--info-representation structured` (the default) additionally emits:
133150

134151
- **The allele layer.** `REF` and each `ALT` item become an ordered
@@ -143,7 +160,8 @@ Float lexical space:
143160
`CHROM` names, with `vcfc:chromosome`.
144161
- **Structured INFO.** One `vcfc:InfoFieldValue` per record and key at
145162
`…#call/{ROW_ID}/info/{KEY}`, linked to the `##INFO` declaration through
146-
`vcfc:declaredBy`. A single-valued field whose declared `Type` is `Integer` or
163+
`vcfc:declaredBy` and carrying `vcfc:fieldIndex`, its position in the INFO
164+
column. A single-valued field whose declared `Type` is `Integer` or
147165
`Float` also gets a typed `fieldValueInteger` / `fieldValueDecimal`; a `Flag`
148166
gets `vcfc:fieldValueBoolean true`.
149167
- **Value items.** A field whose declared `Number` gives its positions meaning —
@@ -154,6 +172,11 @@ Float lexical space:
154172
…) record the width with `vcfc:tupleArity` so a consumer can regroup them.
155173
This closes the multi-valued-INFO gap that earlier versions recorded in
156174
`limitations.md`.
175+
176+
The local-allele codes `LA`/`LR`/`LG` are a special case: they index a
177+
*sample's* allele subset, and an INFO field has no sample. Their items are
178+
therefore still materialized and indexed, but carry no `vcfc:forAllele` —
179+
see the genotype layer below, where the subset exists.
157180
- **The SV carriers.** `SVLEN`, `SVCLAIM`, `IMPRECISE`, `NOVEL`, `END`, `EVENT`
158181
with `EVENTTYPE`, the confidence intervals (`CIPOS`/`CIEND` through FALDO
159182
`InRangePosition`, the rest as `vcfc:ConfidenceInterval`), and the
@@ -224,11 +247,45 @@ instead of relying on repeated `sampleId` literals.
224247
**Expanded mode additionally parses the per-sample values** into the vocabulary's
225248
genotype layer: `GT` becomes a `vcfc:Genotype` with `genotypeString`, `ploidy`,
226249
`phasingStatus` and one `vcfc:GenotypeAlleleCall` per position (each either
227-
pointing at the allele it called or flagged `isNoCall`); `FT` becomes
228-
`sampleFilter`; `PS`/`PSL`/`PSO`/`PSQ` become a `vcfc:PhaseSet`; `LAA` becomes a
229-
`vcfc:LocalAlleleSet`; `CN`/`CNQ`/`CNL`/`CNP` and `HAP`/`AHAP` become their
230-
copy-number and haplotype properties; and the pattern-defined `M`/`DPM`/`ADM`
231-
keys become `vcfc:BaseModification` resources pointing at a ChEBI residue.
250+
pointing at the allele it called or flagged `isNoCall`, and each carrying its
251+
own `vcfc:phaseIndicator`); `FT` becomes `sampleFilter`; `PS`/`PSL`/`PSO`/`PSQ`
252+
become a `vcfc:PhaseSet`; `LAA` becomes a `vcfc:LocalAlleleSet`;
253+
`CN`/`CNQ`/`CNL`/`CNP` and `HAP`/`AHAP` become their copy-number and haplotype
254+
properties; and the pattern-defined `M`/`DPM`/`ADM` keys become
255+
`vcfc:BaseModification` resources pointing at a ChEBI residue.
256+
257+
Four details of that layer are worth stating explicitly:
258+
259+
- **Local-allele fields are resolved through `LAA`, not positionally.** A
260+
`Number=LA`/`LR` value list indexes the **sample's own local allele subset**,
261+
not the record's ALT column. With `LAA=2,4` the local set is
262+
{REF, ALT2, ALT4}, so the three values of an `LR` field belong to global
263+
alleles 0, 2 and 4 — not 0, 1, 2. Each `vcfc:FieldValueItem` is joined with
264+
`vcfc:forAllele` through that mapping. A sample that declares no `LAA` has no
265+
local ordering to resolve against, so its items keep `vcfc:valueIndex` and get
266+
no `vcfc:forAllele` at all: a wrong link is worse than an absent one, because
267+
nothing in the graph would signal it. `LAA` is read before the per-key loop,
268+
since a FORMAT string may legally list it after a field that depends on it.
269+
- **Phasing is per position, not per genotype.** `vcfc:phasingStatus` is
270+
`vcfc:Phased` or `vcfc:Unphased` only when every position agrees. A genotype
271+
whose indicators disagree — `0|1/2` — gets `vcfc:MixedPhasing`, and the
272+
per-call `vcfc:phaseIndicator` carries the precise reading. A single verdict
273+
would misdescribe it.
274+
- **Local-allele position belongs to the membership.** Alongside
275+
`vcfc:hasLocalAllele`, each member of a `vcfc:LocalAlleleSet` gets a
276+
`vcfc:LocalAlleleMembership` with `vcfc:localAllele` and `vcfc:localIndex`.
277+
The ordinal cannot live on the allele resource, which is shared across
278+
samples: two samples may give the same ALT different local positions. The
279+
vocabulary deprecates `vcfc:localAlleleIndex` for that reason, and the
280+
converter no longer emits it.
281+
- **Base-modification aliases resolve.** VCF 4.5 reserves each modification key
282+
in two spellings — `M[0-9]+[ACGTUN]` and a named alias such as `M5mC` for
283+
`M27551C` — across all three families, 30 aliased keys in total. Both
284+
spellings build the same carrier against the same ChEBI residue, while the
285+
key keeps its written spelling in the resource IRI so the graph traces back
286+
to the source column. A `Number=M` field is decomposed over the bases the
287+
genotype's called alleles actually carry, in GT order, since that is the
288+
order the specification defines it over.
232289

233290
**Condensed mode deliberately stops at the vector.** Decomposing GT per sample is
234291
exactly the per-sample materialization the condensed profile exists to avoid.

‎docs/limitations.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,10 @@ declared assembly is recorded but not used for anything.
8080
target to the VCF Core vocabulary closed three gaps that used to be listed here:
8181
symbolic ALTs, breakends and `*` are now classified and parsed; `Number=A/R/G/P`
8282
values are decomposed into per-allele `vcfc:FieldValueItem` resources; and `GT`
83-
is parsed into ordered allele calls with an explicit phasing status. See
83+
is parsed into ordered allele calls with an explicit phasing status. The `ID`,
84+
`FILTER` and `FORMAT` columns followed: each is now decomposed into ordered
85+
resources beside its raw literal, and a genotype whose positions disagree about
86+
phasing is `vcfc:MixedPhasing` rather than forced to a single verdict. See
8487
[`conversion.md`](conversion.md#4-stage-three--the-wrappers-own-emitters).
8588

8689
What remains true:

‎docs/rml-mappings.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ published vocabulary is worth knowing about: the ontology declares
247247
shape constrains them with `sh:datatype xsd:integer`, and a SHACL datatype
248248
constraint compares the datatype IRI exactly. This converter emits
249249
`xsd:integer`, which satisfies both. See
250-
[`validation-migration-notes.md`](validation-migration-notes.md#3-ordinals-are-now-xsdinteger-not-xsdpositiveinteger).
250+
[`validation-migration-notes.md`](validation-migration-notes.md#5-ordinals-are-xsdinteger).
251251

252252
---
253253

‎docs/roadmap.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ a record with `ALT=.` satisfies both the shape and `vcfc:missingValuePolicy`.
3333

3434
### ~~1. The validation suite has not been migrated~~ — **done**
3535

36-
The suite passes: 415 tests, mutation score 96/113 (85%) across 60 mutations.
36+
The suite passes: 707 tests, mutation score 96/113 (85%) across 60 mutations.
3737
The runner and the wrapper now share one vocabulary module instead of mirroring
3838
each other, and the census derives every new resource family from the VCF. Ten
3939
of the eighteen new mutations are recorded gaps, each with what would close it.

‎docs/sample-representation-guide.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,8 @@ a decoder or an application-level vector function. If you need to query
231231
genotypes directly, that is the argument for expanded mode.
232232

233233
Everything *not* per-sample is identical in both profiles: the header layer, the
234-
allele layer, structured INFO with its value items, and the SV carriers are all
235-
emitted the same way.
234+
allele layer, structured INFO with its value items, the SV carriers, and the
235+
record-level ID/FILTER/FORMAT-key decompositions are all emitted the same way.
236236

237237
### Condensed growth
238238

0 commit comments

Comments
 (0)