Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 41 additions & 69 deletions SPECIFICATION.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions core-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Each `<Category>/<ClassName>/v1.0.0/` folder normally contains exactly:

Every `schema.json`'s `required` array carries a sibling `x-required-rule-ids` object mapping each required field's name to the numbered validation rule that documents its presence requirement, and any property whose own constraint - a `_type` discriminator `const`, or a type/shape check beyond bare presence - has a matching numbered rule carries a sibling `x-rule-id` alongside that constraint (as a sibling of `$ref` where the property is declared that way); `x-rule-id` is a single rule-ID string, or an array of them when more than one rule governs the same property (most commonly an `OrRef` field's separate direct-value and reference-form rules, e.g. `AbstractODESimulation.forcePhysicalCorrectness`). A bare `SIdRef` property that must resolve to a particular kind of thing also carries `"x-ref-target": "model"` (SEDBase-0016) or `"x-ref-target": "annotatedData"` (SEDBase-0017). All of these are inert to any JSON Schema validator (unknown keywords are ignored) and exist purely as generator-only metadata - see Design.md's Validation section - so the generator can wire each hand-written `validate()` check straight to the schema location it corresponds to rather than re-deriving that mapping itself. Not every numbered rule has a schema-visible home this way: most are semantic (cross-reference resolution, math well-formedness, tolerance interactions, and the like) with no single JSON Schema keyword to attach to; only presence, discriminator, and simple type/shape constraints get one. A class whose own requiredness is disjunctive rather than a flat list - so far, only `Repeat`, whose subclasses need at least one of `outputVariableMap` or `aggregateOutputVariables` rather than a fixed set - expresses that as an `anyOf` of single-field `required` objects at the class's own top level (sibling to `properties`), with a sibling `x-anyof-required-rule-id` naming the one rule documenting the whole disjunction, rather than forcing it into `x-required-rule-ids`'s per-field mapping. A `oneOf` discriminator built via `x-generated-oneOf` (see Design.md's Classes section) can similarly carry a sibling `x-missing-type-rule-id`, naming a rule for the narrower case where `_type` is absent from the instance entirely, rather than present but unrecognized - a more specific error than the discriminator's own catch-all, for whichever discriminators want to distinguish it (e.g. `AbstractTask-0002`).

**Task output shapes (`outputs.json`).** Every concrete `tasks/` class (one with its own `_type`) additionally carries an `outputs.json`, machine-describing what each output suffix (`[id]`, `[id].model`, `[id].strings`, and any class-specific suffix like `Loop`'s `[id].aggregates`/`[id].range`/`[id].index`) resolves to - replacing the free-text "Valid"/"Invalid"/"Dimensions:" bullets that used to be the only record of this. Each suffix entry has a `valid` flag (`true`/`false`, or a boolean expression string over the task's own fields, e.g. `"outputModel == true"`, `"provided(range)"`), a `type` (`annotatedData`/`model`/`stringList`) when valid, and for `annotatedData` a `dimensions` list. Every place a size, a label set, or a whole shape needs stating, it's tagged with where that knowledge actually comes from - `"static"` (an `expr` computed from the task's own fields: `len(outputVariables)`, `independentVariableRange.numberOfSteps`, `keys(data)`, `[independentVariable] + outputVariables` as an array-literal-plus-concatenation, `shapeOf(input)` to inherit another reference's whole shape, optionally minus a dropped dimension via `- dim(...)`), `"input-file"` (knowable only by reading whatever external resource a field like `model` or `location` points to - `JacobianFull`'s species count, `CsvImport`'s column headers - tagged with `from`/`extract`/`note` rather than a formula, since the actual parsing is format-specific generator work, not something this file does itself), or `"runtime"` (genuinely not derivable without executing the simulation, e.g. `BoundedODESimulation`'s solver-chosen row count under variable step size, or `Loop`'s iteration count - always paired with a `note` explaining why). A `"runtime"` size may also carry an optional `min`, a guaranteed lower bound when one is actually known (e.g. "at least 2 rows"); an index or range that requires more entries than `min` guarantees is flagged by `SEDBase-0014` as a warning rather than an error, since it may still be valid once the simulation actually runs. `dimensions` itself is either a fixed array of per-dimension entries - an ordinary entry, or a `repeat` entry that expands to one dimension per entry of a named array-valued attribute (e.g. `ParameterScan`'s one dimension per entry of `parameterRanges`) - when the shape is decomposable dimension-by-dimension, or a single sourced object describing the whole shape's derivation, for the genuinely non-decomposable cases (`shapeOf(x)` whole-shape inheritance, or a `"runtime"`/`"input-file"` source where the dimension count itself isn't knowable ahead of time). `schema/outputs-meta.schema.json` validates every class's `outputs.json` envelope (the object shapes above); it does not parse `expr`/`note` string contents, which stay documentation for whoever implements the generator's shape-inference step. Abstract/mixin classes (`AbstractSimulation` and friends, `Repeat`) carry no `outputs.json` of their own, the same reasoning as why they carry no `_type` - nothing is ever instantiated as one directly, so "what does referencing this task's output produce" has no meaning until a concrete leaf class answers it, flatly, the same way those classes already flatten every other inherited constraint into their own `schema.json`.
**Task output shapes (`outputs.json`).** Every concrete `tasks/` class (one with its own `_type`) additionally carries an `outputs.json`, machine-describing what each output suffix (`[id]`, `[id].model`, `[id].strings`, and any class-specific suffix like `Loop`'s `[id].aggregates`/`[id].range`/`[id].index`) resolves to - replacing the free-text "Valid"/"Invalid"/"Dimensions:" bullets that used to be the only record of this. A suffix that is listed is valid, and a suffix that is not listed is not valid (a class with no valid suffixes, such as `Span`, lists none). A listed entry may carry an optional `valid` field meaning "valid if": a boolean expression string over the task's own fields, e.g. `"outputModel == true"`, `"provided(range)"`; with no `valid` field the entry is always valid. Each entry has a `type` (`annotatedData`/`model`/`stringList`), and for `annotatedData` a `dimensions` list. Every place a size, a label set, or a whole shape needs stating, it's tagged with where that knowledge actually comes from - `"static"` (an `expr` computed from the task's own fields: `len(outputVariables)`, `independentVariableRange.numberOfSteps`, `keys(data)`, `[independentVariable] + outputVariables` as an array-literal-plus-concatenation, `shapeOf(input)` to inherit another reference's whole shape, optionally minus a dropped dimension via `- dim(...)`), `"input-file"` (knowable only by reading whatever external resource a field like `model` or `location` points to - `JacobianFull`'s species count, `CsvImport`'s column headers - tagged with `from`/`extract`/`note` rather than a formula, since the actual parsing is format-specific generator work, not something this file does itself), or `"runtime"` (genuinely not derivable without executing the simulation, e.g. `BoundedODESimulation`'s solver-chosen row count under variable step size, or `Loop`'s iteration count - always paired with a `note` explaining why). A `"runtime"` size may also carry an optional `min`, a guaranteed lower bound when one is actually known (e.g. "at least 2 rows"); an index or range that requires more entries than `min` guarantees is flagged by `SEDBase-0014` as a warning rather than an error, since it may still be valid once the simulation actually runs. `dimensions` itself is either a fixed array of per-dimension entries - an ordinary entry, or a `repeat` entry that expands to one dimension per entry of a named array-valued attribute (e.g. `ParameterScan`'s one dimension per entry of `parameterRanges`) - when the shape is decomposable dimension-by-dimension, or a single sourced object describing the whole shape's derivation, for the genuinely non-decomposable cases (`shapeOf(x)` whole-shape inheritance, or a `"runtime"`/`"input-file"` source where the dimension count itself isn't knowable ahead of time). `schema/outputs-meta.schema.json` validates every class's `outputs.json` envelope (the object shapes above); it does not parse `expr`/`note` string contents, which stay documentation for whoever implements the generator's shape-inference step. Abstract/mixin classes (`AbstractSimulation` and friends, `Repeat`) carry no `outputs.json` of their own, the same reasoning as why they carry no `_type` - nothing is ever instantiated as one directly, so "what does referencing this task's output produce" has no meaning until a concrete leaf class answers it, flatly, the same way those classes already flatten every other inherited constraint into their own `schema.json`.

