Agreeing on optional user interface hints in templates

In short: Let’s start experimenting with adding vendor neutral hints (e.g. in templates) that can help both automated user interface generators and “manual” form editors create forms with more logic. Details follow.

Current use-case background

There is a current need in multi-vendor projects (at least in Sweden) where we would like at least some GUI/form logic to be possible to author once nationally and then be reused in several different vendor solutions (Cambio, TietoEVRY/Better, SECTRA structured reporting forms. And perhaps e.g. Medblocks & NeoEHR later?)

openEHR historical discussion background

Previous mail list discussions (from 2008) regarding GUI-hints in openEHR templates. Some highlights:

Initial suggestion

  • Let’s start some experiments with (shared vendor neutral) template annotations and/or “ADL(2?) rules” reusing part of the design patterns of “EnableWhen” in FHIR Questionnaires that could (optionally) be used by form building tools as input for creating (possibly vendor specific) conditional logic in forms (like what is now available in e.g. Better’s, DIPS’s and Cambio’s tools)
  • Maybe later look at
    • “Presentation mode visibility” hints - see option in Better’s EHR studio
    • “Hide on form” hints - e.g. useful for marking intermediate level branch levels that are mostly needed for logic storage structure but not contributing to end-users’ understanding at a GUI-level in the template-specific use case.
    • “LOD” - level of detail hints - for subtrees/parts that can be initially collapsed/hidden when screen space (or attention span) is limited. (Like the “More…”-buttons in Clinergy/PEN&PAD)
    • SelectWhen hints - Select an item in a multiple choice list when a condition based on data in other parts of the form is met.
    • Copy data between fields (and possibly discuss the Pandora’s box of “simple” calculations & aggregations based on data from multiple source fields)

When this was discussed in a SEC meeting and following discussioins after the meeting @pieterbos and others suggested lookin at the “rules”-mechanism in ADL (Rules are definitely useful within an archetype - with ADL2 possibly also in templates across data from different archetypes). Nedap are using such rules and the implementation is open source in Archie. It outputs things such as ‘set the value at this path to this’, and ‘this part of the data must exist (is mandatory)’.

@pieterbos also said that Nedap has an implementation running at archetype-editor.nedap.healthcare. (where you can sign up and login, create a repository). Using the basic editor you can only define sums and averages. If you click ‘advanced editor’ at the bottom, you can see and edit the rules to achieve quite a lot more, and there’s a form implementation that executes them as well. Note that this form only does compositions, observations and evaluations. It also outputs a copy of the object provided as input, modified by the rule evaluation, plus whatever assertions failed and succeeded, of course. For editing the rules by hand, he recommended the visual studio code extension, but it cannot execute them on data

Note: User interface hints may not be of interest to all (and thus not expected to be supported by all) but very useful for some, please consider that when responding and let’s explore options.

3 Likes

We do have this in ADL2 (also in ADL1.4) - it doesn’t seem to be used much. See here (ADL2.2).

If level of detail were not simply computable from depth, it would probably require a number on all / some nodes that would be compared to a threshold value to unfold or not.

This is certainly a GUI thing.

We will support computed fields in latest ADL2 - by distinguishing between assertions (if /x/y/z = a then something) and expressions (/x/y/z := a + b * c).

Mainly I think we should clarify the distinction between UI level hints and model-level (archetype / template) rules.

Hi @erik.sundvall we have discussed this in many occasions, here and in Slack.

Because of the many disadvantages of mixing GUI stuff into a template, my opinion was always to avoid this at all costs.

  1. Separation of concerns: visual hints, GUI commands / operators / definitions, etc. are not in the scope of the template, it’s purpose is to specify a data set by constraints, not to define how form fields or data in a screen should be displayed. There is no concept of form, field or data visualization in the template model / AOM, and shouldn’t be added.

  2. Adding those GUI elements to the template are conceptually patches to comply with certain system requirements, that should be satisfied by other metadata constructs, not by templates.

  3. Opening the door for some GUI hints allows to open the door for many other hints and extra requirements that are not in the current scope/purpose of a template.

  4. Limiting the scope of templates just for “defining data sets by constraints” from the start allows these extra GUI requirements to be satisfied elsewhere. For instance, it enables the construction of another model, specific for GUI stuff. I worked on this area before and shared the initial model we reached with the SEC some time ago.

  5. Having another model just for GUI stuff, allows to follow the inherent layered architecture openEHR currently has: RM < AOM < TOM < GUI Templates. I have also proposed other layers for that semantic “sandwich”, for instance the CDS rules is another layer, the “data mappings” (to map openEHR data from/to other standards/models/messages) could also be another layer, a “process spec” could be another layer, etc. etc. So instead of adding elements that belong to a different semantic level to the templates, we create a new semantic layer for those, including: expression language, model, tools. This is just the openEHR approach for everything.

