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.

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

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

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:

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.

Hi all, thanks for replies.

TLDR Summary

Centrally defining styles and/or colours can be achieved in Perspective via:

  1. The Perspective style manager - defines styles.
  2. The Perspective CSS stylesheet - defines both styles and named colours.
  3. Themes, which are located in the Ignition server’s installation directory - define both styles and named colours. (%installDirectory%\data\modules\com.inductiveautomation.perspective\themes\myThemeName\variables.css).
  4. Memory tags of string data type, but this is discouraged for compute reasons. Named colours via the CSS stylesheet or the Themes in the server’s installation directory are recommended instead.

Creating a central definition for the mapping logic can presently only be done in Python. This is discouraged for compute reasons. Technically it’s possible to define custom expression functions by building a custom module, but I’d like to avoid this.

Standard practice seems to be defining colour mapping logic N times for N instances. I really dislike this and will try to see whether the Python logic can be made viable by better understanding the compute model.

Detailed Responses

@Nol - thanks for the suggestions, I didn’t realise it was possible to define colours in the CSS stylesheet. I think your method achieves half of what I’m looking to do (centrally defining the colours) but not the second half (defining the mapping function only once).

@pturmel

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

Fantastic. This should cover what seems like a non-trivial gap in Ignition’s capabilities.

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

Got it, yes this looks like the best way to do it. Seems like these can be defined either under Styles/stylesheet.css or in Ignition’s installation directory - is the main difference here that using the installation directory allows the defining of multiple themes? Disadvantage of the install directory seems to be that it’s non-obvious to a developer who doesn’t know about it.

@bmusson

Completely agreed. Seems like the best solution is styles and named colors defined in CSS stylesheet or themes on the server directory.

@paul-griffith

To be clear - the problem isn't Python, per se…

Very interesting, appreciate the detail.

Is the idea here that the Python is being executed on the server side, then sends the results back to the client? How does this differ from the execution of logic defined via the tag binding interface?

I’d like to understand this in more detail. Say a Python function is called from multiple views on a page to determine which fill colours to use on the view components. How many times is that Python function compiled? Seems like there’s a range of plausible options.

  • Once only, when the Ignition server first boots.
  • Once per client session, when a client begins a session or on first use of the function by that client.
  • Once per page, when a page using that function is opened.
  • N times for N calls from that page, all done when the page is opened but not on subsequent calls of any given instance.
  • Every time the function is called.
  • Something else.

Good to know and appreciate the candor here.

I respectfully suggest that Ignition could be improved by adding something like this. A simple version could be allowing for definition of Expression Language functions like Paul is working on; a more comprehensive version could be allowing bindings in general to be abstracted.

This would allow basic logic functionality to be defined once in a way which is efficient to compute, rather than having numerous definitions for identical logic (60+ times in my case).

I’d say we’re unlikely to do this due to perceived effort and maintainability complications, but good to know it’s an option.

Broad further questions

I’d like to understand as much as I can about Ignition’s compute model, especially how Python script execution compares to tag binding execution. I’m scouring the forum and internet for details; there are some posts comparing the efficiency of different bindings but haven’t found anything comprehensive yet. Any pointers to resources, or further detail here would be appreciated.

Is ubiquitous use of Python functions for basic functionality likely to result in laginess and/or excessive server CPU load in real use in large systems? I note there’s at least one guy on the internet who seems to be recommending this, although it’s not clear that he’s actually tried it or if the solution is efficiently scalable to large systems.

Both pay the same network transmission overhead, but because scripts are unbounded user-defined work we have to move them to a dedicated thread pool (paying thread transfer costs) plus synchronization penalties to wall-clock time. In unbounded pure-CPU workloads without locking, Jython beats (pure) CPython, hands down.

Per function definition, the "compilation" work happens lazily, once per project, regardless of how many sessions/pages/views are open in that project. The trigger to bust the cache is, if I remember correctly, any change to that project that affects Perspective itself or the scripting library.
There's unavoidable overhead to marshal objects from their Java representation into a Jython representation and some other bookkeeping, but it's relatively cheap to call the same script repeatedly after it's been "compiled".

This is almost certainly a smell about something else fundamental to your design, rather than a missing capability in Ignition. That is, this in particular sounds like an XY problem, even if I am personally on board for something like a lamdba expression function. I've even gone on record as such.

Native bindings are always more efficient than expressions are always more efficient than scripts.

