9.4 KiB
Node Geometry Placement Research and Design
Bead: bf-195o
Date: 2026-07-06
Scope: Virtual node geometry placement research and design
Executive Summary
This research documents the current virtual node creation path and recommends a unified approach for realistic node geometry placement across the Spaxel codebase.
1. Current Virtual Node Creation Flow
1.1 Two Separate Implementations
The codebase has two independent implementations of virtual node creation:
A. Legacy CLI Simulator (cmd/sim/main.go)
- Location:
/home/coding/spaxel/cmd/sim/main.go - Function:
createVirtualNodes(count, space, rng)(line 198) - Helper:
generateNodePositions(count, space)(line 219) - Default space: 6m × 5m × 2.5m
B. Mothership CLI Simulator (mothership/cmd/sim/main.go)
- Location:
/home/coding/spaxel/mothership/cmd/sim/main.go - Function:
createVirtualNodes(count, space, rng)(line 387) - Default space: 5m × 5m × 2.5m
- Strategy: Perimeter distribution
1.2 Position Flow to Database
Hello message structure:
hello := map[string]interface{}{
"type": "hello",
"mac": macToString(node.MAC),
"firmware_version": "sim-1.0.0",
"capabilities": []string{"csi", "tx", "rx"},
"chip": "ESP32-S3",
"flash_mb": 16,
"uptime_ms": 1000,
"pos_x": n.Position.X, // ← Position from createVirtualNodes
"pos_y": n.Position.Y,
"pos_z": n.Position.Z,
}
The pos_x/y/z values from the virtual node creation are sent to mothership via WebSocket and persisted in the fleet database.
1.3 Database Default Without Geometry
Without explicit position setting, nodes default to schema default:
- Default position: (0, 0, 1) — origin, 1m height
- Problem: All nodes co-located → degenerate Fresnel geometry → fusion collapses toward zero
2. Authoritative Implementation
2.1 DefaultNodePositions() Function
Location: /home/coding/spaxel/mothership/internal/simulator/node.go (lines 268-331)
Purpose:
- Shared default-geometry source for simulator/spaxel-sim node seeding
- Prevents all nodes from sitting at DB default of (0,0,1)
- Keeps Fresnel excess path non-degenerate so 3D fusion engine can form blobs
Key insight from code comment:
"The core symptom in bf-4q5w was nodes collapsing toward zero geometry"
2.2 Placement Strategy
func DefaultNodePositions(s *Space, count int) []Point {
// Single node: center of the room
if count == 1 {
return []Point{{X: midX, Y: midY, Z: midZ}}
}
// Two nodes: opposite corners (span both X and Y)
if count == 2 {
return []Point{
{X: minX, Y: minY, Z: maxZ},
{X: maxX, Y: maxY, Z: maxZ},
}
}
// Three or more: row-major grid fill
gridSize := int(math.Ceil(math.Sqrt(float64(count))))
// gridSize >= 2 for count >= 3, so grid spans both axes
lowZ := minZ + (maxZ-minZ)*0.25 // 25% height
highZ := minZ + (maxZ-minZ)*0.75 // 75% height
for i := 0; i < count; i++ {
col := i % gridSize
row := i / gridSize
// Alternate Z by cell parity for mixed-height diversity
z := lowZ
if (row+col)%2 != 0 {
z = highZ
}
positions = append(positions, Point{
X: minX + float64(col)*(maxX-minX)/float64(gridSize-1),
Y: minY + float64(row)*(maxY-minY)/float64(gridSize-1),
Z: z,
})
}
}
Algorithm properties:
- Grid-based: ceil(sqrt(count)) × ceil(sqrt(count)) grid
- Full spatial coverage: For count ≥ 3, grid ≥ 2×2, spanning both X and Y axes
- No co-location: Every grid cell has a distinct floor position
- Height diversity: Z alternates between 25% and 75% of room height by (row+col) parity
- Non-degenerate geometry: Ensures Fresnel zones vary across node pairs
3. Room/Zone Size Constants
3.1 Default Spaces
| Location | Width | Depth | Height | Use |
|---|---|---|---|---|
cmd/sim/main.go |
6m | 5m | 2.5m | Legacy CLI |
mothership/cmd/sim/main.go |
5m | 5m | 2.5m | Mothership CLI |
mothership/internal/simulator/space.go |
6m | 5m | 2.5m | DefaultSpace() |
mothership/internal/simulator/space.go |
10m | 10m | 2.5m | Bounds() fallback |
3.2 Coordinate System
- Origin: (0, 0, 0) = floor corner
- X axis: width (left to right)
- Y axis: depth (front to back)
- Z axis: height (floor to ceiling)
3.3 Typical Room Heights
- Standard: 2.5m (8.2 ft)
- High ceilings: 3m (9.8 ft)
- Z bands used: 25% (0.625m) and 75% (1.875m) for 2.5m ceiling
4. Current Placement Behaviors
4.1 Legacy CLI (cmd/sim/main.go)
| Count | Pattern |
|---|---|
| 1 | Center of room |
| 2 | Diagonal corners at ceiling height |
| 3 | 2 corners at ceiling + 1 midpoint on floor |
| 4 | 4 corners at ceiling height |
| 4+ | Grid pattern at half height (Z = 1.25m) |
Example for 6 nodes in 6×5×2.5m space:
- Grid size: ceil(sqrt(6)) = 3
- Positions: (0,0,1.25), (3,0,1.25), (0,2.5,1.25), (3,2.5,1.25), (1.5,1.25,1.25), (4.5,1.25,1.25)
4.2 Mothership CLI (mothership/cmd/sim/main.go)
Perimeter distribution:
perimeter := 2 * (space.Width + space.Depth)
pos := float64(i) / float64(count) * perimeter
// Walk around perimeter: bottom → right → top → left
// All nodes at Z = 2.0m (fixed height)
Example for 4 nodes in 5×5×2.5m space:
- Node 0: (0, 0, 2.0) — bottom edge
- Node 1: (5, 0, 2.0) — right edge
- Node 2: (5, 5, 2.0) — top edge
- Node 3: (0, 5, 2.0) — left edge
4.3 DefaultNodePositions (Authoritative)
Grid with height diversity:
- Row-major fill of sqrt(count) × sqrt(count) grid
- Z alternates between low (25%) and high (75%) bands
- Checkerboard pattern: (row+col) even → lowZ, odd → highZ
Example for 4 nodes in 6×5×2.5m space:
- Node 0: (0, 0, 0.625) — grid[0][0], parity even → low
- Node 1: (6, 0, 1.875) — grid[1][0], parity odd → high
- Node 2: (0, 5, 1.875) — grid[0][1], parity odd → high
- Node 3: (6, 5, 0.625) — grid[1][1], parity even → low
5. Design Recommendations
5.1 Unified Approach
Recommendation: Adopt DefaultNodePositions() from mothership/internal/simulator/node.go as the single source of truth for all virtual node placement.
Rationale:
- Well-documented: Explicit purpose statement explaining why geometry matters
- Non-degenerate: Specifically designed to prevent fusion collapse
- Height diversity: Mixed-height placement improves 3D fusion
- Grid coverage: Guarantees spatial distribution across room
5.2 Recommended Placement Strategy
Grid placement with height diversity:
For a space with dimensions (W, D, H):
- Calculate grid size:
gridSize = ceil(sqrt(count)) - Define Z bands:
lowZ = 0.25 × H(quarter height)highZ = 0.75 × H(three-quarter height)
- Place nodes in row-major order:
- For node index
i:col = i % gridSizerow = i / gridSizeX = col × W / (gridSize - 1)Y = row × D / (gridSize - 1)Z = lowZif(row+col)even, elsehighZ
- For node index
Example implementation constants:
const (
DefaultRoomWidth = 6.0 // meters
DefaultRoomDepth = 5.0 // meters
DefaultRoomHeight = 2.5 // meters
LowHeightRatio = 0.25 // 25% of room height
HighHeightRatio = 0.75 // 75% of room height
)
5.3 Migration Path
Phase 1: Update cmd/sim/main.go to use DefaultNodePositions()
Phase 2: Update mothership/cmd/sim/main.go to use DefaultNodePositions()
Phase 3: Deprecate perimeter-based placement
Phase 4: Add tests for geometry non-degeneracy
6. Acceptance Criteria Status
| Criterion | Status |
|---|---|
| Document current virtual node creation flow | ✅ Complete — Section 1 |
| Identify where positions are set | ✅ Complete — Lines 198, 387, node.go:268 |
| Identify appropriate room/zone size constants | ✅ Complete — Section 3 |
| Design approach documented | ✅ Complete — Section 5 |
7. Key Constants Reference
// WiFi physical constants (from cmd/sim/main.go)
const (
wavelength = 0.123 // meters (2.4 GHz)
halfWavelength = wavelength / 2.0
nSub = 64 // number of subcarriers for HT20
)
// Default space dimensions
const (
defaultSpace = "6x5x2.5" // WxDxH in meters
)
// GDOP computation (from mothership/internal/simulator/space.go)
const (
Wavelength = 0.123 // meters
HalfWavelength = 0.0615 // meters
SubcarrierSpacing = 312.5e3 // Hz
)
// Height ratios for node placement
const (
LowHeightRatio = 0.25 // 25% of room height
HighHeightRatio = 0.75 // 75% of room height
)
8. Related Beads and Issues
- bf-4q5w: Core symptom of nodes collapsing toward zero geometry
- bf-24xp: Schema default position (0,0,1) without geometry persistence
- Parent bead: bf-18yn (split from)
9. Next Steps
This research and design phase is complete. Implementation should proceed with:
- Adopt
DefaultNodePositions()as the single source of truth - Update both simulators to use the unified approach
- Add geometry validation tests to prevent future regressions
- Document the migration in release notes
No code changes required in this research-only bead.