Table Of Contents

Previous topic

Basic Type

Next topic

Type and Initialization

This Page

Feature and Requirement

feature             ::=  featureAnnotation* ( "trait" | "final" "class" ) ID_feature
                           "extends" ( "Feature" | featureType )
                         "{" ( attribute [ "=" initialization ] )* "}"
                         [ invariantObject ]
featureAnnotation   ::=  featureLevel [ "(" <scalaExp : java.lang.String(S)> ")" ]
                         | "@Data" | "@Settable"
featureLevel        ::=  "@Schema" | "@Class" | "@Product"| "@Device" | "@Instance"
featureType         ::=  ID_feature ( "with" ID_feature )*
attribute           ::=  attributeAnnotation* [ "final" ] "val" ID_attribute ":" type
attributeAnnotation ::=  "override" | "@Data" | "@Settable"
                         | "@Const"
                              [ "(" constLevel
                                | "value" "=" constLevel ","
                                  "qualifier" "=" <scalaExp : java.lang.String(S)> ")" ]
                         | "@Multiplicity" "("
                              "lo" "=" <scalaExp : scala.Int(N)>
                              [ "," "hi" "=" ( <scalaExp : scala.Int(N)> | "*" ) ]
                              [ "," "clas" "=" "classOf" "[" <typeName> "]" ] ")"
constLevel          ::=  "SCHEMA" | "CLASS" | "PRODUCT" | "INSTANCE" | "UNSPECIFIED"
invariantObject     ::=  "object" ID_feature "{" invariant* "}"
invariant           ::=  "@Inv" "val" ID_invariant ":" "Predicate" "[" predicateType "]" "="
                           "pred" "{" ID ":" predicateType "=>" <scalaExp : dms.Boolean> "}"
predicateType       ::=  featureType
                         | "(" featureType ( "," featureType )+ ")"
requirement         ::=  [ "@Req" ]
                         "trait" ID_requirement "{" attribute* "}"
                         "object" ID_requirement "{" invariant* "}"

Feature declarations are used to introduce compound types consisting of a set of named attributes for modeling parts or complete medical device features. Attributes are used to model information that may or must present in devices. Moreover, features can declare invariants over attribute values that express information consistency constraints. For example, the dms.example.schema.Range feature is used to model a number range information between min and max (inclusive), and an invariant that states that min is less than or equal to max. (Note that in Scala, backticks can delimit unconventional identifiers).

Requirement declarations are used to introduce constraints for features that depend on other features, which are useful for medical device coordination. For example, the dms.example.requirement.MyReqPulseOx states a requirement for a pulse oximeter that has the specific SpO2 and pulse rate ranges.

Feature

Hierarchy

Similar to basic types, feature sub-type hierarchy forms a lattice with dms.Feature at the top. By allowing multiple feature inheritance (featureType), one can mix-in different features to put together larger device parts or complete device features. As stated in the Well-Formedness Section below, we avoid the dreaded diamond problem associated with multiple inheritance over feature attributes by disallowing it completely instead of allowing mitigations or workarounds.

Annotation

Level

DML (DMS) recognize four main levels of features for staging device type refinement and when constant attribute values should be provided:

  • Schema (@Schema). In DMS, schema features are declared using trait.
  • Class (@Class). In DMS, class features are declared using trait.
  • Product/Device (@Product/@Device). Product and Device differentiate device parts and complete devices, respectively. In DMS, product features are declared using trait, while device features are declared using final class.
  • Instance (@Instance). In DML (DMS), instances are not represented as they belong to a running MDCF instance.

At each level, one can add a String qualifier ([ "(" <scalaExp : java.lang.String(S)> ")" ]) that indicates custom sub-levels within the provided four levels; by default, the qualifier is an empty string ("").

In addition to specifying feature level at each feature declaration, one can dedicate a Scala/Java package for a particular level and annotate the package with @Schema, @Class, @Product, or @Device instead.

