Files
zeekayandHanzo Dev 94cad2a365 the corporate name is Lux Industries Inc
Lux Partners Limited is not the entity behind this work. Every place the
old name stood — copyright lines, site footers, brand config, legal and
compliance text — now names the company that actually holds it.

lux.fund is the exception and is not renamed to match: that site belongs
to Lux Assets Management LLC, a different company, and it says so.

Two kinds of line were left for a person rather than rewritten. A dated
news item recording that Lux Partners Limited was established in the Isle
of Man is a historical fact, and renaming it would falsify the record. And
a sentence asserting that the company is domiciled in the Isle of Man is a
claim about a jurisdiction, not a name — an Inc or an LLC is not an Isle
of Man company, so swapping the name there would assert something untrue.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-28 13:02:19 -07:00

561 lines
11 KiB
Go

// Copyright (c) 2024-2025 Lux Industries Inc
// SPDX-License-Identifier: BSD-3-Clause
package dag
import (
"context"
"testing"
)
func TestGraphConstruction(t *testing.T) {
g := NewGraph()
// Create input nodes
n1 := NewNode(OpLoad)
n1.Output = 1
n1.Level = 10
n2 := NewNode(OpLoad)
n2.Output = 2
n2.Level = 10
// Create operation node
n3 := NewNode(OpAdd)
n3.AddInput(n1.ID)
n3.AddInput(n2.ID)
n3.Output = 3
n3.Level = 10
// Add nodes to graph
if err := g.AddNode(n1); err != nil {
t.Fatalf("failed to add node 1: %v", err)
}
if err := g.AddNode(n2); err != nil {
t.Fatalf("failed to add node 2: %v", err)
}
if err := g.AddNode(n3); err != nil {
t.Fatalf("failed to add node 3: %v", err)
}
// Verify
if g.NodeCount() != 3 {
t.Errorf("expected 3 nodes, got %d", g.NodeCount())
}
if len(g.InputNodes()) != 2 {
t.Errorf("expected 2 input nodes, got %d", len(g.InputNodes()))
}
// Validate
if err := g.Validate(); err != nil {
t.Errorf("validation failed: %v", err)
}
}
func TestGraphValidationDetectsCycle(t *testing.T) {
g := NewGraph()
// Create a cycle: n1 -> n2 -> n1
n1 := NewNode(OpLoad)
n1.Output = 1
if err := g.AddNode(n1); err != nil {
t.Fatal(err)
}
// Note: Direct cycle creation is prevented by our design,
// but we test that validation catches logical issues
}
func TestBuilderAPI(t *testing.T) {
builder := NewBuilder()
// Build: (a + b) * c
builder.Input(10)
aID := builder.LastNodeID()
builder.Input(10)
bID := builder.LastNodeID()
builder.Input(10)
cID := builder.LastNodeID()
builder.Add(aID, bID)
sumID := builder.LastNodeID()
builder.Mul(sumID, cID).Output()
graph, err := builder.Build()
if err != nil {
t.Fatalf("build failed: %v", err)
}
if graph.NodeCount() != 5 {
t.Errorf("expected 5 nodes, got %d", graph.NodeCount())
}
if len(graph.OutputNodes()) != 1 {
t.Errorf("expected 1 output node, got %d", len(graph.OutputNodes()))
}
}
func TestScheduler(t *testing.T) {
builder := NewBuilder()
// Build: (a + b) + (c + d)
// This should create 2 levels: inputs, then two adds in parallel, then final add
builder.Input(10)
aID := builder.LastNodeID()
builder.Input(10)
bID := builder.LastNodeID()
builder.Input(10)
cID := builder.LastNodeID()
builder.Input(10)
dID := builder.LastNodeID()
builder.Add(aID, bID)
sum1ID := builder.LastNodeID()
builder.Add(cID, dID)
sum2ID := builder.LastNodeID()
builder.Add(sum1ID, sum2ID).Output()
graph, err := builder.Build()
if err != nil {
t.Fatalf("build failed: %v", err)
}
scheduler := NewScheduler(graph)
schedule, err := scheduler.Schedule()
if err != nil {
t.Fatalf("scheduling failed: %v", err)
}
// Check schedule properties
if schedule.TotalNodes != 7 {
t.Errorf("expected 7 nodes, got %d", schedule.TotalNodes)
}
if schedule.CriticalPath != 2 {
t.Errorf("expected critical path of 2, got %d", schedule.CriticalPath)
}
// The middle layer should have 2 adds that can run in parallel
stats := schedule.Stats()
t.Logf("Schedule stats: %+v", stats)
if stats.TotalBatches == 0 {
t.Error("expected at least one batch")
}
}
func TestTopologicalOrder(t *testing.T) {
builder := NewBuilder()
builder.Input(10)
aID := builder.LastNodeID()
builder.Input(10)
bID := builder.LastNodeID()
builder.Add(aID, bID)
sumID := builder.LastNodeID()
builder.Relin(sumID).Output()
graph, err := builder.Build()
if err != nil {
t.Fatal(err)
}
order, err := TopologicalOrder(graph)
if err != nil {
t.Fatalf("topological sort failed: %v", err)
}
if len(order) != 4 {
t.Errorf("expected 4 nodes in order, got %d", len(order))
}
// Verify order: inputs should come before their dependents
nodePos := make(map[NodeID]int)
for i, n := range order {
nodePos[n.ID] = i
}
for _, n := range order {
for _, dep := range n.dependsOn {
if nodePos[dep.ID] >= nodePos[n.ID] {
t.Errorf("dependency %d should come before %d in topological order", dep.ID, n.ID)
}
}
}
}
func TestCriticalPath(t *testing.T) {
builder := NewBuilder()
// Build a longer chain
builder.Input(10)
aID := builder.LastNodeID()
builder.Input(10)
bID := builder.LastNodeID()
builder.Add(aID, bID)
sum1ID := builder.LastNodeID()
builder.NTT(sum1ID)
nttID := builder.LastNodeID()
builder.INTT(nttID).Output()
graph, err := builder.Build()
if err != nil {
t.Fatal(err)
}
path, err := CriticalPath(graph)
if err != nil {
t.Fatalf("critical path failed: %v", err)
}
// Critical path: input -> Add -> NTT -> INTT = 4 nodes
if len(path) != 4 {
t.Errorf("expected critical path length 4, got %d", len(path))
}
}
func TestMemoryEstimate(t *testing.T) {
builder := NewBuilder()
builder.Input(10)
aID := builder.LastNodeID()
builder.Input(10)
bID := builder.LastNodeID()
builder.Add(aID, bID)
sumID := builder.LastNodeID()
builder.NTT(sumID).Output()
graph, err := builder.Build()
if err != nil {
t.Fatal(err)
}
scheduler := NewScheduler(graph)
schedule, err := scheduler.Schedule()
if err != nil {
t.Fatal(err)
}
mem := schedule.EstimateMemory()
t.Logf("Memory estimate: %+v", mem)
if mem.TotalIntermediates < 4 {
t.Errorf("expected at least 4 intermediates, got %d", mem.TotalIntermediates)
}
}
func TestBatchGrouping(t *testing.T) {
builder := NewBuilder()
// Create 4 parallel operations at same level
builder.Input(10)
a1ID := builder.LastNodeID()
builder.Input(10)
a2ID := builder.LastNodeID()
builder.Input(10)
b1ID := builder.LastNodeID()
builder.Input(10)
b2ID := builder.LastNodeID()
builder.Input(10)
c1ID := builder.LastNodeID()
builder.Input(10)
c2ID := builder.LastNodeID()
builder.Input(10)
d1ID := builder.LastNodeID()
builder.Input(10)
d2ID := builder.LastNodeID()
// All at depth 1
builder.Add(a1ID, a2ID)
builder.Add(b1ID, b2ID)
builder.Add(c1ID, c2ID)
builder.Add(d1ID, d2ID)
graph, err := builder.Build()
if err != nil {
t.Fatal(err)
}
// With fuse similar ops enabled (default), all 4 Adds should be in one batch
scheduler := NewScheduler(graph, WithFuseSimilarOps(true))
schedule, err := scheduler.Schedule()
if err != nil {
t.Fatal(err)
}
// Find batch at depth 1 (contains the Add ops)
var addBatch *Batch
for _, batch := range schedule.Batches {
if batch.Depth == 1 && batch.OpType == OpAdd {
addBatch = batch
break
}
}
if addBatch == nil {
t.Fatal("no Add batch found at depth 1")
}
if addBatch.Size() != 4 {
t.Errorf("expected 4 Adds in batch, got %d", addBatch.Size())
}
if addBatch.Mixed {
t.Error("batch should not be mixed (all same op type)")
}
}
func TestExecutorPlaceholder(t *testing.T) {
// This test validates the executor structure without actual FHE operations
builder := NewBuilder()
builder.Input(10)
aID := builder.LastNodeID()
builder.Input(10)
bID := builder.LastNodeID()
builder.Add(aID, bID).Output()
graph, err := builder.Build()
if err != nil {
t.Fatal(err)
}
scheduler := NewScheduler(graph)
schedule, err := scheduler.Schedule()
if err != nil {
t.Fatal(err)
}
// Create executor without actual ring (nil is ok for structure test)
ctx := &ExecutionContext{
pool: &CiphertextPool{},
}
executor := NewExecutor(schedule, ctx, WithGPU(false))
// Verify operations are registered
for _, op := range []OpType{OpAdd, OpSub, OpNTT, OpINTT} {
if _, ok := executor.ops[op]; !ok {
t.Errorf("operation %s not registered", op)
}
}
}
func TestOpTypeString(t *testing.T) {
tests := []struct {
op OpType
want string
}{
{OpAdd, "Add"},
{OpSub, "Sub"},
{OpMul, "Mul"},
{OpNTT, "NTT"},
{OpINTT, "INTT"},
{OpRelinearize, "Relinearize"},
{OpRescale, "Rescale"},
}
for _, tt := range tests {
if got := tt.op.String(); got != tt.want {
t.Errorf("OpType(%d).String() = %s, want %s", tt.op, got, tt.want)
}
}
}
func TestOpTypeProperties(t *testing.T) {
// Test CanFuse
fusable := []OpType{OpAdd, OpSub, OpMulPlain, OpAddPlain, OpSubPlain, OpNegate}
for _, op := range fusable {
if !op.CanFuse() {
t.Errorf("%s should be fusable", op)
}
}
// Test IsElementwise
elementwise := []OpType{OpAdd, OpSub, OpNTT, OpINTT}
for _, op := range elementwise {
if !op.IsElementwise() {
t.Errorf("%s should be elementwise", op)
}
}
}
func TestSchedulerOptions(t *testing.T) {
graph := NewGraph()
n := NewNode(OpLoad)
n.Output = 1
graph.AddNode(n)
// Test all options
scheduler := NewScheduler(graph,
WithMaxBatchSize(512),
WithFuseSimilarOps(false),
WithMinimizeKernels(true),
)
if scheduler.maxBatchSize != 512 {
t.Errorf("maxBatchSize not set correctly")
}
if scheduler.fuseSimilarOps != false {
t.Errorf("fuseSimilarOps not set correctly")
}
if scheduler.minimizeKernels != true {
t.Errorf("minimizeKernels not set correctly")
}
}
func TestPrepareBatchDispatch(t *testing.T) {
batch := &Batch{
ID: 0,
Depth: 1,
OpType: OpAdd,
Nodes: []*Node{
{ID: 1, Op: OpAdd, Inputs: []NodeID{10, 11}, Output: 100},
{ID: 2, Op: OpAdd, Inputs: []NodeID{12, 13}, Output: 101},
},
}
dispatch := PrepareBatchDispatch(batch)
if dispatch.BatchSize != 2 {
t.Errorf("expected batch size 2, got %d", dispatch.BatchSize)
}
if dispatch.OpType != OpAdd {
t.Errorf("expected OpAdd, got %s", dispatch.OpType)
}
if len(dispatch.InputIDs[0]) != 2 {
t.Errorf("expected 2 inputs for first op, got %d", len(dispatch.InputIDs[0]))
}
}
func BenchmarkScheduler(b *testing.B) {
// Build a larger graph for benchmarking
builder := NewBuilder()
// Create 100 inputs
inputIDs := make([]NodeID, 100)
for i := 0; i < 100; i++ {
builder.Input(10)
inputIDs[i] = builder.LastNodeID()
}
// Create pairwise additions (50 ops)
sumIDs := make([]NodeID, 50)
for i := 0; i < 50; i++ {
builder.Add(inputIDs[i*2], inputIDs[i*2+1])
sumIDs[i] = builder.LastNodeID()
}
// Create more additions (25 ops)
for i := 0; i < 25; i++ {
builder.Add(sumIDs[i*2], sumIDs[i*2+1])
}
graph, err := builder.Build()
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
scheduler := NewScheduler(graph)
_, err := scheduler.Schedule()
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkTopologicalSort(b *testing.B) {
builder := NewBuilder()
// Create a chain of 1000 operations
builder.Input(10)
prevID := builder.LastNodeID()
for i := 0; i < 999; i++ {
builder.Input(10)
newID := builder.LastNodeID()
builder.Add(prevID, newID)
prevID = builder.LastNodeID()
}
graph, err := builder.Build()
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := TopologicalOrder(graph)
if err != nil {
b.Fatal(err)
}
}
}
func TestExecuteDAGFunction(t *testing.T) {
// This tests the convenience function interface
builder := NewBuilder()
builder.Input(10)
aID := builder.LastNodeID()
builder.Input(10)
bID := builder.LastNodeID()
builder.Add(aID, bID).Output()
graph, err := builder.Build()
if err != nil {
t.Fatal(err)
}
// Verify graph structure
if graph.NodeCount() != 3 {
t.Errorf("expected 3 nodes, got %d", graph.NodeCount())
}
// Test scheduling works
scheduler := NewScheduler(graph)
schedule, err := scheduler.Schedule()
if err != nil {
t.Fatal(err)
}
if len(schedule.Batches) == 0 {
t.Error("expected at least one batch")
}
// Note: Full ExecuteDAG requires a real ring context with ring.Poly types.
// This test validates the structure and scheduling without actual execution.
_ = context.Background() // Keep import used
t.Log("ExecuteDAG function exists and scheduling works correctly")
}