Box in box modeling is killing your project

Seriously. Stop decomposing functions into hierarchies to hide complexity. You’re just making it worse.

There are powerful alternatives to box in box thinking and I’m here to show you one that, especially in large, complex systems, yields better results.

If you want modeling to really, actually…

  • Deliver transparency for your stakeholders and designers
  • Quickly expose real world edge cases and requirements gaps
  • Bake in safety from the beginning
  • Manage complexity
  • Stay stable as the requirements explode all around you

…this might be worth a quick read (11 min)

This isn’t a simple hack and I’m not here to sell a tool.

I’m here to warp your brain.

And while I use AI extensively to assist modeling and code generation, this article is written by me, a human being.

Hi, I’m Leon Starr and I’m going to walk you through problem solving with a concrete example from my 20+ years of modeling safety critical systems.

None of this depends on a particular notation or tool. The same partitioning applies whether you model in SysML v2, SysML v1 in Cameo / CATIA Magic or Rhapsody, Arcadia in Capella, or Executable UML.

Let’s model!


Our example is an automatic lubrication system, an embedded system that lubricates parts on a periodic schedule. We’ll focus on the injector which squirts the grease.

To do this, the injector needs to build up pressure. When enough pressure is attained, the injection begins. Various actions are taken when the pressure cannot be reached or maintained.

Here is the Injector lifecycle modeled with a state machine:

Take a moment to absorb the simplicity of this model.

It’s not a jumble of requirements lashed together. It is not a software design. It is merely a description of the behavior of a component that plays a critical role in our system.

Now let’s ruin that for you.

Where and how are we getting those above and below inject pressure events? Minor detail, eh? But we can’t fully specify our system without obtaining those events from the real world somehow.


Sensors or sensing devices of some sort provide digital or analog data that must be gathered, converted, and scaled to provide a numeric value corresponding to the current pressure. Outputs such as actuators reverse that flow, but we’ll keep the focus on sensor input.

We have an analog sensor that outputs a signal converted to pressure in some continuous range, say from 0-40 MPa accurate to ±0.1 MPa.

We could insert a state in the Injector lifecycle that periodically checks and converts the current pressure like this:

When an Injector first enters the BUILDING PRESSURE state, it sends a Check high enough signal to itself delayed by some polling interval and just sits in the state until the delayed signal arrives.

I use ALL CAPS for states waiting on external events. The Checking above state exits on its own internally generated signal when its activity completes.

After the interval expires, our Injector sees the Check high enough event and proceeds to compare the raw sensor value converted to MPa by the local Get pressure() method. If the value is high enough, the Injector sends an Above inject pressure signal to itself which puts the Injector in the INJECTING AT PRESSURE state where we have the grease flowing out to lubricate the destination equipment.

So we’ve added more executable detail to better express our requirements for monitoring real world input. And if this were just a toy problem we’d be okay here. But I assume you don’t build toy systems so we’re headed down a dark path. Specifically:

  1. Our simple lifecycle has been obscured. We could live with it, but it will get worse as we’ll need another state and two transitions to manage checking below the threshold.
  2. We are starting to cut & paste logic. A little is okay, but this is always a warning sign that something deeper is going on.
  3. If other elements of our system must be monitored, say the grease reservoir’s fill level sensor, we’ll need to keep up the cutting and pasting.
  4. Sensor signals typically flutter and spike causing us to oscillate between our two states, so we’ll want to filter that out with some kind of hysteresis.

A system with one sensor probably has a lot of them. And it is not uncommon to switch out sensor designs in the middle of a project. So cutting and pasting our signal handling will create a lot of extra modeling work and maintenance of our models.

Possible solutions…

1. Defer to implementation

Always a favorite. We could just say that the modeling should focus on the high level (whatever that means) and not get into design details. But that’s sloppy thinking. Numerous system requirements will no doubt constrain our polling intervals, our hysteresis, our required precision, and signal safety ranges. Our models can easily capture those, and they should so that we can ensure that any given implementation is valid.

2. Functional decomposition

Extremely popular! We could try to create a hierarchical state machine and push the sensor processing functionality down somehow. But we’ll still be functionally decomposing in all of our state machines. So the cut and paste just gets pushed down and dispersed across the leaves of our decomposition.