**The `expr`/`valid` notation.** Both fields reuse one small expression language, evaluated against the task's own attribute values at generate/validate time - never against runtime data, and not the SED2 math grammar `Types-0001` through `Types-0004` validate (see this section's `x-rule-id` paragraph above and `Design.md`'s Math section for that separate grammar): `outputs.json` describes shape, `math` describes computation. A bare identifier names one of the task's own attributes and evaluates to its value (`outputVariables`); a dotted path reaches into a nested attribute the same way (`independentVariableRange.numberOfSteps`). `[x]` is an array literal (`[independentVariable]`); JSON numbers and `true`/`false` are literals too. Five functions: `len(x)` - the length of an array-valued `x`; or, when `x` is a Range-family value (a `RangeInline`/`NumericRangeInline`/`ParameterRangeInline`-typed field, or the reserved identifier `self` inside a `repeat` entry - see below), the number of points it resolves to, dispatched on `x`'s own discriminated type: `len(x.values) if provided(x.values) else x.numberOfSteps + 1` for `NumericRange`/`ParameterRange`, or just `len(x.values)` for a bare `Range` (which has no `numberOfSteps` alternative); `keys(x)` - the keys of an object-valued `x`, as an array; `shapeOf(x)` - the whole `dimensions` shape of a referenced AnnotatedData value; `dim(x)` - the dimension(s) named by `x` (a single name or a list of names) within a shape, meaningful only as the right operand of `-`; `provided(x)` - `true` iff attribute `x` was given a value in the document. Operators, at ordinary precedence: `!x` (boolean negation); `x == y` (equality); `x + y` (numeric addition when both operands are numbers, array concatenation when both are arrays - `1 + len(outputVariables)` vs. `[independentVariable] + outputVariables`); `shapeOf(x) - dim(y)` (a shape with the named dimension(s) removed - the only place `-` appears); `x or y` (`x`'s value when `provided(x)` holds, else the literal `y` - used for a field with a documented default, e.g. `dim(appliedDimensions or outermost)`, where `outermost` is a sentinel meaning the shape's own first dimension); and a conditional `x if provided(y) else z` for a value with two different derivations depending on whether an alternate field was given instead (e.g. `NumericRange`'s `len(values) if provided(values) else numberOfSteps + 1`). This is the only formal definition of the notation - `schema/outputs-meta.schema.json` does not parse it, as noted above - so a generator's shape-inference step implements it by hand. Inside a `dimensions` array's `repeat` entry (see above), this same notation applies to that entry's own `size`/`labels`, with two scoping additions: a bare identifier there resolves against the current array entry's own fields, not the task's top-level attributes; and the reserved identifier `self` refers to the current entry as a whole (for taking its `len()`, per above) rather than one of its fields. `ParameterScan`'s `repeat` over `parameterRanges` uses `size: len(self)`, letting each entry's own type (`Range`/`NumericRange`/`ParameterRange`) determine its own length the same way `Scatter`'s `len(range)` and `ExplicitODESimulation`'s `len(independentVariableRange)` do for their own named Range-family fields.

