ThermoFluidFoundation commited on
Commit
0fb6732
·
verified ·
1 Parent(s): bc409ed

Upload main.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. main.py +1541 -0
main.py ADDED
@@ -0,0 +1,1541 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import math
3
+ import random
4
+ from copy import deepcopy
5
+ from pathlib import Path
6
+
7
+ import ollama
8
+
9
+
10
+ # ============================================================
11
+ # Configuration
12
+ # ============================================================
13
+
14
+ LLM_MODEL = "gpt-oss:20b"
15
+
16
+ MAX_ROUNDS = 8
17
+ STOP_PROBABILITY = 0.95
18
+ NOISE_STD = 0.15
19
+
20
+ MODEL_MISMATCH_THRESHOLD = 2.5
21
+
22
+ random.seed(42)
23
+
24
+ OUTPUT_DIR = Path("runs_v04")
25
+ OUTPUT_DIR.mkdir(exist_ok=True)
26
+
27
+
28
+ # ============================================================
29
+ # Candidate boiling mass-transfer models
30
+ # ============================================================
31
+
32
+ MODEL_REGISTRY = {
33
+ "M0": {
34
+ "description": "linear interfacial mass-transfer closure",
35
+ "base": "linear",
36
+ "a": 0.80,
37
+ "corrections": [],
38
+ },
39
+
40
+ "M1": {
41
+ "description": "linear + quadratic mass-transfer closure",
42
+ "base": "linear",
43
+ "a": 0.80,
44
+ "corrections": [
45
+ {
46
+ "type": "quadratic",
47
+ "coefficient": 0.004,
48
+ }
49
+ ],
50
+ },
51
+
52
+ "M2": {
53
+ "description": "saturating rational mass-transfer closure",
54
+ "base": "rational",
55
+ "a": 0.80,
56
+ "b": 0.01,
57
+ "corrections": [],
58
+ },
59
+ }
60
+
61
+
62
+ def model_to_string(model_name):
63
+
64
+ spec = MODEL_REGISTRY[model_name]
65
+
66
+ if spec["base"] == "linear":
67
+ expression = f"{spec['a']:.6g} * ΔT"
68
+
69
+ elif spec["base"] == "rational":
70
+ expression = (
71
+ f"{spec['a']:.6g} * ΔT "
72
+ f"/ (1 + {spec['b']:.6g} * ΔT)"
73
+ )
74
+
75
+ else:
76
+ raise ValueError(
77
+ f"Unknown base model: {spec['base']}"
78
+ )
79
+
80
+ for correction in spec["corrections"]:
81
+
82
+ if correction["type"] == "quadratic":
83
+ c = correction["coefficient"]
84
+ expression += f" + ({c:.6g}) * ΔT^2"
85
+
86
+ elif correction["type"] == "linear":
87
+ c = correction["coefficient"]
88
+ expression += f" + ({c:.6g}) * ΔT"
89
+
90
+ elif correction["type"] == "constant":
91
+ c = correction["coefficient"]
92
+ expression += f" + ({c:.6g})"
93
+
94
+ return expression
95
+
96
+
97
+ def physics_model(model_name, delta_T):
98
+
99
+ spec = MODEL_REGISTRY[model_name]
100
+
101
+ if spec["base"] == "linear":
102
+
103
+ y = spec["a"] * delta_T
104
+
105
+ elif spec["base"] == "rational":
106
+
107
+ y = (
108
+ spec["a"] * delta_T
109
+ / (1.0 + spec["b"] * delta_T)
110
+ )
111
+
112
+ else:
113
+
114
+ raise ValueError(
115
+ f"Unknown base model: {spec['base']}"
116
+ )
117
+
118
+ for correction in spec["corrections"]:
119
+
120
+ correction_type = correction["type"]
121
+ coefficient = correction["coefficient"]
122
+
123
+ if correction_type == "quadratic":
124
+
125
+ y += coefficient * delta_T**2
126
+
127
+ elif correction_type == "linear":
128
+
129
+ y += coefficient * delta_T
130
+
131
+ elif correction_type == "constant":
132
+
133
+ y += coefficient
134
+
135
+ else:
136
+
137
+ raise ValueError(
138
+ f"Unknown correction: {correction_type}"
139
+ )
140
+
141
+ return y
142
+
143
+
144
+ # ============================================================
145
+ # Hidden boiling physical world
146
+ # ============================================================
147
+
148
+ def hidden_physics(delta_T: float) -> float:
149
+ """
150
+ Synthetic boiling interfacial mass-transfer world.
151
+
152
+ The intelligence system never sees this equation.
153
+
154
+ The hidden world contains a nonlinear mass-transfer
155
+ contribution that is not represented exactly by the
156
+ initial candidate model class.
157
+ """
158
+
159
+ return (
160
+ 0.80 * delta_T
161
+ + 0.002 * delta_T**2
162
+ )
163
+
164
+
165
+ def query_hidden_world(delta_T: float) -> dict:
166
+
167
+ clean = hidden_physics(delta_T)
168
+
169
+ noise = random.gauss(
170
+ 0.0,
171
+ NOISE_STD,
172
+ )
173
+
174
+ return {
175
+ "delta_T": delta_T,
176
+ "observed_mass_transfer": clean + noise,
177
+ "noise_std": NOISE_STD,
178
+ }
179
+
180
+
181
+ # ============================================================
182
+ # Scientific state
183
+ # ============================================================
184
+
185
+ state = {
186
+
187
+ "scientific_question": (
188
+ "Determine an adequate constitutive closure for "
189
+ "boiling interfacial mass transfer as a function "
190
+ "of interfacial thermal driving ΔT. "
191
+ "Detect failure of the initial closure class and "
192
+ "construct a revised executable closure if required."
193
+ ),
194
+
195
+ "physical_quantity": (
196
+ "normalized interfacial mass-transfer response"
197
+ ),
198
+
199
+ "candidate_models": {},
200
+
201
+ "allowed_delta_T": [
202
+ 2,
203
+ 5,
204
+ 8,
205
+ 12,
206
+ 16,
207
+ 20,
208
+ 24,
209
+ 28,
210
+ ],
211
+
212
+ "evidence": [],
213
+
214
+ "posterior": {},
215
+
216
+ "round": 0,
217
+
218
+ "revision_history": [],
219
+ }
220
+
221
+
222
+ def synchronize_state_models():
223
+
224
+ state["candidate_models"] = {
225
+ model_name: model_to_string(model_name)
226
+ for model_name in MODEL_REGISTRY
227
+ }
228
+
229
+
230
+ def reset_posterior():
231
+
232
+ n_models = len(MODEL_REGISTRY)
233
+
234
+ state["posterior"] = {
235
+ model_name: 1.0 / n_models
236
+ for model_name in MODEL_REGISTRY
237
+ }
238
+
239
+
240
+ synchronize_state_models()
241
+ reset_posterior()
242
+
243
+
244
+ # ============================================================
245
+ # Deterministic physics tools
246
+ # ============================================================
247
+
248
+ def prediction_table(state):
249
+
250
+ table = {}
251
+
252
+ for delta_T in state["allowed_delta_T"]:
253
+
254
+ table[delta_T] = {}
255
+
256
+ for model in state["candidate_models"]:
257
+
258
+ table[delta_T][model] = physics_model(
259
+ model,
260
+ delta_T,
261
+ )
262
+
263
+ return table
264
+
265
+
266
+ def discrimination_scores(state):
267
+ """
268
+ Rank unused thermal conditions according to the minimum
269
+ pairwise separation between executable mass-transfer
270
+ closures, normalized by observational noise.
271
+ """
272
+
273
+ used = {
274
+ obs["delta_T"]
275
+ for obs in state["evidence"]
276
+ }
277
+
278
+ scores = {}
279
+
280
+ for delta_T in state["allowed_delta_T"]:
281
+
282
+ if delta_T in used:
283
+ continue
284
+
285
+ predictions = [
286
+ physics_model(
287
+ model,
288
+ delta_T,
289
+ )
290
+ for model in state["candidate_models"]
291
+ ]
292
+
293
+ if len(predictions) < 2:
294
+ scores[delta_T] = 0.0
295
+ continue
296
+
297
+ pairwise = []
298
+
299
+ for i in range(len(predictions)):
300
+
301
+ for j in range(
302
+ i + 1,
303
+ len(predictions),
304
+ ):
305
+
306
+ separation = abs(
307
+ predictions[i]
308
+ - predictions[j]
309
+ )
310
+
311
+ pairwise.append(
312
+ separation / NOISE_STD
313
+ )
314
+
315
+ scores[delta_T] = min(pairwise)
316
+
317
+ return scores
318
+
319
+
320
+ # ============================================================
321
+ # Bayesian evidence update
322
+ # ============================================================
323
+
324
+ def gaussian_log_likelihood(
325
+ observed,
326
+ predicted,
327
+ sigma,
328
+ ):
329
+
330
+ z = (
331
+ observed - predicted
332
+ ) / sigma
333
+
334
+ return -0.5 * z**2
335
+
336
+
337
+ def update_posterior(
338
+ state,
339
+ observation,
340
+ ):
341
+
342
+ old = state["posterior"]
343
+
344
+ log_weights = {}
345
+
346
+ for model in state["candidate_models"]:
347
+
348
+ prediction = physics_model(
349
+ model,
350
+ observation["delta_T"],
351
+ )
352
+
353
+ log_likelihood = gaussian_log_likelihood(
354
+ observation["observed_mass_transfer"],
355
+ prediction,
356
+ observation["noise_std"],
357
+ )
358
+
359
+ prior = max(
360
+ old.get(model, 1e-300),
361
+ 1e-300,
362
+ )
363
+
364
+ log_weights[model] = (
365
+ math.log(prior)
366
+ + log_likelihood
367
+ )
368
+
369
+ max_log_weight = max(
370
+ log_weights.values()
371
+ )
372
+
373
+ weights = {
374
+ model: math.exp(
375
+ value - max_log_weight
376
+ )
377
+ for model, value
378
+ in log_weights.items()
379
+ }
380
+
381
+ normalizer = sum(
382
+ weights.values()
383
+ )
384
+
385
+ return {
386
+ model: value / normalizer
387
+ for model, value
388
+ in weights.items()
389
+ }
390
+
391
+
392
+ def recompute_posterior_from_all_evidence():
393
+
394
+ reset_posterior()
395
+
396
+ for observation in state["evidence"]:
397
+
398
+ state["posterior"] = (
399
+ update_posterior(
400
+ state,
401
+ observation,
402
+ )
403
+ )
404
+
405
+
406
+ # ============================================================
407
+ # Model-class adequacy
408
+ # ============================================================
409
+
410
+ def model_mismatch_scores(state):
411
+
412
+ if len(state["evidence"]) < 3:
413
+ return {}
414
+
415
+ scores = {}
416
+
417
+ for model in state["candidate_models"]:
418
+
419
+ residuals = []
420
+
421
+ for obs in state["evidence"]:
422
+
423
+ predicted = physics_model(
424
+ model,
425
+ obs["delta_T"],
426
+ )
427
+
428
+ residual = (
429
+ obs["observed_mass_transfer"]
430
+ - predicted
431
+ )
432
+
433
+ residuals.append(
434
+ residual
435
+ )
436
+
437
+ rmse = math.sqrt(
438
+ sum(
439
+ r**2
440
+ for r in residuals
441
+ )
442
+ / len(residuals)
443
+ )
444
+
445
+ scores[model] = (
446
+ rmse / NOISE_STD
447
+ )
448
+
449
+ return scores
450
+
451
+
452
+ def best_model_by_mismatch(state):
453
+
454
+ scores = model_mismatch_scores(
455
+ state
456
+ )
457
+
458
+ if not scores:
459
+ return None, None
460
+
461
+ best_model = min(
462
+ scores,
463
+ key=scores.get,
464
+ )
465
+
466
+ return (
467
+ best_model,
468
+ scores[best_model],
469
+ )
470
+
471
+
472
+ # ============================================================
473
+ # Residual analysis
474
+ # ============================================================
475
+
476
+ def residual_table(
477
+ state,
478
+ baseline_model,
479
+ ):
480
+
481
+ table = []
482
+
483
+ for obs in state["evidence"]:
484
+
485
+ prediction = physics_model(
486
+ baseline_model,
487
+ obs["delta_T"],
488
+ )
489
+
490
+ residual = (
491
+ obs["observed_mass_transfer"]
492
+ - prediction
493
+ )
494
+
495
+ table.append({
496
+ "delta_T":
497
+ obs["delta_T"],
498
+
499
+ "observed_mass_transfer":
500
+ obs["observed_mass_transfer"],
501
+
502
+ "baseline_prediction":
503
+ prediction,
504
+
505
+ "residual":
506
+ residual,
507
+ })
508
+
509
+ return table
510
+
511
+
512
+ def fit_constant_correction(
513
+ state,
514
+ baseline_model,
515
+ ):
516
+
517
+ residuals = []
518
+
519
+ for obs in state["evidence"]:
520
+
521
+ x = obs["delta_T"]
522
+
523
+ residuals.append(
524
+ obs["observed_mass_transfer"]
525
+ - physics_model(
526
+ baseline_model,
527
+ x,
528
+ )
529
+ )
530
+
531
+ return (
532
+ sum(residuals)
533
+ / len(residuals)
534
+ )
535
+
536
+
537
+ def fit_linear_correction(
538
+ state,
539
+ baseline_model,
540
+ ):
541
+
542
+ numerator = 0.0
543
+ denominator = 0.0
544
+
545
+ for obs in state["evidence"]:
546
+
547
+ x = obs["delta_T"]
548
+
549
+ residual = (
550
+ obs["observed_mass_transfer"]
551
+ - physics_model(
552
+ baseline_model,
553
+ x,
554
+ )
555
+ )
556
+
557
+ numerator += residual * x
558
+ denominator += x**2
559
+
560
+ if denominator == 0:
561
+ return 0.0
562
+
563
+ return numerator / denominator
564
+
565
+
566
+ def fit_quadratic_correction(
567
+ state,
568
+ baseline_model,
569
+ ):
570
+
571
+ numerator = 0.0
572
+ denominator = 0.0
573
+
574
+ for obs in state["evidence"]:
575
+
576
+ x = obs["delta_T"]
577
+
578
+ residual = (
579
+ obs["observed_mass_transfer"]
580
+ - physics_model(
581
+ baseline_model,
582
+ x,
583
+ )
584
+ )
585
+
586
+ numerator += (
587
+ residual * x**2
588
+ )
589
+
590
+ denominator += x**4
591
+
592
+ if denominator == 0:
593
+ return 0.0
594
+
595
+ return (
596
+ numerator / denominator
597
+ )
598
+
599
+
600
+ def correction_fit_rmse(
601
+ state,
602
+ baseline_model,
603
+ correction_type,
604
+ coefficient,
605
+ ):
606
+
607
+ errors = []
608
+
609
+ for obs in state["evidence"]:
610
+
611
+ x = obs["delta_T"]
612
+
613
+ baseline = physics_model(
614
+ baseline_model,
615
+ x,
616
+ )
617
+
618
+ if correction_type == "constant":
619
+
620
+ correction = coefficient
621
+
622
+ elif correction_type == "linear":
623
+
624
+ correction = (
625
+ coefficient * x
626
+ )
627
+
628
+ elif correction_type == "quadratic":
629
+
630
+ correction = (
631
+ coefficient * x**2
632
+ )
633
+
634
+ else:
635
+
636
+ raise ValueError(
637
+ correction_type
638
+ )
639
+
640
+ prediction = (
641
+ baseline + correction
642
+ )
643
+
644
+ errors.append(
645
+ obs["observed_mass_transfer"]
646
+ - prediction
647
+ )
648
+
649
+ return math.sqrt(
650
+ sum(
651
+ error**2
652
+ for error in errors
653
+ )
654
+ / len(errors)
655
+ )
656
+
657
+
658
+ def deterministic_revision_search(
659
+ state,
660
+ baseline_model,
661
+ ):
662
+ """
663
+ Search a deliberately small mathematical correction space.
664
+
665
+ The LLM may interpret the residual structure, but the
666
+ executable revised closure is selected and fitted by
667
+ deterministic numerical tools.
668
+ """
669
+
670
+ candidates = {}
671
+
672
+ constant_c = fit_constant_correction(
673
+ state,
674
+ baseline_model,
675
+ )
676
+
677
+ candidates["constant"] = {
678
+ "coefficient": constant_c,
679
+ "rmse": correction_fit_rmse(
680
+ state,
681
+ baseline_model,
682
+ "constant",
683
+ constant_c,
684
+ ),
685
+ }
686
+
687
+ linear_c = fit_linear_correction(
688
+ state,
689
+ baseline_model,
690
+ )
691
+
692
+ candidates["linear"] = {
693
+ "coefficient": linear_c,
694
+ "rmse": correction_fit_rmse(
695
+ state,
696
+ baseline_model,
697
+ "linear",
698
+ linear_c,
699
+ ),
700
+ }
701
+
702
+ quadratic_c = fit_quadratic_correction(
703
+ state,
704
+ baseline_model,
705
+ )
706
+
707
+ candidates["quadratic"] = {
708
+ "coefficient": quadratic_c,
709
+ "rmse": correction_fit_rmse(
710
+ state,
711
+ baseline_model,
712
+ "quadratic",
713
+ quadratic_c,
714
+ ),
715
+ }
716
+
717
+ best_type = min(
718
+ candidates,
719
+ key=lambda name:
720
+ candidates[name]["rmse"],
721
+ )
722
+
723
+ return (
724
+ best_type,
725
+ candidates[best_type]["coefficient"],
726
+ candidates,
727
+ )
728
+
729
+
730
+ # ============================================================
731
+ # Executable theory revision
732
+ # ============================================================
733
+
734
+ def register_revised_model(
735
+ parent_model,
736
+ correction_type,
737
+ coefficient,
738
+ ):
739
+
740
+ new_index = len(
741
+ MODEL_REGISTRY
742
+ )
743
+
744
+ new_model_name = (
745
+ f"M{new_index}"
746
+ )
747
+
748
+ new_spec = deepcopy(
749
+ MODEL_REGISTRY[parent_model]
750
+ )
751
+
752
+ new_spec["description"] = (
753
+ f"revised boiling mass-transfer closure "
754
+ f"derived from {parent_model}"
755
+ )
756
+
757
+ new_spec["corrections"].append({
758
+ "type":
759
+ correction_type,
760
+
761
+ "coefficient":
762
+ coefficient,
763
+ })
764
+
765
+ MODEL_REGISTRY[
766
+ new_model_name
767
+ ] = new_spec
768
+
769
+ synchronize_state_models()
770
+
771
+ return new_model_name
772
+
773
+
774
+ # ============================================================
775
+ # LLM interface
776
+ # ============================================================
777
+
778
+ def ask_agent(
779
+ system_prompt,
780
+ user_prompt,
781
+ ):
782
+
783
+ response = ollama.chat(
784
+ model=LLM_MODEL,
785
+ messages=[
786
+ {
787
+ "role": "system",
788
+ "content": system_prompt,
789
+ },
790
+ {
791
+ "role": "user",
792
+ "content": user_prompt,
793
+ },
794
+ ],
795
+ )
796
+
797
+ return (
798
+ response["message"]["content"]
799
+ )
800
+
801
+
802
+ # ============================================================
803
+ # Scientific agents
804
+ # ============================================================
805
+
806
+ def proposer(
807
+ state,
808
+ table,
809
+ ):
810
+
811
+ prompt = f"""
812
+ SCIENTIFIC STATE
813
+
814
+ {json.dumps(state, indent=2)}
815
+
816
+ EXECUTABLE BOILING MASS-TRANSFER MODEL PREDICTIONS
817
+
818
+ {json.dumps(table, indent=2)}
819
+
820
+ You are the scientific hypothesis proposer.
821
+
822
+ The problem is constitutive modeling of interfacial mass
823
+ transfer in boiling.
824
+
825
+ The numerical predictions were computed by an external
826
+ physics tool and are authoritative.
827
+
828
+ Do NOT perform new arithmetic.
829
+ Do NOT invent additional models.
830
+
831
+ Using the current evidence:
832
+
833
+ 1. identify which mass-transfer closures remain plausible,
834
+ 2. explain their mathematical differences,
835
+ 3. state what uncertainty remains.
836
+
837
+ Do not assign a microscopic boiling mechanism to a
838
+ mathematical term unless the evidence identifies it.
839
+
840
+ Be concise.
841
+ """
842
+
843
+ return ask_agent(
844
+ "You are a rigorous boiling-physics hypothesis agent.",
845
+ prompt,
846
+ )
847
+
848
+
849
+ def critic(
850
+ state,
851
+ proposal,
852
+ scores,
853
+ ):
854
+
855
+ prompt = f"""
856
+ SCIENTIFIC STATE
857
+
858
+ {json.dumps(state, indent=2)}
859
+
860
+ PROPOSER
861
+
862
+ {proposal}
863
+
864
+ COMPUTED TEST DISCRIMINATION SCORES
865
+
866
+ {json.dumps(scores, indent=2)}
867
+
868
+ You are an independent scientific critic evaluating
869
+ candidate boiling interfacial mass-transfer closures.
870
+
871
+ The numerical values were computed externally.
872
+ Do not recompute them.
873
+
874
+ Assess:
875
+
876
+ 1. overclaiming,
877
+ 2. evidence sufficiency,
878
+ 3. surviving alternatives,
879
+ 4. whether another thermal condition is required,
880
+ 5. whether preference among existing closures could hide
881
+ model-class inadequacy.
882
+
883
+ Do not invent microscopic boiling physics.
884
+ """
885
+
886
+ return ask_agent(
887
+ "You are a skeptical boiling-physics reviewer.",
888
+ prompt,
889
+ )
890
+
891
+
892
+ def select_next_test(
893
+ state,
894
+ proposal,
895
+ critique,
896
+ scores,
897
+ ):
898
+
899
+ ranked = sorted(
900
+ scores.items(),
901
+ key=lambda x: x[1],
902
+ reverse=True,
903
+ )
904
+
905
+ prompt = f"""
906
+ CURRENT SCIENTIFIC STATE
907
+
908
+ {json.dumps(state, indent=2)}
909
+
910
+ PROPOSER
911
+
912
+ {proposal}
913
+
914
+ CRITIC
915
+
916
+ {critique}
917
+
918
+ AVAILABLE THERMAL CONDITIONS RANKED BY
919
+ COMPUTED MODEL DISCRIMINATION
920
+
921
+ {json.dumps(ranked, indent=2)}
922
+
923
+ Select ONE available Delta T condition for the next
924
+ synthetic boiling mass-transfer observation.
925
+
926
+ Return ONLY the numerical Delta T value.
927
+ """
928
+
929
+ answer = ask_agent(
930
+ "You select informative falsification tests.",
931
+ prompt,
932
+ )
933
+
934
+ allowed = list(
935
+ scores.keys()
936
+ )
937
+
938
+ for value in sorted(
939
+ allowed,
940
+ reverse=True,
941
+ ):
942
+
943
+ if str(value) in answer:
944
+ return value
945
+
946
+ return ranked[0][0]
947
+
948
+
949
+ def theory_revision_agent(
950
+ state,
951
+ best_model,
952
+ mismatch_score,
953
+ residuals,
954
+ revision_candidates,
955
+ ):
956
+
957
+ prompt = f"""
958
+ SCIENTIFIC STATE
959
+
960
+ {json.dumps(state, indent=2)}
961
+
962
+ BEST CURRENT EXECUTABLE MASS-TRANSFER CLOSURE
963
+
964
+ {best_model}
965
+
966
+ NORMALIZED MODEL-MISMATCH SCORE
967
+
968
+ {mismatch_score}
969
+
970
+ COMPUTED RESIDUALS
971
+
972
+ {json.dumps(residuals, indent=2)}
973
+
974
+ DETERMINISTIC REVISION SEARCH
975
+
976
+ {json.dumps(revision_candidates, indent=2)}
977
+
978
+ The current model class is inadequate.
979
+
980
+ You are the theory-revision component of an autonomous
981
+ boiling-physics discovery system.
982
+
983
+ The numerical fitting was performed by external tools.
984
+ Do NOT recompute coefficients.
985
+
986
+ Interpret the evidence.
987
+
988
+ Answer concisely:
989
+
990
+ MATHEMATICAL INFERENCE:
991
+ What residual structure is supported?
992
+
993
+ MODEL REVISION:
994
+ What minimal constitutive correction is justified?
995
+
996
+ PHYSICAL HYPOTHESIS:
997
+ What, if anything, can be inferred about missing
998
+ interfacial mass-transfer physics?
999
+
1000
+ FALSIFICATION:
1001
+ What observation would most strongly challenge the
1002
+ revised closure?
1003
+
1004
+ Do not claim a specific microscopic boiling mechanism
1005
+ unless the evidence actually identifies one.
1006
+ """
1007
+
1008
+ return ask_agent(
1009
+ (
1010
+ "You revise inadequate boiling mass-transfer "
1011
+ "closures using falsifiable numerical evidence."
1012
+ ),
1013
+ prompt,
1014
+ )
1015
+
1016
+
1017
+ def final_evaluator(state):
1018
+
1019
+ mismatch_scores = (
1020
+ model_mismatch_scores(state)
1021
+ )
1022
+
1023
+ prompt = f"""
1024
+ FINAL SCIENTIFIC STATE
1025
+
1026
+ {json.dumps(state, indent=2)}
1027
+
1028
+ FINAL NORMALIZED MODEL-MISMATCH SCORES
1029
+
1030
+ {json.dumps(mismatch_scores, indent=2)}
1031
+
1032
+ You are the final scientific evaluator.
1033
+
1034
+ This is a synthetic benchmark for autonomous discovery
1035
+ of a boiling interfacial mass-transfer closure.
1036
+
1037
+ Use only the supplied numerical evidence.
1038
+
1039
+ Answer:
1040
+
1041
+ 1. Which executable closure is best supported?
1042
+ 2. Is it adequate within the observational uncertainty?
1043
+ 3. Was the original model class falsified?
1044
+ 4. Was a revised executable closure generated?
1045
+ 5. What should be tested next?
1046
+
1047
+ Do not invent microscopic physics.
1048
+ """
1049
+
1050
+ return ask_agent(
1051
+ "You are an evidence-based scientific judge.",
1052
+ prompt,
1053
+ )
1054
+
1055
+
1056
+ # ============================================================
1057
+ # Persistent scientific memory
1058
+ # ============================================================
1059
+
1060
+ trace = []
1061
+
1062
+
1063
+ def save_state():
1064
+
1065
+ with open(
1066
+ OUTPUT_DIR / "scientific_state.json",
1067
+ "w",
1068
+ ) as f:
1069
+
1070
+ json.dump(
1071
+ state,
1072
+ f,
1073
+ indent=2,
1074
+ )
1075
+
1076
+ with open(
1077
+ OUTPUT_DIR / "trace.json",
1078
+ "w",
1079
+ ) as f:
1080
+
1081
+ json.dump(
1082
+ trace,
1083
+ f,
1084
+ indent=2,
1085
+ )
1086
+
1087
+ with open(
1088
+ OUTPUT_DIR / "model_registry.json",
1089
+ "w",
1090
+ ) as f:
1091
+
1092
+ json.dump(
1093
+ MODEL_REGISTRY,
1094
+ f,
1095
+ indent=2,
1096
+ )
1097
+
1098
+
1099
+ # ============================================================
1100
+ # Closed self-revising discovery loop
1101
+ # ============================================================
1102
+
1103
+ print("\n" + "=" * 72)
1104
+ print("BOILING INTELLIGENCE v0.4")
1105
+ print("Executable Mass-Transfer Closure Discovery")
1106
+ print("=" * 72)
1107
+
1108
+
1109
+ original_model_class_failed = False
1110
+ revision_generated = False
1111
+ revision_count = 0
1112
+
1113
+ MAX_REVISIONS = 1
1114
+
1115
+
1116
+ for round_id in range(
1117
+ 1,
1118
+ MAX_ROUNDS + 1,
1119
+ ):
1120
+
1121
+ state["round"] = round_id
1122
+
1123
+ print("\n" + "=" * 72)
1124
+ print(f"ROUND {round_id}")
1125
+ print("=" * 72)
1126
+
1127
+ table = prediction_table(
1128
+ state
1129
+ )
1130
+
1131
+ scores = discrimination_scores(
1132
+ state
1133
+ )
1134
+
1135
+ if not scores:
1136
+
1137
+ print(
1138
+ "\nNo unused thermal conditions remain."
1139
+ )
1140
+
1141
+ break
1142
+
1143
+
1144
+ # --------------------------------------------------------
1145
+ # 1. Hypothesis proposer
1146
+ # --------------------------------------------------------
1147
+
1148
+ print("\n[1] HYPOTHESIS PROPOSER")
1149
+
1150
+ proposal = proposer(
1151
+ state,
1152
+ table,
1153
+ )
1154
+
1155
+ print(proposal)
1156
+
1157
+
1158
+ # --------------------------------------------------------
1159
+ # 2. Critic
1160
+ # --------------------------------------------------------
1161
+
1162
+ print("\n[2] SCIENTIFIC CRITIC")
1163
+
1164
+ critique = critic(
1165
+ state,
1166
+ proposal,
1167
+ scores,
1168
+ )
1169
+
1170
+ print(critique)
1171
+
1172
+
1173
+ # --------------------------------------------------------
1174
+ # 3. Falsification-test designer
1175
+ # --------------------------------------------------------
1176
+
1177
+ print("\n[3] TEST DESIGNER")
1178
+
1179
+ delta_T = select_next_test(
1180
+ state,
1181
+ proposal,
1182
+ critique,
1183
+ scores,
1184
+ )
1185
+
1186
+ print(
1187
+ f"Selected ΔT = {delta_T}"
1188
+ )
1189
+
1190
+
1191
+ # --------------------------------------------------------
1192
+ # 4. Synthetic boiling physical world
1193
+ # --------------------------------------------------------
1194
+
1195
+ print(
1196
+ "\n[4] EXECUTABLE BOILING WORLD"
1197
+ )
1198
+
1199
+ observation = query_hidden_world(
1200
+ delta_T
1201
+ )
1202
+
1203
+ print(
1204
+ "Observed normalized mass transfer = "
1205
+ f"{observation['observed_mass_transfer']:.6f}"
1206
+ )
1207
+
1208
+
1209
+ # --------------------------------------------------------
1210
+ # 5. Evidence update
1211
+ # --------------------------------------------------------
1212
+
1213
+ print("\n[5] EVIDENCE UPDATE")
1214
+
1215
+ state["evidence"].append(
1216
+ observation
1217
+ )
1218
+
1219
+ state["posterior"] = (
1220
+ update_posterior(
1221
+ state,
1222
+ observation,
1223
+ )
1224
+ )
1225
+
1226
+ for model, probability in sorted(
1227
+ state["posterior"].items(),
1228
+ key=lambda x: x[1],
1229
+ reverse=True,
1230
+ ):
1231
+
1232
+ print(
1233
+ f"{model}: "
1234
+ f"P = {probability:.4f}"
1235
+ )
1236
+
1237
+
1238
+ best_posterior_model = max(
1239
+ state["posterior"],
1240
+ key=state["posterior"].get,
1241
+ )
1242
+
1243
+ best_probability = (
1244
+ state["posterior"][
1245
+ best_posterior_model
1246
+ ]
1247
+ )
1248
+
1249
+
1250
+ # --------------------------------------------------------
1251
+ # 6. Model adequacy
1252
+ # --------------------------------------------------------
1253
+
1254
+ best_model, mismatch = (
1255
+ best_model_by_mismatch(
1256
+ state
1257
+ )
1258
+ )
1259
+
1260
+ if mismatch is not None:
1261
+
1262
+ print(
1263
+ "\nBest closure by adequacy: "
1264
+ f"{best_model}"
1265
+ )
1266
+
1267
+ print(
1268
+ "Normalized mismatch = "
1269
+ f"{mismatch:.3f} sigma"
1270
+ )
1271
+
1272
+
1273
+ round_record = {
1274
+
1275
+ "round":
1276
+ round_id,
1277
+
1278
+ "proposal":
1279
+ proposal,
1280
+
1281
+ "critique":
1282
+ critique,
1283
+
1284
+ "test_scores":
1285
+ scores,
1286
+
1287
+ "selected_delta_T":
1288
+ delta_T,
1289
+
1290
+ "observation":
1291
+ observation,
1292
+
1293
+ "posterior":
1294
+ state["posterior"].copy(),
1295
+
1296
+ "best_model_by_adequacy":
1297
+ best_model,
1298
+
1299
+ "mismatch_score":
1300
+ mismatch,
1301
+ }
1302
+
1303
+ trace.append(
1304
+ round_record
1305
+ )
1306
+
1307
+ save_state()
1308
+
1309
+
1310
+ print(
1311
+ "\nCurrent Bayesian best candidate:",
1312
+ best_posterior_model,
1313
+ f"(P={best_probability:.4f})",
1314
+ )
1315
+
1316
+
1317
+ # --------------------------------------------------------
1318
+ # 7. Open-set model-class failure
1319
+ # --------------------------------------------------------
1320
+
1321
+ if (
1322
+ mismatch is not None
1323
+ and mismatch >= MODEL_MISMATCH_THRESHOLD
1324
+ and revision_count < MAX_REVISIONS
1325
+ ):
1326
+
1327
+ print("\n" + "=" * 72)
1328
+ print("MODEL-CLASS FAILURE DETECTED")
1329
+ print("=" * 72)
1330
+
1331
+ original_model_class_failed = True
1332
+
1333
+ residuals = residual_table(
1334
+ state,
1335
+ best_model,
1336
+ )
1337
+
1338
+ (
1339
+ correction_type,
1340
+ coefficient,
1341
+ revision_candidates,
1342
+ ) = deterministic_revision_search(
1343
+ state,
1344
+ best_model,
1345
+ )
1346
+
1347
+ print(
1348
+ "\nDeterministic residual search:"
1349
+ )
1350
+
1351
+ for name, result in (
1352
+ revision_candidates.items()
1353
+ ):
1354
+
1355
+ print(
1356
+ f"{name:10s} "
1357
+ f"c={result['coefficient']:.6g} "
1358
+ f"RMSE={result['rmse']:.6g}"
1359
+ )
1360
+
1361
+
1362
+ print(
1363
+ "\n[6] THEORY REVISION AGENT"
1364
+ )
1365
+
1366
+ revision_text = (
1367
+ theory_revision_agent(
1368
+ state,
1369
+ best_model,
1370
+ mismatch,
1371
+ residuals,
1372
+ revision_candidates,
1373
+ )
1374
+ )
1375
+
1376
+ print(revision_text)
1377
+
1378
+
1379
+ # ----------------------------------------------------
1380
+ # 8. Compile revision into executable closure
1381
+ # ----------------------------------------------------
1382
+
1383
+ print(
1384
+ "\n[7] EXECUTABLE MODEL REVISION"
1385
+ )
1386
+
1387
+ new_model = register_revised_model(
1388
+ best_model,
1389
+ correction_type,
1390
+ coefficient,
1391
+ )
1392
+
1393
+ revision_count += 1
1394
+ revision_generated = True
1395
+
1396
+ revision_record = {
1397
+
1398
+ "parent_model":
1399
+ best_model,
1400
+
1401
+ "new_model":
1402
+ new_model,
1403
+
1404
+ "correction_type":
1405
+ correction_type,
1406
+
1407
+ "coefficient":
1408
+ coefficient,
1409
+
1410
+ "equation":
1411
+ model_to_string(
1412
+ new_model
1413
+ ),
1414
+
1415
+ "agent_interpretation":
1416
+ revision_text,
1417
+ }
1418
+
1419
+ state[
1420
+ "revision_history"
1421
+ ].append(
1422
+ revision_record
1423
+ )
1424
+
1425
+ trace.append({
1426
+ "event":
1427
+ "executable_model_revision",
1428
+
1429
+ **revision_record,
1430
+ })
1431
+
1432
+
1433
+ print(
1434
+ f"Created {new_model}"
1435
+ )
1436
+
1437
+ print(
1438
+ "Executable closure:"
1439
+ )
1440
+
1441
+ print(
1442
+ f"{new_model}: "
1443
+ f"{model_to_string(new_model)}"
1444
+ )
1445
+
1446
+
1447
+ # ----------------------------------------------------
1448
+ # Re-evaluate all accumulated evidence with M3
1449
+ # ----------------------------------------------------
1450
+
1451
+ recompute_posterior_from_all_evidence()
1452
+
1453
+ save_state()
1454
+
1455
+
1456
+ print(
1457
+ "\nRe-entering revised closure "
1458
+ "into falsification loop."
1459
+ )
1460
+
1461
+ continue
1462
+
1463
+
1464
+ # --------------------------------------------------------
1465
+ # Stop only when revised model has survived testing
1466
+ # --------------------------------------------------------
1467
+
1468
+ if (
1469
+ revision_generated
1470
+ and mismatch is not None
1471
+ and mismatch < MODEL_MISMATCH_THRESHOLD
1472
+ and round_id >= 4
1473
+ ):
1474
+
1475
+ print("\n" + "=" * 72)
1476
+ print("REVISED CLOSURE SURVIVES FALSIFICATION")
1477
+ print("=" * 72)
1478
+
1479
+ print(
1480
+ f"{best_model}: "
1481
+ f"{model_to_string(best_model)}"
1482
+ )
1483
+
1484
+ break
1485
+
1486
+
1487
+ # ============================================================
1488
+ # Final assessment
1489
+ # ============================================================
1490
+
1491
+ print("\n" + "=" * 72)
1492
+ print("FINAL EVALUATION")
1493
+ print("=" * 72)
1494
+
1495
+ assessment = final_evaluator(
1496
+ state
1497
+ )
1498
+
1499
+ print(
1500
+ assessment
1501
+ )
1502
+
1503
+
1504
+ with open(
1505
+ OUTPUT_DIR / "final_assessment.txt",
1506
+ "w",
1507
+ ) as f:
1508
+
1509
+ f.write(
1510
+ assessment
1511
+ )
1512
+
1513
+
1514
+ with open(
1515
+ OUTPUT_DIR / "discovered_models.txt",
1516
+ "w",
1517
+ ) as f:
1518
+
1519
+ for model_name in MODEL_REGISTRY:
1520
+
1521
+ f.write(
1522
+ f"{model_name}: "
1523
+ f"{model_to_string(model_name)}\n"
1524
+ )
1525
+
1526
+
1527
+ print("\n" + "=" * 72)
1528
+
1529
+ if revision_generated:
1530
+
1531
+ print(
1532
+ "SELF-REVISING BOILING DISCOVERY LOOP COMPLETE"
1533
+ )
1534
+
1535
+ else:
1536
+
1537
+ print(
1538
+ "BOILING DISCOVERY LOOP COMPLETE"
1539
+ )
1540
+
1541
+ print("=" * 72)