MBSE iNsightsMBSE iNsights

· 9 min read

Four Ways to Constrain an Action in SysML v2

A use case tells you whether a step happened. Constraints are how you say how well it was performed — and there are four places a constraint can read from, only one of which needs an output parameter.

If you derive requirements from a SysML v2 use case, each step gives you an obligation shaped roughly like this:

WHEN Request Route has completed, the Navigation Unit shall compute the route.

That is a real requirement, and it is worth having. It is also satisfied by any implementation that returns something — however slowly, however far round the houses, however unsure of itself.

That is not a flaw in the use case. It follows from what a use case is. A use case is an existential statement: there is an interaction that goes like this. “Within two seconds” is a universal claim about every run. One does not produce the other, so if you want the second, something else has to say it.

That something is a constraint. This article is about the four places a constraint can read from, where you write it, and what it turns into.

A constraint is a Boolean expression over what is in scope

That really is the whole of it. A constraint declares a predicate. The scope it sits in decides which names it is allowed to use.

Written inside an action, the names in scope include:

  • the action’s own parameters, unqualified — computeSeconds
  • attributes the subject declares — unit.queueDepth
  • another step’s parameters — 'Compute Route'.computeSeconds
  • use-case-level parameters
  • library functions and enumeration literals

There is a consequence here that is easy to miss: an action does not need an out parameter to be constrainable. A parameter is one source of names, not the mechanism. Of the four forms below, only the first uses one.

Four ways to constrain a step

1. Over its own output

The most familiar form. The action declares what it produces, and the constraint reads it.

action 'Compute Route' {
    out computeSeconds : Real;
    out searchedNodes  : Integer;

    constraint 'Computed Within Two Seconds' { computeSeconds <= 2.0 }
    constraint 'Search Stayed Bounded'       { searchedNodes  <= 50000 }
}

The second one is worth a moment. searchedNodes <= 50000 says the search terminated by design rather than by luck. That is a “how well” claim with nothing to do with time, which is a useful reminder that performance is not the only quality a step can have.

2. Over a state the subject holds

No output parameter at all.

subject unit : NavigationUnit {
    attribute queueDepth : Integer;
}

action 'Accept Requests' {
    constraint 'Queue Stayed Short' { unit.queueDepth <= 10 }
}

The action declares nothing, and the bar still lands on that step’s requirement — because the requirement is about the step’s occurrence, and the constraint is a claim about the world at that point in the run.

One thing to know about held state: it has to be declared, never inferred. Nothing in first A then B says which of A’s effects survive into B, so a latched value and a transient one look identical in the flow. Declaring queueDepth on the participant is what makes it resolvable everywhere — as a part feature, in a guard, in a requirement, and in evidence.

3. Over another step’s output

A relative bar, rather than an absolute one.

action 'Replan Route' {
    out done : Boolean;
    constraint 'No Slower Than The First Plan' { 'Compute Route'.computeSeconds <= 5.0 }
}

This is the form to reach for when the obligation is really this step must not undo what an earlier one achieved.

4. Over an input

action 'Compute Route' {
    in budget : Real;
    out answer : String;

    constraint 'Budget Was Positive' { budget > 0.0 }
    constraint 'Answer Was Produced' { answer != "" }
}

Both of those are written the same way, but they do not mean the same thing — which brings us to the rule that governs it.

Outputs are required; inputs are assumed

A constraint over what a step was handed is a condition on the handover, not an obligation on the step.

If you write that as a require, you are obliging the performer to produce its own input. Downstream, that becomes a test in which the harness checks that its own injection worked — a green result that proves nothing about the system.

So the rule is:

  • A constraint reading only the step’s in parameters derives an assume.
  • One reference to anything else — an output, a held state — and the whole clause becomes a require, because the system is answerable for it.

Applied to the example above:

assume constraint budgetWasPositive { evidence.budget > 0.0 }
require constraint answerWasProduced { evidence.answer != "" }

It is decided from the declaration, not from where the reference happens to resolve. An in parameter has no evidence of its own — what records it is the evidence for the output that feeds it, so resolution lands on the producing step and says nothing about which step is reading. The in keyword on the parameter is what settles it.

Where require and assume actually live

This is the part most worth being slow about, because the relationship is easy to get backwards.

require and assume are not things you write inside an action. They are clauses of a requirement. And the requirement is the thing that has a subject.

The subject is what the requirement is about. It can be an action — and when it is, the require and assume clauses inside that requirement are what constrain that action. So the chain runs:

a requirement has a subject → the subject here is the action → the require clause inside the requirement is what places the obligation on it.

You write the bare constraint in the action. The derivation builds the requirement around it.

WHAT YOU MODELa bare constraint, inside the actionaction 'Compute Route' { out computeSeconds : Real; constraint { computeSeconds <= 2.0 }}derivesWHAT IS DERIVEDrequirement computedInTime { subject : 'Compute Route'; require constraint { evidence.computeSeconds <= 2.0 }}the subject is the action`require` and `assume` are clauses of the requirement, never of the action
You write the bare constraint in the action. The derivation builds a requirement around it, gives that requirement the action as its subject, and turns the predicate into arequire clause. That is how a constraint becomes an obligation on a step.

The language provides this shape outright. From Requirements.sysml in the standard library:

requirement def FunctionalRequirementCheck :> RequirementCheck {
    doc /* A functional requirement specifies an action that a system,
           or part of a system, must perform. */
    subject: Action;
}

requirement def PerformanceRequirementCheck :> RequirementCheck {
    doc /* A performance requirement quantitavely measures the extent to which
           a system, or a system part, satisfies a required capability or
           condition. */
    subject: AttributeValue;
}