Expand All @@ -119,7 +119,7 @@ tasks/Range/v1.0.0/ tasks/NumericRange/v1.0.0/ task

`Range` and `NumericRange` carry an extra `common.schema.json` that `ParameterRange` and `Span` don't need: `Range` is both directly instantiable and the parent of `NumericRange`, and `NumericRange` is in turn both directly instantiable and the parent of `ParameterRange` - so each contributes a `RangeCommon`/`NumericRangeCommon` mixin (composed via `allOf`) carrying the fields its subclass inherits, alongside its own `schema.json` for the standalone-task form. `ParameterRange` and `Span` are leaves - nothing subclasses them - so they need no such split. Each mixin composes the next one up in turn (`NumericRangeCommon` composes `RangeCommon`, which composes `AbstractTaskCommon`), so a concrete class's own `allOf` names only its one most specific ancestor mixin - `ParameterRange` lists just `NumericRangeCommon`, not `RangeCommon` and `NumericRangeCommon` side by side - mirroring the UML chain directly rather than flattening every ancestor into each leaf; see `Design.md`'s Classes section for why.

Each `description.md` in these four folders explains all of that folder's schema files. Everything else in the document - including every cross-reference into these four classes from other Data Sheets' `schema.json` files - still resolves correctly; this was verified by validating real SED2 example documents against the split schemas after the merge. All four live under `tasks/` rather than `auxiliary/`, so each also carries the ordinary `outputs.json` every concrete `tasks/` class does - `Range`, `NumericRange`, and `ParameterRange` describe their `[id]` output for when they're used as a standalone `tasks` entry, and `Span`'s marks every suffix invalid, matching its own `description.md` (`Span` can never be a standalone `tasks` entry, only ever an embedded child, so it has no output of its own).
Each `description.md` in these four folders explains all of that folder's schema files. Everything else in the document - including every cross-reference into these four classes from other Data Sheets' `schema.json` files - still resolves correctly; this was verified by validating real SED2 example documents against the split schemas after the merge. All four live under `tasks/` rather than `auxiliary/`, so each also carries the ordinary `outputs.json` every concrete `tasks/` class does - `Range`, `NumericRange`, and `ParameterRange` describe their `[id]` output for when they're used as a standalone `tasks` entry, and `Span`'s lists no suffixes at all (so every suffix is invalid), matching its own `description.md` (`Span` can never be a standalone `tasks` entry, only ever an embedded child, so it has no output of its own).

