Softanza
DémarrerEnglish
Softanza
Recherche · narration

Intelligence in Motion: Graph Planning in Softanza

Softanza's Graph Module already gives you powerful tools for representing connected systems. With stzGraph, you model networks of nodes and edges. With stzDiagram, you visualize relationships. stzWorkflow captures process flows, stzOrgChart maps hierarchies, stzKnowledgeGraph encodes semantic relationships. These classes help you build the structure of your problem space.

par Mansour Ayouni · dans le dépôt depuis le 2025-12-28 · tutoriel · domaine Diagrammes et graphes

Cet article est écrit en anglais, comme la bibliothèque.

srzgraphplanner-img01.png
srzgraphplanner-img01.png

When Your Code Needs to Think About Routes


Introduction: From Structure to Strategy

But structure alone doesn't make decisions. Once you have a graph—whether it's a warehouse layout, a workflow diagram, or a knowledge network—the next question emerges: How do I navigate it optimally?

This is where stzGraphPlanner enters. It transforms your static graphs into decision spaces. You don't just have a network—you have a reasoning partner that finds optimal paths, explains choices, compares strategies, learns from history, and filters by constraints. Where other graph classes answer "what is the structure?", stzGraphPlanner answers "what should I do?"

Plans Are More Than Paths

A plan in stzGraphPlanner is not just a route—it is a measurable decision artifact. Every plan carries structured attributes: total cost, number of steps, and domain-specific metrics you define (energy, risk, time, reliability, score). These attributes are computed during planning and remain attached to the plan object for its entire lifecycle.

This means plans can be inspected, compared, ranked, filtered, and reused without re-executing the graph traversal. A plan behaves like a record of reasoning: what was chosen, how expensive it was, and why it scored the way it did.

The Question That Makes It Real

You're building a warehouse management system. Using stzGraph, you've modeled the facility—loading docks, aisles, storage zones, and shelves as nodes; corridors as edges with distance and congestion properties. The structure is clear.

Now a robot needs to move from loading dock to shelf 42. Dozens of paths exist through your graph. Some are shorter. Some avoid congestion. Some use less energy.

How do you make this decision?

Traditional answer: Implement complex pathfinding algorithms. Manage data structures. Debug for weeks.

Softanza answer: Describe what you want. The planner figures out how.

To see it by code, let's design the warehouse graph object first:

Haro
oGraph = new stzGraph("warehouse")
oGraph {
	# Node structure of the warehaouse
	AddNodes([ "entrance", "receiving", "aisle_a", "aisle_b", "storage", "shelf_42", "packing", "shipping")
	
	# The "normal" route through aisles along with a distance tag
	AddEdgeXTT("entrance", "receiving", "hallway", [ :distance = 10 ])
	AddEdgeXTT("receiving", "aisle_a", "hallway", [ :distance = 15 ])
	AddEdgeXTT("aisle_a", "aisle_b", "cross_aisle", [ :distance = 12])
	AddEdgeXTT("aisle_b", "storage", "hallway", [ :distance = 10 ])
	AddEdgeXTT("storage", "shelf_42", "aisle", [ :distance = 10 ])
	AddEdgeXTT("shelf_42", "packing", "hallway", [ :distance = 15 ])
	AddEdgeXTT("packing", "shipping", "hallway", [ :distance = 12 ])
	
	# The SHORTCUT that warehouse workers know about (and that our planner should find)!
	AddEdgeXTT("receiving", "storage", "shortcut", [ :distance = 25 ])

    # Get a visual representation of the graph (will fire your borwser or any svg default viewer)
    View()
}

illustration

When View() is executed, Softanza translates this description into a visual graph that mirrors the structure you just declared. Every node becomes a location box. Every edge becomes a corridor. The diagram is not decorative—it is a faithful projection of your model.

The visual graph you get is the following (note how the shortcut edge creates a direct link from receiving bay to cold storage, bypassing aisle_a and aisle_b):

srzgraphplanner-img01.png

From the diagram alone, a human can suspect that the shortcut might be advantageous. But the fact that a direct link exists does not necessarily mean it is shorter than the two-step route through aisle_a and aisle_b. The planner must compare the sum of distances on the two-step path with the distance of the shortcut, then decide objectively which route is optimal.

To check that, let's do our planning staff by creating a stzGraphPlanner object based on the oGraph warehouse object we just designed:

Haro
oPlanner = new stzGraphPlanner(oGraph)
oPlanner {
    // Describing our plan
    AddPlan("warehouse_route")
    Walk(:From = "entrance", :To = "shelf_42")
    Minimizing("distance") # The "distance" tag we attached as a metadata to the grpah edges will define cost

    // Executuing the plan (internal optimisations are made to find the optimal route)
    Execute()
    
    // Now see the results : first what route has been selected by the planner
    ? Route()
    #--> [ "entrance", "receiving", "storage", "shelf_42" ]

    // What is the cost of that route (in terms of "distance")
    ? Cost()
    #--> 45
}

illustration

We can inspect how the planner reached the selected route while minimizing "distance" by using the Explain() method:

    ? @@NL( Explain() )

And so we get:

[
	[ "plan", "warehouse_route" ],
	[
		"actions",
		[
			[
				[ "from", "entrance" ],
				[ "to", "receiving" ],
				[ "cost", 10 ]
			],
			[
				[ "from", "receiving" ],
				[ "to", "storage" ],
				[ "cost", 25 ]
			],
			[
				[ "from", "storage" ],
				[ "to", "shelf_42" ],
				[ "cost", 10 ]
			]
		]
	],
	[ "total_cost", 45 ],
	[
		"route",
		[
			"entrance",
			"receiving",
			"storage",
			"shelf_42"
		]
	],
	[ "steps", 3 ]
]

The output shows that the planner optimized the route in three steps: first from "entrance" to "receiving" (cost 10), then from "receiving" to "storage" (cost 25), and finally from "storage" to "shelf_42" (cost 10), resulting in a total distance of 10 + 25 + 10 = 45, and therefore selecting the route [ "entrance", "receiving", "storage", "shelf_42" ].

Now, we can ask the planner about the alternative routes it did not select by calling the Alternatives() method:

    ? @@NL( Alternatives() )

And so we get:

[
	[ "plan", "warehouse_route" ],
	[
		"decision_points",
		[
			[
				[ "node", "receiving" ],
				[ "chosen", "aisle_a" ],
				[ "total_options", 2 ]
			]
		]
	]
]

The output tells us that at each node with multiple neighbors (in our case, only at the "receiving" node), the planner had two possible options—as shown in the visual graph above: either going right through the direct shortcut edge, or alternatively going left through the "aisle_a" node. This alternative path appears in the output precisely because it reports the route that was considered but not selected.

Three lines to plan. Three lines to understand. No algorithm knowledge required. Hundreds of lines of complex optimization and pathfinding code avoided. The robot finds its way, and you can go further by asking: Why this route? How does the selected route compare to its alternatives? How does it compare to yesterday? And many more.

This is stzGraphPlanner—where graph structure meets strategic intelligence.

The Journey Ahead

This article explores graph planning through three fundamental questions:

How do we find optimal paths? We'll see how to express what matters—whether minimizing distance, avoiding danger, or balancing multiple criteria—and let the planner discover the best routes automatically.

How do we understand our choices? We'll examine what happens after finding a path: explaining costs, comparing alternatives, and understanding why one route was chosen over another.

How do we plan smarter? We'll discover advanced capabilities: comparing multiple strategies at once, learning from past executions, and filtering solutions by real-world constraints.

The progression is deliberate: first finding paths, then understanding them, finally mastering complex planning scenarios. Each capability builds on the previous, moving from single-route optimization to sophisticated multi-criteria decision support.

The concepts may be subtle, but the code remains simple. By the end, you'll see graph planning not as implementing algorithms, but as having a reasoning partner for navigating complex networks.


Part One: The Essence of Planning

When you search, you explore until you find something. When you plan, you reason about the best path before taking a step. The difference is profound.

Let's start with the fundamental act, taking the warehase example we made above as a background: getting from point A to point B. You've built a graph representing your warehouse layout—nodes are locations, edges are paths. Now you want to navigate it:

Haro
oPlanner = new stzGraphPlanner(oGraph)
oPlanner {
    AddPlan("warehouse_route")
    Walk(:From = "entrance", :To = "shelf_42")
    Minimizing("distance")
    Execute()
}

illustration

Three things just happened that matter:

First, you expressed intent ("minimize distance"), not implementation ("use A* algorithm with Euclidean heuristic"). The planner chose the right algorithm automatically.

Second, the result isn't just a path—it's a plan object you can interrogate:

Haro
? Cost()           // How expensive was it?
? Route()         // What's the route?
? Explain()        // Why this route?

illustration

Third, you got transparency. When the plan returns cost: 45 and route entrance → receiving → storage → shelf_42, you can ask why.

The Intelligence: Discovering Hidden Patterns

Your warehouse has a shortcut—a corridor that bypasses two aisles. But it's not necessary obvious from just looking at the graph. You have:

  • Long route: entrance → receiving → aisle_a → aisle_b → storage → shelf_42 (distance: 57)
  • Hidden shortcut: entrance → receiving → storage → shelf_42 (distance: 45)

You don't tell the planner about the shortcut. You just say "minimize distance." The planner discovers the shortcut by exploring intelligently. It uses smart search algorithms (like A*—a pathfinding algorithm that estimates which direction looks most promising) but you never had to implement them.

Principle: Planning is pattern discovery, not path following.

Simulation Before Execution

Planning in Softanza is fundamentally non-destructive. Evaluating a plan does not mean executing it in the real world. The planner simulates outcomes, computes costs, and explores alternatives entirely in memory.

This allows you make as many plans as you need and ask powerful questions safely:

  • What would happen if we chose this strategy?
  • How this plan performed across time?
  • How sensitive is this decision to cost changes?

Only after understanding the decision space do you choose whether—and how—to act.


Part Two: When Destination Becomes Condition

The Paradigm Shift: Goal-Based Thinking

Sometimes you don't know where you want to go. You know what you're looking for.

Imagine a game character needing treasure—any treasure worth at least 1000 gold. You could hardcode "go to the dungeon." But what if game state changes?

Goal-based planning thinks differently: Let's start by design the game world as a stzGraph object...

Haro
oGraph = new stzGraph("rpg_world")
oGraph {
    AddNodeXTT("village", "Starting Village", [
        :gold = 0, :hasKey = FALSE, :danger = 0
    ])
    
    AddNodeXTT("forest", "Dark Forest", [
        :gold = 500, :hasKey = FALSE, :danger = 3
    ])
    
    AddNodeXTT("cave", "Mysterious Cave", [
        :gold = 800, :hasKey = TRUE, :danger = 5
    ])
    
    AddNodeXTT("dungeon", "Ancient Dungeon", [
        :gold = 1500,  # This qualifies!
        :hasKey = FALSE, :danger = 8
    ])
    
    AddNodeXTT("castle", "Abandoned Castle", [
        :gold = 800,  # Less than 1000
        :hasKey = FALSE, :danger = 6
    ])
    
    AddEdgeXTT("village", "forest", "path", [:danger = 2])
    AddEdgeXTT("forest", "cave", "path", [:danger = 3])
    AddEdgeXTT("forest", "dungeon", "path", [:danger = 7])
    AddEdgeXTT("village", "castle", "path", [:danger = 5])
}

illustration

Then, let's make our planning staff inside a stzGraphPlanner object:

Haro
oPlanner = new stzGraphPlanner(oGraph)
oPlanner {
    AddPlan("rpg_plan")
    
    Walk(
        :FromNode = "village",
        :UntilYouReachF = func(node) {
            return node[:properties][:gold] >= 1000
        }
    )
    
    Minimizing("danger")
    Execute()

    ? Cost()
    #--> 9

    ? @@( Route() )
    #--> [ "village", "forest", "dungeon" ]

    ? @@NL( Actions() )
    #--> [
    #      [ [ "from", "village" ], [ "to", "forest" ], [ "cost", 2 ] ],
    #      [ [ "from", "forest" ], [ "to", "dungeon" ], [ "cost", 7 ] ]
    #    ]
}

illustration

You didn't specify a destination node. You specified a destination condition. The planner:

  1. Explores from your starting point
  2. Evaluates each node: "Does this satisfy the goal?"
  3. Returns optimal path to first qualifying node
  4. Still respects optimization criteria

Result: village → forest → dungeon (cost: 9 danger points). The dungeon was nearest location with sufficient treasure, reached via the safest route.

This pattern appears everywhere: - Logistics: "Find warehouse with 500+ units, closest to customer" - Healthcare: "Route to any hospital with cardiac capability, fastest response" - Manufacturing: "Find production line where quality > 95%, with capacity"

You're not routing to a place. You're routing to a state of the world that satisfies your needs.


Part Three: The Transparency Revolution

When Plans Explain Themselves

Most AI systems are black boxes. You get an answer but no understanding. Softanza believes plans should be conversational.

Let's start as ususal with a stzGraph object:

Haro
oGraph = new stzGraph("complex_route")
oGraph {
    AddNode("start")
    AddNode("point_a")
    AddNode("point_b")
    AddNode("end")
    
    AddEdgeXTT("start", "point_a", "road", [:distance = 10, :traffic = 2])
    AddEdgeXTT("point_a", "point_b", "road", [:distance = 15, :traffic = 8])
    AddEdgeXTT("point_b", "end", "road", [:distance = 5, :traffic = 1])
}

illustration

Now we do our planning mission:

oPlanner = new stzGraphPlanner(oGraph)
oPlanner {
    AddPlan("route_analysis")
    Walk(:From = "start", :To = "end")
    Minimize("distance")
    Minimize("traffic")
    Execute()

    ? Rout()

    ? Cost()
}

You've generated a delivery route, but the cost seems high: let's check th cost structure genertae from the plan execution: We write...

    ? @@NL( CostBreakdown() )

and so we get:

    #--> [
    #      [
    #        [ "step", 1 ], [ "from", "start" ], [ "to", "point_a" ],
    #        [ "criteria", [
    #          [ [ "property", "distance" ], [ "value", 10 ], 
    #            [ "weight", 1 ], [ "direction", "minimize" ], 
    #            [ "contribution", 10 ] ],
    #          [ [ "property", "traffic" ], [ "value", 2 ], 
    #            [ "weight", 1 ], [ "direction", "minimize" ], 
    #            [ "contribution", 2 ] ]
    #        ]],
    #        [ [ "total", 12 ] ]
    #      ],
    #      [
    #        [ "step", 2 ], [ "from", "point_a" ], [ "to", "point_b" ],
    #        [ "criteria", [
    #          [ [ "property", "distance" ], [ "value", 15 ], 
    #            [ "contribution", 15 ] ],
    #          [ [ "property", "traffic" ], [ "value", 8 ], 
    #            [ "contribution", 8 ] ]
    #        ]],
    #        [ [ "total", 23 ] ]
    #      ],
    #      [
    #        [ "step", 3 ], [ "from", "point_b" ], [ "to", "end" ],
    #        [ "criteria", [
    #          [ [ "property", "distance" ], [ "value", 5 ], 
    #            [ "contribution", 5 ] ],
    #          [ [ "property", "traffic" ], [ "value", 1 ], 
    #            [ "contribution", 1 ] ]
    #        ]],
    #        [ [ "total", 6 ] ]
    #      ]
    #    ]
}