This is the spec I was writing some time ago:
User Interface Templates for the openEHR specifications stack - 20200922.pdf (471.5 KB)

That is basically a model that maps items from the template into GUI elements/controls/widgets, and allows to bind data back to the openEHR template for data validation and storage.

So IMHO it is better to the whole architecture to extract those hints into a different layer than actually standardizing those to be part of the AOM.

4 Likes

@pablo I’d say what is “GUI-stuff” and what is more of just template/use-case-specific logic and validation/selection-semantics is a greyzone/continuum.

Thanks for linking to your UI-templates document suggestion @pablo. The (green) layout/zone-classes in that document I’d definately think is “GUI stuff” and likely more UI-product/technology/screensize-dependent. On the other hand, the “EnableWhen” example above, inspired by FHIR, would be towards logic end of the grayzone, and could as @thomas.beale hints, probably be solved using ADL rules.

In the mentioned projects we are interested in having a convenient way for informaticians+clinicians to specify some of the logic at the same time as they build the shared template, preferrably using integrated tooling, saving files in a vendor neutral format. If it ends up in separate files or as annotations+rules inside a use-case-specific template file, or a mix thereof, would matter less for these users. Tooling support does matter though. Most current template tools do support annotations., what tools do support rules in templates (not only archetypes) in addition to the one I quoted @pieterbos mentioning?

3 Likes

In understand your thinking, if we analyze those cases (GUI stuff and template use cases and validation/selection), IMO all is out of the scope of the template. In one way or the other those elements/artifacts/constructs end up in areas like: user interface, user experience, user interaction or just workflow definition. None of those cases are specific for “data set definition by constraints”, as I define the current scope of the openEHR templates, which is informal but accurate.

I think further analysis of those cases you mentioned is needed, so we can classify them in the categories mentioned above (GUI, workflow, etc) or create new categories. Then when we formalize that, we can check if there is any overlap with the scope of the templates, and if there is, then we can opt to add support for those requirements in the AOM, if there isn’t, I would prefer not to touch the AOM but formalize those requirements on top of it as another semantic layer as described on the previous message.

That seems exactly as a workflow construct, a definition/rule/constraint on top of the template, so I would argue the advantage of including such construct in the AOM for templates. Though my model didn’t include workflow definitions, it is a better place to include them, and map back to the template fields involved in that rule (using paths or node ids). In fact that something is “enabled” or “disabled” is indeed GUI stuff, then the “when” part is a rule on top of that “enable/disable” directive., which could be formalize as: IF bool_expr THEN enable(path) ELSE disable(path).

Though I think enable/disable applies to widgets (sets of fields) or individual fields, so it could be similar but instead of using paths, using the field id or widget id:

IF bool_expr THEN enable(field_id) ELSE disable(field_id).

or even lists of fields:

IF bool_expr THEN enable(f1, f2, f3) ELSE disable(f1, f2, f3).

That is why I don’t think it is a grey zone, IMHO more analysis is required to conceptualize each requirement correctly at the finest grain possible, current requirements seem big items looking for fast solutions and we need to be careful, because this design will mark implementations for years to come. Tough this is not a perfect vs. good enough discussion, I think it’s wrong to add solutions in the a place that is not for that.

That is the issue, the “convenient” path is not the conceptually correct path for an international spec, that is my personal opinion based on my own experience, not saying I’m right or anything, just trying to open the discussion.

1 Like

We can certainly find some examples where the logic of whether a data hierarchy should only be present based on the value of another element. Classic example:

items
    +--- tobacco user: Y/N
    +--- tobacco use details
               +--- amount per week
               +--- cessation attempts
               |          +--- xxxx
               +--- yyy

rules
    /items[tobacco user] = 'Y' implies exists /items[tobacco use details]

