Decorations
The core data model is intentionally small: Dataset, LabProcess, LabProtocol, Material, Data, and PropertyValue 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 ProcessCore model and makes the extension queryable as typedPropertyValueannotations. - 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 PropertyValue 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.Name <- Some "Proteomics assay"
let source =
Material("Base Culture", additionalType = "Source")
let organism =
PropertyValue(
"organism",
value = "Arabidopsis thaliana",
nameTAN = "https://bioregistry.io/SIO:010000",
valueTAN = "https://bioregistry.io/NCBITaxon:3702",
additionalType = "CharacteristicValue")
source.AddAdditionalProperty(organism)
let roomTemperatureSample =
Material("Cultivation Flask RT", additionalType = "Sample")
let temperature25 =
PropertyValue(
"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 =
Material("Cultivation Flask HT", additionalType = "Sample")
let temperature30 =
PropertyValue(
"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 = LabProtocol(name = "Growth")
growthProtocol.AddLabEquipment(
PropertyValue(
"growth environment",
value = "bioreactor",
nameTAN = "https://bioregistry.io/OBI:0000997",
valueTAN = "https://bioregistry.io/OBI:0001046",
additionalType = "Component"))
let growthAt25 = LabProcess("Growth", executesProtocol = growthProtocol)
growthAt25.AddInputMaterial(source)
growthAt25.AddOutputMaterial(roomTemperatureSample)
assay.AddProcess(growthAt25)
let growthAt30 = LabProcess("Growth", executesProtocol = growthProtocol)
growthAt30.AddInputMaterial(source)
growthAt30.AddOutputMaterial(highTemperatureSample)
assay.AddProcess(growthAt30)
let assayDecoration =
[ "identifier", assay.Identifier
"dataset additionalType", assay.AdditionalType |> valueOrBlank
"processes", string assay.Processes.Count
"materials", string (assay.AllMaterials().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 material roles: the input is a Source, while the outputs are Sample materials.
let materialRoles =
assay.AllMaterials()
|> Seq.countBy (fun material -> material.AdditionalType |> valueOrBlank)
|> Seq.map (fun (role, count) -> role, count)
|> Seq.toList
materialRoles
|
The first Growth process shows a compact ISA-style shape:
- The input material is a
Source. - The output material is a
Sample. - Characteristics are attached to input nodes via
AdditionalProperty. - Factors are attached to output nodes via
AdditionalProperty.
let growthInput =
growthAt25.InputMaterials()
|> Seq.head
let growthOutput =
growthAt25.OutputMaterials()
|> Seq.head
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.LabEquipment |> Seq.map pvSummary |> String.concat "; " ]
growthDecoration
|
Process parameters use the same PropertyValue type, but they live on the LabProcess.ParameterValue slot.
Here, cell lysis records the sonicator, lysis duration, and technical replicate group as ParameterValue decorations.
let sonicator =
PropertyValue(
"sonicator",
value = "Fisherbrand Model 705 Sonic Dismembrator",
nameTAN = "https://bioregistry.io/OBI:0400114",
valueTAN = "https://bioregistry.io/OBI:5453453",
additionalType = "ParameterValue")
let lysisTime =
PropertyValue(
"time",
value = "10",
unit = "minute",
nameTAN = "https://bioregistry.io/PATO:0000165",
unitTAN = "https://bioregistry.io/UO:0000031",
additionalType = "ParameterValue")
let technicalReplicate =
PropertyValue(
"technical replicate group",
value = "1",
nameTAN = "https://bioregistry.io/DPBO:1000184",
additionalType = "ParameterValue")
let lysis = LabProcess("Cell Lysis")
lysis.AddInputMaterial(roomTemperatureSample)
lysis.AddOutputMaterial(Material("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.AllMaterials()
|> Seq.filter (fun material ->
material.AdditionalType = Some "Sample"
&& material.AdditionalProperty
|> Seq.exists (fun pv ->
pv.AdditionalType = Some "FactorValue"
&& pv.Name = "temperature"
&& pv.Value = Some "25"))
|> Seq.map (fun material -> material.Name)
|> Seq.toList
samplesAt25Degrees
|
DynamicObj Extensions
All main ProcessCore 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.Name <- 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 ProcessCore 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
name: 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 materials/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 PropertyValue = inherit DynamicObj new: name: string * ?value: string * ?unit: string * ?nameTAN: string * ?valueTAN: string * ?unitTAN: string * ?additionalType: string * ?instanceOf: FormalParameter -> PropertyValue override Equals: obj: obj -> bool override GetHashCode: unit -> int 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 member Unit: string option with get, set ...
<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 -> PropertyValue
<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 * ?name: string * ?description: string * ?additionalType: string * ?processes: LabProcess seq * ?hasPart: Dataset seq * ?additionalProperty: PropertyValue seq -> Dataset member AddAdditionalProperty: pv: PropertyValue -> unit member AddPart: child: Dataset -> unit member AddProcess: proc: LabProcess -> unit member AllConnectedNodes: node: IONode -> ResizeArray<IONode> member AllData: unit -> ResizeArray<Data> member AllMaterials: unit -> ResizeArray<Material> member AllNodes: unit -> ResizeArray<IONode> member AllProcesses: unit -> ResizeArray<LabProcess> ...
<summary> Container and context for data and processes. schema.org/Dataset </summary>
--------------------
new: identifier: string * ?name: string * ?description: string * ?additionalType: string * ?processes: LabProcess seq * ?hasPart: Dataset seq * ?additionalProperty: PropertyValue seq -> Dataset
type Material = inherit DynamicObj new: name: string * ?additionalType: string * ?additionalProperty: PropertyValue seq -> Material member AddAdditionalProperty: pv: PropertyValue -> unit member AllConnectedNodes: ?scope: ResizeArray<LabProcess> -> ResizeArray<IONode> member AllConnectedProcesses: ?scope: ResizeArray<LabProcess> -> ResizeArray<LabProcess> member AllPropertyValues: ?scope: ResizeArray<LabProcess> -> ResizeArray<PropertyValue> member ConnectedData: ?scope: ResizeArray<LabProcess> -> ResizeArray<Data> member ConnectedMaterials: ?scope: ResizeArray<LabProcess> -> ResizeArray<Material> member DownstreamData: ?scope: ResizeArray<LabProcess> -> ResizeArray<Data> member DownstreamMaterials: ?scope: ResizeArray<LabProcess> -> ResizeArray<Material> ...
<summary> Input or output biological, chemical, or digital material in the process graph. bioschemas.org/Sample </summary>
--------------------
new: name: string * ?additionalType: string * ?additionalProperty: PropertyValue seq -> Material
type LabProtocol = inherit DynamicObj new: ?name: string * ?description: string * ?version: string * ?url: string * ?intendedUse: DefinedTerm * ?additionalType: string * ?parameters: FormalParameter seq * ?labEquipment: PropertyValue seq * ?additionalProperty: PropertyValue seq -> LabProtocol member AddAdditionalProperty: pv: PropertyValue -> unit member AddLabEquipment: pv: PropertyValue -> unit member AddParameter: fp: FormalParameter -> unit override Equals: obj: obj -> bool override GetHashCode: unit -> int member RemoveAdditionalProperty: pv: PropertyValue -> unit member RemoveLabEquipment: pv: PropertyValue -> 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 * ?labEquipment: PropertyValue seq * ?additionalProperty: PropertyValue seq -> LabProtocol
type LabProcess = inherit DynamicObj new: name: string * ?executesProtocol: LabProtocol * ?additionalType: string * ?inputs: IONode seq * ?outputs: IONode seq * ?parameterValue: PropertyValue seq -> LabProcess member AddInput: node: IONode -> unit member AddInputData: d: Data -> unit member AddInputMaterial: m: Material -> unit member AddOutput: node: IONode -> unit member AddOutputData: d: Data -> unit member AddOutputMaterial: m: Material -> unit member AddParameterValue: pv: PropertyValue -> unit member CanonicalizeAllNodes: ds: Dataset -> unit ...
<summary> Core transformation node. Connects inputs to outputs via a protocol. bioschemas.org/LabProcess </summary>
--------------------
new: name: string * ?executesProtocol: LabProtocol * ?additionalType: string * ?inputs: IONode seq * ?outputs: IONode seq * ?parameterValue: PropertyValue seq -> LabProcess
<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 protocol (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