Best practice defining logical functions in large scalable systems?

TLDR

General question: What is best practice for centrally defining logical functions which are heavily utilized, in the context of large scale systems?

Specific context: Will defining colour maps using centrally defined Python scripts be scalable for large systems?

Context

Hi all, I’m currently administrating and improving a medium sized system (on the order of 1,000 control devices and 30k tags) at a process plant, and working on improving the standards. I need to ensure any changes we make will be suitable for future large systems (10k+ control devices, 100k’s tags).

Our library has on the order of 30 device type templates, each of which has an associated popup (so say 60 templates in total). Each of these templates features colours which change dynamically based on the device state (open/closed, stopped/running, healthy / faulted etc with various alarm priority colours).

Current Setup

Currently the colours are defined using property bindings with transforms:

As I understand it this is among the recommended methods and should be computationally efficient when scaling to large systems.

However, from the point of general good programming practice this is far from ideal, for several reasons:

  • Defining the colours and colour map on each of the objects means we have to program the same colours and maps 60 times. Defining the same functionality multiple times is typically considered to be poor programming practice. To quote my high school computing teacher, “If you have to write it twice, use a function”.
  • By implication, it’s possible for one or more instances to be unintentionally different to the others (ie by mistake).
  • Any change needs to be made 60 times which significantly increases the effort required to make changes or improvements.

Proposed Changes

What I would like to do is the following:

  • Define the colours once using both styles, and memory tags of string type (containing the colour hex code). These would be be applied to the objects depending on whether a style or a colour binding is most appropriate in any given situation. The tags and styles would have names like “DeviceActive”, “DevicePassive”, “AlarmCritical”, “AlarmHigh” etc.
  • Define some functions which map device states and alarm priorities to colours. The functions would have names like “getDeviceColorFromDeviceState”, “getColourFromAlarmPriority”, etc and would point to the styles and colour tags.
  • Call those functions as needed on property bindings to dynamically assign the colours.

This would provide a way of defining and mapping the colours which has the following properties, which align with good programming practice:

  • The colours themselves are centrally defined, once, and can be easily changed.
  • The mapping functions are centrally defined, once, and can be easily changed.

I’ve trialed this method and it works well. I have a set of styles and colour tags I can reference, and scripts which get the relevant colours based on alarm priorities and device states.

I can call the scripts inside expression bindings and the scripts correctly obtain the colours.

Styles containing the colours:

Colour tags:

Centrally defined utility scrips which get the colours and styles based on alarm priority:

def getStyleFromAlarmPriority(priority):
    style = ""
    if priority == 0:
        style = "Alarms/AlarmDiagnosticActiveUnAcked"
    elif priority == 1:
        style = "Alarms/AlarmLowActiveUnAcked"
    elif priority == 2:
        style = "Alarms/AlarmMediumActiveUnAcked"
    elif priority == 3:
        style = "Alarms/AlarmHighActiveUnAcked"
    elif priority == 4:
        style = "Alarms/AlarmCriticalActiveUnAcked"
    else:
        style = "Alarms/AlarmLowActiveUnAcked"
    return style
def getColorFromAlarmPriority(priority):
    color = ""
    if priority == "Diagnostic":
        color = system.tag.read("[default]_System/Colors/AlarmDiagnostic").value
    elif priority == "Low":
        color = system.tag.read("[default]_System/Colors/AlarmLow").value
    elif priority == "Medium":
        color = system.tag.read("[default]_System/Colors/AlarmMedium").value
    elif priority == "High":
        color = system.tag.read("[default]_System/Colors/AlarmHigh").value
    elif priority == "Critical":
        color = system.tag.read("[default]_System/Colors/AlarmCritical").value
    else:
        color = system.tag.read("[default]_System/Colors/AlarmNull").value
    return color
def getPriorityStringFromPriorityInt(priorityInt):
    priorityString = ""
    if priorityInt == 0:
        priorityString = "Diagnostic"
    elif priorityInt == 1:
        priorityString = "Low"
    elif priorityInt == 2:
        priorityString = "Medium"
    elif priorityInt == 3:
        priorityString = "High"
    elif priorityInt == 4:
        priorityString = "Critical"
    else:
        priorityString = "InvalidInt"
    return priorityString

