Check asserted constraints from Python
New in v0.11.0
A model can state what must hold as an assert constraint inside a definition.
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 a car with four wheels, three of
which bind a mass. Save it as example_model.sysml:
package WheelBudget {
private import ScalarValues::Real;
part def Wheel {
attribute mass : Real;
assert constraint massLimit { mass < 20.0 }
}
part def Car {
// Each wheel binds its own mass, so the constraint can be checked
// 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;
}
}
Checking is one call. check_model
evaluates every assertion against every usage it applies to and returns the
results as a list. Save the script beside the model as check_constraints.py:
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)
# Every `assert constraint`, checked against every instance it applies to.
results = evaluation.check_model(model)
for result in results:
print(f"{result.verdict.name}: {result.usage.qualified_name}")
if __name__ == "__main__":
main()
Run python check_constraints.py and it prints:
HOLDS: WheelBudget::Car::frontLeft
VIOLATED: WheelBudget::Car::spare
HOLDS: WheelBudget::Car::built
UNDECIDABLE: WheelBudget::Car::unknown
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
usage, the assertion, a verdict, the raw value the compiler returned,
and a detail string that explains the verdict when it is not a plain yes or
no. For unknown above, detail reads “cannot determine whether the constraint
holds for all instances of WheelBudget::Car::unknown: ‘mass’ (has no value
binding) …”.
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 usage 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 usage
check_usage 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 usage raises
UnsupportedUsageError
instead of producing an UNSUPPORTED result, so a caller that asked about one
specific usage 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.