the detailed output show that the cost has been accumulated from two steps:

Step 1: warehouse → suburb_a
  • distance: 12 × 1 (minimize) = 12
  • traffic: 2 × 1 (minimize) = 2
  Total: 14

Step 2: suburb_a → downtown  
  • distance: 15 × 1 (minimize) = 15
  • traffic: 8 × 1 (minimize) = 8
  Total: 23

Now you see it: the second leg has heavy traffic (8 points). Not a mystery—just math, explained.

The "Why" Conversation

Want more context?

Haro
? ExplainWhy("route")

illustration

Sortie promise
This route was selected because:
• Total cost: 37
• Explored 8 nodes to find it
• Optimized for: minimize distance, minimize traffic

And to understand alternatives:

Haro
? ExplainAlternatives()

illustration

Sortie promise
Key decisions made:
• At 'warehouse', chose 'suburb_a' (2 options available)
• At 'suburb_a', chose 'downtown' (3 options available)

Introspection by Design

Every object produced by stzGraphPlanner is introspectable. Plans expose their paths, costs, steps, rankings, and comparison results through explicit queries—not logs or debug output.

This is intentional. Instead of hiding intelligence inside algorithms, Softanza surfaces it as structured knowledge you can explore, explain, and audit. Planning becomes something you reason about, not something you merely run.

In regulated industries (healthcare, finance, transportation), you may be legally required to explain automated decisions. With stzGraphPlanner, explanation is built in. The plan knows why it was created.


