Hanging protocol comparisons
3.14 gives the hanging protocol constraints the meaning of the display set split rule
operators of the same names, where the two can agree today. "Known differences from
@cornerstonejs/metadata" lists the differences that remain. On this page,
@cornerstonejs/metadata means the split rule operators of
cornerstonejs/cornerstone3D#2861,
which are not released yet.
The greaterThan and lessThan constraints of a hanging protocol are now strict. Before
3.14, greaterThan tested >= and lessThan tested <=, and the hanging protocol
documentation said so ("value is >= to rule"). 3.14 changes this documented meaning on
purpose, so that one name has one meaning in hanging protocols and in split rules.
Two new constraints give the old meaning: greaterThanOrEqual and lessThanOrEqual.
| Constraint | 3.13 | 3.14 |
|---|---|---|
greaterThan: n | value >= n | value > n |
lessThan: n | value <= n | value < n |
greaterThanOrEqual: n | not available ("Unknown validator") | value >= n |
lessThanOrEqual: n | not available ("Unknown validator") | value <= n |
What to change
Find each greaterThan and lessThan constraint in your hanging protocols. For each one,
decide which meaning the rule needs:
- The rule needs the old meaning. Rename
greaterThantogreaterThanOrEqual, andlessThantolessThanOrEqual. Keep the value. This change is exact for every finite number value, also for a value that is not an integer. For a text value, an array value or no value, the result can change; see "Number values". - The rule needs the strict meaning. Make no change. For example,
numImageFrames: { greaterThan: 0 }now fails for a display set with 0 image frames, such as a whole slide or ECG display set. Before 3.14,numImageFrames: { greaterThan: { value: 0 } }matched these display sets.
// 3.13: matches a study with 2 or more display sets with images
constraint: { greaterThan: 2 }
// 3.14: the same match
constraint: { greaterThanOrEqual: 2 }
For an integer attribute, greaterThan: n - 1 gives the same result as
greaterThanOrEqual: n.
The OHIF hanging protocols
These rules change, and keep their match for a number value (for a numeric string, see below):
@ohif/hpMammo:numberOfDisplaySetsWithImageschanges fromgreaterThan: 2togreaterThanOrEqual: 2.@ohif/frameView:numImageFrameschanges fromgreaterThan: { value: 16 }togreaterThanOrEqual: { value: 16 }.- The rules
numImageFrames: { greaterThan: { value: 0 } }change togreaterThanOrEqual: { value: 0 }. A display set with 0 image frames (whole slide, ECG) still matches, so the default protocol still shows a whole slide study. The rules are in the default protocol,seriesSelectors,@ohif/hpCompare,@ohif/hpScale, the dynamic volume protocol and the test protocolhpTestSwitch.
These matches change:
- The
OneOrMoreSeriesrulenumberOfDisplaySetsWithImages: { greaterThan: 0 }now runs (see "A falsy operand"). It is in the rule liststudyWithImagesof@ohif/mnGridand@ohif/mnGrid8, and in@ohif/hpScaleand the test protocolhpTestSwitch. Before 3.14 it always passed. Now a study with 0 display sets with images no longer gets its weight. The OHIF modes show no change: the segmentation mode lists only@ohif/mnGrid, and when no protocol scores, the protocol engine uses thedefaultprotocol when the mode lists it, else the first protocol of the mode. @ohif/hpMammohas therequiredrulePatientOrientation: { doesNotContain: ['P', 'FL'] }. A display set has noPatientOrientation, so the rule reads it from the first instance. Before 3.14, the rule failed with an error when the instance had noPatientOrientation(absent ornull), so the display set did not match. Now the rule holds on no value (see "Text values"). So a mammography display set with noPatientOrientationcan now fill that viewport, when its other required rules hold.- A numeric string
numImageFramesnow matches the number rules. The stack SOP class handler copiesNumberOfFramesof a multi-frame instance as it is, and a data source can give it as text. Before 3.14,greaterThanfailed on a string, so such a display set did not match@ohif/frameViewor thenumImageFramesselectors. Now'30'reads as 30.
These rules change their result only for a display set that the stack SOP class handler does
not make, because that handler sets SeriesDescription to '' when the instance has none:
@ohif/hpMammoSeriesDescription: { doesNotContain: 'CC' }(required), and the PT ruleSeriesDescription: { doesNotContain: { value: 'Uncorrected' } }(weight 2) of the TMTV protocol and the dynamic volume protocol. On'', these rules held before 3.14 too. On a display set with noSeriesDescriptionattribute (undefinedornull), they failed with an error before 3.14, and now hold. In TMTV, such a PT display set now scores 4 (the same as a PT whose description does not name the correction, and as a stack PT display set with no description); before, it scored 2.
Number values
greaterThan, greaterThanOrEqual, lessThan and lessThanOrEqual now read the value as
@cornerstonejs/metadata does:
- A numeric string reads as a number:
'5'is 5. Before 3.14,greaterThanfailed on a string value, andlessThanlet JavaScript convert it, so' 'read as 0 and passedlessThan: 5. Now' 'has no number and fails. - An array reads as its first element:
[5]is 5. Before 3.14,greaterThanfailed on an array value, andlessThanlet JavaScript convert it, so['5', '9']failedlessThan: 6and now passes. - A value with no number fails:
null,undefined,'',true. Before 3.14, JavaScript comparednullandtrueas numbers, solessThan: 10passed onnull, andgreaterThan: { value: 0 }passed onnull(null >= 0) and ontrue. Anullvalue comes from an empty instance attribute (dcmjs gives an empty Type 2 element asnull) or from a custom attribute callback. A missing attribute isundefined, which failed before too.Infinityis not a finite number, so it now fails; before 3.14, it passedgreaterThan.
The test value reads in the same way: '4' and ['4'] are 4. A test value with no number
fails: '', [], null, 'a'. Before 3.14, greaterThan: [] passed on every number value.
@cornerstonejs/metadata accepts a finite number only, so greaterThan: '4' is an error
there.
range reads the value and its two bounds in the same way. So range: [1, 10] now holds
for '5' and for [5, 60] (the first element is 5), and fails for [50, 60], null and
''. Before 3.14, range compared an array value as it was, so [50, 60] passed. The
bounds must be two numbers, or two numeric strings, so range: [[1], 10] now fails.
The test values of the OHIF hanging protocols are numbers, and no OHIF hanging protocol uses
range. Their values are numbers, except the numeric string numImageFrames in "The OHIF
hanging protocols".
A falsy operand
Before 3.14, a constraint with the operand 0, false or '' never ran, because
validate.js skips a validator with a falsy operand. So greaterThan: 0 and equals: 0
always passed, whatever the value. The operand { value: 0 } did run. Now the operand 0,
false or '' runs for every OHIF validator. Check each such constraint in your hanging
protocols: it now tests the value.
- An operand
null,undefinedorNaNskips the validator, and the rule passes. Before 3.14, an operandnullmadeHPMatcherthrow "Cannot convert undefined or null to object". - The OHIF validators are
equals,doesNotEqual,includes,doesNotInclude,contains,containsI,containsAll,doesNotContain,doesNotContainI,startsWith,endsWith,greaterThan,greaterThanOrEqual,lessThan,lessThanOrEqual,rangeandnotNull. Avalidate.jsbuilt-in validator keeps its meaning:presence: falseturns it off. A validator that your application adds tovalidate.validatorsalso keeps thevalidate.jsmeaning. - A text validator reads a number or a boolean operand as text:
startsWith: 0tests for'0', anddoesNotContain: 0holds for'abc'(see "Text values"). Before 3.14, the bare operand0skipped the rule, so it passed. The operand{ value: 0 }failed with an error (startsWith) or always failed (doesNotContain). - An operand
''runs the text validators too. Every text contains'', sodoesNotContain: ''anddoesNotContainI: ''now fail on every value that has a text.contains: '',startsWith: '',endsWith: ''andequals: ''now fail on a missing value, andcontainsAll: ''fails with "must be an array". Before 3.14, all of these always passed. - No OHIF hanging protocol uses the operand
0,falseor''with a text validator.
An { attribute, from } operand that gives no value now reaches the validator as that value
(undefined or null). Before 3.14, the validator got the wrapper object. So
equals: { attribute: 'X' } now holds when the value and X are both missing, and
doesNotEqual fails. Two different kinds of no value are not equal: the value '' or []
with X missing fails equals. A text validator gets no text from such an operand, so
contains fails.
HPMatcher reads an attribute of the display set, and, when the display set has no value
(undefined or null), the attribute of its first instance. So a null instance attribute
reaches the validator as null, and equals: { value: null } holds on it. A missing
attribute is undefined, and equals: { value: null } does not hold on it. Use
notNull: false to test for no value of any kind.
notNull: false now means "has no value". Before 3.14, it always passed.
Text values
contains, containsI, doesNotContain, doesNotContainI, startsWith and endsWith
now read the value as @cornerstonejs/metadata does:
- Each element of the value that has a value reads as text:
5is'5', and[null, 'R MLO']is['R MLO']. Before 3.14, a number value failedstartsWithwith "Value must be a string or an array", and passedcontains(a number has noindexOf). - A value with no value gives no text. So
contains,containsI,startsWithandendsWithfail, anddoesNotContainanddoesNotContainIhold. Before 3.14, on a missing value,contains,containsI,doesNotContainanddoesNotContainIfailed with an error ("Something went wrong during validation"), andstartsWithandendsWithfailed with "must be a string or an array". - An object reads as the text
'[object Object]'. So on a person name object (PatientNameas{ Alphabetic }),containsandcontainsInow fail, anddoesNotContainanddoesNotContainInow hold. Before 3.14, an object has noindexOf, socontainsandcontainsIpassed, anddoesNotContainanddoesNotContainIfailed. Read a text field, for example a custom attribute that gives the name as text. - The test value reads as text too (see "A falsy operand"). A
nullorundefinedtest value gives no text.@cornerstonejs/metadataaccepts a string operand only, sostartsWith: 0is an error there.
The rule holds when one text of the value passes the test with one test value, as before.
The @ohif/hpMammo and TMTV results are in "The OHIF hanging protocols".
No value
notNull now reads an empty value as no value, as @cornerstonejs/metadata does. No value is
undefined, null, '', or an array whose elements all have no value ([], [''],
[null]). So notNull: true now also fails on '', [] and an array of empty elements.
Before 3.14, it failed only on null and undefined. 0 and false are values.
The OHIF hanging protocols do not change. The only notNull rule, prior
StudyInstanceUID in @ohif/hpCompare, reads a UID or undefined.
Numbers, numeric strings and booleans
equals, doesNotEqual, includes and doesNotInclude now read a numeric string as a
number when the other value is a number, as @cornerstonejs/metadata does. So
equals: 1 holds for the value '1', and includes: [1, 2] holds for ['2']. An empty
string is not a number, so '' does not equal 0. Two strings still compare as text, so
'1.0' does not equal '1'.
A boolean now equals the text 'true' or 'false', as in @cornerstonejs/metadata. So
equals: true holds for the value 'true', and equals: false holds for 'false'. A
boolean does not equal a number: true does not equal 1, and false does not equal 0.
The OHIF hanging protocols do not change. Each equals: true and equals: { value: true }
in them reads isDisplaySetFromUrl or isReconstructable, which are booleans.
equals, doesNotEqual, includes and doesNotInclude
equals and doesNotEqual now compare the whole value. A single test value is the same as
an array of one value. Before 3.14, a single test value was compared with the first element
of a multi-valued attribute only, against the documentation of equals.
includes and doesNotInclude now accept a single test value. Before 3.14, a single test
value always failed, with "must be an array".
| Constraint | Attribute value | 3.13 | 3.14 |
|---|---|---|---|
equals: 'ORIGINAL' | ['ORIGINAL', 'PRIMARY'] | passes (first element) | fails |
equals: 'ORIGINAL' | ['ORIGINAL'] or 'ORIGINAL' | passes | passes |
doesNotEqual: 'ORIGINAL' | ['ORIGINAL', 'PRIMARY'] | fails | passes |
includes: 'ORIGINAL' | ['ORIGINAL', 'PRIMARY'] | fails ("must be an array") | passes |
doesNotInclude: 'LOCALIZER' | ['ORIGINAL', 'PRIMARY'] | fails ("must be an array") | passes |
What to change: find each equals and doesNotEqual with a single test value on a
multi-valued attribute, for example ImageType. To test that the value holds the test
value, change equals to includes, and doesNotEqual to doesNotInclude:
// 3.13: ImageType value 1 is ORIGINAL
constraint: { equals: 'ORIGINAL' }
// 3.14: ImageType holds ORIGINAL
constraint: { includes: 'ORIGINAL' }
The OHIF hanging protocols do not change. Each equals in them compares a single-valued
attribute, or uses an array with the full value (PatientOrientation).
Known differences from @cornerstonejs/metadata
The hanging protocol constraints and the split rule operators of the same names still differ here:
equals: nullmeans "has no value" in@cornerstonejs/metadata. In a hanging protocol, the operandnullskips the constraint, and the rule passes. UsenotNull: false.greaterThanOrEqualandlessThanOrEqualare not split rule operators yet.@cornerstonejs/metadatarejects an operand of the wrong kind: a number operand of a text operator (startsWith: 0), a text operand of a number operator (greaterThan: '4'), and an array operand ofequalsanddoesNotEqual. The hanging protocol constraints read such an operand as text or as a number, and compare an array operand with the whole value.includes: ''holds on the value''here, and anincludesoperand that gives no value holds on a missing value. The split ruleincludesfails on no value.equals: ''holds on the value['']here. The split rules keep an empty text and an empty array apart.- An empty array operand:
contains: []always fails anddoesNotContain: []always holds here. The split rules reject an empty array operand. - A number operator accepts an array of one number as the operand here
(
greaterThan: [16]). The split rules accept a finite number only. containsAllneeds an array value and an array operand here (['a']withcontainsAll: 'a'fails with "must be an array"), and it compares with===, so['1', 'b']withcontainsAll: [1]fails.@cornerstonejs/metadataaccepts a single operand, and compares a number and a numeric string by value.notNullreads any operand other thanfalseastrue.@cornerstonejs/metadataaccepts a boolean only.- The operand wrappers
{ value }and{ attribute, from }are for hanging protocols only.
Other changes to the constraint list
- The
Constrainttype of@ohif/core(Types.HangingProtocol.Constraint) now lists all of the validators. The type hadnotEquals, which no validator implements. UsedoesNotEqual. The{ value }form of a constraint now also accepts an array of strings or numbers, andnotNullaccepts{ value: boolean }. - The error messages of the validators changed. They show the test value as JSON, for
example
must equal ""andmust include one of [].HPMatchergives them in the match details only. Do not test for the old text.