Some other issues with functional decomposition as it relates to modeling:

Stability and model maintenance: One of the things that exhausts a modeling project is when the models need to be continually refactored throughout a project. So you want your models to be built on those system principles that are least likely to fluctuate. What aspects of your requirements are most likely to change? The functions! After that the states, and finally the underlying theory of the system (semantics/ logic / constraints).

Redundancy and interface complexity: A functional decomposition splits and duplicates state and data information down into the leaves of the models. Then you need complex interfaces and checks to verify that someone’s decomposition hasn’t broken anything. Which leads us to…

Safety: In our example, we must descend through the hierarchy to ensure that signals are handled consistently and correctly for each device, for each sensor, for each threshold. But I’ve seen more than one organization hand each such function down to a separate engineer or team which ensures zero consistency across those deep hidden functions. This seems crazy, but it is often a result of a hardware mindset. The injector team, the Reservoir team, etc. Organizing software on device boundaries is a recipe for this nonsense.

I cover this a bit in my video series, but let’s move on to THE SOLUTION.

3. Factor out the entire subject matter

OK, ready to model outside the box? Forget about the functions and forget about the application.

Instead, focus on what we need to KNOW to manage signals. Is a signal incoming or outgoing? Analog or digital? How is the raw value converted and scaled? Are there particular values, thresholds, or ranges of interest? Multiplexed signal conversion groups? and so on. This is clearly a distinct subject matter that can be modeled.

A Range Limitation class, for example, represents a single limit on an incoming signal that has been converted and scaled. The limit itself is defined by a Point Threshold, which has a limit value and a direction. A rising threshold is crossed when the value goes above the limit, and a falling threshold is crossed when the value drops to or below it. To monitor a band with both an upper and a lower limit, we link the input point to two thresholds, one rising and one falling. That gives us two Range Limitation instances, one per limit, each tracking its own excursions independently. And a simple debounce solution is adequate for our hysteresis purposes.

We can model the data and relationships here:

With the lifecycle of a Range Limitation here:

Here we model a value moving in and out of range. Each time a new input point value is acquired, we compare it against a pre-specified limit. But debouncing is incorporated so that we buffer in the CHECKING IN RANGE or CHECKING OUT OF RANGE states until a pre-specified count is reached before concluding that the signal has truly crossed the limit.

The thing to note is that pressure or fill level itself is not part of the model. These are application concepts. The Signal I/O (SIO) domain can do its job without knowing anything about the meaning of the values it is handling.

But we do rely on the Lubrication domain to provide the specific values we configure for the limits and their directions, signal conditioning, and any configurable debouncing parameters.

You can picture the two domains side by side like this:

There’s no real hierarchy here. You might be tempted to say that Lubrication is high level with Signal I/O being the low level details. But if you model each of these domains, you will need to specify exactly the same level of executable detail in each one. Each domain requires the modeling of the logic, data, behavior, and actions. Same level of detail, just different subjects.

You also can’t say that one domain is ‘implementation’. The models specify platform independent features. It’s only the form in which each domain is realized in code that is platform specific.

It is more natural to picture the organization as a flat graph with the primary source of requirements, Lubrication, on one side pushing across, or down if you like, through any supporting service domains such as Signal I/O.

Fitting the Lubrication models to our Signal I/O domain

The full Injector lifecycle includes sleeping and monitoring states that I left out of the simplified version above to keep it focused. When the Injector enters a monitoring state, we need to let Signal I/O know that it should start monitoring the Injector’s pressure, and when the Injector goes to sleep, that it can stop. From the application’s perspective, pressure monitoring is simply on or off. It’s up to Signal I/O to decide when to activate each of the individual point threshold checks, and to send us an Above inject pressure or Below inject pressure event when one of those limits is crossed.

There are different ways to do this in practice. We could explicitly announce arrival in those states with an action that tickles the Signal I/O domain. Personally, I use an execution model that does this implicitly.

But the end result is that the application lifecycle reverts back to the simple example I showed at the beginning since it relies on Signal I/O to trigger the desired events.

