eyeling 1.16.2 → 1.16.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/HANDBOOK.md +4 -0
  2. package/README.md +0 -1
  3. package/examples/ershov-mixed-computation.n3 +106 -0
  4. package/examples/output/ershov-mixed-computation.n3 +15 -0
  5. package/eyeling.js +510 -263
  6. package/lib/cli.js +22 -12
  7. package/lib/engine.js +488 -251
  8. package/package.json +2 -3
  9. package/arctifacts/README.md +0 -59
  10. package/arctifacts/ackermann.html +0 -678
  11. package/arctifacts/auroracare.html +0 -1297
  12. package/arctifacts/bike-trip.html +0 -752
  13. package/arctifacts/binomial-theorem.html +0 -631
  14. package/arctifacts/bmi.html +0 -511
  15. package/arctifacts/building-performance.html +0 -750
  16. package/arctifacts/clinical-care.html +0 -726
  17. package/arctifacts/collatz.html +0 -403
  18. package/arctifacts/complex.html +0 -321
  19. package/arctifacts/control-system.html +0 -482
  20. package/arctifacts/delfour.html +0 -849
  21. package/arctifacts/earthquake-epicenter.html +0 -982
  22. package/arctifacts/eco-route.html +0 -662
  23. package/arctifacts/euclid-infinitude.html +0 -564
  24. package/arctifacts/euler-identity.html +0 -667
  25. package/arctifacts/exoplanet-transit.html +0 -1000
  26. package/arctifacts/faltings-theorem.html +0 -1046
  27. package/arctifacts/fibonacci.html +0 -299
  28. package/arctifacts/fundamental-theorem-arithmetic.html +0 -398
  29. package/arctifacts/godel-numbering.html +0 -743
  30. package/arctifacts/gps-bike.html +0 -759
  31. package/arctifacts/gps-clinical-bench.html +0 -792
  32. package/arctifacts/graph-french.html +0 -449
  33. package/arctifacts/grass-molecular.html +0 -592
  34. package/arctifacts/group-theory.html +0 -740
  35. package/arctifacts/health-info.html +0 -833
  36. package/arctifacts/kaprekar-constant.html +0 -576
  37. package/arctifacts/lee.html +0 -805
  38. package/arctifacts/linked-lists.html +0 -502
  39. package/arctifacts/lldm.html +0 -612
  40. package/arctifacts/matrix-multiplication.html +0 -502
  41. package/arctifacts/matrix.html +0 -651
  42. package/arctifacts/newton-raphson.html +0 -944
  43. package/arctifacts/peano-factorial.html +0 -456
  44. package/arctifacts/pi.html +0 -363
  45. package/arctifacts/polynomial.html +0 -646
  46. package/arctifacts/prime.html +0 -366
  47. package/arctifacts/pythagorean-theorem.html +0 -468
  48. package/arctifacts/rest-path.html +0 -469
  49. package/arctifacts/roots-of-unity.html +0 -363
  50. package/arctifacts/turing.html +0 -409
  51. package/arctifacts/wind-turbines.html +0 -726