Note the two subjects. A functional requirement takes an Action as its subject. A performance requirement takes an AttributeValue. Both are in the standard library for exactly this purpose.

The model states the predicate; the derivation writes the requirement around it. That is the same division of labor a use-case derivation applies to everything else it reads — an out parameter is not an obligation either until the derivation makes one.

A note on assert

There is a natural-looking way to write a performance bar that means something different from what it appears to:

action 'Compute Route' {
    out computeSeconds : Real;
    assert constraint 'Computed Within Two Seconds' { computeSeconds <= 2.0 }
}

That parses, and it is legal SysML v2. What it says, though, is that the computation is within two seconds — a statement of fact rather than a requirement to meet.

The standard library is explicit about it. From Constraints.sysml:

assertedConstraintChecks is the subset of constraintChecks for ConstraintChecks asserted to be true.

The usage in the shipped libraries matches. There are 38 assert constraint usages across the Systems and Domain libraries, and none at all in the Kernel libraries. Every one is an invariant — something true by construction of the thing being defined:

// Quantities.sysml — a fact about tensor arithmetic
assert constraint orderSum { contravariantOrder + covariantOrder == order }

// MeasurementReferences.sysml — dimensional consistency
assert constraint validSourceTargetDimensions { source.dimensions == target.dimensions }

They sit on item def, attribute def, connection def, calc def and one state def. Not one of them is a target anybody has to hit. Even the two on behavioral definitions state a structural truth about the definition rather than a bar a performance must clear.

So for a quality bar, a bare constraint is the one you want. It asserts nothing. It declares a predicate over the step’s features and leaves open who owes it — which is exactly the question the derivation exists to answer.

What an action body admits

For reference, here is what is actually allowed inside an action:

In an action body
constraint 'Name' { … }declares a predicate
assert constraint 'Name' { … }legal, but claims it is already true
require constraint { … }syntax error
assume constraint { … }syntax error

require and assume are not merely discouraged in an action body — the grammar does not admit them there at all. The published BNF puts them, and subject, inside a requirement:

RequirementBodyItem  →  SubjectMember | RequirementConstraintMember | …
SubjectMember        →  'subject' …
RequirementKind      →  'assume' | 'require'

subject, require and assume are siblings in the same body, which is the clearest statement of the relationship: the requirement is what has a subject, and the require and assume clauses are what it says about that subject. ActionBodyItem admits none of the three.

Where you put the bar decides what it costs

The same numeric bar can be written in three places, and they price differently:

WrittenAboutDerives
constraint in the actionthat one stepa require clause on that step’s requirement
constraint def over an item typeevery step producing that itema named clause wherever the item flows
#postcondition on the use casethe run as a wholea requirement of its own

The difference shows up in what has to be instrumented. A bar over an item the subject hands across the boundary is readable by a test already — it costs nothing extra. A bar over an action’s own internal parameter is a probe the application has to expose, and something has to build it.

That is an architecture decision priced at modeling time, while the design can still change.

It also makes trade studies cheap. Tightening a bar is one edit: computeSeconds <= 2.0 becomes <= 0.5, and the constraint, the shall-statement and the generated assertion all move together. Two constraint defs side by side — say RouteAcceptableEconomy and RouteAcceptablePremium — are two points in a trade study over one unchanged use case. And where no trade study is run at all, the constraints still draw the line between an execution that counts as valid and one that does not, which is what a functional test needs in order to fail honestly.

#observable is not a way to constrain a step

Worth naming, because it can look like one. #observable changes a step’s observability classification — it says the running application publishes this step. It creates nothing to constrain and levies no obligation.

Reach for it to lower the instrumentation bill, not to state a requirement.

How to check a claim like this

Two things that are easy to mistake for verification, both worth knowing about.

A syntax check is not a validity check. Running a file through a SysML v2 parser tells you it parses. assert constraint { computeSeconds <= 2.0 } parses perfectly and means the wrong thing. Syntax cannot catch a misused keyword.

A linter’s silence is not evidence either. A semantic checker that finds constraint blocks with a pattern like

/\b(require\s+)?constraint\s*\{/

only ever matches an unnamed block. A named constraint — assert constraint validSourceTargetDimensions { … } — is never extracted, so it never warns. It is easy to read that silence as confirmation.

What does settle it: the grammar, for what is admissible where; the shipped standard libraries, which are the language authors’ own usage and the only place to learn what a keyword is for rather than where it is allowed; and deriving the model and reading the output, because a construct that is legal and meaningful may still not be read by your tooling.

Two small things that save time

An unfed input gets reported. in budget : Real with no flow supplying it raises a diagnostic — the step consumes data the model never supplies — and any constraint referencing it raises a second, because there is no evidence for it to resolve against. That is the tool working correctly, but it is not what you want appearing during a demonstration.

Name parameters in lowerCamelCase. A derived identifier lower-cases a name’s whole first token, so out secondsA : Real becomes the evidence attribute secondsa. It is self-consistent and it resolves, but it reads as a typo on screen. computeSeconds is safe.

In short

  • A use case yields whether a step happened. Constraints are how you say how well.
  • A constraint is a Boolean expression over what is in scope, so an action needs no output parameter to be constrainable — a held state, another step’s output, or a use-case parameter will do.
  • Outputs are required; inputs are assumed. A bar over what a step was handed is a condition on the handover.
  • require and assume belong to a requirement, and the requirement is what has a subject. When that subject is an action, those clauses are what constrain it.
  • assert constraint states that the predicate is already true. All 38 uses in the shipped libraries are invariants. Use a bare constraint and let the derivation decide who owes it.
  • Where you put the bar decides what it costs to verify, and that price is visible while the design can still change.

More of this, on video

Walkthroughs in SysML v2 and MBSE tooling, with the model on screen.

Subscribe on YouTube