We discussed this in some detail in Models to Code, though since its publication we’ve improved and simplified our domain bridging techniques and there’s better open source tooling to support it.


So what did we gain with this approach?

Simplicity and transparency

By factoring out signal management, our lifecycles remain clean and intuitive. Not only are they easier to understand, it’s easier to communicate the behavior of our system to the stakeholders.

Safety and consistency

It’s not that we’ve decided to abandon detail. Quite the contrary. Now that we have a white hot spotlight trained directly on our application, we can delve into the nuances without distraction. Those little edge case cockroaches have nowhere to hide. Using state tables we can explore unexpected event / state combinations. Our relationship modeling uncovers logical inconsistencies. And our action language uncovers computational oversights.

The subject matter decomposition not only separates out signal management from the application, but it ensures that signals are handled in a uniform and consistent manner. So once we get our Signal I/O pipeline working, we know that it works across the board for all signals. We don’t have to worry that the team/agent handling pressure input makes a mistake that the team/agent handling fill level manages competently. We don’t have to worry that a new hire writes (or prompts) similar but incorrect code.

Stability and maintenance

You can rethink the Signal I/O domain with minimal effect on the application. Lubrication expects an Above inject pressure event, but it doesn’t care what specific mechanism is used to produce that event. So you can change the debouncing / hysteresis algorithm and otherwise manage the signals differently without having to touch the application models.

You can add new devices and properties, different injector types, reservoir configurations, pumps, whatever, without necessarily impacting the Signal I/O models. But you will need to supply new data to the Signal I/O domain to be configured into its parameter values and initial instances.

Of course, a fundamentally new kind of device interface may require further modeling in the Signal I/O domain. And that’s why we have a requirements dependency arrow from Lubrication. Signal I/O must support the needs of our application.

Reusability

By keeping any mention of lubrication out of our Signal I/O models, we can drop that domain into any system with sensors and actuators just by supplying the appropriate configuration data.


Modeling outside the box

But if domain partitioning works so well, why is functional, box in box modeling so common? I think it is because it’s more intuitive. You don’t really need much experience to just start sorting functions and devices into boxes. I mean, it’s what I used to do when I started out! The horrible consequences of functional decomposition don’t reveal themselves immediately, but with scale and complexity, they do. And once you realize it, it’s generally too expensive, even with AI, to go back. without expense and risk.

The two-domain Lubrication / Signal I/O example is intentionally small and focused, but it is representative of what I’ve done in much larger and more complex safety critical systems spanning multiple domains, both modeled and unmodeled.

It really helps to have gone deep into a variety of diverse systems (medical diagnostics and pacing, autonomous driving, aerospace and avionics, battle simulation, semiconductor fabrication) to quickly recognize domain boundaries and see the deep consequences of various partitioning schemes.

My clients are organizations typically building safety critical systems. They use models such as SysML v2/UML as part of the system engineering process.

AI and Model Based Systems Engineering

How AI, Systems Engineering, and Model Based Systems Engineering (MBSE) fit together is an evolving story. The topic deserves its own article, but here are two of my observations from real projects.

1. Domains as AI safety firewalls

Domain partitioning has been helpful in drawing a boundary between two kinds of subject matter. On one side you have a domain where an opaque, non-modeled AI solution is ideal, free to run loose and adapt organically. On the other you can have a transparent, modeled domain that serves as its validator and safety monitor.

2. Model vs. natural language prompts

Depending on what you are building, you can get faster results, fewer iterations, and spend a lot less on tokens by feeding models rather than natural language as prompts. In general, the more nuanced and complex your problem, the more likely that model prompting will get the job done faster and safer than vibing.


Getting results on your project

If you’re just building a marketing dashboard, you can (and probably have) ignored all this, but my clients are focused on substantial safety critical systems that must operate safely over long periods and want:

  • Transparent solutions
  • Rapid identification of edge cases and requirements gaps
  • Complexity management
  • Configurable and reusable solutions
  • Stable, manageable system architectures
  • Safety and a clear path to ongoing validation

If your project or organization is similar, I’m here to help with whatever modeling standards or tools you are using. My contact information is here on the Model Integration site.