spaxel/notes/bf-195o.md

9.4 KiB
Raw Blame History

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:

  1. Grid-based: ceil(sqrt(count)) × ceil(sqrt(count)) grid
  2. Full spatial coverage: For count ≥ 3, grid ≥ 2×2, spanning both X and Y axes
  3. No co-location: Every grid cell has a distinct floor position
  4. Height diversity: Z alternates between 25% and 75% of room height by (row+col) parity
  5. 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:

  1. Well-documented: Explicit purpose statement explaining why geometry matters
  2. Non-degenerate: Specifically designed to prevent fusion collapse
  3. Height diversity: Mixed-height placement improves 3D fusion
  4. Grid coverage: Guarantees spatial distribution across room

Grid placement with height diversity:

For a space with dimensions (W, D, H):

  1. Calculate grid size: gridSize = ceil(sqrt(count))
  2. Define Z bands:
    • lowZ = 0.25 × H (quarter height)
    • highZ = 0.75 × H (three-quarter height)
  3. Place nodes in row-major order:
    • For node index i:
      • col = i % gridSize
      • row = i / gridSize
      • X = col × W / (gridSize - 1)
      • Y = row × D / (gridSize - 1)
      • Z = lowZ if (row+col) even, else highZ

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
)

  • 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:

  1. Adopt DefaultNodePositions() as the single source of truth
  2. Update both simulators to use the unified approach
  3. Add geometry validation tests to prevent future regressions
  4. Document the migration in release notes

No code changes required in this research-only bead.