Check asserted constraints from Python
New in v0.11.0
A model can state what must hold as an assert constraint inside a definition.
syside.evaluation checks those statements against the instances that bind
concrete values, from a script, and returns one verdict per instance and
constraint. Use it when you want the check inside your own Python workflow: a
report, a pre-commit hook, a test. The Solver answers
the same question from the command line and, unlike this package, also covers
values the model leaves open.
You need a model loaded with load_model and an
assert constraint whose feature references resolve to values on the instance
being checked.
Example
The model defines a wheel with a mass limit and four wheels, three of which bind a mass:
package WheelBudget {
private import ScalarValues::Real;
part def Wheel {
attribute mass : Real;
assert constraint massLimit { mass < 20.0 }
}
// Each wheel binds its own mass, so the constraint can be evaluated
// for it.
part frontLeft : Wheel {
:>> mass = 12.0;
}
part spare : Wheel {
:>> mass = 25.0;
}
part built : Wheel = new Wheel(mass = 7.0);
// No value is bound, so nothing can be concluded for this wheel.
part unknown : Wheel;
}
check_model evaluates every assertion
against every non-library usage it applies to:
import pathlib
import syside
from syside import evaluation
EXAMPLE_DIR = pathlib.Path(__file__).parent
MODEL_FILE_PATH = EXAMPLE_DIR / "example_model.sysml"
def main() -> None:
(model, diagnostics) = syside.load_model([MODEL_FILE_PATH])
assert not diagnostics.contains_errors(warnings_as_errors=True)
# One result per (instance, assertion) pair, in model order.
results = evaluation.check_model(model)
for result in results:
print(
f"{result.verdict.name:<11} {result.instance.qualified_name} "
f"against {result.assertion.qualified_name}"
)
if result.detail:
print(f" {result.detail}")
violated = [
result
for result in results
if result.verdict is evaluation.Verdict.VIOLATED
]
print(f"\n{len(violated)} violated constraint(s)")
if __name__ == "__main__":
main()
The script prints:
HOLDS WheelBudget::frontLeft against WheelBudget::Wheel::massLimit
VIOLATED WheelBudget::spare against WheelBudget::Wheel::massLimit
HOLDS WheelBudget::built against WheelBudget::Wheel::massLimit
UNDECIDABLE WheelBudget::unknown against WheelBudget::Wheel::massLimit
cannot determine whether the constraint holds for all instances of WheelBudget::unknown: 'mass' (has no value binding) -- each can be redefined by a type that specializes WheelBudget::unknown, so the verdict here need not hold for every type that specializes it
1 violated constraint(s)
spare is reported because 25 is not below 20. built is checked through its
constructor, new Wheel(mass = 7.0), so a value passed to new counts as bound.
unknown binds nothing, and the verdict says so instead of passing it.
Reading a verdict
Each ConstraintResult carries the
instance, the assertion, a verdict, the raw value the compiler returned,
and a detail string that is filled in when the verdict is not a plain yes or
no.
Verdict |
Meaning |
|---|---|
|
The constraint evaluated to |
|
The constraint evaluated to |
|
The constraint could not be reduced to a single boolean for this instance:
a referenced feature has no value, or the expression is outside what
|
|
The instance both constructs a value with |
A constraint is never reported as holding by default: anything the compiler cannot
decide is UNDECIDABLE, so a script that only looks for VIOLATED should also
count UNDECIDABLE results, or it will miss the constraints it did not check.
Checking one instance
check_instance takes a single usage
and returns the results for the assertions that apply to it. It differs from
check_model in one respect: an unsupported instance raises
UnsupportedInstanceError
instead of producing an UNSUPPORTED result, so a caller that asked about one
specific instance is told rather than handed a verdict it might overlook.
Limits
The evaluable fragment is that of
Compiler.evaluate: model-level evaluable expressions as the SysML v2 specification defines them. Constraints outside it come backUNDECIDABLE.Instances are checked one at a time with the values they bind. To ask whether a constraint holds for every value the model allows, use the Solver.
check_modelscans every usage in the model, so its cost grows with model size times the number of assertions. It is meant for whole-file checks, not for a loop that runs on every edit.