Rule language
Prefix: RL
Status: Draft. The user requirements are under review. The implementation
requirements are the recommended approach, and they can change.
Source: the review discussion on
cornerstonejs/cornerstone3D#2861,
and the revised proposal of 16 October, which follows the shape that the
reviewer proposed on 14 October. The reviewer agreed to the answers of that
proposal on 16 October.
Affects: @cornerstonejs/metadata. Consumers: the display set split rules
in @cornerstonejs/metadata, and later the hanging protocols of OHIF.
Scope
This specification describes one rule language. The rule language has four parts:
- a condition, which tests one subject and gives
trueorfalse, - a value, which reads one value from one subject,
- a sort order, which compares two subjects,
- a context, which gives a name to each value that a rule reads and that does not come directly from a DICOM keyword.
A consumer wraps the rule language in the fields of its own rules. The first consumer is the display set split rule. The second consumer is the hanging protocol. Each consumer gets the same meaning for the same text.
The specification has two requirement sections:
- User requirements (
RL-READ,RL-VALUE,RL-CTX,RL-SHARE,RL-EXT,RL-SCOPE,RL-CODE,RL-SAFE,RL-ERR) state what a rule author, a reviewer, an assistant or a host developer must be able to do or to determine. - Implementation requirements (
RL-PKG,RL-API,RL-NAME,RL-COND,RL-VAL,RL-SORT,RL-EXPR,RL-CTXI,RL-FN,RL-CONS,RL-TEST) state the recommended approach.
Change rule:
- A user requirement changes in two cases only: the requirement describes the behaviour wrongly, or the intended user-facing behaviour itself changes. An implementation that is inconvenient is never a reason to change a user requirement.
- An implementation requirement changes freely when a better approach appears.
Out of scope:
- The engine of the display set split (claims, split keys, priorities). The engine stays as it is. This specification changes only the language that compiles onto the hooks of the engine.
- The scoring and the layout of a hanging protocol. A hanging protocol uses the rule language only to test a study, a display set or an instance.
- The change of the OHIF hanging protocols to the operator meanings of
RL-READ-4. OHIF/Viewers#6386 made the first part of that change: strictgreaterThanandlessThan, anequalsthat compares the whole value, anincludeswith a single operand, and the same reading of numbers, text and no value. The OHIF migration guide Hanging protocol comparisons states the change, and lists the differences that remain. A later OHIF pull request moves the hanging protocols onto the rule language (RL-CONS-4). That pull request converts the old form, or gives a warning for the old form. - A JSON Schema of the rules. A later pull request adds the schema. The
declarations record
vrandvmfor that schema (RL-CTX-6).
Changes from the draft of 13 October
The review of 14 October changed these user requirements: RL-READ-3,
RL-READ-4, RL-READ-5, RL-CODE-4, RL-SCOPE-1, RL-SCOPE-6 and
RL-SCOPE-8. The review added the groups RL-VALUE and RL-CTX.
RL-SCOPE-9 (the deny list) did not change.
Three more user requirements changed after the review of the implementation. Each change has a reason that the change rule permits:
RL-EXT-3: a named attribute no longer computes a value over a whole display set. An{ "expression" }value inattributesdoes. Reason: the intended behaviour changed. The review of 6 October removedscopeAttributes, and the reviewer accepted the{ "expression" }form on 16 October.RL-SCOPE-7: the compile checks a name against the DICOM dictionary and the context, and not against an allow list. Reason: the intended behaviour changed. The review of 14 October asked that a misspelt keyword is an error, and the allow lists went away with the scopes.RL-ERR-3: a loop through a code entry gives an error at the first read. Reason: the earlier text described the behaviour wrongly. The compile cannot read inside a code entry, so no compile can find such a loop.
Changes of 23 October
RL-VALUE-5andRL-READ-2change their meaning. Before, a multi-valued value compared on its first value, and an array test meant "one of". Now==, the scalar test and the array test share one equality: a single value against an array is "in", and two arrays are equal when they have the same elements in the same order. Reason: the intended behaviour changed. A test that read only the first value ofImageTypemissedLOCALIZERin a later position, and "one of" between two arrays meant "overlaps", which is the job ofincludes. SoImageType != 'LOCALIZER'and{ "doesNotEqual": "LOCALIZER" }now hold only when no value isLOCALIZER. Open question 5 lists the places that still read the first value.RL-SCOPE-3andRL-SCOPE-4(values that the host supplies at the start of a split) are withdrawn. Reason: the intended behaviour changed. The review of 6 October found that a value such asrightOnLeftis a hanging protocol decision, and not a split decision. The code has no option for such values.RL-SCOPE-9names each kind of access that the deny list covers: a name, a declaration, and a field of a sequence item. Reason: the earlier text ("strict or optional") described the optional access thatRL-SCOPE-8removed.RL-READ-4lists each operator meaning that is different fromvalidator.js. Reason: the earlier text ("five operators") described the behaviour wrongly.equalswith an array operand,containsAlland the operand wrappers are also different.RL-CTX-7,RL-CTX-9,RL-SHARE-3andRL-SCOPE-10no longer hold implementation detail. The detail is inRL-CTXI-4(the scope rules),RL-CTXI-12(where a series function runs),RL-PKG-2(no import of the display set code) andRL-NAME-5(the text of the messages). The meaning of the user requirements does not change.- More user requirements no longer hold implementation detail, with no change
of meaning:
RL-SCOPE-1merges the name rules ofRL-SCOPE-1andRL-SCOPE-7: a name is in scope or not. The first-letter convention is an implementation detail of the scope, now inRL-NAME-1.RL-SCOPE-10holds the case rules only.RL-SCOPE-7is removed.RL-SCOPE-2asks that each consumer documents its built-in names. Which package names them is inRL-PKG-2.RL-SCOPE-5no longer holds the cost of a series value.RL-CTXI-7holds it.RL-EXT-3states the need: a value over the series and a value over a display set.RL-EXT-5moves toRL-CONS-5.RL-CODE-4no longer gives the reason for the second place.RL-CODE-6was guidance, and moves to the docs (display-sets.md, code entries).RL-ERR-3states the need: every error shows when the rules load. When each check runs is inRL-CTXI-11andRL-NAME-7.RL-VALUE-7no longer holds a history note. Before,+read both sides as numbers.RL-API-1states the intent of the exports, not the names.RL-SCOPE-3andRL-SCOPE-4are removed from the list; this note records the withdrawal.
RL-READ-8changes its meaning: the split key identifies the split group in this study in a form that a host can use again, and a change to the rules, the series or the implementation can change it without warning. Reason: the earlier text promised a key format that the engine does not guarantee.
Other user requirements changed their words only, because the scopes went
away: RL-READ-1, RL-READ-2, RL-SHARE-1, RL-SHARE-3, RL-SHARE-4,
RL-EXT-2, RL-SCOPE-2 to RL-SCOPE-5, RL-CODE-5, RL-SAFE-2 and
RL-ERR-2. RL-READ-7, RL-CODE-6 and RL-ERR-4 are new.
The implementation groups RL-REG (registry) and RL-ENV (scopes and
access) of the earlier draft are removed. The context of RL-CTX and
RL-CTXI replaces them.
The earlier revisions of the pull request had these forms. They are removed, and no release had them:
- The condition forms
all,any,not,classifier,seriesFact, the attribute form{ attribute, equals }, and the expression keys and the comparison keys ofmatches("NumberOfFrames >": 1). - The rule
serieslist, and thecontext.series.*paths. - The option
classifiers. A code entry of the context replaces a classifier. - The option
customAttributePresetsand thecustomAttributesrecipe (set,fromFirstInstance,fromContext,fromOptions,preset).attributesand the code entries of the context replace them. - The value forms
{ join, parts },{ attribute, bucket },{ attribute, number },{ attribute, absent }and{ condition }. - The
compareInstancesdata forms, also{ attribute, descending: true }. A descending order is a{ "sortFunction" }or a function. - The aggregates
some,every,minOf,maxOf,sumOfandcount(list, expr)(RL-EXPR-1).
Changes of 26 October
RL-VALUE-5,RL-READ-2andRL-VAL-3change their meaning again.==and the scalar test are now strict: a single value equals an array only when the array has one element. The array test of a condition map is now a shorthand forincludes. Reason: the intended behaviour changed. The review of the OHIF pull request asked for one meaning ofequalsin the rules and in the hanging protocols. The hanging protocolequalsnow compares the whole value, and itsincludesaccepts a scalar operand. SoImageType == 'LOCALIZER'no longer holds for['ORIGINAL', 'LOCALIZER']; use'LOCALIZER' in ImageType, or{ "includes": "LOCALIZER" }."Modality": ["MR", "CT"]still holds for'MR', and"ImageType": ["LOCALIZER"]now holds for['ORIGINAL', 'LOCALIZER'].
Changes of 27 October
RL-READ-2andRL-VALUE-5change their meaning, andRL-EXPR-7adds one compile error. The scalar shorthand of a condition map is nowincludes(before: the strict==of 26 October), so"ImageType": "LOCALIZER"holds for['ORIGINAL', 'LOCALIZER'], as"ImageType": ["LOCALIZER"]does. The array shorthand staysincludes.equalsanddoesNotEqualstay whole-value tests.==and!=with an array literal of two or more values are a compile error that namesin, soModality == ['MR', 'CT']no longer compiles to a quietly false test; the form can open again later. Reason: the intended behaviour changed. The review found that the two shorthand forms and==with a list read differently from what an author expects. No default rule changes its result: the default rules use the shorthand only on single-valued attributes (Modality) and on booleans.viewportTypesno longer has a meaning in CS3D. No default rule sets it. A rule may still have it (an optional array of strings), for its own reasons:createDisplaySetFromGroupcopies it to the display set unchanged, and CS3D reads it nowhere. The handler alone decides the display set class andisDisplayable(RL-CONS-1,RL-CONS-3). A host chooses the viewport fromhandler(a non-image object) and from the instance data (an image). No user requirement of this specification named the viewport types, so none changes here. The design note of OHIF #6137 and CS3D #2861 changes its user requirementDS-USER-HANDLER-4("the viewport types of a rule are hints").viewportTypeswas released on CS3Dmain(#2738), so a caller sees these removals: the display set fieldpreferredViewportType; the defaults ofviewportTypes(['stack']for a rule with none) and their effect on the class andisDisplayable; theviewportTypesoption ofImageStackDisplaySet.fromInstancesandfromImageIds; theviewportTypeskey of thecustomAttributescontext (an attribute can still setviewportTypes, as plain data); the typeViewportTypeHint; the sentinelNO_VIEWPORT_TYPE('none'); and the functionsgetViewportTypesForRule,getViewportTypesForGroupandgetPreferredViewportType.RL-READ-4no longer lists the meanings that OHIF/Viewers#6386 gave tovalidator.js. It now lists the differences that remain, and it points to the OHIF migration guide Hanging protocol comparisons. Reason: after the merge of OHIF/Viewers#6386, the earlier text described the behaviour ofvalidator.jswrongly. The meaning of the rule language does not change.RL-CTX-9changes its meaning. Before, a name such assliceSpacinginsortByandattributesread the display set forplaneGeometry, and the series fortimeClusters; a group entry chose withover. Now a bare name always reads the series, anddisplaySet.<name>reads the display set. The fieldoveris removed. Reason: the intended behaviour changed. The review found that one name gave two values, and asked that the display set value is explicit.RL-READ-4adds the operatorsgreaterThanOrEqualandlessThanOrEqual, and letsequalsanddoesNotEqualtake an array operand. Reason: the intended behaviour changed. The review asked that the split rules and the hanging protocols give one meaning to each operator name, and the hanging protocols have these operators since OHIF/Viewers#6386. A boolean still does not equal a number.RL-READ-4no longer lists the boolean as a difference: after OHIF/Viewers#6394, a boolean in a hanging protocol also equals only'true'and'false', and not1or0.
Definitions
Subject. The object that a condition or a value tests or reads. For a split rule, the subject is one naturalized DICOM instance. For a hanging protocol, the subject is a study, a display set or an instance.
Tag keyword. A keyword of the DICOM dictionary of dcmjs, for example
Modality. A rule reads the value of a tag keyword from the instance.
Context. A map of named entries. Each entry gives one value. The
global context (the context option) holds the entries for every rule. The
rule context (the context field of a rule) holds entries for that rule
only.
Context name. The name of a context entry. A context name starts with a lowercase letter.
Name. A tag keyword or a context name. Each key of a condition map, each
entry of groupBy and sortBy, each string of attributes, each template
placeholder and each identifier of an expression is a name.
Path. A name and one result field of a group: timeClusters.start.
Group. A context entry that calls one series function, for example
timeClusters. A group gives a result with declared fields.
Alias. A context entry that gives a top-level name to a path:
"sweepStart": "timeClusters.start".
Declaration. A context entry that reads one attribute of the instance that
the dictionary does not hold: { "attribute": "00191010" }.
Absent. The value is undefined, or the subject has no property of that
name. In DICOM terms, the element is not there (Type 3).
Empty. The value is null, "" or []. In DICOM terms, the element is
there with no value (Type 2).
No value. The value is absent or empty.
Instance scope. A value has one value for each instance.
Series scope. A value has one value for the series.
Named attribute. A context entry in code. A host supplies it, and a rule reads it by name like any other context name.
Named sort function. A comparator that the host supplies under a name.
Deny list. The attribute names that no rule can read. __proto__,
prototype and constructor are on every deny list.
Consumer. Code that defines a kind of rule (split rule, hanging protocol), and compiles the conditions, values and sort orders of that rule with the rule language.
Host. The application that calls a consumer, for example OHIF or a server that builds a study index.
Function place. A place in a rule that becomes a function after the compile.
User requirements
Readable rules (RL-READ)
-
RL-READ-1 The rule author writes a condition as a map. The keys are names, and sibling keys must all hold. The author does not need
all,attributeorequalsfor the common case. -
RL-READ-2 The author writes the common tests without an operator. The shorthand is
includesin both forms: a scalar holds when the value is the scalar or holds it ("ImageType": "LOCALIZER"holds for['ORIGINAL', 'LOCALIZER']); an array holds when a single value is one of the elements, or a multi-valued value has one element in common with the array; andnullmeans "no value". A whole-value test usesequals:{ "equals": ["ORIGINAL", "LOCALIZER"] }. -
RL-READ-3 A string in a context entry is an expression. A name or a path is the simplest expression. A key of a condition map is never an expression. A string at every other place of a rule is a name.
-
RL-READ-4 The author uses the operator names of the OHIF hanging protocol constraints (
equals,includes,contains,containsI,greaterThan,range,notNull, and the others). Since OHIF/Viewers#6386, the OHIFvalidator.jsgives most of these operators the meaning that they have here:greaterThanandlessThanare strict (>and<), a numeric string reads as a number,equalscompares the whole value,includesaccepts a single operand, andnotNullreads an empty value as no value.greaterThanOrEqualandlessThanOrEqualinclude the limit (>=and<=), and read a number in the same way asgreaterThanandlessThan. An array operand ofequalsanddoesNotEqualcompares the whole array, element by element in order, when the value is also an array. These meanings are still different fromvalidator.js:- An operand of the wrong kind is a compile error here: a number operand of
a text operator (
startsWith: 0), and a text operand of a number operator (greaterThan: '4').validator.jsreads such an operand as text or as a number. "X": nulland{ "equals": null }mean "no value", andnotNullis the exact opposite ofnull. Invalidator.js, the operandnullskips the constraint, andnotNullreads every operand other thanfalseastrue.containsAllaccepts a scalar operand and a single value, and compares with==.validator.jsneeds two arrays, and compares with===.- An empty text and an empty array stay apart,
includesfails on no value, and an empty array operand is a compile error. Invalidator.js,equals: ''holds on[''],includes: ''holds on'', andcontains: []always fails. - The operand wrappers of
validator.js({ "value": x }, and[x]forgreaterThan,greaterThanOrEqual,lessThanandlessThanOrEqual) are compile errors.
The other operators keep the meaning of
validator.js, except for the rule ofRL-VALUE-4. The section "Known differences from@cornerstonejs/metadata" of the OHIF migration guide Hanging protocol comparisons has the same list, with examples. - An operand of the wrong kind is a compile error here: a number operand of
a text operator (
-
RL-READ-5 The author writes a test over the instances of a series (for example "the series has fewer than 10 instances", "some instances have a b-value") as a context entry with a name. The rule reads the name in the same way as a tag keyword. The rule does not hold the test inline.
-
RL-READ-6 The reviewer reads a rule and sees every named attribute that the rule uses, at the place where the rule uses it.
-
RL-READ-7
falseon a named boolean is its negation. An "or" is a context entry, and the rule reads its name. The rule language has noall,anyornot. -
RL-READ-8 Only the
groupByvalues of a rule group instances. The split key identifies the split group of a display set in this study, in a form that a host can store and use again. A change to the rules, to the series or to the implementation can change the split key without warning.
Absent and empty values (RL-VALUE)
-
RL-VALUE-1 The rules use the DICOM meaning of no value, and not the JavaScript meaning.
undefinedis absent.null,""and[]are empty. An array whose elements all have no value ([""],[null]) is also empty. "No value" is absent or empty. -
RL-VALUE-2 An expression tests the kind of a value with a literal test.
undefined,nullanddefinedare the literals.!=is the opposite of==.XX == undefinedX == nullX == definedX != undefinedabsent ( undefined, no property)truetruefalsefalsenullfalsetruefalsetrue""falsetruefalsetrue[](also[""]and[null])falsetruefalsetrue"CT"(a value)falsefalsetruetrueX == undefinedtests "absent".X != undefinedtests "present" (the value can be empty). So an author can make a distinction between DICOM Type 2 (present and empty) and DICOM Type 3 (absent).X == nulltests "no value".X == definedtests "has a value": present and not empty.- In
matches,"X": nullis the same test asX == null, and{ "notNull": true }is the same test asX == defined. definedis a literal for==and!=only. In every other position, and as a context name,definedis a compile error.
-
RL-VALUE-3 When
==compares two values, two values with no value are equal only when they are of the same kind. SoY == ZisfalsewhenYisnullandZis absent, andnull == ""isfalse. Only the literal tests ofRL-VALUE-2treat the kinds as one. -
RL-VALUE-4 A comparison (
<,<=,>,>=,greaterThan,greaterThanOrEqual,lessThan,lessThanOrEqual,range) givesfalsewhen the value has no value, or is not a finite number. A numeric string is a number, so"2" > 1istrue. A text test (contains,containsI,containsAll,startsWith,endsWith) fails on no value, and itsdoesNot...form holds.doesNotEqualanddoesNotIncludehold on no value. -
RL-VALUE-5
==is the only equality."1" == 1istrue.===and!==are compile errors. A single value equals an array only when the array has one element, equal to the value:['MR'] == 'MR', but['ORIGINAL', 'LOCALIZER'] == 'LOCALIZER'isfalse. Two arrays are equal when they have the same elements in the same order. Theequalsoperator of a condition map uses the same equality. To test that a value is one of a list, usein,includes, or the shorthand ofRL-READ-2.==and!=with an array literal of two or more values are a compile error, whose message namesin:Modality == ['MR', 'CT']reads as "one of", but would be quietly false for'MR'. A one-value array literal and an array value keep the meaning of==. -
RL-VALUE-6 An expression can compare text without case:
lower(text)gives the text in lowercase. -
RL-VALUE-7
-,*,/and%read a numeric string as a number, so"6" - 1is5. When one side has no value or is not a finite number, the result has no value. Sonull * 512has no value, and a test such as{ "lessThan": 100 }on it fails.+follows the type of its left operand: text on the left (also'') joins, so"2" + 3is"23", and no value on the right joins as''; no value on the left gives no value; a number on the left adds, so3 + "2"is5. -
RL-VALUE-8
!,&&,||and?:read no value asfalse.&&and||givetrueorfalse. So"isTall": "Rows && Rows > 1000"isfalsewhenRowsis absent, and"isTall": falseholds.
The context (RL-CTX)
- RL-CTX-1 A site defines a new value in the customization data, with no TypeScript build. The value is an entry of the global context.
- RL-CTX-2 One computation can give several names. Each name has one definition for all rules: an alias of a field of a group.
- RL-CTX-3 An entry has one of five forms: a fixed value (a number, a
boolean,
nullor an array); a string, which is an expression; a declaration{ "attribute", "vr"?, "vm"? }; a template{ "template" }; or a group{ "function", "args"? }. - RL-CTX-4 A rule context replaces a global entry with the same name, for that rule only. The replacement also applies inside the global entries that the rule reads. So one rule text works with a different argument, or with a different group, and every alias follows. A rule context can also define a new name that only that rule reads.
- RL-CTX-5 An entry of a rule context that reads its own name gets the
global value:
"timeClustersMaxGap": "timeClustersMaxGap * 2". - RL-CTX-6 A declaration gives access to an attribute that the dictionary
does not hold: a private tag, a vendor attribute, or a synthetic attribute
such as
imageId. A declaration can recordvrandvm. - RL-CTX-7 No entry declares a scope. The author can tell from the entry
alone whether it has one value for the series or one value for each
instance. The rules of
RL-CTXI-4decide the scope. - RL-CTX-8 A site changes one entry by its name, for example
"dxCrMaxImages": { "$set": 20 }. The site does not copy the expression text of a rule into a$merge. - RL-CTX-9 A rule tests the full series, and describes each display set.
A bare name always means the value of the series, in every place. In
sortByandattributes, the author reads a value of the display set with thedisplaySet.prefix:displaySet.sliceSpacingis the lattice of the instances of the display set.matches,groupByand a context entry reject the prefix.series.instances.*(...)describes the series in every place.RL-CTXI-12gives the implementation.
One language for each consumer (RL-SHARE)
- RL-SHARE-1 The same condition text, on the same subject and the same context, gives the same result in a split rule and in a hanging protocol.
- RL-SHARE-2 The host registers a named attribute once, and both a split rule and a hanging protocol can use the named attribute.
- RL-SHARE-3 A new consumer (for example a rule for a worklist filter)
uses the rule language without a change to the rule language.
RL-PKG-2keeps the rule language free of the display set code. - RL-SHARE-4 The documentation describes the rule language once. The documentation of a consumer describes only the fields of its rules.
Extension by the caller (RL-EXT)
- RL-EXT-1 The host supplies named attributes and named sort functions to the compile, and a rule uses each one by name.
- RL-EXT-2 A rule uses a named attribute at every place that accepts a name: a condition key, a grouping entry, a sort entry, a display set attribute, a template and an expression.
- RL-EXT-3 A rule can use a value over the whole series, and a value over the instances of one display set. A named attribute can read the subject and the other names of the context.
- RL-EXT-4 The author cannot see a difference between a named attribute of the host and a built-in named attribute. The syntax is the same.
Names (RL-SCOPE)
-
RL-SCOPE-1 A name in a rule is in scope or not. The scope holds the attributes of the instance (the DICOM keywords, and the attributes that a declaration names) and the context names. A name that is not in scope is a compile error that names the rule, the place and the name, so a misspelt name (
BodyPartExamind,sweepStrat) fails when the rules load. A field that a group does not declare is also a compile error. A context name cannot be the name of an attribute of the instance, so a context never hides a value of the instance. How the scope decides that a name is in scope is an implementation detail (RL-NAME-1). -
RL-SCOPE-2 Each consumer documents the built-in names of its rules. The host can add context entries.
-
RL-SCOPE-5 A deployment defines computed values from other values, as data or as a function.
-
RL-SCOPE-6 A rule reads a value of the display set (
splitNumber,sopClassUids) only inattributes. A read of such a value at another place is a compile error that names the place. -
RL-SCOPE-8 A rule reads an attribute that the dictionary does not hold only through a declaration (
RL-CTX-6). A rule cannot read a hex tag directly. There is no optional access with?. -
RL-SCOPE-9 No rule reads an attribute on the deny list. The deny list covers each kind of access:
- a name in a condition key,
groupBy,sortBy,attributes, a template or an expression; - a declaration of the attribute (
{ "attribute": "PatientName" }); - a field of a sequence item (
OtherPatientIDsSequence[0].PatientID), also a field with a computed name.
The deny list lets a host keep a value (for example a patient identifier) out of every rule. A rule can read a whole sequence as a value, so a host that denies an attribute also denies each sequence that holds it: to keep
PatientIDout, denyPatientIDandOtherPatientIDsSequence. - a name in a condition key,
-
RL-SCOPE-10 Names and operators are case sensitive, as DICOM keywords are. A name is in scope only with the same case. When a name in scope differs only in case, the error names it, for example
Modalityformodality, andnotNullfornotnull. A value comparison is case sensitive:"CT"does not match"ct".containsI,doesNotContainIandlower()compare text without case.
Code for the complex cases (RL-CODE)
- RL-CODE-1 An extension can put an actual function at every function place of a rule, in place of the data for that place.
- RL-CODE-2 An extension can mix a function at one place with data at the other places of the same rule.
- RL-CODE-3 An extension can write a whole rule as code, with the hooks of the consumer, and put that rule in the same rule set as data rules.
- RL-CODE-4 The compile permits an expression in two places only: a
context entry, and the
{ "expression": "..." }form ofattributes. - RL-CODE-5 An expression can read a named attribute by its name.
Safe data (RL-SAFE)
- RL-SAFE-1 A rule that the host reads from JSON (a configuration file, an HTTP response, a URL customization) cannot run code that the host did not supply.
- RL-SAFE-2 A misspelt key or name does not change the meaning of a rule without a trace. An unknown key or name is an error.
Errors that name themselves (RL-ERR)
- RL-ERR-1 A rule that does not compile gives an error that names the consumer, the rule, the path in the rule, and the accepted forms at that path.
- RL-ERR-2 An unknown name gives an error that names the name, and says how to declare an attribute that the dictionary does not hold.
- RL-ERR-3 Every error of the rule data shows when the rules load, before the consumer uses a rule. A loop of entries is an error that names each entry of the loop.
- RL-ERR-4 A string in
attributesthat is not a name ("SCOUT") is a compile error. The error names the template form of fixed text.
Implementation requirements
Package layout (RL-PKG)
- RL-PKG-1 The rule language is in
@cornerstonejs/metadata, inpackages/metadata/src/safeFunctions/. A later release can rename the module torules/. - RL-PKG-2 No file in
safeFunctions/imports fromdisplayset/, or from any other consumer, and no file insafeFunctions/knows the DICOM dictionary. The consumer gives the reader of each name (readName), and the consumer, not the rule language, names the built-in values of its rules. - RL-PKG-3 The split rule consumer is in
packages/metadata/src/displayset/:displaySetContext.ts: the context, the default context, the name check, the binding of the entries, and the cache.compileSplitRules.ts: the rule fields, and their mapping onto theSplitRulehooks.splitRuleSchema.ts: the table of the rule fields and forms (internal).displaySetRules.js: the default rules, as JSON data.
- RL-PKG-4 The hanging protocol consumer is in OHIF
(
HangingProtocolService), or in a later CS3D package. Either location uses only the public API ofRL-API.
Layout:
packages/metadata/src/
safeFunctions/ generic, knows no consumer and no dictionary
values.ts isAbsent, isEmpty, hasNoValue, looseEquals
matchTests.ts the tests and the operators of a condition map
compile.ts compileCondition, compileValue, templates
schema.ts the table machinery and the generic shapes
expression/ tokenizer, parser, compiler
displayset/ consumer 1
displaySetContext.ts context, default context, binding, cache
compileSplitRules.ts rule fields -> SplitRule hooks
splitRuleSchema.ts
displaySetRules.js default rules as data
...engine files, unchanged
Public API (RL-API)
-
RL-API-1 The safe functions export the functions and the types that a second consumer, for example the OHIF hanging protocols, needs to use the rule language in its own context: to compile its conditions, values, templates and expressions with its own reader of names, to check the names of an expression, to use the same meaning of no value, and to report the errors of the compile. The safe functions export nothing that only the split consumer uses, such as the schema tables.
-
RL-API-2
CompileOptionsholds the path and the name of the definition for the messages, the expression scope of the place, andreadName: the function that gives the reader of a name, or throws a compile error for an unknown name. The defaultreadNamereads an own property of the subject. -
RL-API-3
CreateDisplaySetSplitRulesOptionsholds:type CreateDisplaySetSplitRulesOptions = {
/** Global context entries, over the entries of defaultMetadataContext. */
context?: Record<string, ContextEntry>;
/** Series functions, over planeGeometry and timeClusters. */
seriesFunctions?: Record<string, SeriesFunction | SeriesFunctionDefinition>;
/** Names that no rule can read (RL-SCOPE-9, RL-NAME-7). */
deniedNames?: readonly string[];
/** Comparators for a sortBy entry { sortFunction: name }. */
sortFunctions?: Record<string, Comparator>;
};OHIF reads
contextfrom the standalonemetadataContextcustomization. -
RL-API-4 The split consumer exports the default rules as data, the compiled default rules, the compile functions for a rule set, the default metadata context (
defaultMetadataContext), each SOP class and transfer syntax list of the context under the same name, and the handler names (DISPLAY_SET_HANDLER_NAMES). A name that a release exported stays as a deprecated alias when it is renamed. No release exporteddefaultDisplaySetContext, so it has no alias. -
RL-API-5 The split consumer has an active metadata context:
setMetadataContext(context)sets it (undefinedgoes back todefaultMetadataContext), andgetMetadataContext()returns it.getContextValue(name, instance, context?)reads one instance-scope context name for one instance, with no rules; a series-scope name is an error.isImageInstance,isVideoInstance,isEcgInstanceandisWsiInstanceread the entriesisImage,isVideo,isEcgandisWsiof theircontextargument, else of the active context, so the code tests and the rules give the same answer. Each context object compiles one time, with the context compile ofRL-CTXI; the cache is keyed by the object.createDisplaySetSplitRuleswith no series functions and no deny list reads the same cache, so a split and the code tests share one compile.
Names (RL-NAME)
- RL-NAME-1 The compile resolves each name in this order: a tag keyword
(
resolveHexFromKeywordofpackages/metadata/src/utilities/Tags.tsgives a hex tag), then an entry of the rule context, then an entry of the global context, then (inattributesonly)splitNumberandsopClassUids. Every other name is a compile error. This order is the scope ofRL-SCOPE-1. Today every DICOM keyword starts with an uppercase letter exceptdBdt, and every context name starts with a lowercase letter (RL-NAME-3), but the scope does not depend on the first letter. - RL-NAME-2
resolveHexFromKeywordreads own keys of the dictionary only, soconstructorandtoStringare not keywords. - RL-NAME-3 A context name matches
/^[a-z][A-Za-z0-9_]*$/, is not a tag keyword, is not on the deny list, and is notsplitNumberorsopClassUids. The compile throws for another name. - RL-NAME-4 The expression compiler stays generic, and does not check
names. The consumer gets the names of an expression with
collectReads, and checks each one. This reverses the choice of the earlier doc comment ofcompileExpression, because the declarations handle the private tags, the vendor attributes and the synthetic attributes. - RL-NAME-5 The error message of an unknown name says how to declare the
attribute. For a hex tag, the message names the declaration form. In
attributes, the message also names{ "template": "..." }. A name that differs from a known name only in case givesunknown name 'modality'; did you mean 'Modality'?, and an operator givesdid you mean 'notNull'?. - RL-NAME-6 A keyword or a declaration has no field:
{Modality.foo}andModality.fooare compile errors. An item of a sequence is read with an index (ViewCodeSequence[0].CodeValue). An index on a group (timeClusters['start']) is a compile error; a rule reads a group field by path. - RL-NAME-7 The deny list (
RL-SCOPE-9) is the optiondeniedNamesofcreateDisplaySetSplitRules. A rule or a context entry that names a denied name is a compile error. The compile checks theattributeof each declaration against the deny list. The compile checks each field name of a path into a sequence item, and a computed field name on a sequence item is a compile error when a deny list is set. Thevaluesof a code entry giveundefinedfor a denied name, and the logger gives a warning. A code entry also gets the instance itself, so the host must not register a code entry that reads a denied attribute from the instance.
Conditions (RL-COND)
-
RL-COND-1 A condition is a map of name to test, or a function. Every entry must hold, so
{}istrue. -
RL-COND-2 The test of one map entry is one of:
Test The entry holds when scalar includes: the value is the scalar, or holds itarray of scalars includes: one value is in the arraynullthe value has no value object with one operator the operator holds ( RL-COND-3)function test(value, subject, context)is true -
RL-COND-3 The operators:
Operator Operand The entry holds when equalsscalar, array or nullthe whole value, with ==; an array operand equals an array value in orderdoesNotEqualscalar, array or nullequalsdoes not hold; no value passesnotNullboolean true: the value has a value;false: the value has no valueincludesscalar or array of scalars the value has the element, or one of the elements; a single value is an array doesNotIncludescalar or array of scalars includesdoes not holdgreaterThan,lessThanfinite number the value is a finite number, and >or<the operandgreaterThanOrEqualfinite number the value is a finite number, and >=the operandlessThanOrEqualfinite number the value is a finite number, and <=the operandrangetwo finite numbers, any order the value is a finite number in [min, max]contains,containsIstring or array of strings one element of the value holds one of the strings ( I: without case)doesNotContain,doesNotContainIstring or array of strings the containsform does not holdcontainsAllscalar or array of scalars the value has every element startsWith,endsWithstring or array of strings one element of the value starts or ends with one of the strings -
RL-COND-4 The compile rejects an operand of the wrong kind, a second operator, an unknown operator and an empty array test.
Values (RL-VAL)
- RL-VAL-1 The value helpers are in
safeFunctions/values.ts:isAbsent,isEmpty,hasNoValueandlooseEquals. A number is read withtoFiniteFirstof@cornerstonejs/utils: it givesundefinedfor no value, for a blank string, and for a value that is not a finite number, and reads the first value of a multi-valued attribute. - RL-VAL-2 A template placeholder is a name or a path. A placeholder with
no value gives
"".\{and\}escape a brace. - RL-VAL-3
looseEquals(the==of expressions, andequals): two values with no value are equal only when they are of the same kind (absent,null,"", or an empty array); a value with no value is not equal to a value with one; a scalar and an array are equal when the array has one element, equal to the scalar; two arrays are equal when they have the same length and each element is equal in order; a number and a numeric string compare by value; other scalars compare as text. The expression compiler givesX == undefined,X == nullandX == defined(a literal operand) the tests ofRL-VALUE-2. The parser readsdefinedas a literal, and the compiler rejects it in every other position.
Sort orders (RL-SORT)
-
RL-SORT-1 A sort order is a list of entries. The next entry decides when an entry gives
0orNaN. -
RL-SORT-2 An entry is one of these forms:
Form Order "Name"or"name"ascending by the value of the name: numeric when both values are finite numbers, else by text; an instance with no value goes after the instances with a value { "sortFunction": "x" }the named sort function xofoptions.sortFunctions; an unknown name is a compile errora function passes through ( RL-FN-1)
Expressions (RL-EXPR)
-
RL-EXPR-1 The grammar of the expression language stays as it is, with these changes:
===and!==are compile errors;<,<=,>,>=and the arithmetic operators usetoFiniteFirst(RL-VALUE-7);!,&&,||and?:read no value asfalse(RL-VALUE-8); the new helperslowerandcontains; and the aggregates ofRL-EXPR-2. The helpersincludes,contains,startsWithandendsWith, and the operatorin, use the code of the operators of the same names, so they give the same results. The aggregatessome,every,minOf,maxOf,sumOfandcount(list, expr)do not exist any more. -
RL-EXPR-2 An aggregate names its receiver:
series.instances.min(x),.max(x),.sum(x),.count(x)and.count()run over the instances of the series. A context entry and the{ "expression" }form ofattributescan use them.displaySet.instances.min(x)(andmax,sum,count) run over the instances of this display set. Only the{ "expression" }form ofattributescan use them. At another place, the compile gives an error that names the place.displaySetsis reserved for a later change. Each use is a compile error that refers to issue 3000.- A bare
min(x),max(x),sum(x),count(x)orcount()is a compile error, and the error gives the receiver forms that the place accepts.min(a, b, ...)andmax(a, b, ...)with two or more arguments compare their arguments. series,displaySetanddisplaySetscannot be context names.- The parser accepts a method call only in the form
<receiver>.instances.<aggregate>(...), with these receivers and these four aggregates.
The compiler evaluates the argument one time for each instance. An instance whose argument has no value does not count;
count(x)also skipsfalse.sumof no instance is0.minandmaxof no instance areundefined.count()is the number of instances. The result does not change with the order of the instances. -
RL-EXPR-3 The number helpers (
abs,round,floor,ceil,Number, andminandmaxwith two or more arguments) giveundefinedfor a value that is not a finite number, and neverNaN. -
RL-EXPR-4
collectReadsgives each read of an identifier, with the field of a direct member access (timeClusters.start), an index access (indexed), and whether the read is in the argument of an aggregate. -
RL-EXPR-5 An aggregate in an entry with the instance scope (
"NumberOfFrames == series.instances.max(NumberOfFrames)") runs over the series one time for each instance: the cost grows with the square of the number of instances. Put the aggregate in its own series entry, and read that name. An aggregate in the argument of another aggregate is a compile error, because each level multiplies the cost by the number of instances, and a rule from an untrusted source could then stop the viewer (RL-SAFE-1). -
RL-EXPR-6 A compiled expression gives one warning and
undefinedfor an error at run time, except for aRuleCompileError, which goes to the caller. -
RL-EXPR-7 An array literal compiles to an array, and
==gives it the meaning ofRL-VAL-3. An array literal of literals folds into one array. Next to==or!=, an empty array literal is a compile error, and an array literal of two or more values is a compile error whose message namesinandincludes(...).
The context (RL-CTXI)
-
RL-CTXI-1 The global context is
defaultMetadataContextwith the entries ofoptions.contextover it. An entry of the options replaces the default entry with the same name. -
RL-CTXI-2 The default context:
{
"timeClustersMaxGap": 30,
"timeClusters": {
"function": "timeClusters",
"args": { "maxGap": "timeClustersMaxGap" },
},
"longTimeClusters": {
"function": "timeClusters",
"args": { "maxGap": 60 },
},
"sweepCount": "timeClusters.clusters",
"sweepStart": "timeClusters.start",
"acquisitionSeconds": "timeClusters.time",
"sweepOffset": "round(sweepStart - timeClusters.first)",
"planeGeometryTolerance": 0.1,
"planeGeometry": {
"function": "planeGeometry",
"args": { "tolerance": "planeGeometryTolerance" },
},
"sliceSpacing": "planeGeometry.spacing",
"sliceIndex": "planeGeometry.index",
"sliceDistance": "planeGeometry.distance",
"revisionChains": { "function": "revisionChains" },
"revisionChainUid": "revisionChains.active",
"isActiveRevision": "SOPInstanceUID == revisionChainUid",
"seriesInstanceCount": "series.instances.count()",
"isMultiFrame": "NumberOfFrames > 1",
"imageId": { "attribute": "imageId" },
}The default context also holds the SOP class and transfer syntax lists (
imageSopClassUids,videoSopClassUids,videoTransferSyntaxUids,longVideoSopClassUids,ecgSopClassUids,wholeSlideSopClassUids,segmentationSopClassUids,rtStructureSetSopClassUids,parametricMapSopClassUids,structuredReportSopClassUids,encapsulatedDocumentSopClassUids,bulkAnnotationSopClassUids,chartSopClassUids), the numberlongVideoMinFrames, and one expression for each kind of object that tests the lists:isImage,isVideo,isEcg,isWsi,isSegmentation,isRtStructureSet,isParametricMap,isStructuredReport,isEncapsulatedDocument,isBulkAnnotationandisChart. The default context holds no code entry. It declares each synthetic attribute that the default rules and the series functions read; today that attribute isimageId. -
RL-CTXI-3 Each series function declares its result fields, and the scope of each field. An
instancefield is a map keyed byinstanceKey(theSOPInstanceUID, else theimageId), and a read of the path gives the value of the instance at hand. The built-in fields:Function Series fields Instance fields timeClustersfirst,clusters,untimedCounttime,startplaneGeometrynormal,origin,spacing,regularCount,irregularCount,positions,complete,duplicatesdistance,indexrevisionChainsactiverevisionChainsreadsPredecessorDocumentsSequence>ReferencedSeriesSequenceReferencedSOPSequence>ReferencedSOPInstanceUIDone time for each series. Only a reference to an instance of the same series counts. Each chain end (an instance that no other instance names) starts its own chain, and is its active instance. The split sorts the instances of the series one time, in the default ordersortByInstanceNumber(InstanceNumber, thenSOPInstanceUID), before the series functions run. The ends are visited in that order, and each end takes its predecessors, directly and through other instances, that no earlier chain took; so in a branch, the first chain end in the default order takes the shared predecessors. The input order never matters. An instance that no end reaches (only in a reference cycle) is its own chain.activeis the key of the active instance of the chain of each instance.revisionChainUidis the alias ofrevisionChains.active, andisActiveRevisionisSOPInstanceUID == revisionChainUid. Only the default rulesencapsulatedDocumentandchartread them: they group byrevisionChainUid, sort the active instance last, and give the display set attributeactiveSopInstanceUid.A series function in code with no declared fields has fields that the compile does not check, with the series scope.
-
RL-CTXI-4 The compile infers the scope of each entry (
RL-CTX-7):- A fixed value and a group have the series scope.
- A declaration and a code entry have the instance scope.
- An expression or a template that reads, outside of a
series.instances.*(...)aggregate, a tag keyword, an instance value or an instance field has the instance scope. - Every other entry has the series scope.
- Each argument of a group must have the series scope. An argument with the instance scope is a compile error.
-
RL-CTXI-5 An expression that is one name of a group is an alias of the group: it gives the same result and the same fields (
"timeClusters": "longTimeClusters"). A path whose first name is not a group is a compile error, so"timeClusters": 5fails when an alias readstimeClusters.start. -
RL-CTXI-6 The compile binds the entries for each rule: it resolves each name, infers the scope, finds the cache key, and builds the computation. The compile binds every global entry, and every entry of each rule context, also an entry that no rule reads. A loop of entries is a compile error that gives the loop (
first -> second -> first). -
RL-CTXI-7 The cache key of an entry is its name and the replaced names (with their data) that the entry reads, directly or through other entries. Rules that replace nothing that an entry reads share one computation of that entry for each series. A code entry can read any name, so every replacement of its rule is in its key.
-
RL-CTXI-8 Each compiled rule has a
serieshook. The first hook of a split adds the factdisplaySetContext: the instances of the series and a cache for each compiled rule set. The runtime computes a series value at the first read, one time for each split. When the series gets new instances, the next split computes the value again.orderInstancesForRulewithoptions.seriesalso computes the values again, over the instances of the split and the new instances. It finds the new instances byinstanceKey, so an instance that the host naturalizes again is not counted twice. -
RL-CTXI-9 The logger
cs3d.metadata.displayset.displaySetContextrecords each replacement and each new name of a rule context at the debug level. -
RL-CTXI-10 A hook that a host calls directly, with no split context, computes the values over the instances of the call only: one instance for
matches, the pair for a comparator, the group forcustomAttributes. SoseriesInstanceCountis1in a direct call ofmatches. The logger records the first such call at the debug level. -
RL-CTXI-11 The runtime records the entries that are in progress. A read of an entry that is in progress, for the same instance and at the same level (the global entry, or the replacement of a rule), is a loop. The runtime then throws a
RuleCompileErrorthat names each entry of the loop, in order:first -> second -> third -> first. A read of its own name by a rule entry resolves to the global level, so it is not a loop. Each call of a code entry gets a newvalueswrapper. -
RL-CTXI-12 A bare name always reads the series: in
sortByandattributes, it reads the runtime of the split. AdisplaySet.<name>(ordisplaySet.<name>.<field>,{displaySet.<name>}in a template, anddisplaySet.<name>in an expression) reads a second runtime (displaySetContextFor), in which every group runs over the instances of the display set, andseries.instances.*(...)still runs over the instances of the split (RL-CTX-9). OnlysortByandattributesaccept the prefix;matches,groupByand a context entry reject it. A group entry has no fieldover. The second runtime shares the cache of the split for each value that reads no group and no code entry, so such a value is computed one time for each split, and not one time for each display set (RL-SCOPE-5). -
RL-CTXI-13 A split key part is normalized, so that two hosts that naturalize differently build the same key: no value (absent,
null,'',[]) isnull, a one-element array is its element, and a string that is the canonical text of a finite number is that number.'05'stays text.
Functions (RL-FN)
- RL-FN-1 At every function place, an actual function passes through the
compile as it is. A function place is: a whole condition, a test of a map
entry, a
groupByentry, asortByentry, a wholeattributes(thecustomAttributeshook), and one attribute value. - RL-FN-2 JSON cannot hold a function. So a rule from JSON can run only the
code entries of the context and the functions of the rule language
(
RL-SAFE-1). Only host code puts a function into a rule or a context. - RL-FN-3 A context entry in code is a function
(instance, values), with the instance scope.valuesreads every name. A series entry in code is a group{ "function": fn }. The runtime callsfn(instances, { args }): the instances of the series and the evaluatedargs, and nothing else. A series code entry reads another context value only through an argument expression. Thefunctionof a group can also be a definition{ compute, fields }. - RL-FN-4 An attribute value in code is a function
(instances, values).instancesare the instances of the display set, in their sorted order. - RL-FN-5 One compile path handles the JSON form and the code form.
Consumers (RL-CONS)
-
RL-CONS-1 The fields of a split rule:
Field Holds SplitRulehookprioritya finite number, or nullto exclude the rule(rule set entry) id,groupId,descriptiondata the same names viewportTypesdata that CS3D copies to the display set, and does not read viewportTypeshandlera string; any name compiles handlercontextthe rule context ( RL-CTX-3)(compile only) matchesa condition matchesgroupBynames groupBysortBysort entries ( RL-SORT-2)compareInstancesattributesa map of attribute values ( RL-CONS-2)customAttributesEvery compiled rule also has the
serieshook ofRL-CTXI-8. -
RL-CONS-2 An attribute value is one of:
- a name. A tag keyword reads the first instance of the sorted display set.
An instance value of the context reads the same instance. A series value
is the value of the whole series.
displaySet.<name>is the value over the instances of the display set (RL-CTXI-12). - a fixed number, boolean or
null. { "template": "..." }.{ "template": "SCOUT" }is fixed text.{ "expression": "..." }.displaySet.instances.*(...)runs over the instances of the display set,series.instances.*(...)over the instances of the series, and a tag keyword reads the first instance of the display set.- in code, a function (
RL-FN-4).
- a name. A tag keyword reads the first instance of the sorted display set.
An instance value of the context reads the same instance. A series value
is the value of the whole series.
-
RL-CONS-3
createDisplaySetFromGrouppassesgroup.seriesto thecustomAttributeshook. An attribute reads a bare name over the series, and adisplaySet.<name>over the instances of the display set (RL-CTX-9).createDisplaySetFromGroupcopies thehandlerof the rule to the display set. The handler decides the class andisDisplayable: no handler orimagegives an image stack;unsupportedgives a base display set that is not displayable; any other handler gives a displayable base display set. A rule'sviewportTypesis copied to the display set unchanged, and changes nothing else. An image display set also getsisReconstructable, with the name of the OHIF display set attribute: a first value from the instance attributes (isReconstructableInstances, which follows OHIFisDisplaySetReconstructablewith theplaneGeometryseries function). A clip is not reconstructable, an attribute that setsisReconstructablewins, and a host may refine the value for display (for example a texture size limit).display-sets.mdlists the known differences from OHIF. -
RL-CONS-4 The hanging protocol consumer gives its own subject and its own reader of a name. OHIF/Viewers#6386 moved most of the hanging protocol constraints to the operator meanings of
RL-COND-3. A later OHIF pull request moves the rest (the differences ofRL-READ-4). -
RL-CONS-5 Each consumer supplies its own subjects. A host that shares one named attribute between two consumers knows which subject the function gets from each consumer.
Tests (RL-TEST)
- RL-TEST-1 A test covers the table of absent and empty values:
null,notNull,== null,== undefined, the comparisons and the text tests onundefined,null,"",[]and0. - RL-TEST-2 A test covers each operator meaning that changed from
validator.js. - RL-TEST-3 A test covers the name check at each place: a misspelt keyword,
a misspelt context name, a hex tag, a context name that is a keyword, and
"SCOUT"inattributes. - RL-TEST-4 A test covers a rule context: a replaced argument, a replaced group, a rule entry that reads its own name, and one computation for the rules that replace nothing. A test covers a loop of entries.
- RL-TEST-5 The tests of the default rules stay the guard of their behaviour.
- RL-TEST-6 The error tests check the rule id, the path and the name. Only a small number of tests check the full message text.
Examples
Example 1: usTimeClusters
"usTimeClusters": {
"priority": -1,
"context": { "timeClustersMaxGap": 20 },
"matches": {
"isImage": true,
"Modality": "US",
"isMultiFrame": false,
"sweepCount": { "greaterThan": 1 },
"sweepStart": { "notNull": true }
},
"groupBy": ["SeriesInstanceUID", "sweepStart"],
"sortBy": ["acquisitionSeconds"],
"attributes": { "SeriesDescription": { "template": "{SeriesDescription} +{sweepOffset} s" } }
}
sweepCountis a series value. The rule reads it directly.{ "greaterThan": 1 }is a strict>. So a series with one sweep does not match.- The gap of 20 seconds applies to this rule only. Other rules keep the default
gap of 30 seconds.
sweepStart,sweepCountandsweepOffsetall read the replaced gap (RL-CTX-4). "isMultiFrame": falseholds whenNumberOfFramesis absent, becauseNumberOfFrames > 1givesfalse(RL-VALUE-4).
Example 2: a different group, and the same rule text
"usLongSweeps": {
"priority": -1,
"context": { "timeClusters": "longTimeClusters" },
"matches": { "isImage": true, "Modality": "US", "sweepCount": { "greaterThan": 1 } },
"groupBy": ["SeriesInstanceUID", "sweepStart"],
"sortBy": ["acquisitionSeconds"]
}
sweepStart,sweepCount,acquisitionSecondsandsweepOffsetall readlongTimeClustersfor this rule.- The engine computes
longTimeClustersone time for each series. Each rule that readslongTimeClustersuses that one result (RL-CTXI-7).
Example 3: regularVolume
The module adds one series value to the global context. Both of its rules read
the value. irregularImages is off (priority: null). A site turns it on with
"irregularImages": { "priority": { "$set": -1 } }, and the rule then puts
the other images of the series into one stack:
"context": {
"mixesIrregular": "planeGeometry.regularCount >= 3 && planeGeometry.irregularCount > 0 && planeGeometry.complete"
},
"splitRules": {
"$merge": {
"regularVolume": {
"priority": -2,
"matches": {
"isImage": true,
"Modality": ["CT", "MR", "PT", "NM"],
"mixesIrregular": true,
"sliceIndex": { "notNull": true }
},
"groupBy": ["SeriesInstanceUID"],
"sortBy": ["displaySet.sliceDistance"],
"attributes": { "SeriesDescription": { "template": "{SeriesDescription} ({displaySet.sliceSpacing} mm)" } }
},
"irregularImages": {
"priority": null,
"matches": {
"isImage": true,
"Modality": ["CT", "MR", "PT", "NM"],
"mixesIrregular": true,
"sliceIndex": null
},
"groupBy": ["SeriesInstanceUID"],
"attributes": { "SeriesDescription": { "template": "{SeriesDescription} (other images)" } }
}
}
}
regularCount,irregularCountandcompletehave no alias. The module reads them by path."sliceIndex": nullmeans "no value". Index 0 is a valid index, andnotNullis true for 0.- In
matches,planeGeometryruns over the series, somixesIrregulartests the full series. InattributesandsortBy, thedisplaySet.prefix runsplaneGeometryover the display set (RL-CTX-9). So{displaySet.sliceSpacing}is the spacing of the regular slices of this display set, anddisplaySet.sliceDistanceorders those slices.
Example 4: volumeProjectionOrder
"volume3d": {
"priority": 7,
"matches": {
"isImage": true,
"Modality": ["CT", "MR", "PT", "NM"],
"seriesInstanceCount": { "greaterThan": 1 },
"Rows": { "notNull": true }
},
"groupBy": ["SeriesInstanceUID"],
"sortBy": ["displaySet.sliceDistance"]
}
- The rule replaces the default rule
volume3dunder its own id, so the rule keeps the default priority 7 and the defaultmatches. OnlysortByis new. The OHIF default rules givevolume3dthe priority 4, so the OHIF modulesplit/volumeProjectionOrder.jsoncuses 4. displaySet.sliceDistancerunsplaneGeometryover the display set (RL-CTX-9). This rule groups bySeriesInstanceUID, so the display set holds the full series. The origin is the lowest regular slice, so the order does not change with the arrival order of the instances.- An image outside the dominant plane has no
sliceDistance. That image sorts after the images that have a value (RL-SORT-2). Two such images keep the default order.
Example 5: small DX / CR series, with a configured limit
"context": {
"dxCrMaxImages": 9,
"isSmallSeries": "seriesInstanceCount <= dxCrMaxImages"
},
"splitRules": {
"$merge": {
"dxCrSingleImages": {
"priority": -1,
"matches": { "Modality": ["DX", "CR"], "isImage": true, "isSmallSeries": true },
"groupBy": ["SeriesInstanceUID", "SOPInstanceUID"]
}
}
}
- A site changes the limit for all rules with
"dxCrMaxImages": { "$set": 20 }. One rule changes the limit for itself with"context": { "dxCrMaxImages": 20 }. - The FU Berlin rule "split a CR series only into images of the same size and
orientation" needs no new function. That rule adds
Rows,ColumnsandImageOrientationPatienttogroupBy.
Example 6: values over the instances of the display set
"attributes": {
"maxFrames": { "expression": "displaySet.instances.max(NumberOfFrames)" },
"frameTotal": { "expression": "displaySet.instances.sum(NumberOfFrames)" },
"seriesFrames": { "expression": "series.instances.sum(NumberOfFrames)" },
"label": { "template": "{SeriesDescription} #{splitNumber}" },
"isClip": true
}
- In
attributes,displaySet.instances.*(...)runs over the instances of the display set, andseries.instances.*(...)over the instances of the series. - A tag keyword in
attributesreads the value of the first instance of the sorted group. "label": "SCOUT"is a compile error. Fixed text is{ "template": "SCOUT" }.
Example 7: an "or", and a declaration
"context": {
"isBreastImage": "Modality == 'MG' || (Modality == 'CT' && lower(BodyPartExamined) == 'breast')",
"pixelCount": "Rows * Columns",
"siemensBValue": { "attribute": "00191010", "vr": "FD", "vm": "1" }
},
"matches": {
"isBreastImage": true,
"pixelCount": { "greaterThan": 4000000 },
"siemensBValue": { "notNull": true }
}
- A site replaces the breast test with
"isBreastImage": { "$set": "..." }, and changes the threshold with"pixelCount": { "greaterThan": { "$set": 2000000 } }. - The private tag of dcmjs 1.0 reads a private block as one value, by the group
and the creator. Then a declaration can read the block, and a path can read
one element of the block:
"bValuesGroup": { "attribute": "0019:Siemens b values group" }and"siemensBValue": "bValuesGroup.0010". This syntax can change before dcmjs 1.0 is released.
Example 8: the code form
createDisplaySetSplitRules(rules, {
context: {
// A code entry with the instance scope.
mammoView: (instance) => readMammoView(instance),
// A group in code, with declared fields.
sweepSummary: {
function: {
compute: summarizeSweeps,
fields: { count: 'series', sweep: 'instance' },
},
},
},
sortFunctions: {
byInstanceNumberDescending: (a, b) => b.InstanceNumber - a.InstanceNumber,
},
});
{
matches: { Modality: 'PT', isImage: true }, // data
groupBy: ['SeriesInstanceUID', frameOfReferenceGroup], // data and code
sortBy: [{ sortFunction: 'byInstanceNumberDescending' }],
attributes: { label: { template: 'PET {mammoView}' } },
}
A later $merge of { "priority": 2 } from a JSON module changes only the
priority, and keeps the function of groupBy.
Open questions
-
Hanging protocol migration. Partly resolved. OHIF/Viewers#6386 states the change of
greaterThanfrom>=to>, and oflessThanfrom<=to<, as a change of the behaviour that the user sees. Its migration note is the OHIF migration guide Hanging protocol comparisons. The differences ofRL-READ-4stay open. The later OHIF pull request that moves the hanging protocols onto the rule language (RL-CONS-4) must state each one in a migration note. The boolean equality is closed (27 October): a boolean equals only'true'and'false'(RL-VALUE-5), and not the number1or0. The OHIF hanging protocols have the same meaning after OHIF/Viewers#6394. -
JSON Schema. The schema can list the tag keywords, the pattern of a context name, and the
vrandvmof each declaration. The schema can then check an operator against the VR (for examplegreaterThanon a text VR). -
Private blocks of dcmjs 1.0. The syntax of a declaration of a private block, and of a path to one element of the block, is not final.
-
Multiframe instances. A per-frame subject (frames of one instance) is future work in the split engine. The rule language needs no change for it: the consumer gives a different subject.
-
Multi-valued values outside of equality.
==,equalsanddoesNotEqualread every value (RL-VALUE-5), and the shorthand reads any value (RL-READ-2). These places still read a multi-valued value in another way. WithEchoNumbers = [1, 2]andImageType = ['ORIGINAL', 'PRIMARY', 'AXIAL']:Place Result today Reads <,<=,>,>=,greaterThan,lessThan,rangeEchoNumbers >= 2isfalsethe first value -,*,/,%,+EchoNumbers + 0is1the first value lower(),round()and the other helperslower(ImageType)is'original'the first value sortByorders by 1the first value contains,startsWith,endsWith,includes,intruefor'AXIAL'any value a template {ImageType}'ORIGINAL,PRIMARY,AXIAL'every value, with commas { "equals": [...] }equal in order to an array value every value Suggested fixes, one for each row:
- Comparisons. No fix is clearly right. "Any value" (
EchoNumbers >= 2istrue) matches the array test, but thenX > 1 && X < 3holds for[0, 5], which reads wrong. "Every value" makesEchoNumbers >= 1hold andEchoNumbers >= 2fail, which also surprises. The suggested fix: keep the first value, document it, and give the author an explicit form: the helpersminOf(X)andmaxOf(X)over the values of one attribute, so a rule saysmaxOf(EchoNumbers) >= 2. - Arithmetic. Keep the first value, as for comparisons. A multi-valued
operand is rare in arithmetic, and
minOfandmaxOfcover the rest. - Text helpers. Make
lower()give an array for an array, element by element, so'axial' in lower(ImageType)reads every value. Keep the number helpers on the first value. sortBy. Keep the first value, and document it. An order needs one key for each instance.- Membership tests. No change.
includes,inand the text tests are the "any value" forms on purpose;==is equality. - Templates. Join the values with
\, the DICOM value separator, so the text reads as DICOM text. equalswith an array. Done on 27 October: an array operand is equal in order to an array value.
- Comparisons. No fix is clearly right. "Any value" (