@@ -1,1000 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width,initial-scale=1" />
6
- <title>Exoplanet Transit — ARC Science Explorer</title>
7
- <meta
8
- name="description"
9
- content="A mobile-first ARC-style science page that infers exoplanet size and orbit from a transit light curve, with Answer, Reason, and Check sections." />
10
- <style>
11
- :root {
12
- --bg: #f7fafc;
13
- --panel: #ffffff;
14
- --soft: #f3f8fc;
15
- --text: #102133;
16
- --muted: #5b7084;
17
- --border: #d8e5ef;
18
- --accent: #0ea5e9;
19
- --ok: #0f766e;
20
- --okbg: #ecfdf5;
21
- --warn: #b45309;
22
- --warnbg: #fff7ed;
23
- --bad: #b91c1c;
24
- --badbg: #fef2f2;
25
- --shadow: 0 14px 36px rgba(15, 23, 42, 0.08);
26
- --r: 22px;
27
- }
28
- * {
29
- box-sizing: border-box;
30
- }
31
- html,
32
- body {
33
- height: 100%;
34
- }
35
- body {
36
- margin: 0;
37
- font:
38
- 16px/1.55 Inter,
39
- ui-sans-serif,
40
- system-ui,
41
- -apple-system,
42
- Segoe UI,
43
- Roboto,
44
- sans-serif;
45
- color: var(--text);
46
- background:
47
- radial-gradient(900px 420px at 100% -10%, rgba(14, 165, 233, 0.09), transparent 60%),
48
- radial-gradient(850px 420px at -10% 0%, rgba(37, 99, 235, 0.06), transparent 60%), var(--bg);
49
- }
50
- .wrap {
51
- max-width: 920px;
52
- margin: 0 auto;
53
- padding: 16px 14px 80px;
54
- }
55
- .card {
56
- background: var(--panel);
57
- border: 1px solid var(--border);
58
- border-radius: var(--r);
59
- box-shadow: var(--shadow);
60
- }
61
- header.hero {
62
- padding: 24px 18px;
63
- margin-top: 8px;
64
- }
65
- .kicker {
66
- display: inline-flex;
67
- align-items: center;
68
- gap: 8px;
69
- padding: 7px 11px;
70
- border-radius: 999px;
71
- background: #e0f2fe;
72
- border: 1px solid #bae6fd;
73
- color: #0369a1;
74
- font-size: 12px;
75
- letter-spacing: 0.08em;
76
- text-transform: uppercase;
77
- font-weight: 800;
78
- }
79
- h1 {
80
- margin: 14px 0 10px;
81
- font-size: clamp(30px, 7vw, 50px);
82
- line-height: 1.02;
83
- letter-spacing: -0.04em;
84
- }
85
- h2 {
86
- margin: 0 0 8px;
87
- font-size: clamp(24px, 5.1vw, 34px);
88
- line-height: 1.08;
89
- letter-spacing: -0.03em;
90
- }
91
- h3 {
92
- margin: 0 0 8px;
93
- font-size: 20px;
94
- letter-spacing: -0.02em;
95
- }
96
- p {
97
- margin: 0 0 12px;
98
- }
99
- .lead {
100
- font-size: 18px;
101
- color: var(--muted);
102
- max-width: 40ch;
103
- }
104
- .formula {
105
- margin-top: 16px;
106
- padding: 14px 16px;
107
- border-radius: 18px;
108
- border: 1px solid #dbeafe;
109
- background: #f8fbff;
110
- color: #0c4a6e;
111
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
112
- overflow: auto;
113
- }
114
- .section {
115
- margin-top: 16px;
116
- padding: 18px;
117
- }
118
- .section-head {
119
- display: flex;
120
- align-items: center;
121
- gap: 10px;
122
- margin-bottom: 12px;
123
- }
124
- .badge {
125
- width: 34px;
126
- height: 34px;
127
- border-radius: 50%;
128
- display: inline-flex;
129
- align-items: center;
130
- justify-content: center;
131
- background: #e0f2fe;
132
- border: 1px solid #bae6fd;
133
- color: #0369a1;
134
- font-size: 14px;
135
- font-weight: 900;
136
- flex: 0 0 auto;
137
- }
138
- .sub {
139
- font-size: 14px;
140
- color: var(--muted);
141
- }
142
- .grid {
143
- display: grid;
144
- gap: 12px;
145
- }
146
- .controls {
147
- display: grid;
148
- gap: 10px;
149
- }
150
- .control {
151
- background: var(--soft);
152
- border: 1px solid var(--border);
153
- border-radius: 18px;
154
- padding: 12px 14px;
155
- }
156
- .control label {
157
- display: block;
158
- font-size: 12px;
159
- text-transform: uppercase;
160
- letter-spacing: 0.07em;
161
- color: var(--muted);
162
- font-weight: 800;
163
- margin-bottom: 8px;
164
- }
165
- .vnum {
166
- font-size: 22px;
167
- font-weight: 900;
168
- margin-bottom: 4px;
169
- }
170
- input[type='range'] {
171
- width: 100%;
172
- }
173
- select {
174
- width: 100%;
175
- border: 1px solid var(--border);
176
- border-radius: 12px;
177
- background: white;
178
- padding: 10px 12px;
179
- font: inherit;
180
- }
181
- .actions {
182
- display: flex;
183
- gap: 10px;
184
- flex-wrap: wrap;
185
- margin-top: 16px;
186
- }
187
- button {
188
- appearance: none;
189
- border: none;
190
- cursor: pointer;
191
- border-radius: 14px;
192
- padding: 12px 14px;
193
- font: inherit;
194
- font-weight: 800;
195
- background: var(--accent);
196
- color: white;
197
- box-shadow: 0 10px 20px rgba(14, 165, 233, 0.18);
198
- }
199
- button.secondary {
200
- background: white;
201
- color: var(--accent);
202
- border: 1px solid #cceaf9;
203
- box-shadow: none;
204
- }
205
- .metrics {
206
- display: grid;
207
- grid-template-columns: repeat(2, minmax(0, 1fr));
208
- gap: 10px;
209
- margin-top: 12px;
210
- }
211
- .metric {
212
- background: var(--soft);
213
- border: 1px solid var(--border);
214
- border-radius: 18px;
215
- padding: 12px 14px;
216
- min-height: 92px;
217
- }
218
- .metric .k {
219
- font-size: 12px;
220
- color: var(--muted);
221
- text-transform: uppercase;
222
- letter-spacing: 0.07em;
223
- }
224
- .metric .v {
225
- font-size: 22px;
226
- font-weight: 900;
227
- margin-top: 4px;
228
- word-break: break-word;
229
- }
230
- .mini {
231
- font-size: 13px;
232
- color: var(--muted);
233
- }
234
- .pill-row {
235
- display: flex;
236
- flex-wrap: wrap;
237
- gap: 8px;
238
- margin-top: 10px;
239
- }
240
- .pill {
241
- background: #eff6ff;
242
- border: 1px solid #dbeafe;
243
- color: #1d4ed8;
244
- padding: 7px 10px;
245
- border-radius: 999px;
246
- font-size: 13px;
247
- }
248
- .block {
249
- background: #fcfdff;
250
- border: 1px solid var(--border);
251
- border-radius: 18px;
252
- padding: 14px;
253
- }
254
- .notice {
255
- background: var(--warnbg);
256
- border: 1px solid #fed7aa;
257
- color: var(--warn);
258
- border-radius: 18px;
259
- padding: 12px 14px;
260
- margin-top: 14px;
261
- }
262
- .code {
263
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
264
- word-break: break-word;
265
- }
266
- canvas {
267
- display: block;
268
- width: 100%;
269
- border: 1px solid var(--border);
270
- border-radius: 18px;
271
- background: linear-gradient(180deg, #ffffff, #f8fbff);
272
- }
273
- .legend {
274
- display: flex;
275
- gap: 12px;
276
- flex-wrap: wrap;
277
- margin-top: 8px;
278
- color: var(--muted);
279
- font-size: 13px;
280
- }
281
- .legend span::before {
282
- content: '';
283
- display: inline-block;
284
- width: 10px;
285
- height: 10px;
286
- border-radius: 50%;
287
- margin-right: 6px;
288
- vertical-align: middle;
289
- }
290
- .legend .obs::before {
291
- background: #0ea5e9;
292
- }
293
- .legend .fit::before {
294
- background: #2563eb;
295
- }
296
- .legend .orbit::before {
297
- background: #f59e0b;
298
- }
299
- .status {
300
- display: inline-flex;
301
- align-items: center;
302
- gap: 8px;
303
- padding: 6px 10px;
304
- border-radius: 999px;
305
- font-size: 12px;
306
- font-weight: 900;
307
- }
308
- .pass {
309
- background: var(--okbg);
310
- color: var(--ok);
311
- border: 1px solid #bbf7d0;
312
- }
313
- .fail {
314
- background: var(--badbg);
315
- color: var(--bad);
316
- border: 1px solid #fecaca;
317
- }
318
- .table-wrap {
319
- overflow: auto;
320
- margin-top: 8px;
321
- }
322
- table {
323
- width: 100%;
324
- border-collapse: collapse;
325
- }
326
- th,
327
- td {
328
- text-align: left;
329
- vertical-align: top;
330
- padding: 9px 8px;
331
- border-bottom: 1px dashed var(--border);
332
- }
333
- th {
334
- font-size: 12px;
335
- text-transform: uppercase;
336
- letter-spacing: 0.07em;
337
- color: var(--muted);
338
- }
339
- .refs {
340
- display: flex;
341
- gap: 10px;
342
- flex-wrap: wrap;
343
- margin-top: 10px;
344
- }
345
- .refs a {
346
- text-decoration: none;
347
- color: #0369a1;
348
- background: #f8fbff;
349
- border: 1px solid #dbeafe;
350
- padding: 8px 10px;
351
- border-radius: 999px;
352
- font-size: 14px;
353
- }
354
- @media (min-width: 760px) {
355
- .controls {
356
- grid-template-columns: repeat(2, minmax(0, 1fr));
357
- }
358
- .metrics {
359
- grid-template-columns: repeat(4, minmax(0, 1fr));
360
- }
361
- }
362
- </style>
363
- </head>
364
- <body>
365
- <div class="wrap">
366
- <header class="card hero">
367
- <span class="kicker">Science · answer • reason • check</span>
368
- <h1>Exoplanet Transit</h1>
369
- <p class="lead">
370
- Given a star and a transit signal, estimate the planet's size, orbit, and temperature, then verify the result
371
- by regenerating the observables in the browser.
372
- </p>
373
- <div class="formula">
374
- Key ideas:<br />transit depth ≈ (planet radius / star radius)²<br />orbital size from period via Kepler's
375
- third law<br />equilibrium temperature from stellar heating and inverse-square dilution
376
- </div>
377
- <div class="actions">
378
- <button onclick="document.getElementById('answer').scrollIntoView({ behavior: 'smooth' })">
379
- Start exploring</button
380
- ><button class="secondary" onclick="runAll()">Recompute</button>
381
- </div>
382
- </header>
383
-
384
- <section class="card section" id="answer">
385
- <div class="section-head">
386
- <span class="badge">1</span>
387
- <div>
388
- <h2>Answer</h2>
389
- <div class="sub">The inferred planet for the current transit setup</div>
390
- </div>
391
- </div>
392
-
393
- <div class="controls">
394
- <div class="control">
395
- <label for="preset">Preset scenario</label
396
- ><select id="preset">
397
- <option value="earth">Earth around Sun</option>
398
- <option value="hotjupiter">Hot Jupiter</option>
399
- <option value="subneptune">Warm sub-Neptune</option>
400
- <option value="cooldwarf">Planet around cool dwarf</option>
401
- <option value="custom">Custom values</option>
402
- </select>
403
- </div>
404
- <div class="control">
405
- <label for="noiseToggle">Synthetic data</label
406
- ><select id="noiseToggle">
407
- <option value="off">Clean light curve</option>
408
- <option value="on">Add mild instrument noise</option>
409
- </select>
410
- </div>
411
- <div class="control">
412
- <label for="starRadius">Star radius (R☉)</label>
413
- <div class="vnum" id="starRadiusValue">1.00</div>
414
- <input id="starRadius" type="range" min="0.10" max="2.00" step="0.01" value="1.00" />
415
- </div>
416
- <div class="control">
417
- <label for="starMass">Star mass (M☉)</label>
418
- <div class="vnum" id="starMassValue">1.00</div>
419
- <input id="starMass" type="range" min="0.10" max="2.00" step="0.01" value="1.00" />
420
- </div>
421
- <div class="control">
422
- <label for="starTemp">Star temperature (K)</label>
423
- <div class="vnum" id="starTempValue">5772</div>
424
- <input id="starTemp" type="range" min="2500" max="8000" step="10" value="5772" />
425
- </div>
426
- <div class="control">
427
- <label for="depthPpm">Transit depth (ppm)</label>
428
- <div class="vnum" id="depthPpmValue">84</div>
429
- <input id="depthPpm" type="range" min="30" max="30000" step="1" value="84" />
430
- </div>
431
- <div class="control">
432
- <label for="periodDays">Orbital period (days)</label>
433
- <div class="vnum" id="periodDaysValue">365.25</div>
434
- <input id="periodDays" type="range" min="0.5" max="400" step="0.05" value="365.25" />
435
- </div>
436
- <div class="control">
437
- <label for="albedo">Bond albedo</label>
438
- <div class="vnum" id="albedoValue">0.30</div>
439
- <input id="albedo" type="range" min="0.00" max="0.80" step="0.01" value="0.30" />
440
- </div>
441
- </div>
442
-
443
- <div class="metrics">
444
- <div class="metric">
445
- <div class="k">Planet radius</div>
446
- <div class="v" id="radiusEarth">—</div>
447
- <div class="mini" id="radiusJupiter">—</div>
448
- </div>
449
- <div class="metric">
450
- <div class="k">Orbit size</div>
451
- <div class="v" id="semiMajor">—</div>
452
- <div class="mini" id="semiMajorRs">—</div>
453
- </div>
454
- <div class="metric">
455
- <div class="k">Transit duration</div>
456
- <div class="v" id="durationHours">—</div>
457
- <div class="mini">central-transit approximation</div>
458
- </div>
459
- <div class="metric">
460
- <div class="k">Equilibrium temperature</div>
461
- <div class="v" id="tempEq">—</div>
462
- <div class="mini" id="fluxRel">—</div>
463
- </div>
464
- </div>
465
-
466
- <div class="pill-row">
467
- <span class="pill" id="classPill">planet class —</span><span class="pill" id="orbitPill">orbit class —</span
468
- ><span class="pill" id="heatPill">heating level —</span>
469
- </div>
470
- <div class="notice" id="answerNotice">
471
- The numbers above come directly from the slider inputs using the formulas in the Reason section.
472
- </div>
473
- </section>
474
-
475
- <section class="card section" id="reason">
476
- <div class="section-head">
477
- <span class="badge">2</span>
478
- <div>
479
- <h2>Reason</h2>
480
- <div class="sub">How the page turns a transit signal into physical estimates</div>
481
- </div>
482
- </div>
483
-
484
- <div class="grid">
485
- <div class="block">
486
- <h3>Depth → radius</h3>
487
- <p>
488
- If the planet blocks a fraction of the star's light, the simplest estimate treats the transit depth as the
489
- ratio of the blocked disk area to the stellar disk area.
490
- </p>
491
- <div class="formula">
492
- δ ≈ (R<sub>p</sub> / R<sub>*</sub>)² &nbsp;&nbsp;→&nbsp;&nbsp; R<sub>p</sub> ≈ R<sub>*</sub> √δ
493
- </div>
494
- <div class="mini" id="reasonDepth">—</div>
495
- </div>
496
- <div class="block">
497
- <h3>Period → orbit</h3>
498
- <p>
499
- For a planet whose mass is tiny compared with the star's, Kepler's third law links orbital period to
500
- semi-major axis.
501
- </p>
502
- <div class="formula">
503
- a³ ≈ M<sub>*</sub> P² &nbsp;&nbsp; (a in AU, P in years, M<sub>*</sub> in solar masses)
504
- </div>
505
- <div class="mini" id="reasonKepler">—</div>
506
- </div>
507
- <div class="block">
508
- <h3>Stellar heating → equilibrium temperature</h3>
509
- <p>
510
- A simple radiative-balance estimate combines the inverse-square falloff of stellar energy with blackbody
511
- re-radiation.
512
- </p>
513
- <div class="formula">T<sub>eq</sub> ≈ T<sub>*</sub> (1−A)<sup>1/4</sup> √(R<sub>*</sub> / 2a)</div>
514
- <div class="mini" id="reasonTemp">—</div>
515
- </div>
516
- <div class="block">
517
- <h3>Synthetic light curve</h3>
518
- <p class="mini">
519
- The page generates a stylized transit using the inferred depth and central-transit duration, then
520
- optionally adds mild noise.
521
- </p>
522
- <canvas id="lightCurveCanvas"></canvas>
523
- <div class="legend">
524
- <span class="obs">synthetic observations</span><span class="fit">model transit</span>
525
- </div>
526
- </div>
527
- <div class="block">
528
- <h3>Orbit sketch</h3>
529
- <p class="mini">
530
- The orbit panel is not to scale between different presets, but it uses the inferred star and orbit sizes
531
- consistently within the current scenario.
532
- </p>
533
- <canvas id="orbitCanvas"></canvas>
534
- <div class="legend"><span class="orbit">planet position and orbit</span></div>
535
- </div>
536
- </div>
537
- </section>
538
-
539
- <section class="card section" id="check">
540
- <div class="section-head">
541
- <span class="badge">3</span>
542
- <div>
543
- <h2>Check</h2>
544
- <div class="sub">Independent consistency tests designed to fail loudly when assumptions break</div>
545
- </div>
546
- </div>
547
-
548
- <div class="metrics">
549
- <div class="metric">
550
- <div class="k">Depth identity</div>
551
- <div class="v"><span id="checkDepthBadge" class="status">—</span></div>
552
- <div class="mini" id="checkDepthText">—</div>
553
- </div>
554
- <div class="metric">
555
- <div class="k">Kepler identity</div>
556
- <div class="v"><span id="checkKeplerBadge" class="status">—</span></div>
557
- <div class="mini" id="checkKeplerText">—</div>
558
- </div>
559
- <div class="metric">
560
- <div class="k">Transit geometry</div>
561
- <div class="v"><span id="checkGeomBadge" class="status">—</span></div>
562
- <div class="mini" id="checkGeomText">—</div>
563
- </div>
564
- <div class="metric">
565
- <div class="k">Recovered from synthetic data</div>
566
- <div class="v"><span id="checkRecoverBadge" class="status">—</span></div>
567
- <div class="mini" id="checkRecoverText">—</div>
568
- </div>
569
- </div>
570
-
571
- <div class="block" style="margin-top: 12px">
572
- <h3>Audit table</h3>
573
- <div class="table-wrap">
574
- <table>
575
- <thead>
576
- <tr>
577
- <th>Quantity</th>
578
- <th>Input / estimate</th>
579
- <th>Independent recomputation</th>
580
- </tr>
581
- </thead>
582
- <tbody id="auditTable"></tbody>
583
- </table>
584
- </div>
585
- </div>
586
- <div class="notice">
587
- The check layer is intentionally independent in style: it recomputes identities, tests geometric sanity, and
588
- re-measures the synthetic light curve rather than simply trusting the display values.
589
- </div>
590
- <div class="refs">
591
- <a href="https://science.nasa.gov/citizen-science/exoplanet-watch/exoplanet-watch-glossary/"
592
- >NASA Exoplanet Watch glossary</a
593
- ><a href="https://www.jpl.nasa.gov/edu/resources/lesson-plan/exploring-exoplanets-with-kepler/"
594
- >NASA/JPL Kepler lesson</a
595
- ><a href="https://science.nasa.gov/wp-content/uploads/2023/09/Habitable_Zone.pdf"
596
- >NASA habitable-zone tutorial</a
597
- >
598
- </div>
599
- </section>
600
- </div>
601
-
602
- <script>
603
- 'use strict';
604
- const EARTHS_PER_SUN = 109.076,
605
- JUPITERS_PER_SUN = 9.735,
606
- RSUN_IN_AU = 0.00465047,
607
- TSUN = 5772;
608
- const presets = {
609
- earth: { starRadius: 1, starMass: 1, starTemp: 5772, depthPpm: 84, periodDays: 365.25, albedo: 0.3 },
610
- hotjupiter: { starRadius: 1, starMass: 1, starTemp: 5772, depthPpm: 10000, periodDays: 3.5, albedo: 0.1 },
611
- subneptune: { starRadius: 0.85, starMass: 0.88, starTemp: 5400, depthPpm: 1600, periodDays: 12.0, albedo: 0.2 },
612
- cooldwarf: { starRadius: 0.12, starMass: 0.09, starTemp: 2550, depthPpm: 6500, periodDays: 6.1, albedo: 0.2 },
613
- };
614
- const ids = ['starRadius', 'starMass', 'starTemp', 'depthPpm', 'periodDays', 'albedo'];
615
- const valueEls = Object.fromEntries(ids.map((id) => [id, document.getElementById(id + 'Value')]));
616
- const el = (id) => document.getElementById(id);
617
- const starRadius = el('starRadius'),
618
- starMass = el('starMass'),
619
- starTemp = el('starTemp'),
620
- depthPpm = el('depthPpm'),
621
- periodDays = el('periodDays'),
622
- albedo = el('albedo'),
623
- noiseToggle = el('noiseToggle'),
624
- preset = el('preset');
625
- const radiusEarth = el('radiusEarth'),
626
- radiusJupiter = el('radiusJupiter'),
627
- semiMajor = el('semiMajor'),
628
- semiMajorRs = el('semiMajorRs'),
629
- durationHours = el('durationHours'),
630
- tempEq = el('tempEq'),
631
- fluxRel = el('fluxRel'),
632
- classPill = el('classPill'),
633
- orbitPill = el('orbitPill'),
634
- heatPill = el('heatPill'),
635
- answerNotice = el('answerNotice');
636
- const reasonDepth = el('reasonDepth'),
637
- reasonKepler = el('reasonKepler'),
638
- reasonTemp = el('reasonTemp');
639
- const checkDepthText = el('checkDepthText'),
640
- checkKeplerText = el('checkKeplerText'),
641
- checkGeomText = el('checkGeomText'),
642
- checkRecoverText = el('checkRecoverText'),
643
- auditTable = el('auditTable');
644
- const fmt = (x, d = 2) => Number(x).toFixed(d);
645
- const clamp = (x, a, b) => Math.max(a, Math.min(b, x));
646
- function setStatus(id, ok) {
647
- const el = document.getElementById(id);
648
- el.textContent = ok ? 'PASS' : 'FAIL';
649
- el.className = 'status ' + (ok ? 'pass' : 'fail');
650
- }
651
- function getInputs() {
652
- return {
653
- starRadius: +starRadius.value,
654
- starMass: +starMass.value,
655
- starTemp: +starTemp.value,
656
- depthPpm: +depthPpm.value,
657
- periodDays: +periodDays.value,
658
- albedo: +albedo.value,
659
- noise: noiseToggle.value === 'on',
660
- };
661
- }
662
- function applyPreset(name) {
663
- const p = presets[name];
664
- if (!p) return;
665
- ids.forEach((id) => (document.getElementById(id).value = p[id]));
666
- syncLabels();
667
- }
668
- function syncLabels() {
669
- valueEls.starRadius.textContent = fmt(starRadius.value, 2);
670
- valueEls.starMass.textContent = fmt(starMass.value, 2);
671
- valueEls.starTemp.textContent = starTemp.value;
672
- valueEls.depthPpm.textContent = depthPpm.value;
673
- valueEls.periodDays.textContent = fmt(periodDays.value, 2);
674
- valueEls.albedo.textContent = fmt(albedo.value, 2);
675
- }
676
-
677
- function computeModel(inp) {
678
- const depthFrac = inp.depthPpm / 1e6,
679
- rpOverRs = Math.sqrt(depthFrac),
680
- rpRe = inp.starRadius * EARTHS_PER_SUN * rpOverRs,
681
- rpRj = inp.starRadius * JUPITERS_PER_SUN * rpOverRs;
682
- const periodYears = inp.periodDays / 365.25,
683
- aAU = Math.cbrt(inp.starMass * periodYears * periodYears),
684
- aOverRs = aAU / (inp.starRadius * RSUN_IN_AU);
685
- const lumRel = inp.starRadius * inp.starRadius * Math.pow(inp.starTemp / TSUN, 4),
686
- fluxRel = lumRel / (aAU * aAU),
687
- teq = 278.5 * Math.pow(fluxRel, 0.25) * Math.pow(1 - inp.albedo, 0.25);
688
- const rsAU = inp.starRadius * RSUN_IN_AU,
689
- rpAU = rpOverRs * rsAU,
690
- arg = clamp((rsAU + rpAU) / aAU, 0, 0.999999),
691
- durationDays = (inp.periodDays / Math.PI) * Math.asin(arg),
692
- durationHours = durationDays * 24;
693
- return {
694
- depthFrac,
695
- rpOverRs,
696
- rpRe,
697
- rpRj,
698
- periodYears,
699
- aAU,
700
- aOverRs,
701
- lumRel,
702
- fluxRel,
703
- teq,
704
- rsAU,
705
- rpAU,
706
- durationDays,
707
- durationHours,
708
- };
709
- }
710
- const planetClass = (r) =>
711
- r < 0.8
712
- ? 'sub-Earth'
713
- : r < 1.25
714
- ? 'Earth-size'
715
- : r < 2
716
- ? 'super-Earth'
717
- : r < 4
718
- ? 'sub-Neptune'
719
- : r < 7
720
- ? 'Neptune-class'
721
- : 'giant planet';
722
- const orbitClass = (a) =>
723
- a < 0.1
724
- ? 'very close-in orbit'
725
- : a < 0.5
726
- ? 'inner-system orbit'
727
- : a < 2
728
- ? 'temperate-distance scale'
729
- : 'wide orbit';
730
- const heatClass = (t) => (t < 180 ? 'cold' : t < 320 ? 'temperate-ish' : t < 800 ? 'warm' : 'hot');
731
-
732
- function prepareCanvas(id, cssHeight) {
733
- const canvas = document.getElementById(id),
734
- dpr = window.devicePixelRatio || 1,
735
- w = Math.max(320, Math.floor(canvas.clientWidth || canvas.parentElement.clientWidth || 600)),
736
- h = cssHeight;
737
- canvas.width = Math.floor(w * dpr);
738
- canvas.height = Math.floor(h * dpr);
739
- canvas.style.height = h + 'px';
740
- const ctx = canvas.getContext('2d');
741
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
742
- return { canvas, ctx, w, h };
743
- }
744
- function clearCanvas(ctx, w, h) {
745
- const g = ctx.createLinearGradient(0, 0, 0, h);
746
- g.addColorStop(0, '#ffffff');
747
- g.addColorStop(1, '#f8fbff');
748
- ctx.fillStyle = g;
749
- ctx.fillRect(0, 0, w, h);
750
- }
751
- function line(ctx, x1, y1, x2, y2, color, width = 1) {
752
- ctx.beginPath();
753
- ctx.moveTo(x1, y1);
754
- ctx.lineTo(x2, y2);
755
- ctx.strokeStyle = color;
756
- ctx.lineWidth = width;
757
- ctx.stroke();
758
- }
759
- function circle(ctx, x, y, r, stroke, fill, width = 1) {
760
- ctx.beginPath();
761
- ctx.arc(x, y, r, 0, Math.PI * 2);
762
- if (fill) {
763
- ctx.fillStyle = fill;
764
- ctx.fill();
765
- }
766
- if (stroke) {
767
- ctx.strokeStyle = stroke;
768
- ctx.lineWidth = width;
769
- ctx.stroke();
770
- }
771
- }
772
- function text(ctx, s, x, y, color = '#102133', font = '12px system-ui', align = 'left') {
773
- ctx.fillStyle = color;
774
- ctx.font = font;
775
- ctx.textAlign = align;
776
- ctx.fillText(s, x, y);
777
- }
778
-
779
- function syntheticLightCurve(model, noiseOn) {
780
- const tMax = Math.max(model.durationHours * 1.8, 8),
781
- N = 220,
782
- ingress = model.durationHours * Math.min(0.18, Math.max(0.04, model.rpOverRs * 0.8)),
783
- half = model.durationHours / 2,
784
- flat = Math.max(0, half - ingress);
785
- const times = [],
786
- obs = [],
787
- ideal = [];
788
- for (let i = 0; i < N; i++) {
789
- const t = -tMax + (2 * tMax * i) / (N - 1);
790
- let blocked = 0,
791
- at = Math.abs(t);
792
- if (at <= flat) blocked = model.depthFrac;
793
- else if (at <= half) {
794
- const u = (at - flat) / Math.max(1e-9, ingress);
795
- blocked = model.depthFrac * (1 - u);
796
- }
797
- const fIdeal = 1 - blocked,
798
- noise = noiseOn
799
- ? (Math.sin(i * 1.7) + Math.sin(i * 0.43) * 0.7) * Math.max(2e-6, model.depthFrac * 0.02)
800
- : 0;
801
- times.push(t);
802
- ideal.push(fIdeal);
803
- obs.push(fIdeal + noise);
804
- }
805
- return { times, obs, ideal, tMax };
806
- }
807
- function drawLightCurve(model, lc) {
808
- const { ctx, w, h } = prepareCanvas('lightCurveCanvas', 250);
809
- clearCanvas(ctx, w, h);
810
- const pad = { l: 42, r: 16, t: 16, b: 28 },
811
- xmin = -lc.tMax,
812
- xmax = lc.tMax,
813
- minFlux = Math.min(...lc.obs, ...lc.ideal) - model.depthFrac * 0.15,
814
- ymin = Math.min(minFlux, 1 - model.depthFrac * 1.15),
815
- ymax = 1.0005;
816
- const X = (x) => pad.l + ((x - xmin) * (w - pad.l - pad.r)) / (xmax - xmin),
817
- Y = (y) => h - pad.b - ((y - ymin) * (h - pad.t - pad.b)) / (ymax - ymin);
818
- line(ctx, pad.l, h - pad.b, w - pad.r, h - pad.b, '#d7e3ec', 1);
819
- line(ctx, pad.l, pad.t, pad.l, h - pad.b, '#d7e3ec', 1);
820
- text(ctx, 'hours from transit center', w / 2, h - 8, '#5b7084', '12px system-ui', 'center');
821
- text(ctx, 'normalized flux', 10, 16, '#5b7084', '12px system-ui');
822
- [xmin / 2, 0, xmax / 2].forEach((x) => {
823
- line(ctx, X(x), h - pad.b, X(x), h - pad.b + 4, '#9fb3c4', 1);
824
- text(ctx, fmt(x, 1), X(x), h - 10, '#5b7084', '11px system-ui', 'center');
825
- });
826
- [1, 1 - model.depthFrac].forEach((y) => {
827
- line(ctx, pad.l - 4, Y(y), pad.l, Y(y), '#9fb3c4', 1);
828
- text(ctx, y.toFixed(6), pad.l - 8, Y(y) + 4, '#5b7084', '11px system-ui', 'right');
829
- });
830
- ctx.beginPath();
831
- lc.times.forEach((t, i) => (i === 0 ? ctx.moveTo(X(t), Y(lc.ideal[i])) : ctx.lineTo(X(t), Y(lc.ideal[i]))));
832
- ctx.strokeStyle = '#2563eb';
833
- ctx.lineWidth = 2;
834
- ctx.stroke();
835
- ctx.fillStyle = '#0ea5e9';
836
- lc.times.forEach((t, i) => {
837
- if (i % 3 !== 0) return;
838
- circle(ctx, X(t), Y(lc.obs[i]), 2.2, null, '#0ea5e9');
839
- });
840
- text(
841
- ctx,
842
- `depth ${Math.round(model.depthFrac * 1e6)} ppm`,
843
- w - pad.r - 2,
844
- pad.t + 10,
845
- '#0c4a6e',
846
- '12px system-ui',
847
- 'right',
848
- );
849
- text(
850
- ctx,
851
- `duration ≈ ${fmt(model.durationHours, 2)} h`,
852
- w - pad.r - 2,
853
- pad.t + 26,
854
- '#0c4a6e',
855
- '12px system-ui',
856
- 'right',
857
- );
858
- }
859
- function drawOrbit(model, inp) {
860
- const { ctx, w, h } = prepareCanvas('orbitCanvas', 250);
861
- clearCanvas(ctx, w, h);
862
- const cx = w / 2,
863
- cy = h / 2 + 6,
864
- orbitR = Math.min(w, h) * 0.34,
865
- starR = Math.max(10, orbitR / Math.max(4, model.aOverRs * 0.35)),
866
- planetR = Math.max(3, starR * model.rpOverRs);
867
- circle(ctx, cx, cy, orbitR, '#d8e5ef', null, 2);
868
- const angle = -Math.PI / 5,
869
- px = cx + orbitR * Math.cos(angle),
870
- py = cy + orbitR * Math.sin(angle);
871
- const grad = ctx.createRadialGradient(cx - starR * 0.2, cy - starR * 0.2, 2, cx, cy, starR);
872
- grad.addColorStop(0, '#fff7c2');
873
- grad.addColorStop(1, '#f59e0b');
874
- circle(ctx, cx, cy, starR, '#fcd34d', grad, 1.5);
875
- circle(ctx, px, py, planetR, '#f59e0b', '#f59e0b', 1);
876
- line(ctx, cx, cy, px, py, 'rgba(245,158,11,.35)', 1.5);
877
- text(ctx, `a ≈ ${fmt(model.aAU, 3)} AU`, 18, 22, '#5b7084', '12px system-ui');
878
- text(ctx, `a/R* ≈ ${fmt(model.aOverRs, 1)}`, 18, 40, '#5b7084', '12px system-ui');
879
- text(ctx, `R* = ${fmt(inp.starRadius, 2)} R☉`, 18, 58, '#5b7084', '12px system-ui');
880
- text(ctx, `Rp = ${fmt(model.rpRe, 2)} R⊕`, 18, 76, '#5b7084', '12px system-ui');
881
- text(ctx, 'not to scale across presets', w - 18, h - 12, '#5b7084', '12px system-ui', 'right');
882
- }
883
- function recoverFromSynthetic(lc) {
884
- const minFlux = Math.min(...lc.obs),
885
- depthRecovered = 1 - minFlux,
886
- threshold = 1 - depthRecovered / 2;
887
- let start = null,
888
- end = null;
889
- for (let i = 1; i < lc.obs.length; i++) {
890
- const prev = lc.obs[i - 1],
891
- cur = lc.obs[i];
892
- if (start === null && prev > threshold && cur <= threshold) start = lc.times[i];
893
- if (start !== null && prev <= threshold && cur > threshold) {
894
- end = lc.times[i];
895
- break;
896
- }
897
- }
898
- return { depthRecovered, durationRecovered: start !== null && end !== null ? end - start : null };
899
- }
900
-
901
- function renderAnswer(inp, model) {
902
- radiusEarth.textContent = `${fmt(model.rpRe, 2)} R⊕`;
903
- radiusJupiter.textContent = `${fmt(model.rpRj, 3)} R♃`;
904
- semiMajor.textContent = `${fmt(model.aAU, 3)} AU`;
905
- semiMajorRs.textContent = `${fmt(model.aOverRs, 1)} stellar radii`;
906
- durationHours.textContent = `${fmt(model.durationHours, 2)} h`;
907
- tempEq.textContent = `${Math.round(model.teq)} K`;
908
- fluxRel.textContent = `${fmt(model.fluxRel, 2)} × Earth flux`;
909
- classPill.textContent = `planet class — ${planetClass(model.rpRe)}`;
910
- orbitPill.textContent = `orbit class — ${orbitClass(model.aAU)}`;
911
- heatPill.textContent = `heating level — ${heatClass(model.teq)}`;
912
- answerNotice.textContent = `At ${Math.round(inp.depthPpm)} ppm, the transit implies a radius ratio of ${fmt(model.rpOverRs, 4)}. With a ${fmt(inp.starRadius, 2)} R☉ star, that gives a planet radius near ${fmt(model.rpRe, 2)} Earth radii. The ${fmt(inp.periodDays, 2)} day period around a ${fmt(inp.starMass, 2)} M☉ star implies an orbit near ${fmt(model.aAU, 3)} AU.`;
913
- }
914
- function renderReason(inp, model) {
915
- reasonDepth.textContent = `Here: √δ = √(${fmt(model.depthFrac, 6)}) = ${fmt(model.rpOverRs, 4)}, so Rp ≈ ${fmt(model.rpRe, 2)} R⊕.`;
916
- reasonKepler.textContent = `Here: a ≈ (${fmt(inp.starMass, 2)} × ${fmt(model.periodYears, 4)}²)^(1/3) = ${fmt(model.aAU, 3)} AU.`;
917
- reasonTemp.textContent = `Here: with star temperature ${Math.round(inp.starTemp)} K and albedo ${fmt(inp.albedo, 2)}, the estimate is about ${Math.round(model.teq)} K.`;
918
- const lc = syntheticLightCurve(model, inp.noise);
919
- drawLightCurve(model, lc);
920
- drawOrbit(model, inp);
921
- return lc;
922
- }
923
- function renderChecks(inp, model, lc) {
924
- const depthRe = model.rpOverRs * model.rpOverRs,
925
- depthOk = Math.abs(depthRe - model.depthFrac) < 1e-12;
926
- setStatus('checkDepthBadge', depthOk);
927
- checkDepthText.textContent = `Computed ${(depthRe * 1e6).toFixed(2)} ppm from the inferred radius ratio.`;
928
- const keplerLeft = model.aAU ** 3,
929
- keplerRight = inp.starMass * model.periodYears ** 2,
930
- keplerOk = Math.abs(keplerLeft - keplerRight) < 1e-10;
931
- setStatus('checkKeplerBadge', keplerOk);
932
- checkKeplerText.textContent = `a³ = ${fmt(keplerLeft, 6)} and M*P² = ${fmt(keplerRight, 6)} in normalized units.`;
933
- const geomOk = model.aAU > model.rsAU + model.rpAU && model.durationDays < inp.periodDays / 2;
934
- setStatus('checkGeomBadge', geomOk);
935
- checkGeomText.textContent = geomOk
936
- ? `Transit chord is physically plausible: a > R* + Rp and duration is much shorter than the orbit.`
937
- : `Current sliders create a geometry that is not physically plausible for a simple central transit.`;
938
- const recovered = recoverFromSynthetic(lc),
939
- recDepthPpm = recovered.depthRecovered * 1e6,
940
- recDuration = recovered.durationRecovered,
941
- recDepthOk = Math.abs(recDepthPpm - inp.depthPpm) <= Math.max(10, inp.depthPpm * 0.08),
942
- recDurOk =
943
- recDuration !== null &&
944
- Math.abs(recDuration - model.durationHours) <= Math.max(0.35, model.durationHours * 0.22),
945
- recoverOk = recDepthOk && recDurOk;
946
- setStatus('checkRecoverBadge', recoverOk);
947
- checkRecoverText.textContent =
948
- recDuration === null
949
- ? `The synthetic curve was too shallow/noisy to re-measure cleanly.`
950
- : `Recovered about ${Math.round(recDepthPpm)} ppm depth and ${fmt(recDuration, 2)} h duration from the synthetic curve.`;
951
- const rows = [
952
- ['Transit depth', `${Math.round(inp.depthPpm)} ppm`, `${(depthRe * 1e6).toFixed(2)} ppm from (Rp/R*)²`],
953
- [
954
- 'Semi-major axis',
955
- `${fmt(model.aAU, 3)} AU`,
956
- `${fmt(Math.cbrt(inp.starMass * model.periodYears * model.periodYears), 3)} AU from Kepler`,
957
- ],
958
- [
959
- 'Equilibrium temperature',
960
- `${Math.round(model.teq)} K`,
961
- `${fmt(278.5 * Math.pow(model.fluxRel, 0.25) * Math.pow(1 - inp.albedo, 0.25), 1)} K from flux balance`,
962
- ],
963
- [
964
- 'Transit duration',
965
- `${fmt(model.durationHours, 2)} h`,
966
- recDuration === null ? 'could not recover' : `${fmt(recDuration, 2)} h measured from synthetic light curve`,
967
- ],
968
- ];
969
- auditTable.innerHTML = '';
970
- rows.forEach(([a, b, c]) => {
971
- const tr = document.createElement('tr');
972
- tr.innerHTML = `<td>${a}</td><td class="code">${b}</td><td class="code">${c}</td>`;
973
- auditTable.appendChild(tr);
974
- });
975
- }
976
- function runAll() {
977
- syncLabels();
978
- const inp = getInputs(),
979
- model = computeModel(inp);
980
- renderAnswer(inp, model);
981
- const lc = renderReason(inp, model);
982
- renderChecks(inp, model, lc);
983
- }
984
- preset.addEventListener('change', (e) => {
985
- if (e.target.value !== 'custom') applyPreset(e.target.value);
986
- runAll();
987
- });
988
- noiseToggle.addEventListener('change', runAll);
989
- ids.forEach((id) =>
990
- document.getElementById(id).addEventListener('input', () => {
991
- preset.value = 'custom';
992
- runAll();
993
- }),
994
- );
995
- window.addEventListener('resize', runAll);
996
- applyPreset('earth');
997
- runAll();
998
- </script>
999
- </body>
1000
- </html>