Part Four: Strategy as Language

The Profile System: Named Intentions

You keep optimizing for the same combinations. Fast deliveries minimize time and fuel. Safe routes minimize danger and maximize cover. Budget operations minimize cost and maintenance.

Instead of rewriting criteria every time, profiles encode strategy as named concepts:

Haro
// Instead of this every time:
Minimizing("time").Minimizing("fuel").WithWeight(0.6, 0.4)

// Just say this:
Using(:fastest)

illustration

Six profiles come standard: :fastest, :safest, :cheapest, :shortest, :balanced, :efficient. Each encodes a common optimization strategy.

Fluent Planning Mirrors Human Reasoning

The planner's fluent interface is not syntactic sugar—it mirrors how humans think about decisions. You start with intent, add constraints, explore alternatives, and only then evaluate outcomes.

Each method returns the planner itself, allowing plans to be constructed progressively and read top-to-bottom like a sentence. The code becomes a narrative of reasoning, not a sequence of commands.

In Softanza, fluency is about cognitive alignment between human thought and executable logic.

Profiles in Action

An ambulance dispatch system:

Haro
// During low traffic: prioritize speed
AddPlan("rush_hour")
Walk(:From = "hospital", :To = "emergency")
Using(:fastest)
Execute()

// During peak hours: prioritize reliability
AddPlan("reliable")
Walk(:From = "hospital", :To = "emergency")
Using(:safest)    // Avoids unpredictable routes
Execute()

illustration

Same graph, same endpoints—different strategies. Profiles make strategy part of the domain language, not buried in code.

Here's a complete example showing profile usage:

Haro
oGraph = new stzGraph("delivery_network")
oGraph {
    AddNodeXTT("warehouse", "Main Warehouse", [:x = 0, :y = 0])
    AddNodeXTT("suburb_a", "Suburb A", [:x = 10, :y = 5])
    AddNodeXTT("suburb_b", "Suburb B", [:x = 15, :y = 10])
    AddNodeXTT("customer", "Customer Location", [:x = 25, :y = 20])
    
    AddEdgeXTT("warehouse", "suburb_a", "highway", [
        :distance = 12, :time = 25, :cost = 10
    ])
    AddEdgeXTT("suburb_a", "customer", "road", [
        :distance = 18, :time = 20, :cost = 15
    ])
    
    AddEdgeXTT("warehouse", "suburb_b", "backroad", [
        :distance = 20, :time = 20, :cost = 8
    ])
    AddEdgeXTT("suburb_b", "customer", "highway", [
        :distance = 15, :time = 15, :cost = 12
    ])
}

oPlanner = new stzGraphPlanner(oGraph)
oPlanner {
    AddPlan("fast_delivery")
    Walk(:From = "warehouse", :To = "customer")
    Using(:fastest)
    Execute()

    ? Cost()
    #--> 35
    
    ? @@( Route() )
    #--> [ "warehouse", "suburb_b", "customer" ]
}

illustration


Part Five: The Comparison Problem

When One Solution Isn't Enough

Real decisions involve trade-offs. Do we take the fast route or safe one? Cheap production or high-quality?

Traditional systems give you one answer. Intelligent decision-making requires understanding alternatives.

Comparing Plans

You've planned two delivery routes—one for speed, one for fuel economy:

Haro
AddPlan("fast")
Walk(:From = "warehouse", :To = "customer")
Using(:fastest)
Execute()

AddPlan("economical")
Walk(:From = "warehouse", :To = "customer")
Using(:cheapest)
Execute()

SetCurrentPlan("fast")
? ExplainDifference("economical")

illustration

Output:

PATH ANALYSIS:
Plans use DIFFERENT routes
  Plan 1: [ "warehouse", "highway", "downtown", "customer" ]
  Plan 2: [ "warehouse", "local_road", "suburb", "customer" ]
  
  Paths diverge at step 2

COST ANALYSIS:
  Plan 1 cost: 25 (time)
  Plan 2 cost: 80 (fuel cost)
  ✓ Plan 1 is faster, Plan 2 is cheaper

Here's the complete example:

Haro
oGraph = new stzGraph("trade_off")
oGraph {
    AddNode("factory")
    AddNode("process_premium")
    AddNode("process_standard")
    AddNode("shipping")
    
    AddEdgeXTT("factory", "process_premium", "premium", [
        :cost = 100, :time = 5, :quality = 10
    ])
    AddEdgeXTT("process_premium", "shipping", "finish", [
        :cost = 50, :time = 3, :quality = 10
    ])
    
    AddEdgeXTT("factory", "process_standard", "standard", [
        :cost = 30, :time = 15, :quality = 7
    ])
    AddEdgeXTT("process_standard", "shipping", "finish", [
        :cost = 20, :time = 10, :quality = 7
    ])
}

oPlanner = new stzGraphPlanner(oGraph)
oPlanner {
    AddPlan("budget")
    Walk(:From = "factory", :To = "shipping")
    Using(:cheapest)
    Execute()
    
    AddPlan("rush")
    Walk(:From = "factory", :To = "shipping")
    Using(:fastest)
    Execute()
    
    SetCurrentPlan("budget")
    ? @@NL( ExplainDifferenceWith("rush") )
    #--> [
    #      [ "plan1", "budget" ],
    #      [ "plan2", "rush" ],
    #      [ "same_path", FALSE ],
    #      [ "route1", [ "factory", "process_standard", "shipping" ] ],
    #      [ "route2", [ "factory", "process_premium", "shipping" ] ],
    #      [ "diverge_at_step", 2 ],
    #      [ "cost1", 40.40 ],
    #      [ "cost2", 6.20 ],
    #      [ "cheaper", "rush" ]
    #    ]
}

illustration