Unfortunately, Scala does not support annotation on packages, thus, this should be done using Java’s package-info.java facility. For example, see the following package infos:

The package annotation scheme sets the default level for any feature declared in the package; this default can still be overriden by specifying the level in a feature declaration.

If the feature or package level are not provided, the level is considered as Unspecified.

Data

The @Data annotation indicates that the declared feature’s attributes form inter-device communication structures. Data features should not contain @Settable attributes or attributes whose type contains a @Settable feature. For example, see the various features in dms.example.schema.Schema.scala starting with the MetricAttribute feature.

Settable

The @Settable annotation indicates that the feature attribute values can be assigned; by default, attribute values are read-only. Settable features should not contain @Data or @Const attributes or attributes whose type contains @Data or @Const. For example, see the various features in dms.example.schema.Schema.scala starting with the RangeSetting feature.

Attribute

As mentioned previously, attributes are used to model information stored in devices. Attribute types can be either basic type, feature, or other compound types such as a sequence or a set as described in the Type and Initialization Section. Moreover, an attribute declaration can be accompanied with an initial attribute value.

Attributes are inherited from super-types to their sub-types. A sub-type can refine the attribute type inherited from its super-type. However, no two or more attributes with the same name whose declaring features are different can be inherited (attribute refinements are considered as declarations).

Annotation

In addition to @Data and @Settable similar to feature, attributes can be annotated as overriding previously declared attribute (override), constants (@Const), or multiplity constraints (@Multiplicity) for attributes whose type is a set and or a sequence. Note that override should be listed last to adhere to Scala’s grammar.

Override

The override annotation expressed as a Scala modifier is used to indicate that the declared attribute is a refinement of attributes that have been declared by one of the feature’s super-types. The dms.ModelExtractor auto-detects overriding attributes, however, it is strongly recommended that override is explicitly specified for documentation purpose and to prevent specification mistakes (e.g., typos) that inadvertently lead to introductions of new attributes instead of refining existing ones.

There are three reasons why one wants to override an attribute:

  1. To refine the attribute type to be more specific; for example, see dms.example.schema.IntRange min and max.
  2. To assign an initial (or the constant) value; for example, see dms.example.clas.ICEPulseOx.type
  3. To add multiplicity constraints; for example, see dms.example.clas.ICEPulseOx.physioParams
Const

The constant annotation indicates that the attributes hold non-changing information after they are initialized. The initialization level (and sub-level qualifier) can be specified to enforce that the constant value is assigned at a specific level (and sub-level, if provided). For example, see the type attribute of dms.example.schema.ICEDevice that specifies its constant value has to be provided at the Class level (without qualifier, which means at any sub-level before it is used in the Product/Device level). This is satisfied, for example, by the dms.example.clas.ICEPulseOx feature. A declaration of a constant attribute with initialization should be declared as final.

Multiplicity

For attribute whose type is a sequence or a set (collection), it may contain zero or more elements. DML (DMS) provide a specialized, lightweight construct useful for constraining the size of sequence/set. For example, see dms.example.clas.ICEPulseOx.physioParams. Each multiplicity constraint can specify the low and high bounds (inclusive) for the number of elements in the sequence/set (as usual, the low bound should be equal or less than the high bound); by default, the low bound is 0, and the high bound is unbounded (*). In addition, one can specify the element type that is applicable for the multiplicity constraint. If specified, it means that the low and high bounds are on the number of elements whose type is a sub-type of the specified type. For example, the multiplicity constraint dms.example.clas.ICEPulseOx.physioParams specifies that dms.example.clas.ICEPulseOx.physioParams should at least have one dms.example.clas.ICEPulseOx. If the type is unspecified, then by default, it is Object, which match any element type.

The multiplicity constraint can be expressed using a general invariant construct (described below) such as the general invariants for dms.example.clas.ICEPulseOx. However, using the annotation eases the development of some tool support, for example, a tool that generates UML class diagrams. Thus, multiplicity constraints should be used first whenever possible instead of expressing them as general constraints.

