Logo ProcessCore

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:

  1. Use additionalType and additionalProperty on core objects. This keeps the data close to the ProcessCore model and makes the extension queryable as typed PropertyValue annotations.
  2. Use the inherited DynamicObj property 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
[("identifier", "measurement1"); ("dataset additionalType", "Assay");
 ("processes", "2"); ("materials", "3"); ("data nodes", "0")]

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
[("Source", 1); ("Sample", 2)]

The first Growth process shows a compact ISA-style shape:

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", "Growth"); ("input", "Base Culture (Source)");
 ("input annotations", "CharacteristicValue: organism = Arabidopsis thaliana");
 ("output", "Cultivation Flask RT (Sample)");
 ("output annotations", "FactorValue: temperature = 25 degree Celsius");
 ("protocol components", "Component: growth environment = bioreactor")]

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
["ParameterValue: sonicator = Fisherbrand Model 705 Sonic Dismembrator";
 "ParameterValue: time = 10 minute";
 "ParameterValue: technical replicate group = 1"]

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
["Cultivation Flask RT"]

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
[("facility", "Phytotron A"); ("room", "Growth room 2");
 ("bench", "North bench"); ("location", "A-02-N-03")]

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
true

What To Use When

Task

API

Give a core object a domain role

AdditionalType

Attach characteristics or factors to materials/data

node.AddAdditionalProperty

Attach process parameters

process.AddParameterValue

Attach protocol components

protocol.AddLabEquipment

Keep extensions ontologized and queryable

PropertyValue(name, value, unit, nameTAN, valueTAN, unitTAN)

Preserve metadata outside the core graph

SetProperty, TryGetTypedPropertyValue from DynamicObj

Read/write decorated YAML

ProcessCore.Yaml.Dataset.fromYamlString false, toYamlString

Enforce core-only YAML

ProcessCore.Yaml.Dataset.fromYamlString true

namespace DynamicObj
namespace ProcessCore
val valueOrBlank: (string option -> string)
module Option from Microsoft.FSharp.Core
val defaultValue: value: 'T -> option: 'T option -> 'T
val pvSummary: pv: PropertyValue -> string
val pv: PropertyValue
Multiple items
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
val typeText: string
property PropertyValue.AdditionalType: string option with get, set
<summary> Subtype discriminator (e.g. ParameterValue, CharacteristicValue, FactorValue) </summary>
val valueText: string
property PropertyValue.ValueWithUnitText: string with get
val sprintf: format: Printf.StringFormat<'T> -> 'T
property PropertyValue.Name: string with get, set
val yamlCodeBlock: summary: string -> text: string -> string
val summary: string
val text: string
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
namespace System
namespace System.Net
type WebUtility = static member HtmlDecode: value: string -> string + 1 overload static member HtmlEncode: value: string -> string + 1 overload static member UrlDecode: encodedValue: string -> string static member UrlDecodeToBytes: encodedValue: byte array * offset: int * count: int -> byte array static member UrlEncode: value: string -> string static member UrlEncodeToBytes: value: byte array * offset: int * count: int -> byte array
<summary>Provides methods for encoding and decoding URLs when processing Web requests.</summary>
System.Net.WebUtility.HtmlEncode(value: string) : string
System.Net.WebUtility.HtmlEncode(value: string, output: System.IO.TextWriter) : unit
val assay: Dataset
Multiple items
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
property Dataset.Name: string option with get, set
union case Option.Some: Value: 'T -> Option<'T>
val source: Material
Multiple items
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
val organism: PropertyValue
member Material.AddAdditionalProperty: pv: PropertyValue -> unit
val roomTemperatureSample: Material
val temperature25: PropertyValue
type unit = Unit
val highTemperatureSample: Material
val temperature30: PropertyValue
val growthProtocol: LabProtocol
Multiple items
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
member LabProtocol.AddLabEquipment: pv: PropertyValue -> unit
val growthAt25: LabProcess
Multiple items
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
member LabProcess.AddInputMaterial: m: Material -> unit
member LabProcess.AddOutputMaterial: m: Material -> unit
member Dataset.AddProcess: proc: LabProcess -> unit
val growthAt30: LabProcess
val assayDecoration: (string * string) list
property Dataset.Identifier: string with get, set
property Dataset.AdditionalType: string option with get, set
<summary> Decoration discriminator (e.g. "Investigation", "Study", "Assay") </summary>
property Dataset.Processes: ResizeArray<LabProcess> with get
property System.Collections.Generic.List.Count: int with get
member Dataset.AllMaterials: unit -> ResizeArray<Material>
member Dataset.AllData: unit -> ResizeArray<Data>
val materialRoles: (string * int) list
module Seq from Microsoft.FSharp.Collections
val countBy: projection: ('T -> 'Key) -> source: 'T seq -> ('Key * int) seq (requires equality)
val material: Material
property Material.AdditionalType: string option with get, set
<summary> Decoration discriminator (e.g. "Sample", "Source") </summary>
val map: mapping: ('T -> 'U) -> source: 'T seq -> 'U seq
val role: string
val count: int
val toList: source: 'T seq -> 'T list
val growthInput: Material
member LabProcess.InputMaterials: unit -> ResizeArray<Material>
val head: source: 'T seq -> 'T
val growthOutput: Material
member LabProcess.OutputMaterials: unit -> ResizeArray<Material>
val growthDecoration: (string * string) list
property LabProcess.Name: string with get, set
property Material.Name: string with get, set
property Material.AdditionalProperty: ResizeArray<PropertyValue> with get
module String from Microsoft.FSharp.Core
val concat: sep: string -> strings: string seq -> string
property LabProtocol.LabEquipment: ResizeArray<PropertyValue> with get
<summary> Equipment, reagents, and software used in this protocol (components). </summary>
val sonicator: PropertyValue
val lysisTime: PropertyValue
val technicalReplicate: PropertyValue
val lysis: LabProcess
member LabProcess.AddParameterValue: pv: PropertyValue -> unit
property LabProcess.ParameterValue: ResizeArray<PropertyValue> with get
val samplesAt25Degrees: string list
val filter: predicate: ('T -> bool) -> source: 'T seq -> 'T seq
val exists: predicate: ('T -> bool) -> source: 'T seq -> bool
property PropertyValue.Value: string option with get, set
val facilityDataset: Dataset
val environmentalControls: DynamicObj
Multiple items
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
member DynamicObj.SetProperty: propertyName: string * propertyValue: obj -> unit
val facilityLayout: DynamicObj
val recoveredFacility: DynamicObj option
member DynamicObj.TryGetTypedPropertyValue: propertyName: string -> 'TPropertyValue option
val facilitySummary: (string * string) list
val layout: DynamicObj
union case Option.None: Option<'T>
val facilityYaml: string
namespace ProcessCore.Yaml
module Dataset from ProcessCore.Yaml
val toYamlString: whitespace: int option -> ds: Dataset -> string
val roundTrippedFacility: Dataset
val fromYamlString: processCoreOnly: bool -> s: string -> Dataset
val roundTrippedLayout: DynamicObj option
property Option.IsSome: bool with get

Type something to start searching.