Trade-off Analysis

Haro
? ShowTradeoffs("economical")

illustration

Sortie promise
CRITERION COMPARISON:
  Cost:        Plan 2 wins (saves $35)
  Path Length: Tie

RECOMMENDATION:
  → Choose Plan 1 for time-sensitive deliveries
  → Choose Plan 2 for routine deliveries

The comparison system doesn't make decisions for you. It structures the decision so you can judge intelligently.

Complete trade-off example:

Haro
SetCurrentPlan("budget")
? @@NL( ExplainTradeoffsAgainst("rush") )
#--> [
#      [ "plan1", "budget" ],
#      [ "plan2", "rush" ],
#      [ "cost_winner", "rush" ],
#      [ "cost_savings", 34.20 ],
#      [ "length_winner", "tie" ],
#      [ "length_difference", 0 ],
#      [ "recommendation", "Choose rush for cost optimization" ]
#    ]

? WhichIsCheaper("rush")
#--> rush

? CostSaving("rush")
#--> 34.20

illustration


Part Six: Multi-Plan Intelligence

Comparing Many Strategies Simultaneously

Real scenarios have multiple alternatives. Compare them all at once:

Haro
// Create 4 different strategies
AddPlan("ultra_fast")
Walk(:From = "warehouse", :To = "destination")
Using(:fastest)
Execute()

AddPlan("budget")
Using(:cheapest)
Execute()

AddPlan("short_distance")
Using(:shortest)
Execute()

AddPlan("balanced")
Using(:balanced)
Execute()

// Compare all 4
oMulti = CompareMultiple(["ultra_fast", "budget", "short_distance", "balanced"])

illustration

Ranking as Structured Data

When multiple plans are compared, stzGraphPlanner does not produce text—it produces data. Ranking tables are returned as structured collections that can be consumed programmatically by dashboards, reports, UI components, or analytics pipelines.

This distinction matters. A ranking is not just something to read—it is something to act upon. The same ranking table can power a console display, a decision widget, an automated rule engine, or a visual comparison chart without transformation.

Get rankings:

Haro
? oMulti.RankBy("cost")
//→ [ ["budget", 25], ["balanced", 45], ["short_distance", 50], ["ultra_fast", 90] ]

oMulti.ShowRankingTable()

illustration

Output:

=== PLAN RANKING TABLE ===

Rank | Plan Name      | Cost  | Steps
-----+----------------+-------+------
1    | budget         | 25    | 3
2    | balanced       | 45    | 3
3    | short_distance | 50    | 3
4    | ultra_fast     | 90    | 3

Quick Decision Support

Haro
? oMulti.BestBy("cost")
//→ "budget"

? oMulti.WorstBy("cost")
//→ "ultra_fast"

illustration

Complete multi-plan comparison example:

Haro
oGraph = new stzGraph("multi_strategy")
oGraph {
    AddNode("warehouse")
    AddNode("route_highway")
    AddNode("route_backroad")
    AddNode("route_express")
    AddNode("destination")
    
    AddEdgeXTT("warehouse", "route_highway", "path", [
        :time = 10, :cost = 50, :distance = 20
    ])
    AddEdgeXTT("route_highway", "destination", "path", [
        :time = 8, :cost = 40, :distance = 15
    ])
    
    AddEdgeXTT("warehouse", "route_backroad", "path", [
        :time = 25, :cost = 15, :distance = 30
    ])
    AddEdgeXTT("route_backroad", "destination", "path", [
        :time = 20, :cost = 10, :distance = 25
    ])
    
    AddEdgeXTT("warehouse", "route_express", "path", [
        :time = 5, :cost = 80, :distance = 18
    ])
    AddEdgeXTT("route_express", "destination", "path", [
        :time = 4, :cost = 60, :distance = 12
    ])
}

oPlanner = new stzGraphPlanner(oGraph)
oPlanner {
    AddPlan("ultra_fast")
    Walk(:From = "warehouse", :To = "destination")
    Using(:fastest)
    Execute()
    
    AddPlan("budget")
    Walk(:From = "warehouse", :To = "destination")
    Using(:cheapest)
    Execute()
    
    AddPlan("short_distance")
    Walk(:From = "warehouse", :To = "destination")
    Using(:shortest)
    Execute()
    
    AddPlan("balanced")
    Walk(:From = "warehouse", :To = "destination")
    Using(:balanced)
    Execute()
    
    oMulti = CompareManyQ(["ultra_fast", "budget", "short_distance", "balanced"])
    
    ? @@NL( oMulti.CompareAll() )
    #--> [
    #      [ "total_plans", 4 ],
    #      [ "plans", [
    #        [ [ "plan", "ultra_fast" ], [ "cost", 15.30 ], [ "steps", 3 ],
    #          [ "route", [ "warehouse", "route_express", "destination" ] ] ],
    #        [ [ "plan", "budget" ], [ "cost", 31 ], [ "steps", 3 ],
    #          [ "route", [ "warehouse", "route_backroad", "destination" ] ] ],
    #        [ [ "plan", "short_distance" ], [ "cost", 30 ], [ "steps", 3 ],
    #          [ "route", [ "warehouse", "route_express", "destination" ] ] ],
    #        [ [ "plan", "balanced" ], [ "cost", 42 ], [ "steps", 3 ],
    #          [ "route", [ "warehouse", "route_backroad", "destination" ] ] ]
    #      ]],
    #      [ "best_by_cost", "ultra_fast" ],
    #      [ "best_by_steps", "ultra_fast" ]
    #    ]
    
    ? @@NL( oMulti.RankBy("cost") )
    #--> [
    #      [ "ultra_fast", 15.30 ],
    #      [ "short_distance", 30 ],
    #      [ "budget", 31 ],
    #      [ "balanced", 42 ]
    #    ]
    
    oMulti.ShowRankingTable()
    #--> Displays formatted table
    
    ? oMulti.BestBy("cost")
    #--> ultra_fast
    
    ? oMulti.WorstBy("cost")
    #--> balanced
}