An example expression binding, running one of these scrips to obtain the colour:

runscript("alarms.getColorFromAlarmPriority",0,{view.custom.alarmpPriority})

I’ve tested this setup on a small number of devices and it works perfectly.

However:

According to the best practice guides I’ve read, large scale use of Python scrips is strongly discouraged by the developers for reasons of resource utilization due to Python’s lack of computational efficiency.

Ignition does not seem to provide for defining functions in Expression syntax, only in Python.

So my questions are:

  1. Is there any way to centrally define logical functions in Ignition which execute more efficiently than Python?
  2. If I replace all my colour maps with these Python scrips will it cause excessive compute utilization or cause other problems, both at current system scale or much larger scales?
  3. Are there ways of writing the Python scrips which might be significantly more efficient than using ‘if’ statements?
  4. Is there some other way of achieving what I’m trying to do that I might have missed? The intent is central definition of both the colours and the mapping functions.
  5. Has anyone deployed a large system where the colours and colour maps are centrally defined, or is everyone stuck with defining them N times using transform property bindings?

@nminchin @pturmel

Appreciate any opinions, feedback or experience,
Angus

Not sure if its best practice but personally I'd avoid the scripting and define all my colors in the stylesheet and then create a map transform on each object that maps the name of the appropriate color. If you stay consistent you can recycle the same bindings in many places which speeds up creation. Like the alarm priorities you map once and then just do copy binding and paste it for each type of object using those alarm colors.

If you're doing more than colors (border, change font, etc) then I'd put them into actual Styles as an intermediary step and then apply the style similarly. But sometimes if all I want is a highlight and nothing else I'd rather just map the color than create an entire extra set of style definitions. IMO.

1 Like

Defining functions in expressions (via my Integration Toolkit) is on my to-do list.

(FYI, you should use named colors, not hex colors.)

2 Likes

IMO, any directly applied color styling is a code smell. All color references should come from a standardized palette, and all multi-state displays should use colors that reference the palette.

Example:

1 Like

To be clear - the problem isn't Python, per se - while CPython is in general slow, it's moot in the context of Ignition's environment running Jython. In fact Jython is often faster than CPython over apples to apples tasks (because we're running on the JVM which will do various quasi-magical forms of optimization as code runs in hot paths).

What kills script performance in Ignition, especially in Perspective, is:

  1. The need to sync all property state between the front and backend (and associated synchronization logic across threads to ensure there aren't any race conditions)
  2. The overhead of starting a Jython interpreter and "compiling" the extension function into memory

That said, there are absolutely use cases where scripting is necessary, as even Phil and Nick will readily tell you. Just don't reach for it every time (again, especially in Perspective).

No, not really.
That said, while I wouldn't necessarily encourage it... with a custom module, you can encapsulate whatever business logic you want and present it to end users as a script or expression function. There's obviously a lot of extra overhead at the beginning, but it's not insurmountable, especially in the age of LLM coding assistants.

It's not really a meaningful optimization performance wise, but generally code style in Python is to avoid superfluous variable declarations. That is, your first snippet could be this:

def getStyleFromAlarmPriority(priority):
    if priority == 0:
        return "Alarms/AlarmDiagnosticActiveUnAcked"
    elif priority == 1:
        return "Alarms/AlarmLowActiveUnAcked"
    elif priority == 2:
        return "Alarms/AlarmMediumActiveUnAcked"
    elif priority == 3:
        return "Alarms/AlarmHighActiveUnAcked"
    elif priority == 4:
        return "Alarms/AlarmCriticalActiveUnAcked"
    else:
        return "Alarms/AlarmLowActiveUnAcked"

Less room for human error matching up variable names, less repetition, easier to scan.

However, as others have mentioned - you really should break up presentation from logic.
What color to use for things doesn't belong in a system tag - it belongs in something attached to the user interface, in this case Perspective, and the idiomatic way to centralize color definitions in any web page is via CSS. Define named variables/reference variables from the theme, using map/expression/etc bindings to return these values. Elide as much scripting as possible.

1 Like