If we consider the above rule a semantic property of the data independent of whether it is on the screen, in a message, document, survey or whatever other form, agreed on by the archetype experts, then it belongs within models - either the archetype or somewhere similar. (@erik.sundvall 's case)

However we can undoubtedly find examples where the same structural conditionality is use / context specific, and not applicable to all instantiations of the template as data. We may well need a UI level of mechanism to accommodate this. (@pablo 's case)

So it just means that not everything that looks like a certain kind of business rule for data or UI is necessarily one or the other.

4 Likes

Hi @thomas.beale I agree if there is any data set related rule, it should be in the template itself, or in AOM in general, though each requirement need thorough analysis to know what mechanism should be used to satisfy them, if it’s a data cross-node constraint, or if it’s something that will affect the user workflow or the GUI.

A separate but related topic is how to define and name those IF-THEN-ELSE rules for the data sets. I know we have the assertion language plus other new constructs.

Let’s say certain part of the structure defined by a template, which is optional, should be prohibited depending on a value in another part of the same template. I think we can use the AOM itself to be dynamically changed when these conditions are true, for instance:

IF value_at(path) IN [a,b,c] THEN node_at(path).occurrences = [0…0]

So instead of defining something new, we use the current RM functions and attributes to change their values at runtime. It’s like a RM compatible scripting language.

Same could be done if the optional node should be mandatory depending on the value(s) of another node in the template.

Of course all values should come from COMPOSITION instances, so the “script” can only be evaluated using a given COMPOSITION + TEMPLATE.

The language you described above doesn’t seem to use the RM and seems like OCL (which we use to represent RM invariants in the specs).

About naming, what I don’t like is something like the mentioned “enableWhen”, which kind of implies something related with GUI because of the “enable” part. In a data structure the nodes are not enabled or disabled, but are required, optional or prohibited. So naming is important to avoid misunderstandings with the scope of these rules: someone might want to use this mechanism to represent workflow or GUI rules when that is not the purpose of the rules. Of course, it is also difficult to discourage this usage when people have tools that allow them to do that and use template how they want instead of how they are designed to be used for.

we already have this - we just don’t use exactly that syntax. In the syntax of today, it is :

/path/to/value matches {constraint} implies exists /path/to/other/node

In the newer syntax (future), it would be:

assert name_1 matches {constraint} implies defined(name_2)

or:

assert name_1 matches {constraint} implies name_2 matches {constraint}

where name matches {constraint} will often be name = value for the common simple cases.

In the above, name_1 and name_2 have bindings to paths in a separate place.

We also don’t need special functions like value_at() or node_at() because predicates like events[any_event].items[systolic_bp] can be used, where [any_event]is shorthand for [@archetype_node_id = '#systolic_bp'] and #systolic_bp is a future form of [id23] in ADL2.

This is really just dot-syntax mixed with Xpath-style predicates. Some other more advanced things will be possible using lambdas, as described here.

NB: this new syntax is still being refined; I’m mentioning it here to show a proposed direction, not to say it will be in the next version of ADL. Next versions would potentially support path bindings (see here - look for ‘symbol_bindings’), but we are working with @pieterbos and others to agree on how far to go at each ADL release.

It’s not RM-specific, if that’s what you mean - it’s makes no assumptions about functions that might be available on specific classes in specific models - the way of finding items in a sub-tree or testing existence etc is completely generic, and doesn’t change when you use it with another RM. So it’s more like OCL in that way, true.

I agree with this.

I mean that the expression language could use the RM API (functions from the RM classes) instead of defining a whole new language without reusing the infrastructure we already have, which at implementation time could accelerate things.

Well it could, but then you are relying on functions from each RM to provide general semantics, e.g. finding nodes at paths. Each RM could easily have different functions for doing that. Worse, most RMs are not likely to have the complete set of functions defined that would be needed.

We do however rely on the openEHR Base and Foundation types, which are not openEHR-specific.

For example use of lambdas to find matching sub-parts:

    class Book {
        title: String
        pub_date: Date
    }

    book_list: List<Book>

    book_list [(b:Book) {b.title.contains("Quixote")}]

Well, the Expression Language is based on very common / standard concepts, grammar etc, and the various parts are mostly cherry-picked from existing languages, e.g. Xpath, for path-related predicates.

We do have some of the infrastructure needed for an Expression Language, in:

  • the simple and incomplete one in ADL2
  • the very simple expression part of AQL
  • the expression meta-model in GDL
  • the small expression language in Task Planning.

The main aim of a revised EL is to bring the semantics of all these together, or as close as possible, even if surface syntaxes might differ (as appears inevitable in AQL). This will support:

  • upgraded BMM so that all invariants and functional parts can be represented;
  • smart decision logic like you see here;
  • more interesting possibilities in AQL.

The intermediate form we are currently working on for ADL 2.3 will do a fair bit.

Hi!

I have no strong opinions regarding the naming, I just used FHIR’s name to point to the use-cases and design pattern.

What we have an immediate need for in the multi-vendor project is a vendor netrual possibility to express if/then conditionals like the ones you both have discussed above (e.g like Thomas’ “tobacco user Y/N”).

It looks like the ADL implies+exist rules or future expression language with assert+implies might do the job if I understand you correctly above.

One of the remaining questions:

Are there suitable (template?) authoring tools that can be used to explore the ADL rules approach?

If not, could we (for practical test purposes) make some annotations using present tools with the rule syntax in the annotation data somehow? The template(s) with annotations could then easily be automatically post-processed and convert the annotations into for example either:

  • “proper” ADL rules and insert into the template or
  • current vendor specific if/then rules in openEHR-based form engines (like Betters’s and Cambio’s)

Currently the national project is a “proof of concept”, so it can be used to test the general mechanisms and no essential harm is done if a future official openEHR syntax would change later.

2 Likes

I understand your point, but I think any implementation, for any RM will need to have some node location by path function, isn’t it?

In other words, any reference to a node in a RM instance, openEHR or any other RM, in a expression language, needs to have an underlying API that will resolve the reference (path or another locator) to the right node from the instance, so I’m wondering if we still need to write that API for ANY RM so the expression language can be evaluated, why not making the expression language more openEHR friendly to reduce the burden for openEHR implementers if it will be the same work for other RM implementers?

Note the language is only the syntax part, the complex part is the API that implements the language and executes/checks things then an expression is evaluated against an RM instance, and the syntax will indicate which operations are needed in the API, so we are still imposing the semantics of those operations for any RM, with an unavoidable node_at(path or locator) operation needed at the API.

It just feels we are introducing a new language for every new requirement…

:slight_smile: Tom loves DSLs (and parsers?).

But seriously isn’t the ADL rule language (“matches” + “implies exists” above) already defined in spec and also implemented in some solutions.

Does anybody have opinions on using helper annotations in templates for this functinality in tools that do not yet support rules? (that can then be post processed as described in post #10 above)

items
    +--- tobacco user: Y/N <-- ANNOTATION POSITION A
    +--- tobacco use details <-- ANNOTATION POSITION B
               +--- amount per week
               +--- cessation attempts
               |          +--- xxxx
               +--- yyy

FOR RULE CAPABLE TOOLS:

rules
    /items[tobacco user] = 'Y' implies exists /items[tobacco use details]

ANNOTAIONS on one of the above marked nodes FOR RULE INCAPABLE TOOLS

Perhaps using a “THIS” macro for current node so that a rule annotation on the marked nodes can be written as either of the following table rows

Annotation position Annotation Key Annotation value
A rule THIS = ‘Y’ implies exists /items[tobacco use details]
B rule /items[tobacco user] = ‘Y’ implies exists THIS

Alternative solutions are welcome.

There’s not usually an API - the statements are normally parsed to an augmented AST (abstract syntax tree). That’s what the AOM, BMM and similar models are in fact - special ASTs. Once you have the AST you validate anything that couldn’t be validated at a syntax level, which is usually typing, semantic coherence etc. Then you can execute validated statements / expressions by traversing the AST and performing the computation. You can certainly build that as a class-level API, but it’s not the kind of service API you’d expose through something like REST.

In fact, the operation to get the value at a path within the wouldn’t be handled by any of this, because archetype paths will be mapped to symbolic variables. The runtime system has to work out what data paths an archetype path mentioned in a binding possibly refers to (e.g. it might be 50 systolic BP values, or just one).

To resolve an archetype path against data you could potentially rely on some API that talks to data, and contains path-resolving functions.

But apart from resolving bindings, the language doesn’t rely on an API as such to be executable at run time.

I hope not - we are just trying to upgrade and consolidate a single expression language with a fairly standard syntax and semantics, as per the draft spec.

1 Like

I don’t know about ADL2.

matches is for sure there, then in AOM we have existence in the constraint model, but not sure if implies exists is something already defined.

A related question would be if this is the only rule form we will have or if there are going to be more complex rules, just to check how syntax and underlying API would look like.

With API I’m referring to the classes and functions that implement the logic, once parsed from the syntax in the AST. The ASW is acutally mapped to something that can be instantiated, invoked, and that client code can use the results for such invoked methods/functions. So I guess it is what is binded by the lexer from the parsed DSL if I didn’t mess the terms :slight_smile:

I think this is a reasonable short-term solution for non-rule-supporting systems. I need to think a bit on the details. It would still be better to get to a syntax that didn’t have archetype paths inline (these greatly complicate the semantics) and used bindings for them instead, as per this version of the older Expression Language.

Pretty much everything turns into calls to functions that are either defined on objects of the RM in question, or underlying primitive types available in the language of the execution engine implementation. For example, traversing a piece of AST representing the expression systolic + 0.33 * (systolic - diastolic) will resolve to calls to Real.add() etc. Normally you will replace that with calls to the built-in types.

You can guess how it could be executed from the default generate AST:

The BMM AST is a bit smarter and allows more interesting things to be achieved, mainly to do with validation.

Calls to external functions should also be supported, e.g. to enable function that computes e.g. BMI or pregnancy risk based on certain variables.

In my proposed future EL, decision tables (aka if/then chains and case statements) will also be supported e.g.@

Result := case ipi_raw_score in
        =======================================
        |0..1|  : #ipi_low_risk,
        |2|     : #ipi_intermediate_low_risk,
        |3|     : #ipi_intermediate_high_risk,
        |4..5|  : #ipi_high_risk
        =======================================
    ;

and its default AST:

Also other nice structures:

score := Result.add (
        ---------------------------------------------
        basic.gender = #male                 ? 1 : 0,
        age_score                                   ,
        has_congestive_heart_failure         ? 1 : 0,
        has_hypertension                     ? 1 : 0,
        has_stroke_TIA_thromboembolism       ? 2 : 0,
        has_vascular_disease                 ? 1 : 0,
        has_diabetes                         ? 1 : 0
        ---------------------------------------------
    )
    ;

I’m getting ahead of the original question here of course :wink:

Good suggestion, it also matches tha approach in Cambio’s form editor (see screenshot below) where each node used in a form is assigned a short default id (based on the template’s node name) that can be changed manually.

Just as in ADL Cambio’s rules (called actions) are collected in a single place (not attached to each node).

Edited at 18:00, adding info aboout Better:
In Better’s form editor the rules are anchored to a specific node but can read and modify any node (see “Method code equals Auscultation” in screenshot below), not just the node it is anchored to, so I guess ther rules are actually ass free-standing as in Cambio’s solution

For template tools that do not handle rules (yet) we could then perhaps add an “id” annotation that will through post-processing create a symbol binding based on the path of the annotated node.

Edited: added the (namespace)prefix ‘a.’ to the examples below to reduce risk of name clashes and to make it work better in Ocean’s template designer tool. The letter ‘a’ being the first in the word ‘automation’

Example based on the structrue from post 13:

items
    +--- tobacco user: Y/N <-- ANNOTATION POSITION A
    +--- tobacco use details <-- ANNOTATION POSITION B
               +--- amount per week
               +--- cessation attempts
               |          +--- xxxx
               +--- yyy
Annotation position Annotation Key Annotation value
A a.id tobacco_user
B a.rule tobacco_user = ‘Y’ implies exists THIS

Or if choosing to manually giving an ID to both nodes, more like below. Below an alternative rule version using the ADL2 syntax desctibed in [Archetype Definition Language 2 (ADL2)](https://section 7.11.2.2. (Value-dependent Existence)) and a cambio inspired one is also provided

Annotation position Annotation Key Annotation value
A a.id tobacco_user
B a.id tobacco_details
root of template? a.rule.adl tobacco_user = ‘Y’ implies exists tobacco_details
root of template? a.rule.adl2 check tobacco_user = ‘Y’ implies defined (tobacco_details)
root of template? a.rule.cambio-style tobacco_user == ‘Y’ ASSIGN tobacco_details.hidden = false OTHERWISE tobacco_details.hidden = true
root of template? a.rule.better-style tobacco_user = ‘Y’ THEN tobacco_details show OTHERWISE tobacco_details hide

In the Cambio/Better-style rows above, the CAPITALIZED words correspond to the rule section headers in the rule editors, see screenshots above. I skipped the first (constant) “WHEN” in order to sugest shorter syntax.

1 Like

Just to go a bit further on this: the reason it complicates things is that the mapping between archetype paths and data paths is in general 1:N, due to multiplicities in runtime data. The other thing to remember is that expression languages (at least typical ones) work with data, i.e. real values, not ‘models’.

So a symbolic variable like heart_rate mentioned in an expression has to be populated with an actual heart rate value at runtime. What will this be? Well the binding shows at what archetype path it can be found in openEHR data (nice!) but we still have the problem of dealing with there being e.g. 50 heart rate values over time, all at that same archetype path. Which one(s) does the expression execution engine use? In some cases, there is an implied ‘for all’ operator (as @pieterbos pointed out a long time ago), but that is not universal. That means the EL engine potentially has to execute an expression 50 times or do something else that is not at all obvious from the expression itself. The expression itself is clear however; the challenge lies in how it will be applied to data. I believe we want to separate that complication from the syntax and semantics of expressions, so that expressions are just like they are in other languages.

I won’t bore everyone with further details, hopefully this gives a clue as to why bindings are important. For the masochistic, a long discussion here.

1 Like