illustration

Pattern: When you have many options, ranking tables and quick queries help identify the right strategy for your context.


Part Seven: Learning from History

Historical Comparison: Plans Remember

Every plan execution is stored in history. Compare current performance against past:

Haro
// Execute multiple delivery runs
for i = 1 to 5
    AddPlan("delivery_" + i)
    Walk(:From = "depot", :To = "customer")
    Minimize("cost")
    Execute()
next

// Analyze performance
? HistoryCount()
//→ 5

? HistoricalAverage("cost")
//→ Shows average cost across all 5 runs

? BestHistoricalPlan("cost")
//→ "delivery_3"  (returns best performing plan)

illustration

Is This an Improvement?

Haro
SetCurrentPlan("delivery_5")
oHistComp = CompareWithHistory()

? oHistComp.Explain()

illustration

Output:

=== HISTORICAL COMPARISON ===

Current Plan: delivery_5
  Cost: 30
  Steps: 3

Historical Average:
  Cost: 35
  Steps: 3

✓ Current plan is 14.3% better than average

Best historical plan: delivery_3

Methods:

Haro
? oHistComp.IsImprovement()
//→ TRUE

? oHistComp.ImprovementPercentage()
//→ 14.3

illustration

Complete historical tracking example:

Haro
oGraph = new stzGraph("evolving_network")
oGraph {
    AddNode("start")
    AddNode("mid1")
    AddNode("mid2")
    AddNode("end")
    
    AddEdgeXTT("start", "mid1", "path", [:cost = 10])
    AddEdgeXTT("start", "mid2", "path", [:cost = 15])
    AddEdgeXTT("mid1", "end", "path", [:cost = 20])
    AddEdgeXTT("mid2", "end", "path", [:cost = 8])
}

oPlanner = new stzGraphPlanner(oGraph)
oPlanner {
    // Attempt 1: Initial optimal route
    AddPlan("attempt1")
    Walk(:From = "start", :To = "end")
    Minimize("cost")
    Execute()
    ? Cost()
    #--> 23
    
    ? @@( Route() )
    #--> [ "start", "mid2", "end" ]
    
    // Network degrades - mid2 route congested
    @oGraph.SetEdgeProperty("mid2", "end", "cost", 25)
    
    // Attempt 2: Same plan, worse network
    AddPlan("attempt2")
    Walk(:From = "start", :To = "end")
    Minimize("cost")
    Execute()
    ? Cost()
    #--> 30
    
    ? @@( Route() )
    #--> [ "start", "mid1", "end" ]
    
    // Network improves - mid1 route optimized
    @oGraph.SetEdgeProperty("mid1", "end", "cost", 12)
    
    // Attempt 3: Same plan, improved network
    AddPlan("attempt3")
    Walk(:From = "start", :To = "end")
    Minimize("cost")
    Execute()
    ? Cost()
    #--> 22
    
    ? @@( Route() )
    #--> [ "start", "mid1", "end" ]
    
    // Historical analysis
    ? HistoryCount()
    #--> 3
    
    ? HistoricalAverage("cost")
    #--> 25
    
    ? BestHistoricalPlan("cost")
    #--> attempt3
    
    // Compare current with history
    SetCurrentPlan("attempt3")
    oHistComp = CompareWithHistoryQ()
    
    ? @@NL( oHistComp.Explain() )
    #--> [
    #      [ "current_plan", "attempt3" ],
    #      [ "cost", 22 ],
    #      [ "steps", 3 ],
    #      [ "historical_average_cost", 25 ],
    #      [ "historical_average_steps", 3 ],
    #      [ "observation", "✓ Current plan is 12% better than average" ],
    #      [ "best_historical_plan", "attempt3" ]
    #    ]
    
    ? oHistComp.IsImprovement()
    #--> TRUE
    
    ? oHistComp.ImprovementPercentage()
    #--> 12
}

illustration

Pattern: Historical tracking transforms planning from one-off computation to continuous improvement. You learn what works.


Part Eight: Constraint-Based Filtering

Finding Plans That Meet Requirements

When you have many plans, filter by constraints:

Haro
// Create multiple route plans
AddPlan("cheap_route")
// ... execute ...

AddPlan("fast_route")
// ... execute ...

AddPlan("safe_route")
// ... execute ...

// Filter: cost <= 50
oFilter = FilterPlans([:maxCost = 50])
? oFilter.Count()
//→ 2

oFilter.Show()

illustration

Avoiding Nodes

Haro
// Plans that avoid dangerous downtown area
oFilter = PlansAvoiding("downtown")
oFilter.Show()

illustration

Requiring Waypoints

Haro
// Plans that must visit distribution center
oFilter = PlansRequiring("distribution_center")
? oFilter.Count()

illustration

Tolerance-Based Filtering

Haro
// Find plans within 10% of optimal cost
oFilter = PlansWithin(10, :of = "optimal_plan")
? oFilter.Count()
//→ Shows plans that are "good enough"

? oFilter.BestBy("cost")
//→ Returns cheapest from filtered set

oFilter.ShowRankingTable()

illustration

Multiple Constraints

Haro
// Complex real-world filtering
oFilter = FilterPlans([
    :maxCost, 50,
    :avoid, "downtown",
    :maxSteps, 4
])

? "Found " + oFilter.Count() + " plans matching all constraints"

illustration

Complete filtering example:

Haro
oGraph = new stzGraph("filtered_network")
oGraph {
    AddNode("origin")
    AddNode("cheap_route")
    AddNode("expensive_route")
    AddNode("medium_route")
    AddNode("avoid_me")
    AddNode("destination")
    
    // Cheap: 15 total
    AddEdgeXTT("origin", "cheap_route", "path", [:cost = 10])
    AddEdgeXTT("cheap_route", "destination", "path", [:cost = 5])
    
    // Expensive through avoid_me: 80 total
    AddEdgeXTT("origin", "expensive_route", "path", [:cost = 50])
    AddEdgeXTT("expensive_route", "avoid_me", "path", [:cost = 20])
    AddEdgeXTT("avoid_me", "destination", "path", [:cost = 10])
    
    // Medium: 35 total
    AddEdgeXTT("origin", "medium_route", "path", [:cost = 20])
    AddEdgeXTT("medium_route", "destination", "path", [:cost = 15])
}

oPlanner = new stzGraphPlanner(oGraph)
oPlanner {
    AddPlan("plan_cheap")
    Walk(:From = "origin", :To = "destination")
    Minimize("cost")
    Execute()
    
    AddPlan("plan_via_avoid")
    Walk(:From = "origin", :To = "avoid_me")
    Minimize("cost")
    Execute()
    
    AddPlan("plan_via_medium")
    Walk(:From = "origin", :To = "medium_route")
    Minimize("cost")
    Execute()
    
    // Filter: cost <= 50
    oFilter1 = FilterPlansQ([:maxCost = 50])
    ? oFilter1.Count()
    #--> 2
    
    ? @@( oFilter1.Plans() )
    #--> [ "plan_cheap", "plan_via_medium" ]
    
    ? @@NL( oFilter1.PlansXT() )
    #--> [
    #      [ "constrains_applied", [ [ "maxcost", 50 ] ] ],
    #      [ "plans_matching_count", 2 ],
    #      [ "plans_matching_details", [
    #        [ [ "plan", "plan_cheap" ], [ "cost", 15 ], [ "steps", 3 ],
    #          [ "route", [ "origin", "cheap_route", "destination" ] ] ],
    #        [ [ "plan", "plan_via_medium" ], [ "cost", 20 ], [ "steps", 2 ],
    #          [ "route", [ "origin", "medium_route" ] ] ]
    #      ]]
    #    ]
    
    // Filter: avoid node
    oFilter2 = PlansAvoidingQ("avoid_me")
    ? oFilter2.Count()
    #--> 2
    
    ? @@( oFilter2.Plans() )
    #--> [ "plan_cheap", "plan_via_medium" ]
    
    // Filter: cost >= 20
    oFilter3 = FilterPlansQ([:minCost = 20])
    ? oFilter3.Count()
    #--> 2
    
    ? @@( oFilter3.Plans() )
    #--> [ "plan_via_avoid", "plan_via_medium" ]
}

illustration

Tolerance-based filtering example:

Haro
oGraph = new stzGraph("tolerance_test")
oGraph {
    AddNode("base")
    AddNode("option_a")
    AddNode("option_b")
    AddNode("option_c")
    AddNode("target")
    
    // Optimal: 40
    AddEdgeXTT("base", "option_a", "path", [:cost = 40])
    AddEdgeXTT("option_a", "target", "path", [:cost = 60])
    
    // Near-optimal: 45 (12.5% worse)
    AddEdgeXTT("base", "option_b", "path", [:cost = 45])
    AddEdgeXTT("option_b", "target", "path", [:cost = 60])
    
    // Suboptimal: 70 (75% worse)
    AddEdgeXTT("base", "option_c", "path", [:cost = 70])
    AddEdgeXTT("option_c", "target", "path", [:cost = 60])
}

oPlanner = new stzGraphPlanner(oGraph)
oPlanner {
    AddPlan("optimal")
    Walk(:From = "base", :To = "option_a")
    Minimize("cost")
    Execute()
    
    AddPlan("near_optimal")
    Walk(:From = "base", :To = "option_b")
    Minimize("cost")
    Execute()
    
    AddPlan("suboptimal")
    Walk(:From = "base", :To = "option_c")
    Minimize("cost")
    Execute()
    
    // Filter within 15% of optimal
    oFilter = PlansWithinQ(15, :of = "optimal")
    ? oFilter.Count()
    #--> 2
    
    ? @@( oFilter.Plans() )
    #--> [ "optimal", "near_optimal" ]
    
    ? oFilter.BestBy("cost")
    #--> optimal
    
    oFilter.ShowRankingTable()
    #--> Displays formatted table
}

illustration

Pattern: Filtering transforms "find the best" into "find the best that meets my requirements"—crucial for real-world constraints.


Part Nine: The Architecture of Intelligence

What's Happening Under the Hood

The fluent interface feels simple. Behind it:

  • Smart pathfinding algorithms (like A*, which intelligently estimates distances to find optimal routes) with automatic selection based on your graph structure
  • Best-first search (exploring the most promising paths first) for goal-based planning with early termination when a match is found
  • Multi-criteria optimization combining multiple factors (time, cost, distance) with weighted importance
  • Exploration tracking logging every decision point for later explanation

You never have to know this. The interface abstracts complexity without hiding reasoning.

The Three-Layer Design

Expression Layer: State intent

Haro
Walk(:From = "A", :To = "B").Using(:fastest)

illustration

Reasoning Layer: Algorithms work (hidden but accessible)

Haro
A* exploration, cost calculation, path reconstruction

illustration

Explanation Layer: Transparency emerges

Haro
ExplainWhy(), ExplainAlternatives(), CompareTo()

illustration

Traditional systems have layers 1 and 2. Softanza adds layer 3, transforming from "computational tool" to "reasoning partner."