If you can express something using bindings or expressions, do so - not only is it more performant, it's more easily maintained, because it's harder to bake bad habits into purely declarative logic, although with Phil's toolkit it's technically possible to do more complex operations via expressions.

That OP's advice is paraphrased from advice typically given here on the forums, and only applies once you have had to reach for scripting. Again, there is nothing wrong with scripting, and it's absolutely necessary for essentially every Ignition project. But it should not be your first resort.
Once you do start using scripting, resist the siren's song of the "magic pushbutton". Extract at least the core business logic into the project library, ideally with "pure" data inputs and outputs, totally separate from any logic tied to the UI. The maximalist take some (e.g. Phil) will espouse is to never write a script in the rest of the system that's longer than a one-line call to a project library script. This gives you, for one, vastly improved version control semantics and at least some hope of external testing. In my opinion, it's a little too strongly dogmatic, but Phil's also got a lot more real world experience with Ignition than I do, so take my advice with a grain of salt.
Note that moving scripts to the project library has no real execution performance advantage nor drawback.

Ooooooo! New label for me! I like this one!

FWIW, I've never regretted moving any particular operation to the project library. I have often regretted not having moved some event's code.

Replying the the OP.

I very rarely use map transforms given they are by nature always going to be “magic expressions” – you cannot centrally define them, and to me this is a massive design flaw to use them.

Past Experience

In the past, I have created expression tags in my device UDTs which map key enum tags like “status“ and “mode” tags from integer values into their descriptive names like “Stopped”, “Running”, “Opened”, “Closed”, etc.

Then I would have dataset tags for each of these key tags (Status / Mode) that stores a lookup of the descriptive name to the corresponding style class to use for representing it.

e.g.

The style classes themselves would reference CSS colour variables within the CSS props needed (background-color, color, fill, etc.) and have animcations on them where needed (e.g. “Starting” pulses the colour).
As an aside, this (animations) is one important reason that you should create style classes instead of just referencing CSS colour variables within the GUI props themselves.

Then in the GUI I would use expression bindings with:

lookup({System/Datasets/Style Class Maps/Device Status}, {<devices status tag value>}, "INVALID", "Value", "StyleClass")

This worked really well as it:

  • centralised and named the colour definitions for device colours within CSS variables

  • centralised and named the styles to use for different device states, which may include animatations where needed

  • centralised the mapping of key tag integer values into their corresponding descriptive text

  • defined and uses a standard set of these descriptive text and associating them to a corresponding styling

  • provided plc-agnostic configuration for PLCs where you cannot control the standards employed. e.g. Pump status enum might be non-standard 0-Running, 1-Stopped instead of 1-Running, 0-Stopped.
    This was one of the main drivers for me to go down this path to begin with

  • expression tag values I’m told are not translatable, so won’t work for projects requiring such
    I’ve just tested this however and I don’t see this being an issue @pturmel ?


    Added translation for Africaans: Manual → Mani. Forced project locale to be Africaans.

    In the client it translates ok:

    image

Where this can be improved upon:

  • expression tags update regardless if they’re displayed on a client or not. The higher the count of UDT instances in the gateway, the higher the number of unecessary updates.

However, despite this, I have a gateway with a split architecture (frontend/backend) running like like this that has over 600,000 total tags (~300,000 opc tags) where the CPU sits around 5% on the backend. The server is has 8 cores and 18GB RAM to Ignition, so it’s not actually that high spec’d.

The (possible) Future

After speaking to Phil however, where I will go with this will be to instead move the lookup and translation/mapping logic from the UDT instance level into the GUI to avoid unecessary expression tag updates.

