Skip to content

Commit

Permalink
Sub recipe visualization (#1665)
Browse files Browse the repository at this point in the history
* Add sub recipe to the dsls

* Working version

* Refactor

* Filter out all interactions

* Refactor

* Handle isSensoryEvent

* Clean

* Clean

* Clean

* Clean

* Change graph style

* Add allInteractions
  • Loading branch information
wilmveel authored May 15, 2024
1 parent c97cc9b commit d7b612e
Show file tree
Hide file tree
Showing 16 changed files with 432 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,22 @@ case class CompiledRecipe(name: String,
def getRecipeVisualization(style: RecipeVisualStyle): String =
RecipeVisualizer.visualizeRecipe(this, style)

/**
* Visualise the compiled recipe in DOT format
*
* @return
*/
def getSubRecipeVisualization: String =
RecipeVisualizer.visualizeSubRecipe(this, RecipeVisualStyle.default)

/**
* Visualise the compiled recipe in DOT format
*
* @return
*/
def getSubRecipeVisualization(style: RecipeVisualStyle): String =
RecipeVisualizer.visualizeSubRecipe(this, style)

/**
* Visualise the compiled recipe in DOT format
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ object RecipeVisualStyle extends LazyLogging {
readAttributes("common")
)),
ingredientAttributes =
DotAttr("shape", "circle") +: readAttributes("ingredient"),
DotAttr("shape", "ellipse") +: readAttributes("ingredient"),
providedIngredientAttributes =
DotAttr("shape", "circle") +: readAttributes("ingredient", "fired"),
DotAttr("shape", "ellipse") +: readAttributes("ingredient", "fired"),
eventAttributes =
DotAttr("shape", "diamond") +: readAttributes("event"),
sensoryEventAttributes =
Expand Down Expand Up @@ -88,23 +88,34 @@ case class RecipeVisualStyle(
)
),

subRecipe: List[DotAttr] = List(
DotAttr("shape", "rect"),
DotAttr("style", "filled"),
DotAttr("color", "\"#000000\""),
DotAttr("penwidth", 2),
DotAttr("margin", 0.5D)
),

ingredientAttributes: List[DotAttr] = List(
DotAttr("shape", "circle"),
DotAttr("shape", "ellipse"),
DotAttr("style", "filled"),
DotAttr("color", "\"#FF6200\"")
DotAttr("color", "\"#FF6200\""),
DotAttr("margin", 0.3D)
),

providedIngredientAttributes: List[DotAttr] = List(
DotAttr("shape", "circle"),
DotAttr("shape", "ellipse"),
DotAttr("style", "filled"),
DotAttr("color", "\"#3b823a\"")
DotAttr("color", "\"#3b823a\""),
DotAttr("margin", 0.3D)
),

missingIngredientAttributes: List[DotAttr] = List(
DotAttr("shape", "circle"),
DotAttr("shape", "ellipse"),
DotAttr("style", "filled"),
DotAttr("color", "\"#EE0000\""),
DotAttr("penwidth", "5.0")
DotAttr("penwidth", "5.0"),
DotAttr("margin", 0.3D)
),

eventAttributes: List[DotAttr] = List(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import scalax.collection.edge.WLDiEdge
import scalax.collection.io.dot.implicits._
import scalax.collection.io.dot.{DotAttr, _}

import scala.annotation.nowarn
import scala.language.higherKinds

object RecipeVisualizer {
Expand All @@ -22,7 +21,6 @@ object RecipeVisualizer {

implicit class RecipePetriNetGraphFns(graph: RecipePetriNetGraph) {

@nowarn
def compactNode(node: RecipePetriNetGraph#NodeT): RecipePetriNetGraph = {

// create direct edges from all incoming to outgoing nodes
Expand All @@ -34,11 +32,69 @@ object RecipeVisualizer {
graph - node -- node.incoming -- node.outgoing ++ newEdges
}

def compactAllNodes(fn: RecipePetriNetGraph#NodeT => Boolean): RecipePetriNetGraph =
graph.nodes.foldLeft(graph) {
case (acc, node) if fn(node) => acc.compactNode(node)
case (acc, _) => acc
}
def compactAllNodes(fn: RecipePetriNetGraph#NodeT => Boolean): RecipePetriNetGraph = {
val nodes = graph.nodes.filter(node => fn(node))
val newEdges = nodes.flatMap(node => node.incomingNodes.flatMap { incomingNode =>
node.outgoingNodes.map(n => WLDiEdge[Node, String](incomingNode, n)(0, ""))
})
graph -- nodes ++ newEdges
}

def compactSubRecipes: RecipePetriNetGraph = {
graph.nodes
.filter { node =>
node.value match {
case Right(transition: Transition) => transition.label.startsWith(subRecipePrefix)
case _ => false
}
}
.groupBy { node =>
node.value match {
case Left(place) => place.label.split('$')(2)
case Right(transition: Transition) => transition.label.split('$')(2)
}
}
.foldLeft(graph) { case (acc, (name, subRecipeNodes)) =>

def hasOnlySubInteractionOutNeighbors(nodes: Set[graph.NodeT]): Boolean = {
nodes
.filter(_.value match {
case Right(_: InteractionTransition) => true
case _ => false
})
.diff(subRecipeNodes)
.isEmpty
}

val firstLayer = subRecipeNodes
.flatMap { n => n.outNeighbors }
.filter { e =>
e.value match {
case Right(event: EventTransition) => !event.isSensoryEvent
case _ => false
}
}
val selfRefNodes = firstLayer
.flatMap {
node => {
val secondLayer = node.outNeighbors.filter(_.value match {
case Left(Place(_, IngredientPlace)) => true
case Left(Place(_, EventOrPreconditionPlace)) => true
case _ => false
})
val eventNodes =
if (hasOnlySubInteractionOutNeighbors(node.outNeighbors ++ secondLayer.flatMap(_.outNeighbors))) Set(node)
else Set.empty
eventNodes ++ secondLayer.filter(i => hasOnlySubInteractionOutNeighbors(i.outNeighbors))
}
}

val newNode = Left(Place(name, Place.SubRecipePlace))
val inEdges = subRecipeNodes.flatMap { node => node.inNeighbors.diff(selfRefNodes).map(n => WLDiEdge[Node, String](n, newNode)(0, "")) }
val outEdges = subRecipeNodes.flatMap { node => node.outNeighbors.diff(selfRefNodes).map(n => WLDiEdge[Node, String](newNode, n)(0, "")) }
acc -- selfRefNodes -- subRecipeNodes ++ inEdges ++ outEdges + newNode
}
}
}

/**
Expand All @@ -57,6 +113,7 @@ object RecipeVisualizer {
private def nodeDotAttrFn(style: RecipeVisualStyle): (RecipePetriNetGraph#NodeT, Set[String], Set[String]) => List[DotAttr] =
(node: RecipePetriNetGraph#NodeT, eventNames: Set[String], ingredientNames: Set[String]) =>
node.value match {
case Left(Place(_, SubRecipePlace)) => style.subRecipe
case Left(Place(_, InteractionEventOutputPlace)) => style.choiceAttributes
case Left(Place(_, EventOrPreconditionPlace)) => style.preconditionORAttributes
case Left(Place(_, EmptyEventIngredientPlace)) => style.emptyEventAttributes
Expand All @@ -72,7 +129,7 @@ object RecipeVisualizer {
case Right(_) => style.eventAttributes
}

private def generateDot(graph: RecipePetriNetGraph, style: RecipeVisualStyle, filter: String => Boolean, eventNames: Set[String], ingredientNames: Set[String]): String = {
private def generateDot(graph: RecipePetriNetGraph, style: RecipeVisualStyle, filter: String => Boolean, eventNames: Set[String], ingredientNames: Set[String], subRecipe: Boolean): String = {

val myRoot = DotRootGraph(directed = graph.isDirected,
id = None,
Expand Down Expand Up @@ -110,10 +167,17 @@ object RecipeVisualizer {
}

// compacts all nodes that are not of interest to the recipe
val compactedGraph = graph
.compactAllNodes(placesToCompact)
.compactAllNodes(transitionsToCompact)

val compactedGraph =
if (subRecipe) {
graph
.compactAllNodes(placesToCompact)
.compactAllNodes(transitionsToCompact)
.compactSubRecipes
} else {
graph
.compactAllNodes(placesToCompact)
.compactAllNodes(transitionsToCompact)
}
// filters out all the nodes that match the predicate function
val filteredGraph = compactedGraph -- compactedGraph.nodes.filter(n => !filter(n.toString))

Expand All @@ -124,12 +188,18 @@ object RecipeVisualizer {
}

def visualizeRecipe(recipe: CompiledRecipe,
style: RecipeVisualStyle,
filter: String => Boolean = _ => true,
eventNames: Set[String] = Set.empty,
ingredientNames: Set[String] = Set.empty): String =
generateDot(recipe.petriNet.innerGraph, style, filter, eventNames, ingredientNames)

style: RecipeVisualStyle,
filter: String => Boolean = _ => true,
eventNames: Set[String] = Set.empty,
ingredientNames: Set[String] = Set.empty): String =
generateDot(recipe.petriNet.innerGraph, style, filter, eventNames, ingredientNames, false)

def visualizeSubRecipe(recipe: CompiledRecipe,
style: RecipeVisualStyle,
filter: String => Boolean = _ => true,
eventNames: Set[String] = Set.empty,
ingredientNames: Set[String] = Set.empty): String =
generateDot(recipe.petriNet.innerGraph, style, filter, eventNames, ingredientNames, true)

def visualizePetriNet(graph: PetriNetGraph, placeLabelFn: Place => String = (p: Place) => p.toString, transitionLabelFn: Transition => String = (t: Transition) => t.toString): String = {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ package object il {
val recipeInstanceEventListName = "RecipeInstanceEventList"
val processIdName = "$ProcessID$" //needed for backwards compatibility with V1 and V2
val exhaustedEventAppend = "RetryExhausted"

val checkpointEventInteractionPrefix = "$CheckpointEventInteraction$"
val subRecipePrefix = "$SubRecipe$"

def sha256HashCode(str: String): Long = {
val sha256Digest: MessageDigest = MessageDigest.getInstance("SHA-256")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ object Place {

sealed trait PlaceType {def labelPrepend: String = ""}

case object SubRecipePlace extends PlaceType
case object IngredientPlace extends PlaceType
case object InteractionEventOutputPlace extends PlaceType
case class FiringLimiterPlace(maxLimit: Int) extends PlaceType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import com.ing.baker.il.CompiledRecipe.{OldRecipeIdVariant, Scala212CompatibleJa
import com.ing.baker.il.RecipeValidations.postCompileValidations
import com.ing.baker.il.petrinet.Place._
import com.ing.baker.il.petrinet._
import com.ing.baker.il.{CompiledRecipe, EventDescriptor, ValidationSettings, checkpointEventInteractionPrefix}
import com.ing.baker.il.{CompiledRecipe, EventDescriptor, ValidationSettings, checkpointEventInteractionPrefix, subRecipePrefix}
import com.ing.baker.petrinet.api._
import com.ing.baker.recipe.common._
import com.ing.baker.recipe.{javadsl, kotlindsl}
Expand All @@ -14,7 +14,6 @@ import scalax.collection.edge.WLDiEdge
import scalax.collection.immutable.Graph

import scala.annotation.nowarn
import scala.collection.immutable.Seq
import scala.language.postfixOps

object RecipeCompiler {
Expand Down Expand Up @@ -201,28 +200,47 @@ object RecipeCompiler {
requiredEvents = e.requiredEvents,
requiredOneOfEvents = e.requiredOneOfEvents)

def flattenSubRecipesToInteraction(recipe: Recipe): Set[InteractionDescriptor] = {
def copyInteraction(i: InteractionDescriptor) = Interaction(
name = s"${subRecipePrefix}${recipe.name}$$${i.name}",
inputIngredients = i.inputIngredients,
output = i.output,
requiredEvents = i.requiredEvents,
requiredOneOfEvents = i.requiredOneOfEvents,
predefinedIngredients = i.predefinedIngredients,
overriddenIngredientNames = i.overriddenIngredientNames,
overriddenOutputIngredientName = i.overriddenOutputIngredientName,
maximumInteractionCount = i.maximumInteractionCount,
failureStrategy = i.failureStrategy,
eventOutputTransformers = i.eventOutputTransformers,
isReprovider = i.isReprovider,
oldName = Option(i.originalName)
)
recipe.interactions.map(copyInteraction).toSet ++ recipe.subRecipes.flatMap(flattenSubRecipesToInteraction)
}

val precompileErrors: Seq[String] = Assertions.preCompileAssertions(recipe)

// Extend the interactions with the checkpoint event interactions
val allInteractions = recipe.interactions ++ recipe.checkpointEvents.map(convertCheckpointEventToInteraction)
// Extend the interactions with the checkpoint event interactions and sub-recipes
val actionDescriptors: Seq[InteractionDescriptor] = recipe.interactions ++
recipe.checkpointEvents.map(convertCheckpointEventToInteraction) ++
recipe.subRecipes.flatMap(flattenSubRecipesToInteraction)

//All ingredient names provided by sensory events or by interactions
val allIngredientNames: Set[String] =
recipe.sensoryEvents.flatMap(e => e.providedIngredients.map(i => i.name)) ++
allInteractions.flatMap(i => i.output.flatMap { e =>
// check if the event was renamed (check if there is a transformer for this event)
i.eventOutputTransformers.get(e) match {
case Some(transformer) => e.providedIngredients.map(ingredient => transformer.ingredientRenames.getOrElse(ingredient.name, ingredient.name))
case None => e.providedIngredients.map(_.name)
}
actionDescriptors.flatMap(i => i.output.flatMap { e =>
// check if the event was renamed (check if there is a transformer for this event)
i.eventOutputTransformers.get(e) match {
case Some(transformer) => e.providedIngredients.map(ingredient => transformer.ingredientRenames.getOrElse(ingredient.name, ingredient.name))
case None => e.providedIngredients.map(_.name)
}
}
)

val actionDescriptors: Seq[InteractionDescriptor] = allInteractions

// For inputs for which no matching output cannot be found, we do not want to generate a place.
// It should be provided at runtime from outside the active petri net (marking)
val interactionTransitions = allInteractions.map(_.toInteractionTransition(recipe.defaultFailureStrategy, allIngredientNames))
val interactionTransitions = actionDescriptors.map(_.toInteractionTransition(recipe.defaultFailureStrategy, allIngredientNames))

val allInteractionTransitions: Seq[InteractionTransition] = interactionTransitions

Expand Down
Loading

0 comments on commit d7b612e

Please sign in to comment.