Implementation: Building the Instance Graph
📍 Where we are: Part V · Implementation — the phase where the classes, axioms, and alignments of Parts III–IV become individuals in a loadable file. The methodology is still SAMOD's test-first loop — an ontology-building method that, like test-driven programming, writes the acceptance questions before the model and grows the model until they pass — but the test data is a real mAb campaign: one frozen vial of engineered CHO cells, expanded, harvested, captured, polished, and filled into vials. We instantiate that campaign, then ask the lineage and release questions a manufacturer actually asks — which bank made this lot, is it within passage, what shares a failed lot's fate — against the graph we built.
A vocabulary that no batch ever populates is a hypothesis, not an ontology. Implementation is the act of asserting the individuals of one antibody campaign — the working cell bank bp:WCB-CHO-001, the clarified harvest bp:CLAR-001, the released drug substance bp:DS-001 — so the questions a quality unit lives by stop being promises and become queries with answers. (The bp: on every identifier is just a short prefix standing in for the long web address of our local bioprocess vocabulary; read bp:DS-001 as "the drug-substance lot named DS-001 in our namespace.") This chapter walks the single file that holds the whole worked example, instances.ttl, end to end: from the transfection that creates the engineered line expressing the lead antibody, down the master-and-working cell-bank tiers, through the seed train where passage accumulates, across the harvest boundary where one tank of broth becomes two materials, through capture's many-cycles-into-one pool on a resin reused across batches, to the drug substance where eleven ancestors converge and release is decided. Every block below is a verbatim excerpt of that file. Nothing is invented; the headline numbers — passage 8 against a validated limit of 40, monomer 98.611 (the percentage of the product that is intact, single antibody molecules rather than clumps or fragments — high is good), eleven ancestors of DS-001, an out-of-spec HMW aggregate that trips the gate — are the ones a reasoner checks.
A class diagram is a set of forms: blank boxes labelled "cell bank," "batch," "drug substance." But a cell bank is not a blank box — it is a vial of living, frozen cells that every later batch descends from, the way every loaf traces back to a bakery's sourdough starter. Implementation is filling the forms in for one specific antibody: writing WCB-CHO-001 on the starter vial, BATCH-2026-001 on the production run it grew into, DS-001 on the released lot, and drawing the arrows that say this lot came from that bank. Once the forms are filled, you can finally ask a real manufacturing question — "if this lot fails, which others came from the same bank?" — instead of admiring empty boxes. This chapter fills in every form for one mAb campaign and shows the file that holds them.
RCB-CHO-001 → MCB-CHO-001 → WCB-CHO-001 → SEEDFLASK-001 → SEED-001
→ BATCH-2026-001 → CLAR-001 → PApool-001 → VIpool-001 → VFpool-001
→ POLpool-001 → DS-001 → { DP-001 , DP-002 }
out-of-spec sibling: WCB-CHO-001 → SEED-004 → BATCH-2026-004
→ PApool-004 → DS-004 → DP-004
Each arrow reads derived from — the node it points to was made from the node before it. Decoding the abbreviations once: RCB/MCB/WCB are the research, master, and working cell banks (frozen vials of the cells); SEEDFLASK and SEED are the two seed-train growth stages; BATCH is the production bioreactor run; CLAR is the clarified (cell-free) harvest; PApool/VIpool/VFpool/POLpool are the four purification pools — Protein A capture, viral inactivation, viral filtration, and polishing; DS is the bulk drug substance; DP is the filled drug product. Every section below explains its own segment in full; this is the map.
Every H2 below walks one segment of this spine, lifted verbatim from the instances.ttl header. The eleven ancestors DS-001 returns are exactly the eleven nodes that come before DS-001 on the chain (everything from RCB-CHO-001 through POLpool-001, excluding DS-001 itself and the DP lots that follow it) — so the headline count is auditable before a single excerpt is read.
How an individual is written into the graph. Each block below looks ad hoc, but every one is the same four-move procedure — the implementation analogue of Part I's competency-question recipe — applied to whichever campaign object the segment names. Worked end to end on the released drug substance bp:DS-001:
- Pick the class the object instantiates. A released bulk antibody lot is a
bp:DrugSubstance— a leaf of the Material taxonomy, grounded under BFO (the Basic Formal Ontology, the shared top-level scaffold our terms hang from) as an independent continuant — a thing that exists and persists through time, as opposed to an occurrent, an event or process that unfolds and is over. The lot is the continuant; the UF/DF processing that produced it is the occurrent. Choosing the class first is what stops a lot being conflated with its making — the thing with the event that made it. - Mint a local IRI in the
bp:namespace, or borrow a public one if it exists. An IRI is a global web identifier, and thebp:prefix is just shorthand for the long address of our local bioprocess vocabulary; to mint one is to coin a new such identifier. The lot is program-specific, so it gets a freshbp:DS-001mint; contrast the host organism, which borrows the public NCBI Taxon IRI rather than minting a string. Mint only what is genuinely local — the same reuse-first discipline Part II applies to classes, here applied to individuals. - Assert the typing, then the datatype properties measured on this lot.
bp:DS-001 a bp:DrugSubstance, then the release panel — the set of quality tests a lot must pass to be released — as literals (raw values like a number or string, as opposed to links to other nodes) on the node a sample was actually assayed (laboratory-tested) from:bp:monomerPct "98.611",bp:hmwPct "1.287",bp:hcpPpm "12.0", and the rest — each a real CQA (critical quality attribute), none invented. - Wire it into the spine with the structural relations.
bp:derivedFrom bp:POLpool-001places the lot on the lineage chain CQ-01 walks;bp:participatesIn bp:UFDF-001ties it to the occurrent that made it;bp:hasSpecification bp:Spec-DS-mAb-Aandbp:approvedByattach the release gate and signature. The lot is now reachable, traceable, and gateable.
The same four moves write every node above — only the class, the literals, and which structural edges apply change. The procedure also handles the one boundary the spine crosses twice: the bulk-to-discrete transition. A bp:DrugSubstance/bp:DrugProduct lot is a bulk material, but the packaging segment must name an individual countable vial — so step 1 picks bp:SerializedUnit for bp:VIAL-DP-001-000042, step 4 wires it bp:derivedFrom bp:DP-001 (the vial inherits the lot's release verdict rather than re-running it) and bp:contains it up the carton-case-pallet hierarchy — containment, not lineage. Going from a pooled bulk lot to a uniquely-coded pack is the same physical step Book 1's serialization performs on the line; the graph records it as one more application of the four-move recipe, with a different class at step 1.
Start from the questions a manufacturer asks
The instance graph exists to answer the lineage and impact questions that decide whether a medicine ships. Four competency questions — the plain-English questions a manufacturer needs the graph to answer, each later turned into a query and a pass/fail test — drive almost everything below, and each is a real disposition question (about what a material can do or undergo — bind, fail, derive), not merely a structural one about how nodes connect. CQ-01 — given any downstream material, which materials does it derive from, to any depth? — is the question an investigator asks when a released lot is suspect: trace it back through every purification step to the cell bank, answered by the derivedFrom chain we lay edge by edge. CQ-02 — given a working cell bank, which materials across the whole campaign descend from it? — is the question a recall scopes: if the bank is in doubt, everything aliquoted from it is too, answered by the inverse traversal from bp:WCB-CHO-001. CQ-03 — what is the originating bioreactor batch of a drug-substance lot, and its release monomer value? — links the released lot back to the run that made it, answered by walking from bp:DS-001 to bp:BATCH-2026-001 and reading 98.611. And CQ-04 — when a lot fails, which others share its fate? — is the one that turns a confident recall into a query rather than a guess. The supporting gates (CQ-17 cell-bank characterization, CQ-18 passage limit) also bind to individuals built here. Every instance we assert earns its place by serving one of these; nothing is modeled to look thorough.
Implement the making: the transfection that creates the engineered line
The genealogy of a biologic begins not with a thing but with an event — and keeping the event distinct from the thing is what later lets the graph answer how was this made? rather than only what is it? The axioms chapter declared CellLine ⊑ ∃createdBy.Transfection — formal-logic shorthand reading "every cell line must be created by some transfection" (⊑ is "is a kind of / always satisfies," and ∃ is "there exists at least one") — encoding a manufacturing fact: an engineered CHO line does not simply exist, it was produced by introducing the antibody gene into a host. Implementation must satisfy that existential restriction (the "there exists at least one" rule) with a real occurrent — an actual transfection event in the graph. The transfection consumes the expression vector (the engineered ring of DNA carrying the antibody gene, dropped into the host so the cell will read it) and produces the engineered line, which expresses — manufactures — the discovery lead bp:mAb-A (the candidate monoclonal antibody, "mAb," picked in discovery):
# instances.ttl — the line was MADE (an occurrent), satisfying CellLine ⊑ ∃createdBy.Transfection.
bp:CHO-host a bp:HostOrganism ; rdfs:label "Chinese hamster (Cricetulus griseus)" .
bp:CONSTRUCT-mAb-A a bp:GeneticConstruct ; rdfs:label "mAb-A expression vector" .
bp:CELLLINE-001 a bp:CellLine ; rdfs:label "engineered CHO line for mAb-A" ;
bp:hasHostOrganism bp:CHO-host ;
bp:createdBy bp:TF-001 ;
bp:expresses bp:mAb-A .
bp:TF-001 a bp:Transfection ; rdfs:label "transfection of mAb-A construct into CHO" ;
bp:hasInput bp:CONSTRUCT-mAb-A ; bp:hasOutput bp:CELLLINE-001 .
bp:CLONE-7 a bp:Clone ; rdfs:label "selected single-cell clone #7" .
The host is bp:CHO-host, typed bp:HostOrganism and aligned up to the NCBI Taxonomy IRI for Cricetulus griseus, never the string "CHO" [1]. This is the reuse discipline made concrete at the instance level, and it matters for a manufacturing reason: CHO is the workhorse host of commercial antibody production, its genome is sequenced and public, and a downstream tool that reads the taxon IRI can line our host up with anyone else's CHO line. The biology is a borrowed public identifier; only the program-specific clone and line are local mints. The bp:createdBy edge to one transfection is functional — a functional property may point to only one value, so if two transfection records are entered for the same line, a reasoner concludes they must be the same event rather than two — a built-in deduplication of duplicate provenance that a flat spreadsheet of cell-bank records would silently admit twice.
Implement the lineage: RCB → MCB → WCB-CHO-001, the anchor root
Then the banking — a derivedFrom chain whose root is the most important node in the graph, because every later batch traces back to it and an error in its identity propagates to every descendant. The reason a manufacturing program builds three tiers rather than one is a regulatory and safety one: a single research bank (RCB) is characterized into a master bank (MCB), exhaustively tested and frozen once, and each campaign draws a working bank (WCB) from the master so the precious master is never exhausted. The working bank carries its passage count and the four characterization results the cell-bank gate requires [2]:
# instances.ttl — the cell-bank tiers: RCB -> MCB -> WCB (the root of the genealogy).
bp:RCB-CHO-001 a bp:ResearchCellBank ; rdfs:label "RCB-CHO-001" ;
bp:expresses bp:mAb-A ; bp:passageNumber 2 .
bp:MCB-CHO-001 a bp:MasterCellBank ; rdfs:label "MCB-CHO-001" ;
bp:derivedFrom bp:RCB-CHO-001 ;
bp:hasClone bp:CLONE-7 ;
bp:passageNumber 5 .
bp:WCB-CHO-001 a bp:WorkingCellBank ; rdfs:label "WCB-CHO-001" ;
bp:derivedFrom bp:MCB-CHO-001 ;
bp:hasHostOrganism bp:CHO-host ;
bp:expresses bp:mAb-A ;
bp:passageNumber 8 ;
bp:hasCharacterization bp:CR-identity , bp:CR-sterility , bp:CR-viral , bp:CR-genetic .
bp:PassageLimit-mAb-A a bp:ValidatedPassageLimit ; rdfs:label "validated passage limit, mAb-A line" ;
bp:validatedPassageLimit 40 .
This is the implementation answer to CQ-18: the bank stands at passage 8, the validated limit is 40, and within limit is just a comparison the graph evaluates by reading the passage number off the bank and the limit off the validated-limit node and checking 8 <= 40. The passage number is not bureaucratic bookkeeping — it bounds how long the living culture may be grown before productivity drift and product-quality shift become a real risk, so the comparison is a genuine GMP gate. The four bp:CharacterizationResult individuals each test something a misidentified or contaminated bank would fail: CR-identity (is this really the mAb-A line?), CR-sterility (no mycoplasma), CR-viral (no adventitious agents), CR-genetic (the construct is stable). Each is bp:isAbout bp:WCB-CHO-001 with bp:verdict "PASS", satisfying CQ-17 and the qualified-cardinality restriction written in the axioms chapter — a rule that counts links of a specific kind, here "every working bank bears at least one characterization result." The passage count established here is the clock the seed train increments.
What a passage counts is itself a modeling choice the number hides: the regulator expects cell age tracked as population doublings (how many times the whole population has doubled in number, also called generations), not merely the number of vessel-to-vessel splits (each time the culture is divided and moved to fresh vessels), because it is cumulative doublings — not transfers — that drive the slow drift an aging culture suffers: gene-copy loss, falling antibody output, and shifts in the sugar chains attached to the antibody (N-glycans), all of which can change the product. The limit of 40 is not a round number a quality unit picked; it is the end-of-production-cell-age — the oldest the cells are ever allowed to be at the end of a production run — established under ICH Q5D (the international guideline on characterizing the cell line a biologic is made from) by growing the line beyond routine production and demonstrating that identity, genetic stability, and the antibody's quality attributes still hold that far out. So 8 <= 40 is shorthand for "this culture is comfortably inside the window over which the line was proven to stay the line."
Two subtleties the spine forces are worth stating plainly — and they turn on the two languages this ontology uses together. OWL (the Web Ontology Language) is the language of definitions and inference: it states what terms mean and lets a reasoner deduce new facts. SHACL (the Shapes Constraint Language) is the language of validation: it checks the data you actually have against required shapes and reports pass or fail. They differ in a way that matters here.
First, because bp:derivedFrom is owl:TransitiveProperty — transitive meaning if A derives from B and B from C then A derives from C, which is exactly what makes the deep lineage walk possible — it counts as a non-simple property, and OWL 2 DL (the decidable dialect of OWL the profile uses) forbids a non-simple property from also being declared asymmetric or irreflexive. In plain terms: OWL will not let us also say "derivedFrom can never point back on itself." So the ontology cannot, in OWL, rule out a lineage cycle — a graph asserting A derivedFrom B and B derivedFrom A (a thing that is its own ancestor, which is physically impossible) would still be logically consistent. Acyclicity of the genealogy is therefore a SHACL/convention guarantee, not an OWL entailment, and the release gate (not the reasoner) is where a self-referential lineage would be caught: the transitivity that makes the lineage walk cheap is the very feature that costs us the ability to forbid a cycle in the logic.
Second, the two mechanisms guarding this bank are not the same logic, because OWL and SHACL make opposite assumptions about missing data. The OWL restriction (working banks carry at least one CharacterizationResult) is open-world: it assumes whatever is not stated might still be true elsewhere, so it lets a reasoner infer that any working bank has a characterization, but it can never be violated — an under-characterized bank simply leaves the requirement unsatisfied, with no error raised. CQ-17 is therefore a closed-world SHACL scenario, not an OWL check: SHACL is closed-world — it assumes what is not present is absent — so under sh:minCount/target-class semantics it actively fails a bank missing one of the four results. Open-world OWL says "nothing forbids more"; closed-world SHACL says "show me all four, here, now" — and the cell-bank gate needs the second.
The implemented root: a transfection creates the engineered line, a clone is banked through the research, master, and working tiers as a
derivedFrom lineage, and WCB-CHO-001 becomes the anchor every downstream individual transitively traces back to.
Original diagram by the authors, created with AI assistance.
Implement the seed train: passage accumulates as the cells expand
A WCB vial holds only a few million cells; production needs billions. The seed train bridges the gap by thawing the vial and growing the cells through ever-larger vessels — a shake flask, then a seed bioreactor — and each scale-up is modeled as a real expansion occurrent that consumes one cell material and produces a larger one, with the passage clock advancing at every stage (shake flask at 12, seed bioreactor at 16):
# instances.ttl — the seed train: real expansion stages, accumulating passage.
bp:SEEDFLASK-001 a bp:ShakeFlaskCulture ; rdfs:label "shake-flask seed culture" ;
bp:derivedFrom bp:WCB-CHO-001 ;
bp:participatesIn bp:EXP-001 ;
bp:passageNumber 12 .
bp:SEED-001 a bp:SeedBioreactorCulture ; rdfs:label "SEED-001 (seed bioreactor culture)" ;
bp:derivedFrom bp:SEEDFLASK-001 ;
bp:participatesIn bp:EXP-002 ;
bp:hasHostOrganism bp:CHO-host ;
bp:passageNumber 16 .
bp:BATCH-2026-001 then derivedFrom bp:SEED-001, so the production batch traces transitively back to WCB-CHO-001 — the spine CQ-01 walks, no matter how many expansion stages sit between. The passage climbs 8 → 12 → 16, all under the validated 40, so the GMP question was this batch inoculated from cells within the limit? is now a query rather than a reconstruction from lab notebooks. But the tidy two-node chain hides a real modeling judgment: a seed train is not actually a sequence of discrete events, it is continuous growth, cells dividing every day and occasionally split between vessels. Where to put the node boundaries — each vessel transfer, each day, each flask split — is a choice the biology does not make for you. Model too coarsely and you cannot trace a contamination to a specific transfer; too finely and the graph drowns in nodes nobody queries. We mint a node per expansion stage because that is the granularity at which cells are sampled and passage is counted, but the nodes are deliberate simplifications of a living continuum, not facts the biology hands over. The batch carries bp:monomerPct "98.611" as the in-process value the digital thread — the connected trail of data that follows a product from raw material to release — later reads when answering CQ-03 — though, as we will see, that result's faithful home is the released lot, not the batch.
Implement material individuation: harvest is one in, two out
Harvest and clarification are the downstream template, and they force the question hiding under every derivedFrom edge: when does one material stop being itself and become a new thing the graph should name? The product molecules in the clarified harvest are the same molecules that were in the broth — nothing was created, only separated from the cells and debris. So is the clarified harvest a new material entity deserving its own IRI, or the same material relabeled? The chemistry does not decide; the model does. The convention this book takes is to mint a new material node at each unit operation boundary — a unit operation being one discrete processing step such as clarification, capture, or polishing — because that is the granularity at which materials get sampled, tested, pooled, and released — and clarification produces two such materials: the clarified harvest we keep and the spent biomass we discard [3]. This is the graph's reading of the physical step Book 1's harvest and clarification performs: one cloudy tank of broth spun and filtered into a clear, cell-free liquid plus a discarded slurry of cells and debris — exactly the one-in-two-out the two derivedFrom edges below encode:
# instances.ttl — harvest clarification: one material in, two out (the downstream template).
bp:CLAR-001 a bp:ClarifiedHarvest ; rdfs:label "clarified harvest of BATCH-2026-001" ;
bp:derivedFrom bp:BATCH-2026-001 ;
bp:participatesIn bp:HARV-001 ;
bp:turbidityNtu "3.2"^^xsd:float .
bp:BIOMASS-001 a bp:SpentBiomass ; rdfs:label "spent biomass (cells + debris)" ;
bp:derivedFrom bp:BATCH-2026-001 .
bp:HARV-001 a bp:Clarification ; rdfs:label "harvest clarification" ;
bp:hasInput bp:BATCH-2026-001 ;
bp:hasOutput bp:CLAR-001 , bp:BIOMASS-001 .
In the excerpt, "3.2"^^xsd:float reads "the value 3.2, typed as a decimal number" — the ^^xsd:float suffix just tells a machine the literal is a floating-point number rather than text. The clarified harvest earns its own node because it is the thing that gets a turbidity result (3.2 NTU — Nephelometric Turbidity Units, a measure of how cloudy the liquid is, here its first in-process quality) and a forward handoff; the spent biomass earns one too, so contamination tracing and mass balance have a discarded stream to point at. The famous upstream-to-downstream boundary that organizes the textbooks is, to the graph, just another derivedFrom edge in one continuous chain. One thing the edge does not enforce is mass balance: derivedFrom records lineage, not conservation — it says "this came from that," not "this much came from that much." The graph will happily present a perfectly traversable genealogy in which the kept harvest plus the waste do not reconcile with what went in, because catching that requires modeling quantities explicitly and validating them. The honest standard is that the graph indexes lineage while mass balance lives in the process records. The bare 3.2 is honest but thin until it is unit-qualified, which is the subject of the next chapter — and even there it previews a unit-coverage wrinkle: when 3.2 is unit-qualified — attached to a formal unit using QUDT (a standard library of units and quantity kinds) and UCUM (a standard set of unit codes) — it gets qudt:numericValue 3.2, but its UCUM code is [NTU], and NTU has no clean QUDT or UCUM unit IRI, because nephelometric turbidity is a method-defined scale (its value depends on the instrument and method, not a fundamental physical unit), not an SI-coherent unit built from metres, kilograms, and seconds. So this one scalar lands as an honestly-flagged illustrative unit string rather than a resolvable unit: IRI — a reminder that "attach a QUDT unit" is a clean rule with real-world exceptions at exactly the messy in-process measurements an upstream graph is full of.
Implement the pooling fork: PApool-001, one pool from many cycles
Capture chromatography — passing the harvest through a packed column whose resin selectively grabs the product — is the first place the simple parent-child edge breaks down. Protein A is a protein, immobilized on the resin, that binds antibodies and almost nothing else, so Protein A capture binds the antibody specifically and washes everything else away, removing the bulk of impurities in one operation — but a column holds only so much per load, so the step is run as many cycles, each eluting a fraction, all combined into one pool. For the first time a material derivedFrom several parents at once, and the lineage forks backward [4]. The implementation refuses to collapse that fork: three cycle eluates are real individuals, and PApool-001 records exactly which it pooled through bp:includedFraction, a typed sub-property of derivedFrom — a more specific kind of "derived from," so every includedFraction link is automatically also a derivedFrom link, which keeps the coarse lineage chain reachable while recording the finer cycle detail underneath:
# instances.ttl — the capture pool, with its cycle-level provenance preserved.
bp:ELU-001a a bp:CycleEluate ; rdfs:label "capture cycle 1 eluate" ; bp:derivedFrom bp:CLAR-001 .
bp:ELU-001b a bp:CycleEluate ; rdfs:label "capture cycle 2 eluate" ; bp:derivedFrom bp:CLAR-001 .
bp:ELU-001c a bp:CycleEluate ; rdfs:label "capture cycle 3 eluate (excluded by pooling)" ; bp:derivedFrom bp:CLAR-001 .
bp:PApool-001 a bp:CapturePool ; rdfs:label "PApool-001 (Protein A capture pool)" ;
bp:derivedFrom bp:CLAR-001 ;
bp:fromBatch bp:BATCH-2026-001 ;
bp:includedFraction bp:ELU-001a , bp:ELU-001b ;
bp:participatesIn bp:CAP-001 ;
bp:hasInProcessResult bp:IPR-PApool-hcp .
The headline edge PApool-001 derivedFrom CLAR-001 is asserted directly so the coarse chain stays reachable for CQ-01, while the cycle detail underneath answers the forensic "which load contributed?" question an investigation actually asks. Cycle 3 (bp:ELU-001c) was excluded by the real-time pooling decision and is kept as an individual routed through bp:POOL-DEC-001, so the pool's composition is explained, not merely recorded. Capture also introduces a consumable the earlier steps did not stress — a resin that is reused across batches:
# instances.ttl — the resin as a persisting consumable with a usage history — carryover is answerable.
bp:CAP-001 a bp:CaptureChromatography ; rdfs:label "Protein A capture step" ;
bp:hasInput bp:CLAR-001 ; bp:hasOutput bp:PApool-001 ;
bp:performedOn bp:RESIN-PrA-07 .
bp:RESIN-PrA-07 a bp:ResinLot ; rdfs:label "Protein A resin lot PrA-07" ;
bp:hasDisposition bp:PrA-bind ;
bp:cycleCount 38 ; bp:cycleLifetimeLimit 200 .
bp:PrA-bind a bp:BindingDisposition ; rdfs:label "Protein A antibody-binding disposition" ;
bp:isRealizedIn bp:CAP-001 .
The Protein A resin is expensive and used for many cycles across many batches before disposal, so it is a tracked material entity — bp:RESIN-PrA-07 carrying its bp:cycleCount (38) against a bp:cycleLifetimeLimit (200) — bearing a disposition to bind antibody. Because the lot persists across batches, it creates a genealogy the product chain alone misses: a degradation or carryover on the resin could link batches that share no product lineage, which is what makes "which batches shared this resin?" a query rather than an archaeology project. Two honesties about this node are worth naming. First, alignment honesty: aligning a local term means declaring it equivalent to (or a sub-type of) a term in a shared external ontology, here the IOF (Industrial Ontology Foundry, a public library of manufacturing terms) — bp:ChromatographyColumn and bp:ChromatographyResin align up to verified IOF terms (iof:ChromatographyColumn, iof:ChromatographyMedium), the alignments living in a separate file align.ttl. But bp:CaptureColumn and bp:ResinLot are marked ILLUSTRATIVE local placeholders there, because no settled matching external term (an external leaf — a specific, ready-made class in someone else's published ontology) for a Protein A capture column or a single resin lot exists yet — the discipline is to anchor what genuinely anchors and flag the rest, not to fake a borrow. Second, validated versus measured: a validated claim is one proven once in a dedicated study and then trusted for every batch; a measured value is assayed fresh on each batch. The cleaning that controls carryover (residual product or contaminant carried from one cycle into the next) between cycles is a validated claim about the cleaning process, not a per-batch measurement — and the resin at cycle 200 has the same living-thing identity softness a cell line has, since it degrades and is cleaned and retired. The graph carries the cycle count that makes carryover answerable, but the full cross-batch usage history is a cost-versus-fidelity trade every plant negotiates.
The capture pool also earns an in-process result the released lot does not duplicate: bp:IPR-PApool-hcp records 850 ppm host-cell protein right after Protein A. Protein A delivers the antibody at high purity but still hundreds-to-low-thousands of ppm HCP; the polishing train then drives it to the 12 ppm carried on DS-001, against a 100 ppm release limit. Modeled as a trajectory of one impurity across nodes — 850 ppm on the pool, 12 ppm on the lot — the graph turns "is HCP controlled?" into a walk that shows where it was controlled, exactly the clearance evidence a reviewer asks for. The authority levels stay distinct on purpose: the 850 is an InProcessResult (informational, trending the step), the 12 is a release Result tied to the specification — same quantity kind, different governance.
The same validated-not-measured discipline governs the two viral-safety steps the lineage threads through between capture and polishing. VI-001 (a low-pH hold at pH 3.6 for 60 min) carries a validated LRV of 4.5 and VF-001 (virus-retentive nanofiltration) a validated LRV of 4.2. LRV is the log-reduction value — how many factors of ten a step removes any virus by, so 4.5 means a 10,000-fold-and-more reduction; because it is logarithmic, two steps' LRVs add rather than multiply. The two are declared orthogonalTo each other — inactivation by chemistry versus removal by size, two independent mechanisms that a single virus cannot survive both of — so the graph can sum them to the claimed 8.7 total clearance while keeping the per-batch conditions (the pH and hold time) as measured qualities of the cleared material. The clearance number is a claim about a spiking study, not a per-batch assay; the pH and time are the per-batch evidence that the validated condition was met. Two mechanisms that fail independently is the whole safety argument — a number a regulator will not let you claim by adding two log-reductions of the same mechanism.
The implemented capture pool: a material that derives from its pooled cycle eluates (the backward fork) with cycle 3 excluded by the pooling decision, produced on a tracked resin lot reused across batches — pooling provenance preserved as individuals, not collapsed to a single edge.
Original diagram by the authors, created with AI assistance.
Implement the convergence: DS-001 and its eleven ancestors
The drug substance is where every upstream lineage individual converges to a single material, and where the binding release specification attaches [5]. UF/DF (ultrafiltration / diafiltration — filtering through a membrane that holds the antibody back while letting small molecules and water pass) concentrates the polished product and exchanges it into its formulation buffer — the final liquid, with the salts and stabilizers the drug will be stored in — yielding DS-001. Between the capture pool and this convergence sit four more material nodes the file carries and the formalization chapters model in their own right — the two viral-safety pools VIpool-001 and VFpool-001 (orthogonal clearance, a validated 4.5 + 4.2 = 8.7 LRV) and the polishing intermediate POLpool-001. Through transitive derivedFrom, bp:DS-001 traces back through the polishing intermediate, the two viral pools, the capture pool, the clarified harvest, the bioreactor batch, the seed train, to WCB-CHO-001 and on up the cell-bank tiers to RCB-CHO-001 — eleven ancestors the lineage walk returns. That count is not a flourish but a literal tally of the spine: cell-bank tiers (3), seed train (2), batch, clarified harvest, capture pool, the two viral pools, and the polishing pool. This is the lot a release decision is made on, so the full CQA panel sits here, on the lot it actually describes:
# instances.ttl — the convergence node carrying the full release panel.
bp:DS-001 a bp:DrugSubstance ; rdfs:label "DS-001" ;
bp:derivedFrom bp:POLpool-001 ;
bp:participatesIn bp:UFDF-001 ;
bp:hasSpecification bp:Spec-DS-mAb-A ;
bp:releaseStatus "PASS" ;
bp:monomerPct "98.611"^^xsd:float ;
bp:hmwPct "1.287"^^xsd:float ;
bp:cexMainPct "70.686"^^xsd:float ;
bp:hcpPpm "12.0"^^xsd:float ;
# ... residual DNA, endotoxin (more panel scalars) elided ...
bp:proteinConcMgPerMl "50.2"^^xsd:float ;
# ... QUDT value nodes (monomerValue, cexValue) elided ...
bp:approvedBy bp:SIG-DS-001 ;
bp:hasCertificate bp:CofA-DS-001 .
Each scalar on this panel is a real antibody quality attribute with a manufacturing reason to exist, measured by the standard size- and charge-variant methods of mAb characterization — SEC (size-exclusion chromatography, which separates molecules by size) for monomer and HMW aggregate, CEX (cation-exchange chromatography, which separates by surface charge) for charge variants [7]. monomerPct (98.611) is the fraction of intact, correctly-folded single antibody molecules; hmwPct (1.287) is the high-molecular-weight aggregate — clumps of several antibodies stuck together — that the polishing step removes, controlled because aggregates can provoke an immune response in patients; cexMainPct (70.686) is the main charge-variant peak. Charge variants are molecules of the same antibody that carry slightly different surface charge because of small post-production modifications, so the main-peak percentage is a proxy for post-translational homogeneity — how uniform the product is after all such modifications. A main peak in roughly the 60-80% range is typical and healthy for a mAb, the rest being the acidic and basic charge variants, so 70.686% is in spec, not low; hcpPpm (12.0) is residual host-cell protein. Carrying the release monomerPct (98.611) on DS-001 — the lot a sample was actually assayed from — is the deliberate attribution correction the release-gate chapter made in the open. The bioreactor batch keeps its own monomerPct literal too, but as an in-process SEC value (a process record), not as the release CQA: the broth was never assayed for final monomer purity, so the gate validates the value on DS-001, while the digital thread can still walk derivedFrom forward from the lot to read the originating in-process number. The same scalar lives in two places with two different authorities — which is itself the point. The lesson is general and worth keeping: where a result attaches is a modeling decision with consequences, and a graph can be perfectly traversable and still subtly mis-locate the fact it is supposed to certify.
The panel is broader than the four scalars shown: DS-001 also carries bp:residualDnaPgPerMg "8.0" and bp:endotoxinEuPerMg "0.5", the two safety impurities a release decision cannot omit. Residual host-cell DNA at 8 pg/mg (8 picograms of leftover host DNA per milligram of antibody — a picogram is a trillionth of a gram) sits far under the conventional WHO/ICH dosing concern (historically of order ~10 ng of host-cell DNA per dose), and endotoxin at 0.5 EU/mg (endotoxin units per milligram, the standard measure of bacterial-fragment contamination) is comfortably below the compendial (set by the official pharmacopoeias) bacterial-endotoxins limit — which is dose-based, set per maximum dose rather than as a flat per-mg figure, the patient's tolerated endotoxin load per kilogram per hour driving the per-mg ceiling. For a parenteral biologic — one injected or infused, bypassing the gut's defenses — DNA and endotoxin are the attributes an inspector checks first, and placing them on the same convergence node as monomer and HCP is what lets one release query read the whole safety-and-quality picture of the lot.
From this one node the lineage forks forward: DP-001 and DP-002 both derivedFrom DS-001 (the fill fork, mirror of capture's backward pooling fork), and the out-of-spec sibling lineage WCB-CHO-001 → SEED-004 → BATCH-2026-004 → PApool-004 → DS-004 → DP-004 shares the same root — which is precisely why a recall-impact query can reach both fates from the one bank.
The recipe as portable process knowledge
Not every implemented individual is a material. The package that carried the whole campaign onto the plant floor — the master recipe — is knowledge in transferable form, and it is modeled as a generically dependent continuant — the third BFO category, after the independent continuant (a thing) and the occurrent (an event): a piece of information that needs some carrier to exist but is not tied to any one, so it can be copied from site to site without being used up, the way a recipe is the same recipe in every kitchen that holds a copy. Its structure comes from ISA-88 (IEC 61512), the batch-control standard, which models a recipe as a hierarchy of procedures, operations, and phases and separates the recipe from the equipment that executes it:
# instances.ttl — the recipe as ISA-88 information, realized by a run, transferred between sites.
bp:Recipe-mAb-A a bp:MasterBatchRecord ; rdfs:label "master recipe, mAb-A" ;
bp:hasRecipeElement bp:RP-production .
bp:RP-production a bp:RecipePhase ; rdfs:label "production-phase recipe element" ;
bp:prescribesParameter bp:FeedRate , bp:Temperature ;
bp:requiresEquipment bp:REQ-2000L .
bp:REQ-2000L a bp:EquipmentRequirement ; rdfs:label "2000 L single-use production bioreactor requirement" .
bp:BR-204 a bp:ProductionBioreactor ; rdfs:label "BR-204 (receiving-site vessel)" ;
bp:locatedAt bp:SITE-B ; bp:qualifiesFor bp:REQ-2000L .
bp:TT-001 a bp:TechTransfer ; rdfs:label "tech transfer of mAb-A to site B" ;
bp:transferredFrom bp:SITE-A ; bp:transferredTo bp:SITE-B ; bp:isAbout bp:Recipe-mAb-A .
The move that makes a recipe portable is modeling what each step needs separately from what any site has. RP-production does not require "vessel BR-101"; it requires bp:REQ-2000L — a production bioreactor of a given class and scale — an equipment requirement that a real vessel fills by playing a role. The cell-culture run bp:CCP-001 bp:realizes bp:Recipe-mAb-A at the originating site, while bp:BR-204 at site B declares bp:qualifiesFor bp:REQ-2000L, so the same recipe runs on a different vessel without rewriting and the graph can check, mechanically, whether a candidate vessel qualifies. This information-artifact-versus-occurrent split is what lets one recipe be realized by many runs and transferred between sites. The B2MML XML (the standard XML format for exchanging batch recipes between systems) that the originating MES (Manufacturing Execution System — the plant-floor software that runs and records production) actually exchanges is walked element-by-element into exactly these triples by the companion's loader (the example project's script that reads the XML and writes the graph) — the same recipe, serialized as the wire format on one side and as IOF-aligned RDF on the other. What the model cannot guarantee is the process: a culture at 2,000 L mixes, oxygenates, and shears differently than one at 2 L (Book 1's scale-up physics), and a new site's water and raw-material lots behave subtly differently, so a CQA can shift even with every modeled parameter held identical. The graph flags the transfer and links the engineering and confirmation runs that probe it; it cannot predict the shift. A portable model is not a portable process.
Evaluation: does the loaded graph answer its questions?
A model is validated by loading it and running the competency questions [6]. validate.py parses bioproc.ttl + align.ttl + instances.ttl — 2120 triples as authored (a triple is the atomic unit of an RDF graph: a subject–predicate–object statement like "DS-001 — derivedFrom — POLpool-001," and the whole graph is just a pile of them) — and the lineage questions are answered against that raw graph, with no reasoner (the software that deduces new triples from the rules) in the loop. The depth comes from a SPARQL 1.1 property path. SPARQL is the standard query language for RDF graphs (the graph equivalent of SQL); a property path lets one query follow a chain of edges of arbitrary length. Here (bp:derivedFrom)+ means one-or-more derivedFrom hops, so the engine walks the asserted parent-child edges to whatever depth the chain runs — only the immediate-parent edges are hand-asserted, and the path supplies the reach. The backward walk from bp:DS-001 returns eleven ancestors (answering CQ-01 and, reading the originating batch's 98.611, CQ-03), and the inverse traversal answers CQ-02 — none of these needs a long-range edge such as DS-001 derivedFrom WCB-CHO-001 to be hand-asserted or materialised, because the path does the traversal. (OWL-RL — a lightweight, rule-based reasoning profile of OWL — can instead materialise every transitive edge: that is, actually compute and store each implied "derived from" link (DS-001 derivedFrom WCB-CHO-001, and so on) as a new triple rather than discovering it at query time. Adding all those inferred triples is the closure of the graph, and it takes it from 2120 to 7137 triples; it is consumed structurally by exactly one competency question, CQ-22, which verifies that the reasoner inferred the long-range chain and the equipment-is-material typing. The lineage CQs deliberately use the property path rather than the closure, because a property path needs no reasoner at query time.) The inverse property path is the one CQ-02 runs:
# queries/CQ-02.rq — Cell-bank impact: every material that descends from the working cell bank.
PREFIX bp: <https://example.org/bioproc#>
SELECT ?descendant WHERE {
?descendant (bp:derivedFrom)+ bp:WCB-CHO-001 .
} ORDER BY ?descendant
Run over the asserted edges — the property path does the inward walk — it returns the entire campaign — both the golden DP-001/DP-002 lineage and the out-of-spec DP-004 sibling — every node tracing to this one bank, exactly the cell-bank-level reach a contamination concern needs. The impact query CQ-04 sharpens it: when DP-004 fails, walking up its lineage to the shared WCB-CHO-001 and back down returns ['DP-001', 'DP-002'] — the shared-fate set a recall must scope, computed by query instead of by quarantining the whole plant:
# queries/CQ-04.rq — Impact analysis: when DP-004 fails, which drug products share its lineage?
PREFIX bp: <https://example.org/bioproc#>
SELECT DISTINCT ?affected WHERE {
bp:DP-004 (bp:derivedFrom)+ ?shared . # an ancestor of the failed lot
?affected (bp:derivedFrom)+ ?shared . # anything else derived from it
?affected a bp:DrugProduct .
FILTER(?affected != bp:DP-004)
} ORDER BY ?affected
The closed-world SHACL gate then confirms WCB-CHO-001 carries its four characterizations and passage count (CQ-17), and the release gate flags only hmwPct on DS-004/DP-004 (2.41 against a limit of 2.0 %) — the aggregate that is the actual failure mode here, since monomer is in spec at 98.687 — isolating the one real violation while every other panel value, on every other lot, passes.
That the failure lands on HMW aggregate and not on monomer is realistic, not arbitrary. A sibling lot from the same cell bank can still aggregate more for batch-specific reasons the bank never sees: a low-pH viral hold that drifted high or ran long, a thermal excursion in a pool hold, an overloaded polishing column, or an extended hold before UF/DF — any of which nudge HMW up while leaving monomer and charge largely intact. So DP-004's 2.41 % HMW is a process-borne, not bank-borne, excursion — and the value of CQ-04 is precisely that it does not assume the cause is the shared bank: it scopes the lots that could share a root-cause and leaves the causal mechanism to the investigation, the honest division of labour between a lineage graph and a deviation report.
The same graph is the ground truth a model must not contradict
This file is not only a release record; it is also the ground truth a machine-learning model is grounded against — the verified facts an AI system is anchored to so it cannot invent its own. Three uses of these very individuals recur across the ML companion volume, and each turns a relation we already asserted into infrastructure the model depends on.
The same
derivedFrom lineage the release queries walk is, for a model, the leave-one-batch-out grouping key, the SHACL admission gate on a training subgraph, and the GraphRAG retrieval step — nothing new is asserted, each use repurposes a relation already in the graph.
Original diagram by the authors, created with AI assistance.
The lineage edges are the grouping variable that stops a model fooling itself. A model that learns over these instances — predicting, say, final monomer or HMW from a batch's process record — must be validated so its score answers how will this do on the next batch?, not how well does it memorize this one? The cardinal sin is a row-wise split that scatters near-identical timepoints from one batch across both train and test; the fix is a batch-grouped split — every row of a batch goes wholly to train or wholly to test — and its cross-validation form, leave-one-batch-out (Book 5's data chapter). The graph already carries the grouping key: bp:fromBatch and the bp:derivedFrom lineage say which batch every material and every result belongs to, so the same edges that scope a recall also define the groups a leak-free split must respect. Two results that share a bp:derivedFrom ancestor are not independent evidence, and the lineage that makes CQ-02 answerable is exactly what tells a validator they must not straddle the train/test line. A model evaluated without that grouping reports a fantasy accuracy; the instance graph is where the honest grouping lives.
The SHACL gate that refuses a bad release also refuses a bad training set. The closed-world release gate checks that every node carries what it must — a working bank its four characterizations, a drug substance its full CQA panel, a result its value and signer. Built to refuse a non-conformant release, the same sh:minCount/target-class shapes refuse a non-conformant retrieval: before a subgraph is handed to a model as training data or as a retrieved context, conformance certifies it is complete and well-typed rather than a partial load a fluent model will cheerfully fill in from memory (the AI-frontier chapter). A graph that fails its shapes is the hollow or mislabeled input that makes a model confidently wrong — and SHACL is how you catch it before the model does. In the open-source stack this is one pyshacl run over the RDFLib-built graph, the same validation report that gates the load, reused as the gate on the data a model is allowed to learn from.
The lineage query is the retrieval step of a grounded AI answer. Ask a language model whether DP-004 was released and it will compose a confident, grammatical, possibly-invented answer; it has no way to tell a true lineage from a plausible one. GraphRAG — retrieval-augmented generation whose trusted store is this knowledge graph — closes that gap by answering from the graph instead of from training memory: the retrieval step is byte-for-byte the (bp:derivedFrom)+ property path CQ-01 already runs, pointed at bp:DP-004, so the model narrates the real eleven-ancestor chain rather than guessing it (ontologies as ground truth for AI). The graph does the knowing; the model does the talking. And the deepest reason the instance graph matters to AI is the validation asymmetry it embodies: a locked, validated ML model is trusted because it was frozen and proven once against held-out data (Book 5's validation chapter), whereas this graph is trusted because every fact in it is individually identified, typed, and reasoned — a static model watching a moving process versus a record that is true by construction. The model can drift away from the plant; a SHACL-conformant lineage cannot drift away from what it asserts. That is why the survey volume's verdict is that the bottleneck on manufacturing AI is not the model but the governed, identified, semantically-typed data underneath it — the very thing this chapter spent its length building.
The unsolved part: a loadable graph is a snapshot, not a living record
The instance graph is honest, traversable, and reasoned — and it is frozen. It captures one campaign at one moment: passage 8, monomer 98.611, eleven ancestors. But two deeper truths the chapter surfaced sit underneath that freeze. The first is that the things the graph names are alive and continuous. The cell bank is a population of cells that mutate and drift over generations, so "the working bank" is not genetically identical to the master, and no owl:sameAs (the OWL relation that asserts two identifiers name the exact same thing) answers whether the culture at passage 60 is the same entity as at passage 5 — identity here is a useful fiction bounded by characterization, not the crisp sameness an IRI implies. Worse, and historically real, is misidentification: cell lines have been confused and cross-contaminated across the life sciences for decades, and for a manufacturing root node it is the worst possible error, because it is asserted with full confidence, propagates through every derivedFrom edge, and no downstream data integrity catches it — every downstream fact is correctly derived from a wrongly identified root. The second truth is that the individuation conventions the graph commits to are not metaphysical facts: minting bp:CLAR-001 at a unit-operation boundary, treating PApool-001 as one pool from discrete cycles, anchoring release on a discrete bp:DS-001 lot all assume a batch world. As processing goes continuous — product flowing through the steps in an unbroken stream rather than being made in distinct batches — those discrete individuals lose their natural boundaries, and "the release lot" becomes a time-bounded convention imposed on a continuum rather than a natural object — one the ontology can record but not derive. So the loadable graph proves the model can answer its questions on real data; it does not prove the data is what it claims at the root, nor that the discrete nodes survive a process with no batches, both of which are governance and maintenance problems, not implementation ones.
Why it matters
Individuals are where an ontology stops being a diagram and starts being a manufacturing record. The 2120 triples of instances.ttl, closing to 7137 under the reasoner, are the difference between claiming the model can trace a lot to its cell bank and showing it return eleven ancestors of DS-001, then scope a recall to DP-001 and DP-002 when DP-004 fails — in two queries, not two weeks of spreadsheet archaeology. Every competency question the release gate and the digital thread depend on is answered by traversing edges asserted here. Build the instance graph faithfully — the transfection as an occurrent, the cell-bank tiers as the root, lineage on the one transitive spine, pooling forks preserved on a tracked resin, release attributes on the lots they describe — and the questions that gate a medicine resolve by query. Skip it, or hang the purity result on the wrong node, and the release-critical part of the graph is an untested hypothesis no reasoner ever ran.
In the real world
Every commercial mammalian-cell program already maintains the RCB/MCB/WCB lineage, the seed-train passage history, the per-step pooled materials and tracked resin lots, and the released drug-substance lot with its certificate — the cell-bank hierarchy and its characterization are a regulatory expectation, not optional good practice [1][3][4]. What is uneven is that those records live scattered across an MES, a LIMS, an ELN, and chromatography logbooks, so "we have traceability" is a claim about filing. The implementation modeled here — a single loadable file where the cell bank, the pool, the resin, and the drug substance are nodes joined by typed derivedFrom edges — is exactly the artifact that turns that claim into a property a query can verify, and turns a recall from a campaign-wide quarantine into a scoped set of two sibling lots.
Key terms
- Instance graph — the asserted individuals of one mAb campaign (
bp:WCB-CHO-001,bp:CLAR-001,bp:DS-001…) populating the vocabulary's classes;instances.ttl, the loadable running example. - Material individuation — the convention of minting one new material node per unit operation; pinned to boundaries like clarification, stressed by pooling, splitting, and continuous flow, and not a fact the biology hands you.
- Mass balance (not enforced) —
derivedFromrecords lineage, not conservation; a traversable genealogy can be quantitatively impossible unless quantities are modeled. - Backward pooling fork / carryover lineage —
PApool-001 derivedFromseveral cycle eluates viabp:includedFraction, on a resin (RESIN-PrA-07) reused across batches that links lots no product lineage connects. - Convergence node —
bp:DS-001, where the entire upstream lineage meets one material with eleven transitive ancestors and the binding release specification; the corrected home of themonomerPctrelease result. - Transitive lineage —
bp:derivedFromis declaredowl:TransitiveProperty, so depth is reachable two ways: a SPARQL(bp:derivedFrom)+property path over the asserted edges (what the lineage CQs use, on the raw graph), or OWL-RL materialisation of every transitive edge (2120 → 7137 triples, consumed structurally by CQ-22 alone). Either way, no long-range edge is hand-asserted. - Portable information artifact — the master recipe, an ISA-88-structured generically dependent continuant realized by a run and transferred between sites by binding to equipment requirements, not vessels.
- Graph as ground truth (for ML/AI) — the verified instance graph an ML model is anchored to: its
bp:fromBatch/bp:derivedFromedges supply the leave-one-batch-out grouping that keeps a model's score honest, its SHACL shapes certify a training subgraph (not just a release) is complete, and its(bp:derivedFrom)+lineage query is the retrieval step of a grounded GraphRAG answer. A model is trusted because it was frozen and proven once; the graph is trusted because every fact is identified, typed, and reasoned.
Where this leads
The vocabulary is now a populated campaign graph, but several of its scalars — the harvest's bare 3.2 NTU, the run's bare setpoints — carry no unit and no provenance, which is honest but thin. The next chapter, From the Wire to the Graph, shows how those numbers actually arrive: off an OPC UA transmitter, through a historian, into a unit-qualified qudt:QuantityValue and an Allotrope-backed result — closing the gap between a clean instance graph and the messy plant signals that feed it.