Decorations
The core data model is intentionally small: Dataset, Process, Recipe, Sample, Data, and Annotation describe the shape of a process graph.
Domain specificity is added as decoration on top of that shared shape.
There are two complementary ways to do this:
- Use
additionalTypeandadditionalPropertyon core objects. This keeps the data close to the ARC Core model and makes the extension queryable as typedAnnotationannotations. - Use the inherited
DynamicObjproperty bag for information that must be preserved but does not fit into the core model.
This page shows both approaches.
Typed Decorations
The preferred extension path is to specialize core objects with additionalType and then attach ontologized Annotation records to the appropriate slot.
The example below builds a small proteomics-style assay without introducing new graph node types.
let assay = Dataset("measurement1", additionalType = "Assay")
assay.Title <- Some "Proteomics assay"
let source =
Sample("Base Culture", additionalType = "Source")
let organism =
Annotation(
"organism",
value = "Arabidopsis thaliana",
nameTAN = "https://bioregistry.io/SIO:010000",
valueTAN = "https://bioregistry.io/NCBITaxon:3702",
additionalType = "CharacteristicValue")
source.AddAdditionalProperty(organism)
let roomTemperatureSample =
Sample("Cultivation Flask RT", additionalType = "Sample")
let temperature25 =
Annotation(
"temperature",
value = "25",
unit = "degree Celsius",
nameTAN = "https://bioregistry.io/NCRO:0000029",
unitTAN = "https://bioregistry.io/UO:0000027",
additionalType = "FactorValue")
roomTemperatureSample.AddAdditionalProperty(temperature25)
let highTemperatureSample =
Sample("Cultivation Flask HT", additionalType = "Sample")
let temperature30 =
Annotation(
"temperature",
value = "30",
unit = "degree Celsius",
nameTAN = "https://bioregistry.io/NCRO:0000029",
unitTAN = "https://bioregistry.io/UO:0000027",
additionalType = "FactorValue")
highTemperatureSample.AddAdditionalProperty(temperature30)
let growthProtocol = Recipe(name = "Growth")
growthProtocol.AddComponent(
Annotation(
"growth environment",
value = "bioreactor",
nameTAN = "https://bioregistry.io/OBI:0000997",
valueTAN = "https://bioregistry.io/OBI:0001046",
additionalType = "Component"))
let growthAt25 = Process("Growth", executesRecipe = growthProtocol)
growthAt25.SetInputSample(source)
growthAt25.SetOutputSample(roomTemperatureSample)
assay.AddProcess(growthAt25)
let growthAt30 = Process("Growth", executesRecipe = growthProtocol)
growthAt30.SetInputSample(source)
growthAt30.SetOutputSample(highTemperatureSample)
assay.AddProcess(growthAt30)
let assayDecoration =
[ "identifier", assay.Identifier
"dataset additionalType", assay.AdditionalType |> valueOrBlank
"processes", string assay.Processes.Count
"samples", string (assay.AllSamples().Count)
"data nodes", string (assay.AllData().Count) ]
assayDecoration
|
The dataset is still a Dataset, but additionalType = "Assay" tells downstream code which domain role it plays.
The same pattern is used for sample roles: the input is a Source, while the outputs are Sample samples.
let sampleRoles =
assay.AllSamples()
|> Seq.countBy (fun sample -> sample.AdditionalType |> valueOrBlank)
|> Seq.map (fun (role, count) -> role, count)
|> Seq.toList
sampleRoles
|
The first Growth process shows a compact ISA-style shape:
- The input sample is a
Source. - The output sample is a
Sample. - Characteristics are attached to input nodes via
AdditionalProperty. - Factors are attached to output nodes via
AdditionalProperty.
let growthInput =
growthAt25.InputSample()
|> Option.get
let growthOutput =
growthAt25.OutputSample()
|> Option.get
let growthDecoration =
[ "process", growthAt25.Name
"input", sprintf "%s (%s)" growthInput.Name (growthInput.AdditionalType |> valueOrBlank)
"input annotations", growthInput.AdditionalProperty |> Seq.map pvSummary |> String.concat "; "
"output", sprintf "%s (%s)" growthOutput.Name (growthOutput.AdditionalType |> valueOrBlank)
"output annotations", growthOutput.AdditionalProperty |> Seq.map pvSummary |> String.concat "; "
"protocol components", growthProtocol.Components |> Seq.map pvSummary |> String.concat "; " ]
growthDecoration
|
Process parameters use the same Annotation type, but they live on the Process.ParameterValue slot.
Here, cell lysis records the sonicator, lysis duration, and technical replicate group as ParameterValue decorations.
let sonicator =
Annotation(
"sonicator",
value = "Fisherbrand Model 705 Sonic Dismembrator",
nameTAN = "https://bioregistry.io/OBI:0400114",
valueTAN = "https://bioregistry.io/OBI:5453453",
additionalType = "ParameterValue")
let lysisTime =
Annotation(
"time",
value = "10",
unit = "minute",
nameTAN = "https://bioregistry.io/PATO:0000165",
unitTAN = "https://bioregistry.io/UO:0000031",
additionalType = "ParameterValue")
let technicalReplicate =
Annotation(
"technical replicate group",
value = "1",
nameTAN = "https://bioregistry.io/DPBO:1000184",
additionalType = "ParameterValue")
let lysis = Process("Cell Lysis")
lysis.SetInputSample(roomTemperatureSample)
lysis.SetOutputSample(Sample("Eppi RT 1", additionalType = "Sample"))
lysis.AddParameterValue(sonicator)
lysis.AddParameterValue(lysisTime)
lysis.AddParameterValue(technicalReplicate)
assay.AddProcess(lysis)
lysis.ParameterValue
|> Seq.map pvSummary
|> Seq.toList
|
The practical benefit of this approach is that extensions remain easy to query. For example, all samples produced under the 25 degree Celsius growth factor can be found with ordinary F# sequence operations.
let samplesAt25Degrees =
assay.AllSamples()
|> Seq.filter (fun sample ->
sample.AdditionalType = Some "Sample"
&& sample.AdditionalProperty
|> Seq.exists (fun pv ->
pv.AdditionalType = Some "FactorValue"
&& pv.Name = "temperature"
&& pv.Value = Some "25"))
|> Seq.map (fun sample -> sample.Name)
|> Seq.toList
samplesAt25Degrees
|
DynamicObj Extensions
All main ARC Core implementation classes inherit from DynamicObj. This gives each object a property bag for extension data that should be preserved, but that does not naturally belong in the process graph.
Use this for metadata such as facility layout, local tracking fields, UI state, or profile-specific fields that a core-only library should not interpret. The example below adds an experimental facility layout to a dataset.
let facilityDataset = Dataset("facility-layout-demo", additionalType = "Assay")
facilityDataset.Title <- Some "Greenhouse proteomics assay"
let environmentalControls = DynamicObj()
environmentalControls.SetProperty("temperatureSetpoint", "22 degree Celsius")
environmentalControls.SetProperty("relativeHumiditySetpoint", "60 percent")
environmentalControls.SetProperty("photoperiod", "16 h light / 8 h dark")
let facilityLayout = DynamicObj()
facilityLayout.SetProperty("facilityName", "Phytotron A")
facilityLayout.SetProperty("room", "Growth room 2")
facilityLayout.SetProperty("bench", "North bench")
facilityLayout.SetProperty("instrumentBay", "LC-MS bay 1")
facilityLayout.SetProperty("coordinateSystem", "room-grid")
facilityLayout.SetProperty("locationCode", "A-02-N-03")
facilityLayout.SetProperty("environmentalControls", environmentalControls)
facilityDataset.SetProperty("experimentalFacilityLayout", facilityLayout)
let recoveredFacility =
facilityDataset.TryGetTypedPropertyValue<DynamicObj>("experimentalFacilityLayout")
let facilitySummary =
match recoveredFacility with
| Some layout ->
[ "facility", layout.TryGetTypedPropertyValue<string>("facilityName") |> valueOrBlank
"room", layout.TryGetTypedPropertyValue<string>("room") |> valueOrBlank
"bench", layout.TryGetTypedPropertyValue<string>("bench") |> valueOrBlank
"location", layout.TryGetTypedPropertyValue<string>("locationCode") |> valueOrBlank ]
| None ->
[ "facility", "missing" ]
facilitySummary
|
The YAML writer emits DynamicObj properties as overflow fields after the known ARC Core fields.
This keeps the data round-trippable without requiring the core model to know what an experimentalFacilityLayout is.
let facilityYaml =
ProcessCore.Yaml.Dataset.toYamlString (Some 2) facilityDataset
Show dataset YAML with DynamicObj extension
type: Dataset
identifier: facility-layout-demo
additionalType: Assay
title: Greenhouse proteomics assay
experimentalFacilityLayout:
facilityName: Phytotron A
room: Growth room 2
bench: North bench
instrumentBay: LC-MS bay 1
coordinateSystem: room-grid
locationCode: A-02-N-03
environmentalControls:
temperatureSetpoint: 22 degree Celsius
relativeHumiditySetpoint: 60 percent
photoperiod: 16 h light / 8 h dark
Read it back in lenient mode to preserve the extension field. Strict mode is for core-only documents and rejects unknown fields.
let roundTrippedFacility =
ProcessCore.Yaml.Dataset.fromYamlString false facilityYaml
let roundTrippedLayout =
roundTrippedFacility.TryGetTypedPropertyValue<DynamicObj>("experimentalFacilityLayout")
roundTrippedLayout.IsSome
|
What To Use When
Task |
API |
|---|---|
Give a core object a domain role |
|
Attach characteristics or factors to samples/data |
|
Attach process parameters |
|
Attach protocol components |
|
Keep extensions ontologized and queryable |
|
Preserve metadata outside the core graph |
|
Read/write decorated YAML |
|
Enforce core-only YAML |
|
type Annotation = inherit DynamicObj new: name: string * ?value: string * ?unit: string * ?nameTAN: string * ?valueTAN: string * ?unitTAN: string * ?additionalType: string * ?instanceOf: FormalParameter -> Annotation override Equals: obj: obj -> bool override GetHashCode: unit -> int member NameEquals: term: DefinedTerm -> bool member AdditionalType: string option with get, set member InstanceOf: FormalParameter option with get, set member Name: string with get, set member NameTAN: string option with get, set member NameText: string ...
<summary> Extensible key-value-unit triple. Primary extension mechanism of ProcessCore. schema.org/PropertyValue </summary>
--------------------
new: name: string * ?value: string * ?unit: string * ?nameTAN: string * ?valueTAN: string * ?unitTAN: string * ?additionalType: string * ?instanceOf: FormalParameter -> Annotation
<summary> Subtype discriminator (e.g. ParameterValue, CharacteristicValue, FactorValue) </summary>
val string: value: 'T -> string
--------------------
type string = System.String
<summary>Provides methods for encoding and decoding URLs when processing Web requests.</summary>
System.Net.WebUtility.HtmlEncode(value: string, output: System.IO.TextWriter) : unit
type Dataset = inherit DynamicObj new: identifier: string * ?title: string * ?description: string * ?additionalType: string * ?license: string * ?datePublished: string * ?dateCreated: string * ?dateModified: string * ?processes: Process seq * ?hasPart: Dataset seq * ?dataFiles: Data seq * ?agents: Agent seq * ?citations: ScholarlyArticle seq * ?dataContexts: DataContext seq * ?additionalProperty: Annotation seq -> Dataset member AddAdditionalProperty: pv: Annotation -> unit member AddAgent: agent: Agent -> unit member AddCitation: article: ScholarlyArticle -> unit member AddDataContext: dataContext: DataContext -> unit member AddDataFile: data: Data -> unit member AddPart: child: Dataset -> unit member AddProcess: proc: Process -> unit member AllAgents: unit -> ResizeArray<Agent> ...
<summary> Container and context for data, processes, administrative metadata, and datamap entries. schema.org/Dataset </summary>
--------------------
new: identifier: string * ?title: string * ?description: string * ?additionalType: string * ?license: string * ?datePublished: string * ?dateCreated: string * ?dateModified: string * ?processes: Process seq * ?hasPart: Dataset seq * ?dataFiles: Data seq * ?agents: Agent seq * ?citations: ScholarlyArticle seq * ?dataContexts: DataContext seq * ?additionalProperty: Annotation seq -> Dataset
type Sample = inherit DynamicObj new: name: string * ?additionalType: string * ?additionalProperty: Annotation seq -> Sample member AddAdditionalProperty: pv: Annotation -> unit member AllAnnotations: ?scope: ResizeArray<Process> -> ResizeArray<Annotation> member AllConnectedNodes: ?scope: ResizeArray<Process> -> ResizeArray<IONode> member AllConnectedProcesses: ?scope: ResizeArray<Process> -> ResizeArray<Process> member ConnectedData: ?scope: ResizeArray<Process> -> ResizeArray<Data> member ConnectedSamples: ?scope: ResizeArray<Process> -> ResizeArray<Sample> member DownstreamAnnotations: ?recipeName: string * ?scope: ResizeArray<Process> -> ResizeArray<Annotation> member DownstreamData: ?scope: ResizeArray<Process> -> ResizeArray<Data> ...
<summary> Input or output biological, chemical, or digital sample in the process graph. bioschemas.org/Sample </summary>
--------------------
new: name: string * ?additionalType: string * ?additionalProperty: Annotation seq -> Sample
type Recipe = inherit DynamicObj new: ?name: string * ?description: string * ?version: string * ?url: string * ?intendedUse: DefinedTerm * ?additionalType: string * ?parameters: FormalParameter seq * ?components: Annotation seq * ?additionalProperty: Annotation seq -> Recipe member AddAdditionalProperty: pv: Annotation -> unit member AddComponent: pv: Annotation -> unit member AddParameter: fp: FormalParameter -> unit override Equals: obj: obj -> bool override GetHashCode: unit -> int member RemoveAdditionalProperty: pv: Annotation -> unit member RemoveComponent: pv: Annotation -> unit member RemoveParameter: fp: FormalParameter -> unit ...
<summary> Description of a planned procedure. bioschemas.org/LabProtocol </summary>
--------------------
new: ?name: string * ?description: string * ?version: string * ?url: string * ?intendedUse: DefinedTerm * ?additionalType: string * ?parameters: FormalParameter seq * ?components: Annotation seq * ?additionalProperty: Annotation seq -> Recipe
type Process = inherit DynamicObj new: name: string * ?executesRecipe: Recipe * ?additionalType: string * ?input: IONode * ?output: IONode * ?parameterValue: Annotation seq -> Process member AddParameterValue: pv: Annotation -> unit member AnnotationsByName: name: string -> ResizeArray<Annotation> member CanonicalizeAllNodes: ds: Dataset -> unit member ClearInput: unit -> unit member ClearOutput: unit -> unit override Equals: obj: obj -> bool override GetHashCode: unit -> int member GetParameterValue: name: string -> Annotation ...
<summary> Core transformation node. Connects inputs to outputs by executing a recipe. bioschemas.org/LabProcess </summary>
--------------------
new: name: string * ?executesRecipe: Recipe * ?additionalType: string * ?input: IONode * ?output: IONode * ?parameterValue: Annotation seq -> Process
<summary> Decoration discriminator (e.g. "Investigation", "Study", "Assay") </summary>
<summary> Decoration discriminator (e.g. "Sample", "Source") </summary>
<summary> Equipment, reagents, and software used in this recipe (components). </summary>
namespace DynamicObj
--------------------
type DynamicObj = inherit DynamicObject new: unit -> DynamicObj member DeepCopyProperties: ?includeInstanceProperties: bool -> obj member DeepCopyPropertiesTo: target: #DynamicObj * ?overWrite: bool * ?includeInstanceProperties: bool -> unit override Equals: o: obj -> bool override GetDynamicMemberNames: unit -> string seq override GetHashCode: unit -> int member GetProperties: includeInstanceProperties: bool -> KeyValuePair<string,obj> seq member GetPropertyHelpers: includeInstanceProperties: bool -> PropertyHelper seq member GetPropertyNames: includeInstanceProperties: bool -> string seq ...
--------------------
new: unit -> DynamicObj
ProcessCore