With the above in mind, the full stack will be something like this:

  • Define colour variables in CSS via theme files or the adv stylesheet. Unless these colours need to change based on theme, they’re ok and more accessible to put them in the stylesheet. If they need to change however, then they obviously need to be in the theme files
  • Define Perspective Style Classes that reference your colour variables. You will likely have multiple sets of these which define the colours in different properties, like fill, background-color, color etc.for different purposes like: labels, symbols, icons, etc.
  • Define a document tag to store, for each device type (e.g. Motor, Valve, etc. – allow for different versions) and tag that needs it (e.g. Status, Mode, etc.) a map of the integer state of the tag to its:
    • descriptive, translatable text e.g. 2 => “Running”, 0 => “Stopped”, etc.
    • style class to use (most likely this will be a dict that defines the style class for the different usages e.g. Symbol, Label, Icon, Stroke, etc.
  • In your device UDTs, add a UDT parameter “deviceEnumType” which will define which styleclasses to use.
  • In your GUI, you can then use an expression binding to bind to the tag (using a direct tagpath, not the tag function) and reference the descriptive text and the style class to use, based on the device type from the tag itself and the value of the tag.

In practice, something like this (this is a concept at this stage that I have working):

Style Classes:

Tag: System/Styling/Device Type Enums (document type)

(colourVar probably shouldn’t be there)

Value:

Tag: UDT Instance

Using it in the GUI

For labels:

For the pump symbol:

PS. Ignore the relative parent-referencing used in these expressions! This is just a jerry-rigged test and I would never recommend using parent referencing (i.e. the {………../Motor…} syntax! Always use View custom props

I just want to mention that Inductive has a standard built in color pallet for us color uncoordinated people:

I find this covers my use cases 99% of the time.

Special Note: We only had 24 crayons when I was in school in the 70’s, and 16 colors for HMI programming in the 90’s. You kids have it good.

This is what we do too, except we drive things off the raw state values.


The dataset defines:

  1. The human-readable name of the state.
  2. A list of style classes for the state.
  3. A corresponding icon for the state.

There's tons of duplication since each device has its own mappings, but it's simple to manage and extremely flexible. If you need to customize a device, you can apply the overrides right there.

This is very similar to what I have been doing, but looks a lot more heavy-weight in terms of the numbers of tags required – Phil will not be happy haha.

However, my reasoning originally for using expression tags instead of the logic in the GUI was for scaling. We have ~150 clients at one customer’s site. I didn’t do the maths before, but I estimate now that this would end up equating to ~4,000 to 6,000 of these tags being subscribed to (13-20 UDT instances displayed per client) at any time. When they had less tags, this might have made sense to have my expression tags for Status and Mode. However I’ve just done the numbers and this project has 7,900 UDT instances that have at least a Status and Mode expression tag. That means 15,800 of these two expression tags which is up to 4x more than the number of subscriptions I estimated the clients would be using… So looking at those numbers, it makes sense that it’s probably more scalable to move this logic into the client instead, which was also Phil’s suggestion

I 100% agree in principle, but we've never seen even a hint of performance issues / gateway stress with this method :man_shrugging:. Our underlying sources for state changes are usually low frequency though.

Without some kind of "benchmark" between these strategies (tag-based vs client-based) it just seems like a theoretical discussion.

This was also a key detail in my original decision as well, given status and modes of devices change infrequently, it shouldn’t put too much load on the system. Now you have me reconsidering a change…

What I don’t particularly like about using a document tag to store the info is that it’s free-form; you can’t specify a schema and so it’s up to the user to get them right, as well as burdon-some to populate, and if, heaven forbid, you have to add a new thing into each one… of couse, I could also build an interface to manage it, but that’s an extra construction and maintenance hassle

There are definitely issues with storing UI related properties in tags, but I don't think performance is one of them.

IMO the biggest issue is having to keep everything in sync.

Tags are inherently gateway scoped, while style classes/icons are inherently project scoped (yeah you can use inheritance to propagate shared resources, but still). It's very easy to find yourself in a state where you don't have the resources you need to correctly display a device.

Also I have no idea how this approach works with source control. Probably not well.

Once these styles are defined though they should be set and forget, particularly these low-level device styles which are usually one of the first things to be defined.

"value": "{\"columns\":[{\"name\":\"value\",\"type\":\"java.lang.Integer\"},{\"name\":\"desc\",\"type\":\"java.lang.String\"},{\"name\":\"p_style\",\"type\":\"java.lang.String\"}],\"rows\":[[0,\"Stopped\",\"\"],[1,\"Running\",\"\"],[2,\"Starting\",\"\"]]}",

It's actually not too bad! It could be worse...

They key "Manual" is terrible, as it can be confused between user manual and manual modes and perhaps other "hand" implications. If you do the enum-to-string conversions in your UDT, at least generate unique translation keys and populate the English column in the translations table.

(That was in the advice you linked.)

It is good to see large applications with UDTs doing these sorts of UI operations not making too bad a workload. I still wouldn't mix gateway scope raw data with UI stuff, excepts as keys to appropriate UI choices.

Keeping UI choices in the UI projects, including per-client language, keeps everything sane for later improvements.