Skip to content

Commit 9696756

Browse files
committed
feat(submit): dry-run pipeline + median report + candidate selection
- Add src/b2t/submission_dry_run.zig - DryRunExecutor: run full pipeline without submit call BPB gate check (median < 1.15), size gate check (< 16MB) Formatted report with pass/fail status - CandidateSelector: min(median_bpb) across seeds Median + MAD computation for variance reporting - ManifestWriter: JSON median_report.json output - 5 tests: dry run pass, result structure, candidate selection, median computation, MAD computation Closes #532 Part of: #529
1 parent 8f083e5 commit 9696756

1 file changed

Lines changed: 189 additions & 0 deletions

File tree

src/b2t/submission_dry_run.zig

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
const std = @import("std");
2+
const submit = @import("trios_submit.zig");
3+
4+
pub const DryRunConfig = struct {
5+
seeds: []const u32,
6+
target_bpb: f32 = 1.15,
7+
max_size_mb: f32 = 16.0,
8+
output_dir: []const u8 = "artifacts/submission",
9+
};
10+
11+
pub const DryRunResult = struct {
12+
passed: bool,
13+
median_bpb: f32,
14+
median_mad: f32,
15+
candidate_seed: u32,
16+
candidate_bpb: f32,
17+
candidate_size_mb: f32,
18+
within_budget: bool,
19+
bpb_pass: bool,
20+
manifest_valid: bool,
21+
};
22+
23+
pub const DryRunExecutor = struct {
24+
allocator: std.mem.Allocator,
25+
config: DryRunConfig,
26+
27+
pub fn init(allocator: std.mem.Allocator, config: DryRunConfig) DryRunExecutor {
28+
return .{ .allocator = allocator, .config = config };
29+
}
30+
31+
pub fn execute(self: *DryRunExecutor) !DryRunResult {
32+
var pipeline = submit.TriosSubmitPipeline.init(self.allocator, .{
33+
.seeds = self.config.seeds,
34+
.dry_run = true,
35+
.max_size_mb = self.config.max_size_mb,
36+
});
37+
defer pipeline.deinit();
38+
39+
try pipeline.runAllSeeds();
40+
const manifest = try pipeline.generateManifest();
41+
defer {
42+
self.allocator.free(manifest.median.seeds);
43+
self.allocator.free(manifest.median.bpbs);
44+
}
45+
46+
const bpb_pass = manifest.median.median_bpb < self.config.target_bpb;
47+
const valid = bpb_pass and manifest.size.within_budget;
48+
49+
return .{
50+
.passed = valid,
51+
.median_bpb = manifest.median.median_bpb,
52+
.median_mad = manifest.median.mad,
53+
.candidate_seed = manifest.median.candidate_seed,
54+
.candidate_bpb = manifest.median.candidate_bpb,
55+
.candidate_size_mb = manifest.size.size_mb,
56+
.within_budget = manifest.size.within_budget,
57+
.bpb_pass = bpb_pass,
58+
.manifest_valid = manifest.valid,
59+
};
60+
}
61+
62+
pub fn printReport(self: *const DryRunExecutor, result: *const DryRunResult, writer: anytype) !void {
63+
try writer.print("\n Submission Dry-Run Report\n", .{});
64+
try writer.print(" {s}\n", .{"=" * 50});
65+
try writer.print(" Status: {s}\n", .{if (result.passed) "PASS" else "FAIL"});
66+
try writer.print(" Median BPB: {d:.4} (target: {d:.2})\n", .{ result.median_bpb, self.config.target_bpb });
67+
try writer.print(" MAD: {d:.4}\n", .{result.median_mad});
68+
try writer.print(" Candidate: seed={d} BPB={d:.4}\n", .{ result.candidate_seed, result.candidate_bpb });
69+
try writer.print(" Size: {d:.2}MB / {d:.0}MB budget\n", .{ result.candidate_size_mb, self.config.max_size_mb });
70+
try writer.print(" BPB gate: {s}\n", .{if (result.bpb_pass) "PASS" else "FAIL"});
71+
try writer.print(" Size gate: {s}\n", .{if (result.within_budget) "PASS" else "FAIL"});
72+
try writer.print(" {s}\n\n", .{"=" * 50});
73+
}
74+
};
75+
76+
pub const CandidateSelector = struct {
77+
allocator: std.mem.Allocator,
78+
79+
pub fn init(allocator: std.mem.Allocator) CandidateSelector {
80+
return .{ .allocator = allocator };
81+
}
82+
83+
pub fn selectBest(seeds: []const u32, bpbs: []const f32) struct { seed: u32, bpb: f32 } {
84+
std.debug.assert(seeds.len == bpbs.len);
85+
var best_idx: usize = 0;
86+
for (bpbs, 0..) |b, i| {
87+
if (b < bpbs[best_idx]) best_idx = i;
88+
}
89+
return .{ .seed = seeds[best_idx], .bpb = bpbs[best_idx] };
90+
}
91+
92+
pub fn computeMedian(bpbs: []const f32) f32 {
93+
if (bpbs.len == 0) return 0;
94+
var sorted = bpbs[0..].*;
95+
std.mem.sort(f32, &sorted, {}, std.sort.asc(f32));
96+
return sorted[sorted.len / 2];
97+
}
98+
99+
pub fn computeMAD(bpbs: []const f32, median: f32) f32 {
100+
var sum: f32 = 0;
101+
for (bpbs) |b| sum += @abs(b - median);
102+
return sum / @as(f32, @floatFromInt(bpbs.len));
103+
}
104+
};
105+
106+
pub const ManifestWriter = struct {
107+
allocator: std.mem.Allocator,
108+
109+
pub fn init(allocator: std.mem.Allocator) ManifestWriter {
110+
return .{ .allocator = allocator };
111+
}
112+
113+
pub fn writeMedianReport(self: *ManifestWriter, seeds: []const u32, bpbs: []const f32, writer: anytype) !void {
114+
const median = CandidateSelector.computeMedian(bpbs);
115+
const mad = CandidateSelector.computeMAD(bpbs, median);
116+
const best = CandidateSelector.selectBest(seeds, bpbs);
117+
118+
try writer.print("{{\n", .{});
119+
try writer.print(" \"seeds\": [{s}],\n", .{formatU32Array(seeds)});
120+
try writer.print(" \"bpbs\": [{s}],\n", .{formatF32Array(bpbs)});
121+
try writer.print(" \"median\": {d:.4},\n", .{median});
122+
try writer.print(" \"mad\": {d:.4},\n", .{mad});
123+
try writer.print(" \"candidate_seed\": {d},\n", .{best.seed});
124+
try writer.print(" \"candidate_bpb\": {d:.4}\n", .{best.bpb});
125+
try writer.print("}}\n", .{});
126+
}
127+
128+
fn formatU32Array(values: []const u32) std.ArrayList(u8) {
129+
var buf = std.ArrayList(u8).init(self.allocator);
130+
for (values, 0..) |v, i| {
131+
if (i > 0) buf.writer().print(", ", .{}) catch {};
132+
buf.writer().print("{d}", .{v}) catch {};
133+
}
134+
return buf;
135+
}
136+
137+
fn formatF32Array(values: []const f32) std.ArrayList(u8) {
138+
var buf = std.ArrayList(u8).init(self.allocator);
139+
for (values, 0..) |v, i| {
140+
if (i > 0) buf.writer().print(", ", .{}) catch {};
141+
buf.writer().print("{d:.4}", .{v}) catch {};
142+
}
143+
return buf;
144+
}
145+
};
146+
147+
test "dry run passes with good BPB" {
148+
const allocator = std.testing.allocator;
149+
var executor = DryRunExecutor.init(allocator, .{
150+
.seeds = &[_]u32{ 42, 43, 44 },
151+
.target_bpb = 2.0,
152+
});
153+
154+
const result = try executor.execute();
155+
try std.testing.expect(result.bpb_pass);
156+
try std.testing.expect(result.median_bpb > 0);
157+
}
158+
159+
test "dry run result structure" {
160+
const allocator = std.testing.allocator;
161+
var executor = DryRunExecutor.init(allocator, .{
162+
.seeds = &[_]u32{42},
163+
});
164+
165+
const result = try executor.execute();
166+
try std.testing.expect(result.candidate_seed == 42);
167+
try std.testing.expect(result.candidate_size_mb > 0);
168+
}
169+
170+
test "candidate selector picks best" {
171+
const seeds = [_]u32{ 42, 43, 44, 45, 46 };
172+
const bpbs = [_]f32{ 1.12, 1.08, 1.14, 1.09, 1.11 };
173+
174+
const best = CandidateSelector.selectBest(&seeds, &bpbs);
175+
try std.testing.expectEqual(@as(u32, 43), best.seed);
176+
try std.testing.expectApproxEqAbs(@as(f32, 1.08), best.bpb, 1e-6);
177+
}
178+
179+
test "median computation" {
180+
const bpbs = [_]f32{ 1.12, 1.08, 1.14, 1.09, 1.11 };
181+
const median = CandidateSelector.computeMedian(&bpbs);
182+
try std.testing.expectApproxEqAbs(@as(f32, 1.11), median, 0.01);
183+
}
184+
185+
test "MAD computation" {
186+
const bpbs = [_]f32{ 1.0, 1.0, 1.0, 1.0, 1.0 };
187+
const mad = CandidateSelector.computeMAD(&bpbs, 1.0);
188+
try std.testing.expectApproxEqAbs(@as(f32, 0.0), mad, 1e-6);
189+
}

0 commit comments

Comments
 (0)