Part Ten: Planning in the Wild

Real Systems, Real Decisions

Warehouse Robotics: Robots receive pick orders with varying priority and size. The planner: - Minimizes travel time for urgent orders (:fastest) - Minimizes energy for routine orders (:cheapest) - Avoids congested aisles during peak hours - Explains routing decisions to managers

Result: 30% faster picks, 20% energy savings, zero confusion.

Emergency Dispatch: 911 calls need nearest appropriate unit. The planner: - Uses goal-based search: "Find any ambulance where distance < 10 miles AND capabilities match" - Optimizes for actual response time, not just distance - Compares multiple plans: fastest vs most reliable - Provides explanation for dispatch logs (regulatory requirement)

Result: 2-minute improvement in response time. More importantly: explainable decisions.

Manufacturing Workflow: Custom orders have quality requirements, deadlines, cost constraints. The planner: - Routes orders through production stages - Optimizes per order (luxury: :balanced, commodity: :cheapest) - Compares alternative workflows showing trade-offs - Explains decisions to customers

Result: Better communication, strategic resource allocation, 15% cost reduction.

Pattern: The planner isn't replacing judgment—it's structuring decisions so humans can judge better. This is augmented intelligence.

The Softanza Advantage

Comparison with Leading Graph Planning Libraries

Feature Softanza NetworkX (Python) JGraphT (Java) Boost Graph (C++) igraph (Python/R) Pathfinding.js
Intent-Based API ✅ Natural language ❌ Algorithm-centric ❌ Class-based ❌ Template-heavy ❌ Function calls ❌ Config objects
Built-in Explanations ✅ Multi-level ❌ None ❌ None ❌ None ❌ None ❌ None
Multi-Plan Comparison ✅ Native support ❌ Manual ❌ Manual ❌ Manual ❌ Manual ❌ Manual
Historical Tracking ✅ Automatic ❌ External ❌ External ❌ External ❌ External ❌ External
Constraint Filtering ✅ Declarative ⚠️ List comprehensions ⚠️ Streams/filters ⚠️ STL algorithms ⚠️ Vector subset ⚠️ Array.filter
Named Strategy Profiles ✅ :fastest, :safest ❌ Weight dicts ❌ Builder patterns ❌ Policy classes ❌ Parameters ❌ Options object
Goal-Based Search ✅ First-class ⚠️ Custom predicates ⚠️ Interface impl ⚠️ Visitor pattern ⚠️ Lambda functions ❌ Not built-in
Fluent Interface ✅ Full fluency ❌ Function chaining ⚠️ Builder only ❌ Separate calls ❌ Separate calls ⚠️ Method chaining
Learning Curve Gentle Moderate Steep Very steep Moderate Gentle
Target Audience Domain experts Data scientists Enterprise Java C++ experts Researchers Web developers
Explainability Conversational Debug prints Logging Debug output Verbose mode Console logs

Legend: ✅ Native/Excellent | ⚠️ Possible but manual | ❌ Not available


These are excellent libraries, each powerful in its domain. NetworkX excels at graph analysis, JGraphT at enterprise reliability, Boost at performance, igraph at statistical computing, Pathfinding.js at web visualization.

Softanza's distinction: It transforms graph planning from algorithmic implementation to strategic conversation. Where others require you to construct searches, Softanza lets you express intent. Where others return paths, Softanza explains reasoning. Where others process one query, Softanza compares strategies, learns from history, and filters by constraints—all through a fluent, natural interface.

The difference isn't in the algorithms (A is A). It's in how you interact with them. Traditional libraries speak the language of computer science. Softanza speaks the language of your problem domain.

DISCLAIMER: stzGraphPlanner is a pragmatic planning library optimized for business applications, educational programming, and data analytics. Unlike academic planners focused on worst-case theoretical guarantees, we prioritize: * Explainability: Every decision has a Why(), CostBreakdown(), and Tradeoffs() explanation * Comparison: Built-in multi-plan ranking, historical analysis, and constraint filtering * Usability: Intuitive API with profiles (:fastest, :cheapest, :balanced) for common scenarios * Real-world focus: Optimized for graphs with 10-1000 nodes, not academic benchmarks For research-grade features like mutex relations or CSP integration, consider PDDL-based systems. For production planning, route optimization, and decision analytics, stzGraphPlanner delivers enterprise-ready results with minimal complexity.


Conclusion

Graph planning in Softanza isn't about mastering algorithms—it's about expressing intent and understanding outcomes. Where traditional libraries demand you construct searches, stzGraphPlanner lets you describe strategies. Where others return paths, Softanza explains reasoning, compares alternatives, learns from history, and filters by constraints.

The transformation is fundamental: from computational tool to reasoning partner. You state what matters (:fastest, :safest, "within 10%", "avoid downtown"), the planner handles how, and together you explore the decision space through conversation.

This is planning that teaches as it works, explains as it decides, and remembers as it improves. Not artificial intelligence replacing judgment—augmented intelligence structuring it.

Lectures liées

Citer

Mansour Ayouni. « Intelligence in Motion: Graph Planning in Softanza ». Softanza narrations, 2025. https://mayouni.github.io/stzsite/fr/narrations/stzgraphplannerdoc.html

Non exécuté : son code touche aux fichiers, au réseau, à l'horloge ou au hasard. L'article est publié tel qu'il est écrit ; ses blocs sont des illustrations. Le code des articles est du code Haro. Là où un article a été écrit avant que la langue prenne son nom actuel, le nom a été changé et rien d'autre. Genre, domaine et série lus dans le nom du fichier, année tirée du premier commit : dérivés, jusqu'à ce que l'article porte son propre en-tête. Le fichier sur GitHub(s'ouvre dans un nouvel onglet)