Invariant

Invariant declarations are used to state feature consistency constraints. Instead of declaring them in the feature trait or class, invariants are declared in the feature companion object (invariantObject).

Invariants are inherited from super types to their sub-types. However, they are not allowed to be overriden. That is, a sub-type cannot declare an invariant with the same name as with any of its super types’ invariants. The effective invariant of a feature is a conjunction of all the invariants declared by the feature and all invariants inherited from its super types.

In DMS, each invariant declaration is represented using a val that is annotated with @Inv and whose type (predicateType) is a dms.Predicate over the type of the feature (dms.Predicate returns dms.Boolean, i.e., not to be confused with scala.Boolean). For example, dms.example.schema.Range’s invariant should have the type of a dms.Predicate over dms.example.schema.Range, i.e., dms.Predicate[dms.example.schema.Range].

The invariant val should be initialized by calling the dms.pred macro that accepts a scala.Function1 (that in turn, accepts a feature type object and returns a dms.Boolean object). While any Scala expression returning a function from the feature type to dms.Boolean would be accepted, one should use the Scala anonymous function syntax variant as described in the invariant grammar.

The body of the function (<scalaExp : dms.Boolean>) cannot depend on variables declared outside the function (i.e., no free variables are allowed). The exact expression language subset is not specified at this point in time. Currently, we are considering the following subset:

  1. Attribute/field navigation, i.e., <scalaExp>.ID_attribute.
  2. Assigment of the form val ID = <scalaExp>.
  3. Function application, where the function is a basic type <basicOpMethod> or methods from compound types (e.g., set or sequence methods).
  4. Pattern matching, where the matching is over type, e.g., case x : PulseOx => <scalaExp>.
  5. Closure.

As DML (DMS) are evolved, we will settle on the invariant expression language.

Note

The dms.pred macro is used to retrieve the fully-resolved Scala AST of the invariant function expression that works similarly to scala.reflect.api.Universe‘s reify; the difference is that dms.pred enforces the expression’s type to be a function type returning dms.Boolean. The fully-resolved Scala AST is then retrieved by the dms.ModelExtractor during model extraction process.

Representation Classes

Below is a table that maps grammar productions related to feature and its DML AST representation classes:

Grammar Non-Terminals DML AST Classes
feature dml.ast.Feature with name = fully-qualified name of the feature
featureAnnotation

dml.ast.FeatureAnnotation

featureLevel dml.ast.FeatureLevel
featureType dml.ast.NamedType (list of)
attribute dml.ast.Attribute
attributeAnnotation

dml.ast.AttributeAnnotation

constLevel dml.ast.FeatureLevel
invariantObject not represented; invariants stored directly in dml.ast.Feature members
invariant dml.ast.Invariant

Currently, dml.ast.Invariant predicates are not represented using custom DML AST classes (the predicate type is java.lang.Object); the predicate is currently represented using Scala scala.reflect.api.Trees types. As DML (DMS) expression language is evolved, we will introduce custom AST classes for representing predicates.

Symbol Table

There are several methods provided by the symbol table API related to features. Below is the list of relevant methods (please see the documentation in the dml.symbol.SymbolTable that describes the methods):

Requirement

Requirements are used to express dependency constraints for medical device coordinations. That is, if a device requires some features from other devices with some specific properties, the device can advertise the properties as requirements.

A requirement consists of attributes and invariants that state the required feature properties. The form of a requirement invariant is similar to feature`s invariant, except that a requirement invariant predicate function is allowed to work over a tuple of feature types (predicateType: "(" `featureType` ( "," `featureType` )+ ")").

Representation Classes

Requirements are represented using the dml.ast.Requirement AST class (with name = fully-qualified name of the requirement) and requirement invariant is represented using dml.ast.Invariant similar to a feature invariant.

Symbol Table

There are several methods provided by the symbol table API related to requirements. Below is the list of relevant methods (please see the documentation in the dml.symbol.SymbolTable that describes the methods):

Well-Formedness