#include "storm/storage/prism/ToJaniConverter.h" #include "storm/storage/expressions/ExpressionManager.h" #include "storm/storage/prism/Program.h" #include "storm/storage/prism/CompositionToJaniVisitor.h" #include "storm/storage/jani/Model.h" #include "storm/storage/jani/Property.h" #include "storm/storage/jani/TemplateEdge.h" #include "storm/storage/jani/expressions/JaniExpressionSubstitutionVisitor.h" #include "storm/storage/jani/expressions/FunctionCallExpression.h" #include "storm/settings/SettingsManager.h" #include "storm/utility/macros.h" #include "storm/exceptions/NotImplementedException.h" namespace storm { namespace prism { storm::jani::Model ToJaniConverter::convert(storm::prism::Program const& program, bool allVariablesGlobal, std::string suffix, bool standardCompliant) { labelRenaming.clear(); rewardModelRenaming.clear(); formulaToFunctionCallMap.clear(); std::shared_ptr manager = program.getManager().getSharedPointer(); bool produceStateRewards = !standardCompliant || program.getModelType() == storm::prism::Program::ModelType::CTMC || program.getModelType() == storm::prism::Program::ModelType::MA; // Start by creating an empty JANI model. storm::jani::ModelType modelType; switch (program.getModelType()) { case Program::ModelType::DTMC: modelType = storm::jani::ModelType::DTMC; break; case Program::ModelType::CTMC: modelType = storm::jani::ModelType::CTMC; break; case Program::ModelType::MDP: modelType = storm::jani::ModelType::MDP; break; case Program::ModelType::CTMDP: modelType = storm::jani::ModelType::CTMDP; break; case Program::ModelType::MA: modelType = storm::jani::ModelType::MA; break; default: modelType = storm::jani::ModelType::UNDEFINED; } storm::jani::Model janiModel("jani_from_prism", modelType, 1, manager); janiModel.getModelFeatures().add(storm::jani::ModelFeature::DerivedOperators); // Add all constants of the PRISM program to the JANI model. for (auto const& constant : program.getConstants()) { janiModel.addConstant(storm::jani::Constant(constant.getName(), constant.getExpressionVariable(), constant.isDefined() ? boost::optional(constant.getExpression()) : boost::none)); } // Add all formulas of the PRISM program to the JANI model. // Also collect a substitution of formula placeholder variables to function call expressions. for (auto const& formula : program.getFormulas()) { // First find all variables that occurr in the formula definition (including the ones used in called formulae) and the called formulae std::set variablesInFormula, placeholdersInFormula; for (auto const& var : formula.getExpression().getVariables()) { // Check whether var is an actual variable/constant or another formula auto functionCallIt = formulaToFunctionCallMap.find(var); if (functionCallIt == formulaToFunctionCallMap.end()) { variablesInFormula.insert(var); } else { storm::expressions::FunctionCallExpression const& innerFunctionCall = dynamic_cast(functionCallIt->second.getBaseExpression()); for (auto const& innerFunctionArg : innerFunctionCall.getArguments()) { variablesInFormula.insert(innerFunctionArg->asVariableExpression().getVariable()); } placeholdersInFormula.insert(var); } } // Add a function argument and parameter for each occurring variable and prepare the substitution for the function body std::map functionBodySubstitution; std::vector functionParameters; std::vector> functionArguments; for (auto const& var : variablesInFormula) { functionArguments.push_back(var.getExpression().getBaseExpressionPointer()); functionParameters.push_back(manager->declareVariable(formula.getName() + "__param__" + var.getName() + suffix, var.getType())); functionBodySubstitution[var] = functionParameters.back().getExpression(); } for (auto const& formulaVar : placeholdersInFormula) { functionBodySubstitution[formulaVar] = storm::jani::substituteJaniExpression(formulaToFunctionCallMap[formulaVar], functionBodySubstitution); } storm::jani::FunctionDefinition funDef(formula.getName(), formula.getType(), functionParameters, storm::jani::substituteJaniExpression(formula.getExpression(), functionBodySubstitution)); janiModel.addFunctionDefinition(funDef); auto functionCallExpression = std::make_shared(*manager, formula.getType(), formula.getName(), functionArguments); formulaToFunctionCallMap[formula.getExpressionVariable()] = functionCallExpression->toExpression(); } // Maintain a mapping from expression variables to JANI variables so we can fill in the correct objects when // creating assignments. std::map> variableToVariableMap; // Add all global variables of the PRISM program to the JANI model. for (auto const& variable : program.getGlobalIntegerVariables()) { if (variable.hasInitialValue()) { storm::jani::BoundedIntegerVariable const& createdVariable = janiModel.addVariable(storm::jani::BoundedIntegerVariable(variable.getName(), variable.getExpressionVariable(), variable.getInitialValueExpression(), false, variable.getLowerBoundExpression(), variable.getUpperBoundExpression())); variableToVariableMap.emplace(variable.getExpressionVariable(), createdVariable); } else { storm::jani::BoundedIntegerVariable const& createdVariable = janiModel.addVariable(storm::jani::BoundedIntegerVariable(variable.getName(), variable.getExpressionVariable(), variable.getLowerBoundExpression(), variable.getUpperBoundExpression())); variableToVariableMap.emplace(variable.getExpressionVariable(), createdVariable); } } for (auto const& variable : program.getGlobalBooleanVariables()) { if (variable.hasInitialValue()) { storm::jani::BooleanVariable const& createdVariable = janiModel.addVariable(storm::jani::BooleanVariable(variable.getName(), variable.getExpressionVariable(), variable.getInitialValueExpression(), false)); variableToVariableMap.emplace(variable.getExpressionVariable(), createdVariable); } else { storm::jani::BooleanVariable const& createdVariable = janiModel.addVariable(storm::jani::BooleanVariable(variable.getName(), variable.getExpressionVariable())); variableToVariableMap.emplace(variable.getExpressionVariable(), createdVariable); } } // Add all actions of the PRISM program to the JANI model. for (auto const& action : program.getActions()) { // Ignore the empty action as every JANI program has predefined tau action. if (!action.empty()) { janiModel.addAction(storm::jani::Action(action)); } } // Because of the rules of JANI, we have to make all variables of modules global that are read by other modules. // Create a mapping from variables to the indices of module indices that write/read the variable. std::map> variablesToAccessingModuleIndices; for (uint_fast64_t index = 0; index < program.getNumberOfModules(); ++index) { storm::prism::Module const& module = program.getModule(index); for (auto const& command : module.getCommands()) { std::set variables = command.getGuardExpression().getVariables(); for (auto const& variable : variables) { variablesToAccessingModuleIndices[variable].insert(index); } for (auto const& update : command.getUpdates()) { for (auto const& assignment : update.getAssignments()) { variables = assignment.getExpression().getVariables(); for (auto const& variable : variables) { variablesToAccessingModuleIndices[variable].insert(index); } variablesToAccessingModuleIndices[assignment.getVariable()].insert(index); } } } } // Create a mapping from variables to a flag indicating whether it should be made global std::map variablesToMakeGlobal; for (auto const& varMods : variablesToAccessingModuleIndices) { assert(!varMods.second.empty()); // If there is exactly one module reading and writing the variable, we can make the variable local to this module. variablesToMakeGlobal[varMods.first] = allVariablesGlobal || (varMods.second.size() > 1); } // Go through the labels and construct assignments to transient variables that are added to the locations. std::vector transientLocationAssignments; for (auto const& label : program.getLabels()) { bool renameLabel = manager->hasVariable(label.getName()) || program.hasRewardModel(label.getName()); std::string finalLabelName = renameLabel ? "label_" + label.getName() + suffix : label.getName(); if (renameLabel) { STORM_LOG_INFO("Label '" << label.getName() << "' was renamed to '" << finalLabelName << "' in PRISM-to-JANI conversion, as another variable with that name already exists."); labelRenaming[label.getName()] = finalLabelName; } auto newExpressionVariable = manager->declareBooleanVariable(finalLabelName); storm::jani::BooleanVariable const& newTransientVariable = janiModel.addVariable(storm::jani::BooleanVariable(newExpressionVariable.getName(), newExpressionVariable, manager->boolean(false), true)); transientLocationAssignments.emplace_back(storm::jani::LValue(newTransientVariable), label.getStatePredicateExpression()); // Variables that are accessed in the label predicate expression should be made global. std::set variables = label.getStatePredicateExpression().getVariables(); for (auto const& variable : variables) { variablesToMakeGlobal[variable] = true; } } // Create an initial state restriction if there was an initial construct in the program. if (program.hasInitialConstruct()) { janiModel.setInitialStatesRestriction(program.getInitialConstruct().getInitialStatesExpression()); // Variables in the initial state expression should be made global std::set variables = program.getInitialConstruct().getInitialStatesExpression().getVariables(); for (auto const& variable : variables) { variablesToMakeGlobal[variable] = true; } } else { janiModel.setInitialStatesRestriction(manager->boolean(true)); } // Go through the reward models and construct assignments to the transient variables that are to be added to // edges and transient assignments that are added to the locations. std::map> transientEdgeAssignments; for (auto const& rewardModel : program.getRewardModels()) { std::string finalRewardModelName; if (rewardModel.getName().empty()) { finalRewardModelName = "default_reward_model"; } else { if (manager->hasVariable(rewardModel.getName())) { // Rename finalRewardModelName = "rewardmodel_" + rewardModel.getName() + suffix; STORM_LOG_INFO("Rewardmodel '" << rewardModel.getName() << "' was renamed to '" << finalRewardModelName << "' in PRISM-to-JANI conversion, as another variable with that name already exists."); rewardModelRenaming[rewardModel.getName()] = finalRewardModelName; } else { finalRewardModelName = rewardModel.getName(); } } auto newExpressionVariable = manager->declareRationalVariable(finalRewardModelName); storm::jani::RealVariable const& newTransientVariable = janiModel.addVariable(storm::jani::RealVariable(finalRewardModelName, newExpressionVariable, manager->rational(0.0), true)); if (rewardModel.hasStateRewards()) { storm::expressions::Expression transientLocationExpression; for (auto const& stateReward : rewardModel.getStateRewards()) { storm::expressions::Expression rewardTerm = stateReward.getStatePredicateExpression().isTrue() ? stateReward.getRewardValueExpression() : storm::expressions::ite(stateReward.getStatePredicateExpression(), stateReward.getRewardValueExpression(), manager->rational(0)); if (transientLocationExpression.isInitialized()) { transientLocationExpression = transientLocationExpression + rewardTerm; } else { transientLocationExpression = rewardTerm; } } transientLocationAssignments.emplace_back(storm::jani::LValue(newTransientVariable), transientLocationExpression); // Variables that are accessed in a reward term should be made global. std::set variables = transientLocationExpression.getVariables(); for (auto const& variable : variables) { variablesToMakeGlobal[variable] = true; } } std::map actionIndexToExpression; for (auto const& actionReward : rewardModel.getStateActionRewards()) { storm::expressions::Expression rewardTerm = actionReward.getStatePredicateExpression().isTrue() ? actionReward.getRewardValueExpression() : storm::expressions::ite(actionReward.getStatePredicateExpression(), actionReward.getRewardValueExpression(), manager->rational(0)); auto it = actionIndexToExpression.find(janiModel.getActionIndex(actionReward.getActionName())); if (it != actionIndexToExpression.end()) { it->second = it->second + rewardTerm; } else { actionIndexToExpression[janiModel.getActionIndex(actionReward.getActionName())] = rewardTerm; } } for (auto const& entry : actionIndexToExpression) { auto it = transientEdgeAssignments.find(entry.first); if (it != transientEdgeAssignments.end()) { it->second.push_back(storm::jani::Assignment(storm::jani::LValue(newTransientVariable), entry.second)); } else { std::vector assignments = {storm::jani::Assignment(storm::jani::LValue(newTransientVariable), entry.second)}; transientEdgeAssignments.emplace(entry.first, assignments); } // Variables that are accessed in a reward term should be made global. std::set variables = entry.second.getVariables(); for (auto const& variable : variables) { variablesToMakeGlobal[variable] = true; } } STORM_LOG_THROW(!rewardModel.hasTransitionRewards(), storm::exceptions::NotImplementedException, "Transition reward translation currently not implemented."); } STORM_LOG_THROW(transientEdgeAssignments.empty() || transientLocationAssignments.empty() || !program.specifiesSystemComposition(), storm::exceptions::NotImplementedException, "Cannot translate reward models from PRISM to JANI that specify a custom system composition."); // If we are not allowed to produce state rewards, we need to create a mapping from action indices to transient // location assignments. This is done so that all assignments are added only *once* for synchronizing actions. std::map> transientRewardLocationAssignmentsPerAction; if (!produceStateRewards) { for (auto const& action : program.getActions()) { auto& list = transientRewardLocationAssignmentsPerAction[janiModel.getActionIndex(action)]; for (auto const& assignment : transientLocationAssignments) { if (assignment.isTransient() && assignment.getVariable().isRealVariable()) { list.emplace_back(assignment); } } } } // Now create the separate JANI automata from the modules of the PRISM program. While doing so, we use the // previously built mapping to make variables global that are read by more than one module. std::set firstModules; bool firstModule = true; for (auto const& module : program.getModules()) { // Keep track of the action indices contained in this module. std::set actionIndicesOfModule; storm::jani::Automaton automaton(module.getName(), manager->declareIntegerVariable("_loc_prism2jani_" + module.getName() + "_" + suffix)); for (auto const& variable : module.getIntegerVariables()) { storm::jani::BoundedIntegerVariable newIntegerVariable = *storm::jani::makeBoundedIntegerVariable(variable.getName(), variable.getExpressionVariable(), variable.hasInitialValue() ? boost::make_optional(variable.getInitialValueExpression()) : boost::none, false, variable.getLowerBoundExpression(), variable.getUpperBoundExpression()); auto findRes = variablesToMakeGlobal.find(variable.getExpressionVariable()); if (findRes != variablesToMakeGlobal.end()) { bool makeVarGlobal = findRes->second; storm::jani::BoundedIntegerVariable const& createdVariable = makeVarGlobal ? janiModel.addVariable(newIntegerVariable) : automaton.addVariable(newIntegerVariable); variableToVariableMap.emplace(variable.getExpressionVariable(), createdVariable); } else { STORM_LOG_INFO("Variable " << variable.getName() << " is declared but never used."); } } for (auto const& variable : module.getBooleanVariables()) { storm::jani::BooleanVariable newBooleanVariable = *storm::jani::makeBooleanVariable(variable.getName(), variable.getExpressionVariable(), variable.hasInitialValue() ? boost::make_optional(variable.getInitialValueExpression()) : boost::none, false); auto findRes = variablesToMakeGlobal.find(variable.getExpressionVariable()); if (findRes != variablesToMakeGlobal.end()) { bool makeVarGlobal = findRes->second; storm::jani::BooleanVariable const& createdVariable = makeVarGlobal ? janiModel.addVariable(newBooleanVariable) : automaton.addVariable(newBooleanVariable); variableToVariableMap.emplace(variable.getExpressionVariable(), createdVariable); } else { STORM_LOG_INFO("Variable " << variable.getName() << " is declared but never used."); } } automaton.setInitialStatesRestriction(manager->boolean(true)); // Create a single location that will have all the edges. uint64_t onlyLocationIndex = automaton.addLocation(storm::jani::Location("l")); automaton.addInitialLocation(onlyLocationIndex); // If we are translating the first module that has the action, we need to add the transient assignments to the location. // However, in standard compliant JANI, there are no state rewards if (firstModule) { storm::jani::Location& onlyLocation = automaton.getLocation(onlyLocationIndex); for (auto const& assignment : transientLocationAssignments) { if (assignment.getVariable().isBooleanVariable() || produceStateRewards) { onlyLocation.addTransientAssignment(assignment); } } } for (auto const& command : module.getCommands()) { std::shared_ptr templateEdge = std::make_shared(command.getGuardExpression()); automaton.registerTemplateEdge(templateEdge); actionIndicesOfModule.insert(janiModel.getActionIndex(command.getActionName())); boost::optional rateExpression; if (program.getModelType() == Program::ModelType::CTMC || program.getModelType() == Program::ModelType::CTMDP || (program.getModelType() == Program::ModelType::MA && command.isMarkovian())) { for (auto const& update : command.getUpdates()) { if (rateExpression) { rateExpression = rateExpression.get() + update.getLikelihoodExpression(); } else { rateExpression = update.getLikelihoodExpression(); } } } std::vector> destinationLocationsAndProbabilities; for (auto const& update : command.getUpdates()) { std::vector assignments; for (auto const& assignment : update.getAssignments()) { assignments.push_back(storm::jani::Assignment(storm::jani::LValue(variableToVariableMap.at(assignment.getVariable()).get()), assignment.getExpression())); } if (rateExpression) { destinationLocationsAndProbabilities.emplace_back(onlyLocationIndex, update.getLikelihoodExpression() / rateExpression.get()); } else { destinationLocationsAndProbabilities.emplace_back(onlyLocationIndex, update.getLikelihoodExpression()); } templateEdge->addDestination(storm::jani::TemplateEdgeDestination(assignments)); } // Then add the transient assignments for the rewards. Note that we may do this only for the first // module that has this action, so we remove the assignments from the global list of assignments // to add after adding them to the created edge. auto transientEdgeAssignmentsToAdd = transientEdgeAssignments.find(janiModel.getActionIndex(command.getActionName())); if (transientEdgeAssignmentsToAdd != transientEdgeAssignments.end()) { for (auto const& assignment : transientEdgeAssignmentsToAdd->second) { templateEdge->addTransientAssignment(assignment); } } if (!produceStateRewards) { transientEdgeAssignmentsToAdd = transientRewardLocationAssignmentsPerAction.find(janiModel.getActionIndex(command.getActionName())); for (auto const& assignment : transientEdgeAssignmentsToAdd->second) { templateEdge->addTransientAssignment(assignment, true); } } // Create the edge object. storm::jani::Edge newEdge; if (command.getActionName().empty()) { newEdge = storm::jani::Edge(onlyLocationIndex, storm::jani::Model::SILENT_ACTION_INDEX, rateExpression, templateEdge, destinationLocationsAndProbabilities); } else { newEdge = storm::jani::Edge(onlyLocationIndex, janiModel.getActionIndex(command.getActionName()), rateExpression, templateEdge, destinationLocationsAndProbabilities); } // Finally add the constructed edge. automaton.addEdge(newEdge); } // Now remove for all actions of this module the corresponding transient assignments, because we must // not deal out this reward multiple times. // NOTE: This only works for the standard composition and not for any custom compositions. This case // must be checked for earlier. for (auto actionIndex : actionIndicesOfModule) { // Do not delete rewards dealt out on non-synchronizing edges. if (actionIndex == janiModel.getActionIndex("")) { continue; } auto it = transientEdgeAssignments.find(actionIndex); if (it != transientEdgeAssignments.end()) { transientEdgeAssignments.erase(it); } } // if there are formulas and if the current module was renamed, we need to apply the renaming to the resulting function calls before replacing the formula placeholders. // Note that the formula placeholders of non-renamed modules are replaced later. if (program.getNumberOfFormulas() > 0 && module.isRenamedFromModule()) { auto renaming = program.getFinalRenamingOfModule(module); std::map renamingAsSubstitution; for (auto const& renamingPair : renaming) { if (manager->hasVariable(renamingPair.first)) { assert(manager->hasVariable(renamingPair.second)); renamingAsSubstitution.emplace(manager->getVariable(renamingPair.first), manager->getVariableExpression(renamingPair.second)); } } std::map renamedFormulaToFunctionCallMap; for (auto const& formulaToFunctionCall : formulaToFunctionCallMap) { renamedFormulaToFunctionCallMap[formulaToFunctionCall.first] = storm::jani::substituteJaniExpression(formulaToFunctionCall.second, renamedFormulaToFunctionCallMap); } automaton.substitute(renamedFormulaToFunctionCallMap); } janiModel.addAutomaton(automaton); firstModule = false; } // Set the standard system composition. This is possible, because we reject non-standard compositions anyway. if (program.specifiesSystemComposition()) { CompositionToJaniVisitor visitor; janiModel.setSystemComposition(visitor.toJani(program.getSystemCompositionConstruct().getSystemComposition(), janiModel)); } else { janiModel.setSystemComposition(janiModel.getStandardSystemComposition()); } // if there are formulas, replace the remaining placeholder variables by actual function calls in all expressions if (program.getNumberOfFormulas() > 0) { janiModel.getModelFeatures().add(storm::jani::ModelFeature::Functions); janiModel.substitute(formulaToFunctionCallMap); } janiModel.finalize(); return janiModel; } bool ToJaniConverter::labelsWereRenamed() const { return !labelRenaming.empty(); } bool ToJaniConverter::rewardModelsWereRenamed() const { return !rewardModelRenaming.empty(); } std::map const& ToJaniConverter::getLabelRenaming() const { return labelRenaming; } std::map const& ToJaniConverter::getRewardModelRenaming() const { return rewardModelRenaming; } storm::jani::Property ToJaniConverter::applyRenaming(storm::jani::Property const& property) const { storm::jani::Property result; bool initialized = false; if (rewardModelsWereRenamed()) { result = property.substituteRewardModelNames(getRewardModelRenaming()); initialized = true; } if (labelsWereRenamed()) { storm::jani::Property const& currProperty = initialized ? result : property; result = currProperty.substituteLabels(getLabelRenaming()); initialized = true; } if (!formulaToFunctionCallMap.empty()) { storm::jani::Property const& currProperty = initialized ? result : property; result = currProperty.substitute(formulaToFunctionCallMap); initialized = true; } if (!initialized) { result = property.clone(); } return result; } std::vector ToJaniConverter::applyRenaming(std::vector const& properties) const { std::vector result; for (auto const& p : properties) { result.push_back(applyRenaming(p)); } return result; } } }