Source code for momba.jani.dump_model

# -*- coding:utf-8 -*-
#
# Copyright (C) 2019-2020, Maximilian Köhl <koehl@cs.uni-saarland.de>

from __future__ import annotations

import typing as t

import dataclasses
import enum
import functools
import json
import warnings

from .. import model
from ..model import context, expressions, operators, properties, types, functions
from ..utils import checks
from ..metadata import version


JSON = t.Union[None, int, float, str, t.Sequence["JSON"], t.Mapping[str, "JSON"]]

_JANIMap = t.Dict[str, JSON]


[docs]class ModelFeature(enum.Enum): """ An enum representing optional JANI model features. """ ARRAYS = "arrays" """ Support for arrays. """ DATATYPES = "datatypes" """ Support for datatypes. """ DERIVED_OPERATORS = "derived-operators" """ Support for derived operators. """ EDGE_PRIORITIES = "edge-priorities" """ Support for edge priorities. """ FUNCTIONS = "functions" """ Support for functions. """ HYPERBOLIC_FUNCTIONS = "hyperbolic-functions" """ Support for hyperbolic functions. """ NAMED_EXPRESSIONS = "named-expressions" """ Support for named expressions. """ NONDET_SELECTION = "nondet-selection" """ Suport for non-deterministic selection expressions. """ STATE_EXIT_REWARDS = "state-exit-rewards" """ Support for state exit rewards. """ TRADEOFF_PROPERTIES = "tradeoff-properties" """ Support for tradeoff properties. """ TRIGONOMETRIC_FUNCTIONS = "trigonometric-functions" """ Support for trigonometric functions. """ X_MOMBA_OPERATORS = "x-momba-operators" """ Support for Momba non-standard operators. """ X_MOMBA_VALUE_PASSING = "x-momba-value-passing" """ Support for Momba non-standard value passing. """
_NamedObject = t.Union[model.Location, model.Automaton] @dataclasses.dataclass class JANIContext: allow_momba_operators: bool = False _name_counter: int = 0 _actions: t.Set[str] = dataclasses.field(default_factory=set) _names: t.Dict[_NamedObject, str] = dataclasses.field(default_factory=dict) _features: t.Set[ModelFeature] = dataclasses.field(default_factory=set) @property def features(self) -> t.AbstractSet[ModelFeature]: return self._features @property def actions(self) -> t.AbstractSet[str]: return self._actions def get_unique_name(self) -> str: name = f"__momba_{self._name_counter}" self._name_counter += 1 return name def get_name(self, obj: _NamedObject) -> str: if obj.name is None: try: return self._names[obj] except KeyError: name = self._names[obj] = self.get_unique_name() return name return obj.name def require_action(self, action: str) -> str: self._actions.add(action) return action def require(self, feature: ModelFeature) -> None: self._features.add(feature) @functools.singledispatch def _dump_type(typ: model.Type, ctx: JANIContext) -> JSON: raise NotImplementedError(f"dump has not been implemented for type {typ}") @_dump_type.register def _dump_type_integer(typ: types.IntegerType, ctx: JANIContext) -> JSON: return "int" @_dump_type.register def _dump_type_real(typ: types.RealType, ctx: JANIContext) -> JSON: return "real" @_dump_type.register def _dump_type_bool(typ: types.BoolType, ctx: JANIContext) -> JSON: return "bool" @_dump_type.register def _dump_type_clock(typ: types.ClockType, ctx: JANIContext) -> JSON: return "clock" @_dump_type.register def _dump_type_continuous(typ: types.ContinuousType, ctx: JANIContext) -> JSON: return "continuous" @_dump_type.register def _dump_bounded_type(typ: types.BoundedType, ctx: JANIContext) -> JSON: jani_type: _JANIMap = {"kind": "bounded", "base": _dump_type(typ.base, ctx)} if typ.upper_bound: jani_type["upper-bound"] = _dump_prop(typ.upper_bound, ctx) if typ.lower_bound: jani_type["lower-bound"] = _dump_prop(typ.lower_bound, ctx) return jani_type @_dump_type.register def _dump_array_type(typ: types.ArrayType, ctx: JANIContext) -> JSON: ctx.require(ModelFeature.ARRAYS) return {"kind": "array", "base": _dump_type(typ.element, ctx)} checks.check_singledispatch( _dump_type, types.Type, ignore={types.StateType, types.SetType} ) @functools.singledispatch def _dump_prop(prop: model.Expression, ctx: JANIContext) -> JSON: raise NotImplementedError(f"dump has not been implemented for property {prop}") @_dump_prop.register def _dump_identifier(expr: expressions.Name, ctx: JANIContext) -> JSON: return expr.identifier @_dump_prop.register def _dump_boolean_constant(expr: expressions.BooleanConstant, ctx: JANIContext) -> JSON: return expr.boolean @_dump_prop.register def _dump_integer_constant(expr: expressions.IntegerConstant, ctx: JANIContext) -> JSON: return expr.integer @_dump_prop.register def _dump_real_constant(expr: expressions.RealConstant, ctx: JANIContext) -> JSON: if isinstance(expr.real, expressions.NamedReal): return {"constant": expr.real.symbol} if not isinstance(expr.real, float) and expr.real != float(expr.real): warnings.warn( f"imprecise conversion: JSON does not support the number type {type(expr.real)}" ) return float(expr.real) @_dump_prop.register def _dump_conditional(expr: expressions.Conditional, ctx: JANIContext) -> JSON: return { "op": "ite", "if": _dump_prop(expr.condition, ctx), "then": _dump_prop(expr.consequence, ctx), "else": _dump_prop(expr.alternative, ctx), } _DERIVED_OPERATORS = { operators.BooleanOperator.IMPLY, operators.ComparisonOperator.GT, operators.ComparisonOperator.GE, operators.ArithmeticBinaryOperator.MIN, operators.ArithmeticBinaryOperator.MAX, operators.ArithmeticUnaryOperator.ABS, operators.ArithmeticUnaryOperator.SGN, operators.ArithmeticUnaryOperator.TRC, } _Transform = t.Callable[[expressions.Expression], expressions.Expression] def normalize_xor(expr: expressions.Expression) -> expressions.Expression: assert ( isinstance(expr, expressions.Boolean) and expr.operator is operators.BooleanOperator.XOR ) return expressions.logic_or( expressions.logic_and(expressions.logic_not(expr.left), expr.right), expressions.logic_and(expr.right, expressions.logic_not(expr.left)), ) def normalize_equiv(expr: expressions.Expression) -> expressions.Expression: assert ( isinstance(expr, expressions.Boolean) and expr.operator is operators.BooleanOperator.EQUIV ) return expressions.logic_and( expressions.logic_implies(expr.left, expr.right), expressions.logic_implies(expr.right, expr.left), ) def normalize_floor_div(expr: expressions.Expression) -> expressions.Expression: assert ( isinstance(expr, expressions.ArithmeticBinary) and expr.operator is operators.ArithmeticBinaryOperator.FLOOR_DIV ) return expressions.floor(expressions.real_div(expr.left, expr.right)) _MOMBA_OPERATORS: t.Mapping[operators.BinaryOperator, _Transform] = { operators.BooleanOperator.XOR: normalize_xor, operators.BooleanOperator.EQUIV: normalize_equiv, operators.ArithmeticBinaryOperator.FLOOR_DIV: normalize_floor_div, } @_dump_prop.register def _dump_binary_expression( expr: expressions.BinaryExpression, ctx: JANIContext ) -> JSON: if expr.operator in _DERIVED_OPERATORS: ctx.require(ModelFeature.DERIVED_OPERATORS) if expr.operator in _MOMBA_OPERATORS: if ctx.allow_momba_operators: ctx.require(ModelFeature.X_MOMBA_OPERATORS) else: return _dump_prop(_MOMBA_OPERATORS[expr.operator](expr), ctx) return { "op": expr.operator.symbol, "left": _dump_prop(expr.left, ctx), "right": _dump_prop(expr.right, ctx), } @_dump_prop.register def _dump_unary_expression(expr: expressions.UnaryExpression, ctx: JANIContext) -> JSON: return {"op": expr.operator.symbol, "exp": _dump_prop(expr.operand, ctx)} @_dump_prop.register def _dump_derivative(expr: expressions.Derivative, ctx: JANIContext) -> JSON: return {"op": "der", "var": expr.identifier} @_dump_prop.register def _dump_array_access(expr: expressions.ArrayAccess, ctx: JANIContext) -> JSON: ctx.require(ModelFeature.ARRAYS) return { "op": "aa", "exp": _dump_prop(expr.array, ctx), "index": _dump_prop(expr.index, ctx), } @_dump_prop.register def _dump_array_value(expr: expressions.ArrayValue, ctx: JANIContext) -> JSON: ctx.require(ModelFeature.ARRAYS) return { "op": "av", "elements": list(_dump_prop(element, ctx) for element in expr.elements), } @_dump_prop.register def _dump_array_constructor( expr: expressions.ArrayConstructor, ctx: JANIContext ) -> JSON: ctx.require(ModelFeature.ARRAYS) return { "op": "av", "var": expr.variable, "length": _dump_prop(expr.length, ctx), "exp": _dump_prop(expr.expression, ctx), } @_dump_prop.register def _dump_sample(expr: expressions.Sample, ctx: JANIContext) -> JSON: return { "distribution": expr.distribution.jani_name, "args": [_dump_prop(argument, ctx) for argument in expr.arguments], } @_dump_prop.register def _dump_selection(expr: expressions.Selection, ctx: JANIContext) -> JSON: ctx.require(ModelFeature.NONDET_SELECTION) return { "op": "nondet", "var": expr.variable, "exp": _dump_prop(expr.condition, ctx), } @_dump_prop.register def _dum_call(expr: functions.CallExpression, ctx: JANIContext) -> JSON: ctx.require(ModelFeature.FUNCTIONS) return { "op": "call", "function": expr.function, "args": [_dump_prop(argument, ctx) for argument in expr.arguments], } def _dump_prop_interval(pi: properties.Interval, ctx: JANIContext) -> JSON: prop_interval: _JANIMap = {} if pi.lower is not None: prop_interval["lower"] = _dump_prop(pi.lower, ctx) if pi.lower_exclusive is not None: prop_interval["lower-exclusive"] = _dump_prop(pi.lower_exclusive, ctx) if pi.upper is not None: prop_interval["upper"] = _dump_prop(pi.upper, ctx) if pi.upper_exclusive is not None: prop_interval["upper-exclusive"] = _dump_prop(pi.upper_exclusive, ctx) return prop_interval def _dump_reward_bound(rb: properties.RewardBound, ctx: JANIContext) -> JSON: return { "exp": _dump_prop(rb.expression, ctx), "accumulate": [elem.value for elem in rb.accumulate], "bounds": _dump_prop_interval(rb.bounds, ctx), } def _dump_reward_instant(ri: properties.RewardInstant, ctx: JANIContext) -> JSON: return { "exp": _dump_prop(ri.expression, ctx), "accumulate": [elem.value for elem in ri.accumulate], "instant": _dump_prop(ri.instant, ctx), } @_dump_prop.register def _dump_filter(prop: properties.Aggregate, ctx: JANIContext) -> JSON: return { "op": "filter", "fun": prop.function.symbol, "values": _dump_prop(prop.values, ctx), "states": _dump_prop(prop.predicate, ctx), } @_dump_prop.register def _dump_probability(prop: properties.Probability, ctx: JANIContext) -> JSON: return { "op": "Pmax" if prop.operator is operators.MinMax.MAX else "Pmin", "exp": _dump_prop(prop.formula, ctx), } @_dump_prop.register def _dump_path_formula(prop: properties.PathQuantifier, ctx: JANIContext) -> JSON: return { "op": prop.quantifier.value, "exp": _dump_prop(prop.formula, ctx), } @_dump_prop.register def _dump_expected(prop: properties.ExpectedReward, ctx: JANIContext) -> JSON: expected: _JANIMap = { "op": "Emax" if prop.operator is operators.MinMax.MAX else "Emin", "exp": _dump_prop(prop.reward, ctx), } if prop.accumulate is not None: expected["accumulate"] = [elem.value for elem in prop.accumulate] if prop.reachability is not None: expected["reach"] = _dump_prop(prop.reachability, ctx) if prop.step_instant is not None: expected["step-instant"] = _dump_prop(prop.step_instant, ctx) if prop.time_instant is not None: expected["time-instant"] = _dump_prop(prop.time_instant, ctx) if prop.reward_instants is not None: expected["reward-instants"] = [ _dump_reward_instant(ri, ctx) for ri in prop.reward_instants ] return expected @_dump_prop.register def _dump_steady(prop: properties.SteadyState, ctx: JANIContext) -> JSON: steady: _JANIMap = { "op": "Smax" if prop.operator is operators.MinMax.MAX else "Smin", "exp": _dump_prop(prop.formula, ctx), } if prop.accumulate is not None: steady["accumulate"] = [elem.value for elem in prop.accumulate] return steady @_dump_prop.register def _dump_timed(prop: properties.BinaryPathFormula, ctx: JANIContext) -> JSON: timed: _JANIMap = { "op": prop.operator.value, "left": _dump_prop(prop.left, ctx), "right": _dump_prop(prop.right, ctx), } if prop.step_bounds is not None: timed["step-bounds"] = _dump_prop_interval(prop.step_bounds, ctx) if prop.time_bounds is not None: timed["time-bounds"] = _dump_prop_interval(prop.time_bounds, ctx) if prop.reward_bounds is not None: timed["reward-bounds"] = [ _dump_reward_bound(rb, ctx) for rb in prop.reward_bounds ] return timed @_dump_prop.register def _dump_state_selector(prop: properties.StateSelector, ctx: JANIContext) -> JSON: return {"op": prop.predicate.value} @_dump_prop.register def _dump_unary_path_formula( prop: properties.UnaryPathFormula, ctx: JANIContext ) -> JSON: jani_prop: _JANIMap = { "op": prop.operator.value, "exp": _dump_prop(prop.formula, ctx), } if prop.step_bounds is not None: jani_prop["step-bounds"] = _dump_prop_interval(prop.step_bounds, ctx) if prop.time_bounds is not None: jani_prop["time-bounds"] = _dump_prop_interval(prop.time_bounds, ctx) if prop.reward_bounds is not None: jani_prop["reward-bounds"] = [ _dump_reward_bound(rb, ctx) for rb in prop.reward_bounds ] return jani_prop checks.check_singledispatch(_dump_prop, model.Expression) def _dump_var_decl(decl: context.VariableDeclaration, ctx: JANIContext) -> JSON: jani_declaration: _JANIMap = { "name": decl.identifier, "type": _dump_type(decl.typ, ctx), } if decl.is_transient is not None: jani_declaration["transient"] = decl.is_transient if decl.initial_value is not None: jani_declaration["initial-value"] = _dump_prop(decl.initial_value, ctx) return jani_declaration def _dump_const_decl(decl: context.ConstantDeclaration, ctx: JANIContext) -> JSON: jani_declaration: _JANIMap = { "name": decl.identifier, "type": _dump_type(decl.typ, ctx), } if decl.value is not None: jani_declaration["value"] = _dump_prop(decl.value, ctx) return jani_declaration def _dump_assignment(assignment: model.Assignment, ctx: JANIContext) -> JSON: jani_assignment: _JANIMap = { "ref": _dump_prop(assignment.target, ctx), "value": _dump_prop(assignment.value, ctx), } if assignment.index != 0: jani_assignment["index"] = assignment.index return jani_assignment def _dump_location(loc: model.Location, ctx: JANIContext) -> JSON: jani_location: _JANIMap = { "name": ctx.get_name(loc), "x-momba-anonymous": loc.name is None, } if loc.progress_invariant is not None: jani_location["time-progress"] = { "exp": _dump_prop(loc.progress_invariant, ctx) } if loc.transient_values is not None: jani_location["transient-values"] = [ _dump_assignment(assignment, ctx) for assignment in loc.transient_values ] return jani_location def _dump_destination(dst: model.Destination, ctx: JANIContext) -> JSON: jani_destination: _JANIMap = {"location": ctx.get_name(dst.location)} if dst.probability is not None: jani_destination["probability"] = {"exp": _dump_prop(dst.probability, ctx)} if dst.assignments: jani_destination["assignments"] = [ _dump_assignment(assignment, ctx) for assignment in dst.assignments ] return jani_destination def _dump_edge(edge: model.Edge, ctx: JANIContext) -> JSON: jani_edge: _JANIMap = { "location": ctx.get_name(edge.location), "destinations": [_dump_destination(dst, ctx) for dst in edge.destinations], } if edge.action_pattern is not None: jani_edge["action"] = _dump_action_pattern(edge.action_pattern, ctx) if edge.rate is not None: jani_edge["rate"] = {"exp": _dump_prop(edge.rate, ctx)} if edge.guard is not None: jani_edge["guard"] = {"exp": _dump_prop(edge.guard, ctx)} return jani_edge def _dump_automaton(automaton: model.Automaton, ctx: JANIContext) -> JSON: jani_automaton: _JANIMap = { "name": ctx.get_name(automaton), "x-momba-anonymous": automaton.name is None, "variables": [ _dump_var_decl(var_decl, ctx) for var_decl in automaton.scope.variable_declarations ], "locations": [_dump_location(loc, ctx) for loc in automaton.locations], "edges": [_dump_edge(edge, ctx) for edge in automaton.edges], "initial-locations": [ctx.get_name(loc) for loc in automaton.initial_locations], } if automaton.initial_restriction is not None: jani_automaton["restrict-initial"] = { "exp": _dump_prop(automaton.initial_restriction, ctx) } return jani_automaton def _dump_action_pattern( pattern: t.Optional[model.ActionPattern], ctx: JANIContext ) -> JSON: if pattern is not None: assert ( not pattern.arguments ), "exporting action patterns with arguments to Jani is currently not supported" # FIXME: add support for exporting patterns with arguments # if pattern.identifiers: # ctx.require(ModelFeature.X_MOMBA_VALUE_PASSING) # return { # "name": pattern.action_type.name, # "identifiers": list(pattern.identifiers), # } # else: return pattern.action_type.label return None def _dump_link( instance_vector: t.Sequence[model.Instance], link: model.Link, ctx: JANIContext, ) -> JSON: jani_sync: _JANIMap = { "synchronise": [ _dump_action_pattern(link.vector.get(instance, None), ctx) for instance in instance_vector ], } if link.result is not None: jani_sync["result"] = _dump_action_pattern(link.result, ctx) return jani_sync def _dump_system(network: model.Network, ctx: JANIContext) -> JSON: instance_vector = list(network.instances) jani_elements: t.List[_JANIMap] = [] for instance in instance_vector: jani_instance: _JANIMap = {"automaton": ctx.get_name(instance.automaton)} if instance.input_enable: jani_instance["input-enable"] = list( action_typ.label for action_typ in instance.input_enable ) jani_elements.append(jani_instance) return { "elements": jani_elements, "syncs": [_dump_link(instance_vector, link, ctx) for link in network.links], } def _dump_metadata(model_ctx: model.Context) -> _JANIMap: jani_metadata: _JANIMap = {} for field in {"version", "author", "description", "doi", "url"}: try: jani_metadata[field] = model_ctx.metadata[field] except KeyError: pass return jani_metadata def _dump_action_type(action_type: model.ActionType, ctx: JANIContext) -> _JANIMap: jani_action: _JANIMap = {"name": action_type.label} jani_parameters: t.List[_JANIMap] = [] for parameter in action_type.parameters: ctx.require(ModelFeature.X_MOMBA_VALUE_PASSING) jani_parameter: _JANIMap = {"type": _dump_type(parameter.typ, ctx)} if parameter.comment is not None: jani_parameter["comment"] = parameter.comment jani_parameters.append(jani_parameter) if jani_parameters: jani_action["parameters"] = jani_parameters if action_type.comment is not None: jani_action["comment"] = action_type.comment return jani_action def dump_structure( network: model.Network, *, allow_momba_operators: bool = False, properties: t.Optional[t.Mapping[str, model.Expression]] = None, ) -> JSON: ctx = JANIContext(allow_momba_operators=allow_momba_operators) jani_metadata: t.Dict[str, str] = {} if properties is None: properties = { definition.name: definition.expression for definition in network.ctx.properties.values() } if "name" in network.ctx.metadata: jani_metadata jani_model: _JANIMap = { "jani-version": 1, "x-generator": f"Momba (v{version})", "x-momba-release": version, "name": network.name or "A Momba Model", "x-momba-anonymous": network.name is None, "metadata": _dump_metadata(network.ctx), "x-momba-metadata": dict(network.ctx.metadata), "type": network.ctx.model_type.name.lower(), "variables": [ _dump_var_decl(var_decl, ctx) for var_decl in network.ctx.global_scope.variable_declarations ], "constants": [ _dump_const_decl(const_decl, ctx) for const_decl in network.ctx.global_scope.constant_declarations ], "actions": [ _dump_action_type(action_type, ctx) for action_type in network.ctx.action_types.values() ], "automata": [ _dump_automaton(automaton, ctx) for automaton in network.ctx.automata ], "properties": [ {"name": name, "expression": _dump_prop(prop, ctx)} for name, prop in properties.items() ], "system": _dump_system(network, ctx), # important: has to be at the end, because we collect the features while dumping "features": [feature.value for feature in ctx.features], } if network.initial_restriction is not None: jani_model["restrict-initial"] = { "exp": _dump_prop(network.initial_restriction, ctx) } return jani_model
[docs]def dump_model( network: model.Network, *, indent: t.Optional[int] = None, allow_momba_operators: bool = False, properties: t.Optional[t.Mapping[str, model.Expression]] = None, ) -> str: """ Takes a Momba automaton :class:`~momba.model.Network` and returns a JANI string representing the network. The `indent` parameter controls the indentation of the resulting JANI string. `None` means no indentation. Set `indent` to an integer, e.g., :code:`2`, to enable pretty formatting. Momba supports some non-standard operators which can either be preserved or desugared into standard JANI. This behavior is controlled via the flag `allow_momba_operators`. If the flag is set to `True`, then the `x-momba-operators` JANI model feature is enabled and the non-standard operators will be preserved in the output. Otherwise, if it is set to `False` (the default case), non-standard operators are desugared into standard JANI. The `properties` parameter allows to provide an mapping from :class:`str` to :class:`~momba.model.Expression`. The provided expressions will be put as additional properties into the JANI-model output. """ jani_structure = dump_structure( network, allow_momba_operators=allow_momba_operators, properties=properties ) return json.dumps( jani_structure, indent=indent, ensure_ascii=False, )