**Exception - the two `*Common` mixins.** `AbstractTaskCommon` and `AbstractOutputCommon` are schema-only mixins (composed via `allOf`) rather than modeled classes with their own UML box: they contribute the fields every concrete Task (or Output) picks up beyond `SEDBase` - `taskParameters`/`outputParameters` - but have no `_type` of their own and are never instantiated directly. Rather than give them placeholder folders, each is merged into its family's base-class folder as a second schema file:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@
},
"outputVariableMap": {
"col": "#tasks:loop1:subTasks:sub1.model"
},
"range": {
"_type": "numericRange",
"values": [
1,
2,
3
]
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@
},
"outputVariableMap": {
"col": "#tasks:loop1:subTasks:sub1.model"
},
"range": {
"_type": "numericRange",
"values": [
1,
2,
3
]
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@
},
"outputVariableMap": {
"col": "#constants:myNumberArray"
},
"range": {
"_type": "numericRange",
"values": [
1,
2,
3
]
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@
},
"outputVariableMap": {
"col": "#tasks:loop1:subTasks:sub1.model"
},
"range": {
"_type": "numericRange",
"values": [
1,
2,
3
]
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@
"_type": "aggregationCalculation",
"input": "#constants:mynumber"
}
},
"range": {
"_type": "numericRange",
"values": [
1,
2,
3
]
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@
"location": "model.xml",
"language": "urn:sedml:language:sbml"
}
},
"range": {
"_type": "numericRange",
"values": [
1,
2,
3
]
}
},
"r1": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@
"location": "model.xml",
"language": "urn:sedml:language:sbml"
}
},
"range": {
"_type": "numericRange",
"values": [
1,
2,
3
]
}
},
"r1": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@
]
}
}
},
"range": {
"_type": "numericRange",
"values": [
1,
2,
3
]
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,31 +41,47 @@
]
}
},
"ss1": {
"_type": "steadyState",
"model": "#tasks:m1.model",
"outputVariables": [
"S1"
],
"outputModel": false
},
"s2": {
"_type": "explicitODESimulation",
"model": "#tasks:ss1.model",
"independentVariable": "time",
"outputVariables": [
"S1",
"S2"
],
"independentVariableRange": {
"loop1": {
"_type": "loop",
"loopVariables": {
"lv1": {
"initialValue": 0,
"subsequentValues": "#tasks:loop1:subTasks:s1"
}
},
"subTasks": {
"s1": {
"_type": "explicitODESimulation",
"model": "#tasks:m1.model",
"independentVariable": "time",
"outputVariables": [
"S1",
"S2"
],
"independentVariableRange": {
"_type": "numericRange",
"values": [
0,
1,
2,
3
]
}
}
},
"range": {
"_type": "numericRange",
"values": [
0,
1,
2,
3
]
}
},
"r1": {
"_type": "relabelData",
"input": "#tasks:loop1.aggregates",
"labels": "#constants:k_strings"
}
}
}
Loading
Loading