Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

dijkstrademo.gno

7.25 Kb · 199 lines
  1// Package dijkstrademo is a small gnoweb demo of the shortest-path search
  2// provided by the [p/moul/x/daily/dijkstra](/p/moul/x/daily/dijkstra/v0)
  3// library: the cheapest route to every node, the order the search settles
  4// them in, and the tie-break that makes both reproducible.
  5//
  6// It contains no search logic of its own. Stateless, so Render is
  7// deterministic: every figure below comes from the fixed graphs declared at
  8// the top of this file, never from chain state, which is what lets an example
  9// test pin the page.
 10package dijkstrademo
 11
 12import (
 13	"strconv"
 14	"strings"
 15
 16	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/kit/ui/v0"
 17	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/x/daily/dijkstra/v0"
 18)
 19
 20// link is one arc of the worked example: a small directed delivery network
 21// where the obvious route is the wrong one twice over.
 22type link struct {
 23	from, to string
 24	cost     int
 25}
 26
 27var network = []link{
 28	{"depot", "north", 2},
 29	{"depot", "east", 4},
 30	{"north", "east", 1},
 31	{"north", "ridge", 3},
 32	{"east", "port", 6},
 33	{"ridge", "port", 3},
 34	{"port", "beacon", 2},
 35}
 36
 37// The source of the worked example, and a node nothing reaches, to show that
 38// unreachable is an answer rather than an error.
 39const (
 40	source   = "depot"
 41	isolated = "quarry"
 42)
 43
 44// fanout is the determinism example: one hub, three spokes, all at the same
 45// cost. Which one settles first is decided by the order they were declared
 46// in, never by a map.
 47var fanout = []string{"west", "south", "crater"}
 48
 49// Render renders the demo for gnoweb.
 50//
 51//	Render("") / Render("/") -> the full demo
 52func Render(path string) string {
 53	var b strings.Builder
 54
 55	b.WriteString("# Dijkstra\n\n")
 56	b.WriteString("The cheapest route from one node to every other node of a ")
 57	b.WriteString("weighted graph, demoing the ")
 58	b.WriteString("[`p/moul/x/daily/dijkstra`](/p/moul/x/daily/dijkstra/v0) library.\n\n")
 59
 60	g := dijkstra.New()
 61	for _, l := range network {
 62		g.AddEdge(l.from, l.to, l.cost)
 63	}
 64	g.AddNode(isolated)
 65	r := g.From(source)
 66
 67	b.WriteString("## The network\n\n")
 68	b.WriteString("Arcs are one-way. ")
 69	b.WriteString(strconv.Itoa(g.Len()) + " nodes, " + strconv.Itoa(g.Edges()))
 70	b.WriteString(" arcs, searched from `" + source + "`.\n\n")
 71	nt := ui.NewTable("from", "to", "cost")
 72	for _, l := range network {
 73		nt.Row(ui.Cell(l.from), ui.Cell(l.to), strconv.Itoa(l.cost))
 74	}
 75	b.WriteString(nt.String() + "\n")
 76	b.WriteString("`" + isolated + "` is in the graph with no arcs at all.\n\n")
 77
 78	b.WriteString("## Cheapest route to each node\n\n")
 79	rt := ui.NewTable("node", "distance", "arrives via", "route")
 80	for _, n := range g.Names() {
 81		d, ok := r.Dist(n)
 82		if !ok {
 83			rt.Row(ui.Cell(n), "unreachable", "", "")
 84			continue
 85		}
 86		via, hasVia := r.Via(n)
 87		if !hasVia {
 88			via = "source"
 89		}
 90		rt.Row(ui.Cell(n), strconv.Itoa(d), ui.Cell(via), route(r.Path(n)))
 91	}
 92	b.WriteString(rt.String() + "\n")
 93	b.WriteString("Two of those rows are the point of the algorithm. `east` is ")
 94	b.WriteString("reachable directly for 4, and the answer is 3, through ")
 95	b.WriteString("`north`. `port` is first offered at 9 by `east`, and ends at ")
 96	b.WriteString("8 through `ridge`, which the search only discovers two ")
 97	b.WriteString("settlements later.\n\n")
 98
 99	b.WriteString("## The order the search settles nodes\n\n")
100	b.WriteString("Always the closest node still outstanding, never the next ")
101	b.WriteString("one along an arc. That is the whole difference from a ")
102	b.WriteString("breadth-first walk, and it is why a cheap long way round ")
103	b.WriteString("still wins.\n\n")
104	ot := ui.NewTable("step", "settles", "at distance")
105	for i, n := range r.Order() {
106		d, _ := r.Dist(n)
107		ot.Row(strconv.Itoa(i+1), ui.Cell(n), strconv.Itoa(d))
108	}
109	b.WriteString(ot.String() + "\n")
110	b.WriteString(strconv.Itoa(r.Reachable()) + " of " + strconv.Itoa(g.Len()))
111	b.WriteString(" nodes were reached, in " + strconv.Itoa(r.Relaxations()))
112	b.WriteString(" improvements. An arc that is looked at and rejected costs ")
113	b.WriteString("a comparison; only an improvement costs a write.\n\n")
114
115	b.WriteString("## Ties, and why a realm should care\n\n")
116	b.WriteString("Three spokes, all 5 away from the hub. Nothing in the ")
117	b.WriteString("distances separates them, so something else has to, and ")
118	b.WriteString("that something must not be map iteration order: a `Render` ")
119	b.WriteString("that reshuffled between validating nodes would be a ")
120	b.WriteString("consensus bug, not a cosmetic one.\n\n")
121	tt := ui.NewTable("declared", "settle order")
122	tt.Row(names(fanout), route(settleFanout(fanout)))
123	reversed := reverse(fanout)
124	tt.Row(names(reversed), route(settleFanout(reversed)))
125	b.WriteString(tt.String() + "\n")
126	b.WriteString("The tie resolves to the order the graph was built in, and ")
127	b.WriteString("nothing else. Build it the same way twice and you get the ")
128	b.WriteString("same page twice.\n\n")
129
130	b.WriteString("## What it will not do\n\n")
131	b.WriteString("A negative arc panics at `AddEdge`, where the caller can ")
132	b.WriteString("still fix it. Accepting one would not return an error ")
133	b.WriteString("later, it would return a confident wrong answer: the ")
134	b.WriteString("search never revisits a settled node, so a negative arc ")
135	b.WriteString("pointing back at one is simply never seen.\n\n")
136	lt := ui.NewTable("limit", "value", "why")
137	lt.Row("`MaxNodes`", strconv.Itoa(dijkstra.MaxNodes),
138		"the frontier holds at most one entry per node, so this bounds memory")
139	lt.Row("`MaxEdges`", strconv.Itoa(dijkstra.MaxEdges),
140		"arcs bound the work, not the memory")
141	lt.Row("negative arc", "panics", "the algorithm's precondition, not a simplification")
142	lt.Row("self-loop", "panics", "it can never shorten a path, so it is a caller mistake")
143	b.WriteString(lt.String() + "\n")
144	b.WriteString("Both caps are small on purpose. This is a routing table ")
145	b.WriteString("for a realm, not a road atlas, and a caller that can state ")
146	b.WriteString("the worst case up front can price the gas before it sends ")
147	b.WriteString("the transaction.\n")
148
149	return b.String()
150}
151
152// route joins a path with an arrow. Each node name is escaped on its own and
153// the separator never is: running the whole joined string through the
154// escaper would turn the arrows into a literal backslash soup, which is the
155// bug this spelling exists to avoid.
156func route(path []string) string {
157	if len(path) == 0 {
158		return ""
159	}
160	cells := make([]string, len(path))
161	for i, n := range path {
162		cells[i] = ui.Cell(n)
163	}
164	return strings.Join(cells, " -> ")
165}
166
167// names renders a declaration order as a comma-separated list, escaped the
168// same way.
169func names(in []string) string {
170	cells := make([]string, len(in))
171	for i, n := range in {
172		cells[i] = ui.Cell(n)
173	}
174	return strings.Join(cells, ", ")
175}
176
177// settleFanout builds the hub-and-spoke graph with the spokes declared in the
178// given order and returns the order the search settles them in.
179func settleFanout(spokes []string) []string {
180	g := dijkstra.New()
181	g.AddNode("hub")
182	for _, s := range spokes {
183		g.AddNode(s)
184	}
185	for _, s := range spokes {
186		g.AddEdge("hub", s, 5)
187	}
188	return g.From("hub").Order()
189}
190
191// reverse copies a slice back to front. Written out rather than imported
192// because there is no sensible shared home for three lines of it.
193func reverse(in []string) []string {
194	out := make([]string, len(in))
195	for i, s := range in {
196		out[len(in)-1-i] = s
197	}
198	return out
199}