nx_closed_loop.nx source
↩ module page · 295 lines · 10520 B
1// nx_closed_loop.nx -- measure-adjust-rerender closed-loop primitive.
2//
3// THE substrate's structural answer to the LoRA / negative-prompt /
4// ControlNet patch problem. Per cardinal
5// feedback-loras-and-negatives-are-patches-not-systems:
6//
7// * Modern image-gen + LLM stacks are BLACK BOXES. You prompt,
8// you hope. Predictability is patched via LoRAs / neg prompts.
9// * Substrate's offer: closed-loop measurement. Generate ->
10// measure -> if off-target, adjust + re-generate. Iterate
11// until target verdict hits or budget exhausted.
12// * User's example: "predictably make an 18 year old human."
13// Today's systems can't. This primitive + caller's age-
14// measurement callback CAN.
15//
16// L4 generic composer. Pure composition -- no new math, no new
17// containers. Caller plugs in the four callbacks:
18//
19// generate_fn : generates a candidate output given a seed and
20// adjustment state
21// measure_fn : measures the candidate and returns a verdict
22// (per-axis i64 values, e.g. measured age,
23// fidelity score, hallucination flag)
24// verdict_fn : judges measurement vs target spec; returns
25// 0 = OK / 1 = ADJUST / 2 = ABORT
26// adjust_fn : given current state + measurement, mutates
27// state for next attempt
28//
29// The substrate handles the loop discipline + budget + sealed
30// verdict semantics. Caller owns the domain logic (what age
31// is "18", what hallucination is, etc).
32//
33// ===== Why this is the anti-LoRA ==================================
34//
35// LoRA approach:
36// 1. Train a LoRA on 1000 examples of "18-year-old faces"
37// 2. Hope the LoRA biases the base model correctly
38// 3. Can't measure: no closed loop; just generate + visually check
39// 4. Fails on out-of-distribution prompts
40//
41// Closed-loop substrate approach:
42// 1. Generate face
43// 2. Measure: facial-landmark + age estimator returns predicted age
44// 3. If predicted age in [17, 19]: OK; emit
45// 4. Else: adjust seed / sampler temperature / guidance / prompt
46// embedding; re-generate
47// 5. Loop until OK or budget exhausted; emit verdict either way
48//
49// No model retraining. No LoRA file. No hallucinated "should be
50// good enough." Measurable predictability.
51//
52// Caller picks the measurement at the right granularity (per-token
53// for LLM hallucination, per-frame for video, per-element for
54// structured output).
55//
56// genealogy_id: cybernetics_wiener_1948 + feedback_control_bode_1945 +
57// rejection_sampling_von_neumann_1951 +
58// quality_engineering_taguchi_1986
59// lineage_id: substrate_closed_loop_v1
60
61// nx_safety_envelope:
62// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
63// sil_target: SIL1
64// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
65// verdict: NOT_YET_EVALUATED
66
67import "nx_syscalls.nx"
68import "nx_tier.nx"
69import "nx_loop.nx"
70
71// ===== Sealed-enum: ClosedLoopVerdict =============================
72
73const NX_CL_OK: nx_int = 0 // generation matched target
74const NX_CL_BUDGET: nx_int = 1 // out of attempts, last attempt off-target
75const NX_CL_ABORT: nx_int = 2 // measurement function asked to abort
76const NX_CL_ERR_BAD_BUDGET: nx_int = 3
77const NX_CL_N_VERDICTS: nx_int = 4
78
79func nx_cl_verdict_is_valid(v: nx_int) -> nx_int {
80 if v < 0 { return 0 }
81 if v >= NX_CL_N_VERDICTS { return 0 }
82 return 1
83}
84
85// ===== Sealed-enum: VerdictFnReturn ===============================
86//
87// What the caller's verdict_fn can return per measurement:
88// NX_CL_VFN_OK -> on-target, stop
89// NX_CL_VFN_ADJUST -> off-target, try again
90// NX_CL_VFN_ABORT -> abandoned (e.g. measurement saw catastrophic
91// hallucination, no point continuing)
92
93const NX_CL_VFN_OK: nx_int = 0
94const NX_CL_VFN_ADJUST: nx_int = 1
95const NX_CL_VFN_ABORT: nx_int = 2
96const NX_CL_VFN_N: nx_int = 3
97
98func nx_cl_vfn_is_valid(v: nx_int) -> nx_int {
99 if v < 0 { return 0 }
100 if v >= NX_CL_VFN_N { return 0 }
101 return 1
102}
103
104// ===== Result envelope ============================================
105//
106// One per closed-loop run. Caller reads .verdict to know what
107// happened; .n_attempts for the budget side; .last_measurement_ptr
108// for the final measurement bytes (or null if none).
109
110struct NxClosedLoopResult {
111 verdict: nx_int, // NX_CL_OK / BUDGET / ABORT
112 n_attempts: nx_int, // attempts consumed (1-based)
113 last_vfn_ret: nx_int // last verdict_fn return value
114}
115
116const NX_CL_RESULT_BYTES: nx_int = 24 // 3 fields * 8
117
118// ===== Main closed-loop entrypoint ================================
119//
120// state_ptr: caller-owned opaque pointer (cast to i64). Holds
121// the seed / adjustment params / shared buffers.
122// max_attempts: hard budget (per JPL Rule 2 + bounded-loop cardinal)
123//
124// generate_fn(state_ptr) -> nx_int (caller's result code; 0 = OK)
125// measure_fn(state_ptr) -> nx_int (caller's measurement code; same)
126// verdict_fn(state_ptr) -> nx_int (NX_CL_VFN_*)
127// adjust_fn(state_ptr) -> nx_int (caller's update result; 0 = OK)
128//
129// Loop body:
130// 1. generate
131// 2. measure
132// 3. verdict
133// 4. if OK -> exit
134// 5. if ABORT -> exit with abort verdict
135// 6. if ADJUST -> call adjust_fn, increment attempt counter,
136// goto 1
137//
138// Returns *NxClosedLoopResult (caller-owned via sys_mmap).
139
140func nx_closed_loop_run(
141 state_ptr: i64, max_attempts: nx_int,
142 generate_fn: func(i64) -> nx_int,
143 measure_fn: func(i64) -> nx_int,
144 verdict_fn: func(i64) -> nx_int,
145 adjust_fn: func(i64) -> nx_int) -> *NxClosedLoopResult {
146
147 let r: *NxClosedLoopResult = sys_mmap(NX_CL_RESULT_BYTES) as *NxClosedLoopResult
148 r.verdict = NX_CL_BUDGET
149 r.n_attempts = 0
150 r.last_vfn_ret = NX_CL_VFN_ADJUST
151
152 if max_attempts <= 0 {
153 r.verdict = NX_CL_ERR_BAD_BUDGET
154 return r
155 }
156
157 var iter: nx_int = 0
158 var verdict: nx_int = NX_LOOP_RUNNING
159 let BUDGET: nx_int = max_attempts
160 while verdict == NX_LOOP_RUNNING && iter < BUDGET {
161 r.n_attempts = iter + 1
162
163 // 1. Generate candidate.
164 let v_gen: nx_int = generate_fn(state_ptr)
165 if v_gen != 0 { verdict = NX_LOOP_ABORTED }
166
167 // 2. Measure.
168 if verdict == NX_LOOP_RUNNING {
169 let v_meas: nx_int = measure_fn(state_ptr)
170 if v_meas != 0 { verdict = NX_LOOP_ABORTED }
171 }
172
173 // 3. Verdict.
174 if verdict == NX_LOOP_RUNNING {
175 let v_vfn: nx_int = verdict_fn(state_ptr)
176 r.last_vfn_ret = v_vfn
177 if v_vfn == NX_CL_VFN_OK {
178 r.verdict = NX_CL_OK
179 verdict = NX_LOOP_DONE_EXIT
180 }
181 if v_vfn == NX_CL_VFN_ABORT {
182 r.verdict = NX_CL_ABORT
183 verdict = NX_LOOP_DONE_EXIT
184 }
185 }
186
187 // 4. Adjust + re-loop.
188 if verdict == NX_LOOP_RUNNING {
189 adjust_fn(state_ptr)
190 }
191
192 iter = iter + 1
193 }
194 // If we exited the while normally (budget hit) the verdict
195 // field already has NX_CL_BUDGET from init.
196 return r
197}
198
199// ===== Self-test ==================================================
200//
201// Tiny closed-loop: synthesize a target-value-converging walker.
202// State: a counter we step toward a target. Measurement: how far
203// from target. Verdict: OK if within tolerance, ADJUST else.
204// Adjust: step toward target.
205//
206// Closed-form invariants:
207// (a) Walker converges within tolerance before budget -> NX_CL_OK
208// (b) With tight tolerance + small budget -> NX_CL_BUDGET
209// (c) Verdict-range gate
210
211const NX_TEST_TARGET: nx_int = 100
212const NX_TEST_TOLERANCE: nx_int = 2
213
214struct TestState {
215 counter: nx_int,
216 measurement:nx_int,
217 target: nx_int,
218 tolerance: nx_int
219}
220
221func _test_state_alloc(initial: nx_int, target: nx_int, tol: nx_int) -> *TestState {
222 let s: *TestState = sys_mmap(32) as *TestState
223 s.counter = initial
224 s.measurement = 0
225 s.target = target
226 s.tolerance = tol
227 return s
228}
229
230func _test_generate(state_ptr: i64) -> nx_int {
231 // No-op generate in the test (the walker IS the generation).
232 return 0
233}
234
235func _test_measure(state_ptr: i64) -> nx_int {
236 let s: *TestState = state_ptr as *TestState
237 let diff: nx_int = s.counter - s.target
238 if diff < 0 { s.measurement = 0 - diff }
239 if diff >= 0 { s.measurement = diff }
240 return 0
241}
242
243func _test_verdict(state_ptr: i64) -> nx_int {
244 let s: *TestState = state_ptr as *TestState
245 if s.measurement <= s.tolerance { return NX_CL_VFN_OK }
246 return NX_CL_VFN_ADJUST
247}
248
249func _test_adjust(state_ptr: i64) -> nx_int {
250 let s: *TestState = state_ptr as *TestState
251 if s.counter < s.target { s.counter = s.counter + 1 }
252 if s.counter > s.target { s.counter = s.counter - 1 }
253 return 0
254}
255
256func main() -> i64 {
257 // --- (a) Converges within budget ---
258 let s1: *TestState = _test_state_alloc(0, NX_TEST_TARGET, NX_TEST_TOLERANCE)
259 let r1: *NxClosedLoopResult = nx_closed_loop_run(
260 s1 as i64, 200,
261 _test_generate, _test_measure, _test_verdict, _test_adjust)
262 if r1.verdict != NX_CL_OK { return 10 }
263 // n_attempts should be ~100 (one per step toward target).
264 if r1.n_attempts < 95 { return 11 }
265 if r1.n_attempts > 105 { return 12 }
266
267 // --- (b) Budget exhaustion ---
268 let s2: *TestState = _test_state_alloc(0, NX_TEST_TARGET, NX_TEST_TOLERANCE)
269 let r2: *NxClosedLoopResult = nx_closed_loop_run(
270 s2 as i64, 10, // tiny budget, can't reach 100
271 _test_generate, _test_measure, _test_verdict, _test_adjust)
272 if r2.verdict != NX_CL_BUDGET { return 20 }
273 if r2.n_attempts != 10 { return 21 }
274
275 // --- (c) Bad budget rejected ---
276 let s3: *TestState = _test_state_alloc(0, NX_TEST_TARGET, NX_TEST_TOLERANCE)
277 let r3: *NxClosedLoopResult = nx_closed_loop_run(
278 s3 as i64, 0,
279 _test_generate, _test_measure, _test_verdict, _test_adjust)
280 if r3.verdict != NX_CL_ERR_BAD_BUDGET { return 30 }
281
282 // --- (d) Verdict + vfn gates ---
283 var vi: nx_int = 0
284 while vi < NX_CL_N_VERDICTS {
285 if nx_cl_verdict_is_valid(vi) != 1 { return 40 + vi }
286 vi = vi + 1
287 }
288 var vj: nx_int = 0
289 while vj < NX_CL_VFN_N {
290 if nx_cl_vfn_is_valid(vj) != 1 { return 50 + vj }
291 vj = vj + 1
292 }
293
294 return 0
295}