Browse Source

Lots of massaging

Jonathan D. Storm 1 month ago
parent
commit
95b542f952
3 changed files with 144 additions and 93 deletions
  1. 5 3
      go.mod
  2. 8 8
      go.sum
  3. 131 82
      main.go

+ 5 - 3
go.mod

@@ -2,12 +2,14 @@ module graph_layout
 
 go 1.23.4
 
-require gioui.org v0.7.1
+require (
+	gioui.org v0.8.0
+	gioui.org/x v0.8.1
+)
 
 require (
-	gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 // indirect
 	gioui.org/shader v1.0.8 // indirect
-	github.com/go-text/typesetting v0.1.1 // indirect
+	github.com/go-text/typesetting v0.2.1 // indirect
 	golang.org/x/exp v0.0.0-20240707233637-46b078467d37 // indirect
 	golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 // indirect
 	golang.org/x/image v0.18.0 // indirect

+ 8 - 8
go.sum

@@ -1,16 +1,16 @@
 eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY=
 eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA=
-gioui.org v0.7.1 h1:l7OVj47n1z8acaszQ6Wlu+Rxme+HqF3q8b+Fs68+x3w=
-gioui.org v0.7.1/go.mod h1:5Kw/q7R1BWc5MKStuTNvhCgSrRqbfHc9Dzfjs4IGgZo=
+gioui.org v0.8.0 h1:QV5p5JvsmSmGiIXVYOKn6d9YDliTfjtLlVf5J+BZ9Pg=
+gioui.org v0.8.0/go.mod h1:vEMmpxMOd/iwJhXvGVIzWEbxMWhnMQ9aByOGQdlQ8rc=
 gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
-gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc=
-gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
 gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
 gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
-github.com/go-text/typesetting v0.1.1 h1:bGAesCuo85nXnEN5LmFMVGAGpGkCPtHrZLi//qD7EJo=
-github.com/go-text/typesetting v0.1.1/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI=
-github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY=
-github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
+gioui.org/x v0.8.1 h1:Q2wumEOfjz3XfRa3TEi6w7dq8+cxV8zsYK8xXQkrCRk=
+gioui.org/x v0.8.1/go.mod h1:v2g60aiZtIVR7lNFXZ123+U0kijJeOChODSuqr7MFSI=
+github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
+github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
+github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
+github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
 golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
 golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
 golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 h1:SOSg7+sueresE4IbmmGM60GmlIys+zNX63d6/J4CMtU=

+ 131 - 82
main.go

@@ -5,6 +5,7 @@ import (
 	"image"
 	"image/color"
 	"log"
+	"math"
 	"math/rand"
 	"os"
 	"strings"
@@ -23,6 +24,8 @@ import (
 	"gioui.org/widget/material"
 )
 
+const MAX_LOOP = 100_000_000
+
 type C = layout.Context
 type D = layout.Dimensions
 
@@ -36,6 +39,8 @@ var darkPalette = material.Palette{
 func main() {
 	go func() {
 		window := new(app.Window)
+		window.Option(app.Title("Not-a-graph-layout-algorithm graph layout algorithm"))
+
 		err := run(window)
 		if err != nil {
 			log.Fatal(err)
@@ -52,9 +57,11 @@ func run(window *app.Window) error {
 	theme.Shaper =
 		text.NewShaper(text.WithCollection(gofont.Collection()))
 
+	seed := int64(0)
 	g := Graph{}
-	g.random = rand.New(rand.NewSource(0))
+	g.random = rand.New(rand.NewSource(seed))
 	g.offsets = g._offsets[:]
+	g.degrees = g._degrees[:]
 	g.nodes = g._nodes[:0]
 	g.allocs = g._allocs[:]
 	g.edges = g._edges[:0]
@@ -64,20 +71,21 @@ func run(window *app.Window) error {
 	title := material.Body1(theme, "")
 	title.Color = theme.Fg
 	title.Alignment = text.Start
+	title.WrapPolicy = text.WrapWords
 
 	simPeriod := 50 * time.Millisecond
 	lastStep := time.Now()
 	bumpShift := 1
 	lastSecond := time.Now()
-	frameCnt := 0
-	stepCnt := 0
+	frameCnt, stepCnt := 0, 0
 	fps, sps := 0, 0
-	lastFinish := time.Now()
+	simFinishedTime := time.Now()
+	simCnt := int64(0)
 	var (
 		stepOut         strings.Builder
 		statusText      strings.Builder
 		ops             op.Ops
-		screensaverMode bool
+		screensaverMode bool = true
 		finished        bool
 	)
 	for {
@@ -117,7 +125,7 @@ func run(window *app.Window) error {
 						}
 					case "S":
 						if evt.State == key.Press {
-							screensaverMode = true
+							screensaverMode = !screensaverMode
 						}
 					case "<":
 						if evt.State == key.Press {
@@ -132,46 +140,21 @@ func run(window *app.Window) error {
 					}
 				}
 			}
+
 			if resetRequested ||
-				(screensaverMode &&
-					time.Since(lastFinish) >= 10*time.Second) {
+				(screensaverMode && finished &&
+					time.Since(simFinishedTime) >= 5*time.Second) {
 				(&g).Reset()
-				lastFinish = time.Now()
+				resetRequested = false
 				finished = false
-			}
-			paint.Fill(&ops, theme.Bg)
-
-			//margin := layout.UniformInset(unit.Dp(8))
-			layout.Flex{
-				Axis:      layout.Vertical,
-				Alignment: layout.Start,
-				Spacing:   layout.SpaceEnd,
-			}.Layout(gtx,
-				layout.Rigid(func(gtx C) D {
-					return (&g).Layout(gtx, theme)
-				}),
-				layout.Rigid(func(gtx C) D {
-					return title.Layout(gtx)
-				}),
-				layout.Rigid(func(gtx C) D {
-					return (&g).LayoutDist(gtx, theme)
-				}),
-			)
-
-			if time.Since(lastSecond) >= time.Second {
-				fps, sps = frameCnt, stepCnt
-				frameCnt, stepCnt = 0, 0
-				lastSecond = time.Now()
+				simCnt++
 			}
 
-			statusText.Reset()
-			statusText.WriteString(fmt.Sprintf("simulation rate: %s per step\n", simPeriod))
-			statusText.WriteString(fmt.Sprintf("fps=%d; sps=%d\n", fps, sps))
-
 			if time.Since(lastStep) >= simPeriod {
 				if int(g.order) == cap(g.nodes) && bumpRequested {
 					bump(&g, bumpShift)
 					bumpShift++
+					bumpRequested = false
 				}
 				stepOut.Reset()
 				stepOut.WriteString((&g).Step())
@@ -181,11 +164,54 @@ func run(window *app.Window) error {
 				addRandomNode(&g)
 				stepCnt++
 			}
+
+			paint.Fill(&ops, theme.Bg)
+
+			layout.UniformInset(
+				unit.Dp(10),
+			).Layout(gtx, func(gtx C) D {
+				return layout.Flex{
+					Axis:      layout.Horizontal,
+					Alignment: layout.Start,
+					Spacing:   layout.SpaceEnd,
+				}.Layout(gtx,
+					layout.Rigid(func(gtx C) D {
+						return (&g).Layout(gtx, theme)
+					}),
+					layout.Rigid(func(gtx C) D {
+						return layout.Flex{
+							Axis:      layout.Vertical,
+							Alignment: layout.Start,
+							Spacing:   layout.SpaceEnd,
+						}.Layout(gtx,
+							layout.Rigid(func(gtx C) D {
+								return (&g).LayoutDist(gtx, theme)
+							}),
+							layout.Flexed(1.0, func(gtx C) D {
+								return title.Layout(gtx)
+							}),
+						)
+					}),
+				)
+			})
+
+			if time.Since(lastSecond) >= time.Second {
+				fps, sps = frameCnt, stepCnt
+				frameCnt, stepCnt = 0, 0
+				lastSecond = time.Now()
+			}
+
+			statusText.Reset()
+			statusText.WriteString(fmt.Sprintf("seed %d, simulation %d\n", seed, simCnt+1))
+			statusText.WriteString(fmt.Sprintf("simulation rate: %s per step\n", simPeriod))
+			statusText.WriteString(fmt.Sprintf("fps=%d; sps=%d\n", fps, sps))
+
 			statusText.WriteString(stepOut.String())
 			title.Text = statusText.String()
 
 			if int(g.order) == cap(g.nodes) && !finished {
 				finished = true
+				simFinishedTime = time.Now()
 			}
 			e.Source.Execute(op.InvalidateCmd{})
 
@@ -224,8 +250,8 @@ func bump(g *Graph, shift int) int {
 			delete(nextNodes, n)
 			nodes[i] = n
 			i++
-			if log2(g.nodes[n])+uint32(shift) >= 31 {
-				// Out of address space
+			if log2(g.nodes[n])+uint32(shift) >= 30 {
+				log.Printf("Out-of-address-space condition for node %d", n)
 				continue
 			}
 			g.nodes[n] <<= shift
@@ -235,15 +261,9 @@ func bump(g *Graph, shift int) int {
 }
 
 func addRandomNode(g *Graph) (order uint32) {
-	order = g.order
 	if g.order == uint32(cap(g.nodes)) ||
 		g.size == uint32(cap(g.edges)) {
-		return
-	}
-	threshold := 0.02 * float64(g.order)
-
-	if g.random.ExpFloat64() <= threshold {
-		return
+		return g.order
 	}
 	g.edges = g.edges[:cap(g.edges)]
 	g.nodes = g.nodes[:cap(g.nodes)]
@@ -252,23 +272,24 @@ func addRandomNode(g *Graph) (order uint32) {
 	g.nodes[i] = 1
 	g.offsets[i] = EdgeId(g.size)
 
-	threshold = float64(2.0)
+	for j := i + 1; j < len(g.nodes); j++ {
+		g.edges[g.size] = NodeId(j)
 
-	for j := 1; i+j < len(g.nodes); j++ {
-		g.edges[g.size] = NodeId(i + j)
+		distance := float64(j - i)
+		edgeProbability := 1 / (1 + math.Exp(0.25*distance))
 
-		if g.random.ExpFloat64() <= threshold {
+		if g.random.Float64() > edgeProbability {
 			continue
 		}
+		g.degrees[i]++
 		g.size++
 	}
 	g.order++
-	order = g.order
 	g.offsets[g.order] = EdgeId(g.size)
 	g.nodes = g.nodes[:g.order]
 	g.edges = g.edges[:g.size]
 
-	return
+	return g.order
 }
 
 func generateRandomGraph(g *Graph) {
@@ -299,34 +320,38 @@ func generateRandomGraph(g *Graph) {
 type NodeId uint32
 type EdgeId uint32
 
-const GraphOrder = 64
+const GraphOrder int = 64
 
 type Graph struct {
 	_offsets [GraphOrder + 1]EdgeId
 	_nodes   [GraphOrder]uint32
 	_allocs  [GraphOrder]uint32
-	_edges   [(GraphOrder * (GraphOrder - 1)) >> 2]NodeId
+	_edges   [(GraphOrder * (GraphOrder - 1)) >> 1]NodeId
+	_degrees [GraphOrder]byte
 	random   *rand.Rand
 	offsets  []EdgeId
 	nodes    []uint32
 	allocs   []uint32 // Address allocation state, per node
-	edges    []NodeId
-	size     uint32 // Number of edges added to graph
-	order    uint32 // Number of nodes added to graph
+	edges    []NodeId // Adjacent node ids
+	degrees  []byte   // Node degrees
+	size     uint32   // Number of edges added to graph
+	order    uint32   // Number of nodes added to graph
+	source   NodeId   // A selected source node
+	target   NodeId   // A selected target node
 }
 
 func (g *Graph) Reset() {
-	for o := range g.offsets {
-		g.offsets[o] = 0
+	for i := range g.offsets {
+		g.offsets[i] = 0
 	}
-	for n := range g.nodes {
-		g.nodes[n] = 0
+	for i := range g.nodes {
+		g.nodes[i] = 0
 	}
-	for a := range g.allocs {
-		g.allocs[a] = 0
+	for i := range g.allocs {
+		g.allocs[i] = 0
 	}
-	for e := range g.edges {
-		g.edges[e] = 0
+	for i := range g.edges {
+		g.edges[i] = 0
 	}
 	g.nodes = g.nodes[:0]
 	g.edges = g.edges[:0]
@@ -356,22 +381,35 @@ func (g *Graph) Step() string {
 		}
 		shift := log2(g.nodes[n]) + 1
 
+		if log2(g.allocs[n])+shift >= 30 {
+			log.Printf("Out-of-address-space condition for node %d", n)
+			continue
+		}
+		edges := g.edges[g.offsets[n]:g.offsets[n+1]]
+
+		// Shift a node containing exactly one 1
+		if (n == 0 || g.nodes[n] > 1) && // root or assigned
+			g.nodes[n]&(g.nodes[n]-1) == 0 { // ...contains one 1
+			if len(edges) > 0 {
+				e := edges[0]
+				if uint32(e) < g.order && g.nodes[n] >= g.nodes[e] {
+					g.nodes[e] = g.nodes[n] << 1
+				}
+			}
+		}
 		// Assign addresses to neighbors
-		for _, e := range g.edges[g.offsets[n]:g.offsets[n+1]] {
+		for i := 1; i < len(edges); i++ {
+			nextAlloc := (g.allocs[n] << shift) | g.nodes[n]
+			e := edges[i]
 			if uint32(e) >= g.order ||
-				g.nodes[n] < g.nodes[e] {
-				continue
-			}
-			if log2(g.allocs[n])+shift >= 30 {
-				// Out of address space
+				(g.nodes[e] != 1 && nextAlloc >= g.nodes[e]) {
 				continue
 			}
-			nextAddr := (g.allocs[n] << shift) | g.nodes[n]
-			g.nodes[e] = nextAddr
+			g.nodes[e] = nextAlloc
 			g.allocs[n]++
 		}
 	}
-	return fmt.Sprintf("nodes=%v\n", g.nodes)
+	return fmt.Sprintf("nodes=%v\nedges=%v", g.nodes, g.edges)
 }
 
 func (g *Graph) Layout(
@@ -385,22 +423,24 @@ func (g *Graph) Layout(
 	convFactor := float32(squareMax) / 65535.0
 	circle := clip.Ellipse{Max: image.Pt(16, 16)}
 
+	// Outer margin
 	defer op.Offset(image.Pt(10, 10)).Push(gtx.Ops).Pop()
 
 	var path clip.Path
 	for n := 0; n < len(g.nodes); n++ {
-		if g.nodes[n] == 1 { // Ignore root and orphaned nodes
-			continue
-		}
 		x1, y1 := idToPt(g.nodes[n], convFactor)
 
 		for e := range g.edges[g.offsets[n]:g.offsets[n+1]] {
 			if uint32(e) >= g.order {
 				continue
 			}
+			if g.nodes[n] == 1 { // Skip orphans and roots
+				continue
+			}
 			color := th.Fg
 			if distance(g.nodes[n], g.nodes[e]) > 4 {
 				color = th.ContrastBg
+				color.A = 0xd0
 			}
 			x2, y2 := idToPt(g.nodes[e], convFactor)
 
@@ -414,7 +454,9 @@ func (g *Graph) Layout(
 				}.Op(),
 			)
 		}
-		shift := int(log2(g.nodes[n]) >> 3)
+		// Circle size is proportional to address length.
+		length := log2(g.nodes[n])
+		shift := int(length >> 3)
 		scaled := circle
 		scaled.Max.X = scaled.Max.X >> shift
 		scaled.Max.Y = scaled.Max.Y >> shift
@@ -422,9 +464,16 @@ func (g *Graph) Layout(
 
 		pos := image.Pt(gtx.Dp(x1)-halfW, gtx.Dp(y1)-halfH)
 
+		macro := op.Record(gtx.Ops)
 		stack := op.Offset(pos).Push(gtx.Ops)
-		paint.FillShape(gtx.Ops, th.Fg, scaled.Op(gtx.Ops))
+		nodeColor := th.Fg
+		if length >= 24 {
+			nodeColor = color.NRGBA{R: 0xaa, G: 0, B: 0xaa, A: 0xff}
+		}
+		paint.FillShape(gtx.Ops, nodeColor, scaled.Op(gtx.Ops))
 		stack.Pop()
+		c := macro.Stop()
+		op.Defer(gtx.Ops, c)
 	}
 	return layout.Dimensions{Size: image.Pt(squareMax, squareMax)}
 }
@@ -500,14 +549,14 @@ func idToPt(id uint32, convFactor float32) (x, y unit.Dp) {
 
 func log2(x uint32) uint32 {
 	b := []uint32{0x2, 0xC, 0xF0, 0xFF00, 0xFFFF0000}
-	S := []uint32{1, 2, 4, 8, 16}
+	s := []uint32{1, 2, 4, 8, 16}
 
 	var r uint32 = 0
 
 	for i := 4; i >= 0; i-- {
 		if x&b[i] != 0 {
-			x >>= S[i]
-			r |= S[i]
+			x >>= s[i]
+			r |= s[i]
 		}
 	}
 	return r