@descix/frqtl-node 0.0.0-stage → 0.6.0-p0.1
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.
- package/CLAUDE.md +87 -0
- package/README.md +84 -2
- package/dist/dist-manifest.json +218 -0
- package/dist/eyes/index.js +109 -0
- package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
- package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
- package/dist/fat/pkg-node/package.json +12 -0
- package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
- package/dist/frqtl/compat/boot-node.js +34 -0
- package/dist/frqtl/compat/engine.js +127 -0
- package/dist/frqtl/compat/package-root.js +24 -0
- package/dist/frqtl/compat/providers/wasm-engine.js +2501 -0
- package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
- package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
- package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
- package/dist/frqtl/compat/renderer/Filters.js +163 -0
- package/dist/frqtl/compat/renderer/RenderFamily.js +85 -0
- package/dist/frqtl/compat/renderer/RenderMesh.js +100 -0
- package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
- package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
- package/dist/frqtl/compat/renderer/captureStill-common.js +259 -0
- package/dist/frqtl/compat/renderer/display-slot.js +96 -0
- package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
- package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
- package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
- package/dist/frqtl/compat/renderer/providers/index.js +22 -0
- package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
- package/dist/frqtl/compat/renderer/providers/three-provider.js +2474 -0
- package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
- package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
- package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
- package/dist/frqtl/compat/vendor.js +196 -0
- package/dist/frqtl/node-entry.js +17 -0
- package/dist/frqtl/objects/FrqtlApp.js +2922 -0
- package/dist/frqtl/objects/ObjectFactory.js +297 -0
- package/dist/frqtl/objects/RecipeContext.js +322 -0
- package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
- package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
- package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
- package/dist/frqtl/objects/instruments/capture-host.js +453 -0
- package/dist/frqtl/objects/instruments/capture.js +711 -0
- package/dist/frqtl/objects/instruments/hosted-eyes.js +985 -0
- package/dist/frqtl/objects/kinds-entry.js +14 -0
- package/dist/frqtl/objects/kinds.js +190 -0
- package/dist/frqtl/objects/objects/Box.js +273 -0
- package/dist/frqtl/objects/objects/Bullet.js +155 -0
- package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
- package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
- package/dist/frqtl/objects/objects/DirectedTunnel.js +412 -0
- package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
- package/dist/frqtl/objects/objects/LightSource.js +354 -0
- package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
- package/dist/frqtl/objects/objects/Tube.js +471 -0
- package/dist/frqtl/objects/objects/absorbRadiation.js +310 -0
- package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
- package/dist/frqtl/objects/objects/controlAction.js +89 -0
- package/dist/frqtl/objects/objects/controlBind.js +278 -0
- package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
- package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
- package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
- package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
- package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
- package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
- package/dist/frqtl/objects/objects/makeControlsForObject.js +824 -0
- package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
- package/dist/frqtl/objects/objects/slitLinerScatter.js +263 -0
- package/dist/frqtl/objects/objects/sustainedSource.js +299 -0
- package/dist/frqtl/objects/objects/tagger.js +228 -0
- package/dist/frqtl/objects/schema/engine-scopes.js +469 -0
- package/dist/frqtl/objects/schema/index.js +256 -0
- package/dist/frqtl/objects/schema/scopes.js +457 -0
- package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
- package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
- package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
- package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
- package/dist/frqtl/objects/setup/_worldInit.js +162 -0
- package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
- package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
- package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
- package/dist/frqtl/objects/setup/setupConeEmitter3D.js +251 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit.js +327 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
- package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
- package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
- package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
- package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
- package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
- package/dist/frqtl/objects/setup/setupLightSourceLab.js +280 -0
- package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
- package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
- package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
- package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1259 -0
- package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
- package/dist/frqtl/objects/setup/setupTunnelLab.js +242 -0
- package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
- package/dist/frqtl/objects/setup/setupWaveInterference.js +178 -0
- package/dist/frqtl/objects/v1-replacements.js +30 -0
- package/dist/frqtl/package-core.js +23 -0
- package/dist/frqtl/parts/CameraControls.js +643 -0
- package/dist/frqtl/parts/CameraPath.js +575 -0
- package/dist/frqtl/parts/CollisionActions.js +35 -0
- package/dist/frqtl/parts/CollisionTypes.js +31 -0
- package/dist/frqtl/parts/DetectorWall.js +496 -0
- package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
- package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
- package/dist/frqtl/parts/FrqtlVision.js +818 -0
- package/dist/frqtl/parts/GraphingDetectorWall.js +490 -0
- package/dist/frqtl/parts/LargeObject.js +406 -0
- package/dist/frqtl/parts/LightColor.js +255 -0
- package/dist/frqtl/parts/LiveChart.js +285 -0
- package/dist/frqtl/parts/LogicGate.js +720 -0
- package/dist/frqtl/parts/OvenBox.js +612 -0
- package/dist/frqtl/parts/PointSource.js +763 -0
- package/dist/frqtl/parts/Rectangle.js +200 -0
- package/dist/frqtl/parts/SlitLightSource.js +234 -0
- package/dist/frqtl/parts/SlitScreen.js +628 -0
- package/dist/frqtl/parts/SphereEmitter.js +946 -0
- package/dist/frqtl/parts/Trace.js +508 -0
- package/dist/frqtl/parts/Tunnel.js +1057 -0
- package/dist/frqtl/parts/Wall.js +1211 -0
- package/dist/frqtl/parts/index.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall.js +32 -0
- package/dist/frqtl/scenes/atomic-model.js +88 -0
- package/dist/frqtl/scenes/big-bang.js +46 -0
- package/dist/frqtl/scenes/blackbody.js +81 -0
- package/dist/frqtl/scenes/circuit-sat.js +127 -0
- package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
- package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
- package/dist/frqtl/scenes/counting-wall.js +38 -0
- package/dist/frqtl/scenes/double-slit.js +31 -0
- package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
- package/dist/frqtl/scenes/dse-one-slit.js +114 -0
- package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
- package/dist/frqtl/scenes/dse-tunnel-rebuild.js +244 -0
- package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
- package/dist/frqtl/scenes/dse-two-slits.js +132 -0
- package/dist/frqtl/scenes/empty.js +14 -0
- package/dist/frqtl/scenes/laser-buildup.js +49 -0
- package/dist/frqtl/scenes/mirror-3d.js +56 -0
- package/dist/frqtl/scenes/mirror-route.js +140 -0
- package/dist/frqtl/scenes/mirror.js +38 -0
- package/dist/frqtl/scenes/omni-3d.js +27 -0
- package/dist/frqtl/scenes/omni-source.js +22 -0
- package/dist/frqtl/scenes/self-interference.js +39 -0
- package/dist/frqtl/scenes/smoking-gun.js +122 -0
- package/dist/frqtl/scenes/straight-walk.js +22 -0
- package/dist/frqtl/scenes/sun.js +39 -0
- package/dist/frqtl/scenes/two-source.js +37 -0
- package/dist/frqtl/scenes/wave.js +26 -0
- package/dist/shim/bundle/frqnb.js +112 -0
- package/dist/shim/bundle/index.js +121 -0
- package/dist/shim/bundle/indexHtml.js +107 -0
- package/dist/shim/bundle/manifest.js +95 -0
- package/dist/shim/bundle/zip.js +243 -0
- package/dist/shim/bus.js +67 -0
- package/dist/shim/controls/index.js +627 -0
- package/dist/shim/controls/registry.js +274 -0
- package/dist/shim/engine/artifact-shell.js +212 -0
- package/dist/shim/engine/boot-phases.js +44 -0
- package/dist/shim/engine/engine-stack-boot.js +59 -0
- package/dist/shim/engine/router.js +692 -0
- package/dist/shim/experiment-frame/index.js +296 -0
- package/dist/shim/expr/ast-schema.js +369 -0
- package/dist/shim/field-descriptor/index.js +326 -0
- package/dist/shim/frame-data/index.js +224 -0
- package/dist/shim/index.js +67 -0
- package/dist/shim/notebook/canvas-guard.js +82 -0
- package/dist/shim/notebook/catalog.js +35 -0
- package/dist/shim/notebook/format.js +736 -0
- package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
- package/dist/shim/notebook/kernel.js +672 -0
- package/dist/shim/notebook/knob.js +151 -0
- package/dist/shim/notebook/module-resolver.js +260 -0
- package/dist/shim/notebook/notebook-knobs.js +65 -0
- package/dist/shim/notebook/produced-bindings.js +67 -0
- package/dist/shim/notebook/realm-registry.js +121 -0
- package/dist/shim/notebook/realm-transport.js +73 -0
- package/dist/shim/notebook/setup-builder.js +510 -0
- package/dist/shim/notebook/store.js +159 -0
- package/dist/shim/notebook/vendor-importmap.js +134 -0
- package/dist/shim/notebook/view-builder.js +189 -0
- package/dist/shim/notebook/view-ctx.js +100 -0
- package/dist/shim/notebook/view-handle.js +202 -0
- package/dist/shim/notebook/warmup.js +102 -0
- package/dist/shim/package.json +4 -0
- package/dist/shim/page-params/index.js +214 -0
- package/dist/shim/project/identity.js +112 -0
- package/dist/shim/project/index.js +19 -0
- package/dist/shim/project/link.js +402 -0
- package/dist/shim/project/node-specifier-hook.mjs +35 -0
- package/dist/shim/project/register-node.mjs +17 -0
- package/dist/shim/project/specifiers.js +106 -0
- package/dist/shim/registry/copy-gate.js +110 -0
- package/dist/shim/registry/exhibits.js +455 -0
- package/dist/shim/registry/experiments.js +215 -0
- package/dist/shim/registry/index.js +19 -0
- package/dist/shim/registry/scene-source.js +157 -0
- package/dist/shim/registry/walkthrough.js +807 -0
- package/dist/shim/render/capture-envelope.js +279 -0
- package/dist/shim/render/heat-colormap.js +92 -0
- package/dist/shim/render/render-stages.js +855 -0
- package/dist/shim/render/sun-look.js +33 -0
- package/dist/shim/render-state/apply.js +262 -0
- package/dist/shim/render-state/index.js +74 -0
- package/dist/shim/rpc/index.js +207 -0
- package/dist/shim/runtime/index.js +1023 -0
- package/dist/shim/storage/boot.js +69 -0
- package/dist/shim/storage/index.js +169 -0
- package/dist/shim/storage/local-storage-provider.js +230 -0
- package/dist/shim/storage/make-namespace-store.js +132 -0
- package/dist/shim/storage/modules.js +249 -0
- package/dist/shim/storage/projects.js +150 -0
- package/llms.txt +12 -0
- package/package.json +62 -4
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LogicGate — THE SWITCHING PART. Two current-sensing input terminals, one
|
|
5
|
+
* powered output terminal, assembled entirely from stock: a `Wall{destroy,count}`
|
|
6
|
+
* ammeter per input, a mirror `Tunnel` output channel, a `SphereEmitter` supply
|
|
7
|
+
* rail, and a `Wall{destroy}` SHUNT VALVE across the outlet.
|
|
8
|
+
*
|
|
9
|
+
* var g = new LogicGate(universe, { logic: 'AND', left: 200, top: 120, bore: 3 });
|
|
10
|
+
* g.inPortA; // [200, 126] — route a Trace here
|
|
11
|
+
* g.inPortB; // [200, 168]
|
|
12
|
+
* g.outPort; // [248, 147] — route the next Trace from here
|
|
13
|
+
* g.onTick(tickData); // once per tick — sense, decide, actuate
|
|
14
|
+
* g.getState(); // { a, b, out, currentA, currentB }
|
|
15
|
+
*
|
|
16
|
+
* ── THE PACKAGED MECHANISM, IN ONE SENTENCE ─────────────────────────────────
|
|
17
|
+
* A `LogicGate` is a HOST TICK-FN that (1) reads each input terminal's collected
|
|
18
|
+
* collision count out of this tick's TickData, (2) thresholds the two windowed
|
|
19
|
+
* currents into booleans with hysteresis, and (3) writes ONE property — the
|
|
20
|
+
* shunt valve's active flag — so the always-on supply either reaches the outlet
|
|
21
|
+
* or is dumped to ground. Sense with a counting Wall; decide between ticks;
|
|
22
|
+
* actuate with `setActive`. That is the documented control loop
|
|
23
|
+
* (`INTERPRETER_ARCHITECTURE.md`), not a new engine concept.
|
|
24
|
+
*
|
|
25
|
+
* ── WHY A POWERED (VALVE) GATE AND NOT A CURRENT-SUMMING ONE ────────────────
|
|
26
|
+
* Both were measured (`docs/internal/physics-lab/logic-gate-bench/`). Currents DO
|
|
27
|
+
* add at a directed junction, so a wired threshold gate is buildable — but its
|
|
28
|
+
* output level is a multiple of its INPUT level, so levels decay down a chain and
|
|
29
|
+
* a two-deep circuit needs re-tuned thresholds at every stage. The valve gate's
|
|
30
|
+
* output is its OWN supply rail, so every gate emits the same HIGH level no matter
|
|
31
|
+
* what its inputs did: levels are RESTORED and gates cascade unchanged. A
|
|
32
|
+
* half-adder is two gates deep, so this is not a preference — it is the reason the
|
|
33
|
+
* part works at all. The merge measurement is kept in the bench README as the
|
|
34
|
+
* fan-in datum, not as a second gate family.
|
|
35
|
+
*
|
|
36
|
+
* ── PURE COMPOSITION — zero engine, zero new physics (canonical-bench SU1) ───
|
|
37
|
+
* A LogicGate adds NO collision logic. Every piece is a part already on the floor:
|
|
38
|
+
* INPUT TERMINAL → `Wall{ collisionBehavior:'destroy', hooks:['count'] }`
|
|
39
|
+
* — the ammeter; identical to a `Trace`'s own ground, and it
|
|
40
|
+
* uses `Trace.GROUND_COUNTS_PER_ARRIVAL` for calibration
|
|
41
|
+
* rather than re-deriving it (ONE authority).
|
|
42
|
+
* OUTPUT CHANNEL → `Tunnel{ mode:'mirror' }` — the same insulation a Trace run
|
|
43
|
+
* is made of, so the gate shrinks on the same rules.
|
|
44
|
+
* SUPPLY RAIL → `SphereEmitter{ preset:'laser' }` — voltage = burst/tick.
|
|
45
|
+
* SHUNT VALVE → `Wall{ collisionBehavior:'destroy' }` + `lo.setActive()`.
|
|
46
|
+
* SUPPLY CAP → `Wall{ collisionBehavior:'destroy' }` closing the inlet.
|
|
47
|
+
* The comparator is arithmetic on collected data BETWEEN ticks. If building a gate
|
|
48
|
+
* ever seems to need engine or provider code, STOP: it is walls, a tunnel and an
|
|
49
|
+
* emitter.
|
|
50
|
+
*
|
|
51
|
+
* ── Layer 1: API contract ────────────────────────────────────────────────────
|
|
52
|
+
*
|
|
53
|
+
* `new LogicGate(universe, opts)` — builds + engine-registers every sub-part. It
|
|
54
|
+
* does NOT add itself to a runner render list; the caller iterates
|
|
55
|
+
* `gate.largeObjects` (or `runner.setup()` auto-wires them).
|
|
56
|
+
*
|
|
57
|
+
* ORDERING PRECONDITION — `universe.init()` MUST already have run (a gate
|
|
58
|
+
* registers LargeObjects in its constructor and the engine throws on
|
|
59
|
+
* registration against a fresh universe). The sketch order is always
|
|
60
|
+
* world.init() FIRST → CONSTRUCT parts → WIRE → RUN.
|
|
61
|
+
*
|
|
62
|
+
* @param {Object} universe — an EGPTUniverse (JS or WASM compat shim).
|
|
63
|
+
* @param {Object} opts
|
|
64
|
+
* @param {string} opts.logic — REQUIRED. A key of `LogicGate.TRUTH`:
|
|
65
|
+
* AND | OR | XOR | NAND | NOR | XNOR. FAIL-LOUD on anything else. The logic is
|
|
66
|
+
* a comparator ROW, not a geometry — every value builds the identical hardware,
|
|
67
|
+
* which is why an inverting gate costs exactly what a non-inverting one costs
|
|
68
|
+
* here (unlike silicon).
|
|
69
|
+
* @param {number} opts.left, opts.top — the gate cell's top-left corner, px.
|
|
70
|
+
* @param {number} [opts.bore=3] — the lane aperture, px. Sets the lane pitch
|
|
71
|
+
* (`bore + 2*wallThickness`) and therefore the whole footprint.
|
|
72
|
+
* @param {number} [opts.wallThickness=6] — lane insulation, px. This is the knob
|
|
73
|
+
* that sets the shrink floor — see Layer 2 and the `Trace` datasheet.
|
|
74
|
+
* @param {number} [opts.length=48] — the output channel's x extent, px. The floor
|
|
75
|
+
* is `LogicGate.MIN_LENGTH` (20 px — below it the supply inlet and the shunt
|
|
76
|
+
* valve would overlap), and 20 px is also the length the shipped minimal cell
|
|
77
|
+
* uses. FAIL-LOUD below the floor.
|
|
78
|
+
* @param {number} [opts.supplyVoltage=6] — the rail: emitter burst per tick, and
|
|
79
|
+
* therefore the gate's HIGH output level in q/tick. LIVE (`setSupplyVoltage`).
|
|
80
|
+
* @param {number} [opts.thresholdHigh] — input current (q/tick) at or above which
|
|
81
|
+
* an input is called HIGH. DEFAULT `0.5 * supplyVoltage` — referenced to the
|
|
82
|
+
* rail so a gate driven by another gate switches at half that gate's own HIGH
|
|
83
|
+
* level, which is what makes a chain work without per-stage tuning.
|
|
84
|
+
* @param {number} [opts.thresholdLow] — current below which a HIGH input reverts
|
|
85
|
+
* to LOW (hysteresis). DEFAULT `0.25 * supplyVoltage`. Must be < thresholdHigh.
|
|
86
|
+
* @param {number} [opts.windowTicks=20] — sliding window for the current average.
|
|
87
|
+
* Sets BOTH the noise (sd ~ sqrt(I/W)) and half the switching delay.
|
|
88
|
+
* @param {'x'} [opts.axis='x'] — flow axis. Only +x is built and measured;
|
|
89
|
+
* FAIL-LOUD on anything else rather than shipping an unmeasured orientation.
|
|
90
|
+
* @param {Array} [opts.color] — RGB for the channel mirrors.
|
|
91
|
+
* @param {string} [opts.name='gate'] — identifier prefix.
|
|
92
|
+
*
|
|
93
|
+
* @property {Array} inPortA, inPortB — `[x,y]` input terminal faces. Route a
|
|
94
|
+
* `Trace` so its `outPort` lands exactly here (gap 0 — the terminal slab starts
|
|
95
|
+
* at the port).
|
|
96
|
+
* @property {Array} outPort — `[x,y]` the powered outlet. Route the next `Trace`'s
|
|
97
|
+
* `inPort` here.
|
|
98
|
+
* @property {Array} footprint — `[left, top, w, h]` the gate cell's full extent.
|
|
99
|
+
* @property {Array<Wall>} terminals — `[ammeterA, ammeterB]`.
|
|
100
|
+
* @property {Wall} valve, supplyCap — the shunt valve and the inlet cap.
|
|
101
|
+
* @property {Tunnel} channel — the output channel.
|
|
102
|
+
* @property {Array<LargeObject>} largeObjects — every inner engine LO (render wiring).
|
|
103
|
+
*
|
|
104
|
+
* Methods:
|
|
105
|
+
* `onTick(tickData)` — THE ONE per-tick call: sense → decide → actuate.
|
|
106
|
+
* `attachToRunner(runner)` — subscribe `onTick` to the runner's tick fan-out.
|
|
107
|
+
* `getState()` → `{ a, b, out, currentA, currentB }` — the two input booleans,
|
|
108
|
+
* the output boolean, and the two windowed currents in q/tick.
|
|
109
|
+
* `getOutputLevel()` → the nominal HIGH level in q/tick (0 when the output is LOW).
|
|
110
|
+
* `setSupplyVoltage(v)` / `getSupplyVoltage()` — LIVE rail knob. Re-derives the
|
|
111
|
+
* two thresholds only if the caller never pinned them explicitly.
|
|
112
|
+
* `setLogic(name)` / `getLogic()` — LIVE comparator row swap (no rebuild: the
|
|
113
|
+
* hardware is identical for every row).
|
|
114
|
+
* `footprintArea()` → px² — the miniaturization figure of merit.
|
|
115
|
+
* `dispose()` — tears down every sub-part (idempotent).
|
|
116
|
+
*
|
|
117
|
+
* ── Layer 2: physics behavior ────────────────────────────────────────────────
|
|
118
|
+
* ZERO physics of its own (SU1). Everything below is the emergent behaviour of the
|
|
119
|
+
* stock parts, MEASURED on this construction (WASM engine, 2D, fundamental dim 0,
|
|
120
|
+
* inter-quantum collisions OFF, 3 seeds; the rigs are
|
|
121
|
+
* `docs/internal/physics-lab/logic-gate-bench/characterize.cjs` and
|
|
122
|
+
* `gate-acceptance.cjs`, and the numbers are quoted from `LogicGate.MEASURED`):
|
|
123
|
+
*
|
|
124
|
+
* THE INPUT IS AN AMMETER AND IT IS LINEAR. A `Wall{destroy,count}` terminal
|
|
125
|
+
* reads I = G·V with G = 1.01 ± 0.01 and no knee from V = 1 to V = 16, and reads
|
|
126
|
+
* EXACTLY 0.00 with no source. That linearity is why a threshold can be quoted
|
|
127
|
+
* as a FRACTION OF THE RAIL and still mean the same thing at every voltage.
|
|
128
|
+
*
|
|
129
|
+
* THE VALVE IS A HARD ZERO. Closed (the destroy slab active) the outlet delivers
|
|
130
|
+
* 0.000 q/tick — not "small", zero: `destroy` is a plain kill (bench LO6) and
|
|
131
|
+
* nothing gets past it. So the ON/OFF ratio is not a finite number, and the LOW
|
|
132
|
+
* output level is not a level at all. All of a gate's noise lives on the INPUT
|
|
133
|
+
* side.
|
|
134
|
+
*
|
|
135
|
+
* THE SWITCHING THRESHOLD AND ITS TOLERANCE. The terminal current is a counting
|
|
136
|
+
* process: the sd of a W-tick sliding mean goes as ~0.75·sqrt(I/W), so at the
|
|
137
|
+
* default rail (6 q/tick) and W = 20 the input reads 6.05 ± 0.42 — a 7.2 sd
|
|
138
|
+
* margin to the 3.0 threshold, and 5.7 sd even at a rail of 4. Shrinking W speeds
|
|
139
|
+
* the gate up and narrows the margin as 1/sqrt(W); the shipped default is the
|
|
140
|
+
* last W at which every rail from 4 up clears 4 sd.
|
|
141
|
+
*
|
|
142
|
+
* …AND THE ASYMMETRY THAT COMES WITH HYSTERESIS. The comparator rises on the
|
|
143
|
+
* FIRST EXCURSION above `thresholdHigh` and is then LATCHED by `thresholdLow`, so
|
|
144
|
+
* the EFFECTIVE switching point sits about half a standard deviation BELOW the
|
|
145
|
+
* nominal threshold: a drive whose terminal MEAN measured 2.86 q/tick — under the
|
|
146
|
+
* 3.0 threshold — read HIGH on every seed, and never chattered (zero output
|
|
147
|
+
* transitions were measured anywhere in the drive sweep). The rule that follows:
|
|
148
|
+
* an input is reliably LOW only while it stays ~4 sd BELOW `thresholdHigh`, which
|
|
149
|
+
* at the shipped defaults means under about a THIRD of the rail, not merely under
|
|
150
|
+
* half of it. The asymmetry is in the safe direction (a marginal input latches
|
|
151
|
+
* rather than oscillating), but it is not what "threshold = half the rail" reads
|
|
152
|
+
* like, so design to the 4 sd rule, not to the nominal number.
|
|
153
|
+
*
|
|
154
|
+
* PROPAGATION DELAY: ~8 TICKS OF GATE, THE REST IS WIRE. Measured end-to-end at
|
|
155
|
+
* 268 ticks from an input source switching on to the output boolean following —
|
|
156
|
+
* of which 260 ticks are the current FLYING the 260 px input trace at 1 px/tick
|
|
157
|
+
* (bench M1) and only ~8 are the comparator crossing its threshold. The output
|
|
158
|
+
* CURRENT then needs the outlet-to-load flight on top (measured 529 ticks total
|
|
159
|
+
* over a further 240 px). So the part's own latency is ~8 ticks and everything
|
|
160
|
+
* else in a circuit's settling time is LAYOUT: place a gate near its source and
|
|
161
|
+
* it responds almost immediately; a chain's settling time is the sum of its
|
|
162
|
+
* wires, not of its gates.
|
|
163
|
+
*
|
|
164
|
+
* LANE CROSSTALK IS A NON-ISSUE, AND HERE IS THE ARITHMETIC. The three lanes sit
|
|
165
|
+
* at `Trace.lanePitch` — wall against wall, never sharing mirror pixels (two
|
|
166
|
+
* coincident mirrors leak badly; that is in the Trace datasheet). At the tightest
|
|
167
|
+
* shipped geometry the published lane-to-lane crosstalk is 0.98 % and it reaches
|
|
168
|
+
* ONLY the immediately adjacent lane, so at a rail of 6 the output lane puts
|
|
169
|
+
* ~0.06 q/tick onto an input terminal — about 25x below `thresholdLow`. Widen the
|
|
170
|
+
* insulation and it measures exactly zero. The output lane is deliberately in the
|
|
171
|
+
* MIDDLE so both inputs see the identical (tiny) coupling.
|
|
172
|
+
*
|
|
173
|
+
* IT SHRINKS, BUT IT HAS ITS OWN FLOOR — LOWER THAN THE WIRE'S. A `Trace` still
|
|
174
|
+
* conducts at bore 1 (G = 0.987 with 10 px of insulation). A GATE at bore 1 does
|
|
175
|
+
* NOT: it measured 0.14 q/tick and failed its truth table outright, because a gate
|
|
176
|
+
* is not just a run — it puts a 4 px inlet cap and a 4 px shunt valve across the
|
|
177
|
+
* bore and parks the supply 8 px inside it, and that machinery does not fit a 1 px
|
|
178
|
+
* aperture. Expecting the wire's floor to be the gate's floor was WRONG; the
|
|
179
|
+
* measured floor is bore 2-3.
|
|
180
|
+
*
|
|
181
|
+
* AND "IT RETURNS THE RIGHT ANSWER" IS NOT THE ACCEPTANCE TEST. bore 2 / wall 3
|
|
182
|
+
* (a 480 px² cell) holds all sixteen truth-table rows — on a conductor leaking
|
|
183
|
+
* ~23 %, delivering only 4.65 q/tick against a 6 q/tick rail. The BOOLEAN is right
|
|
184
|
+
* and the part is still not shippable, because the HIGH level is what the NEXT
|
|
185
|
+
* gate thresholds against and level restoration is the property the cascade rests
|
|
186
|
+
* on. Acceptance is therefore two conditions — table correct AND HIGH ≥ 90 % of
|
|
187
|
+
* the rail — which makes the shipped minimum `LogicGate.MINIMAL`: bore 3 / wall 3
|
|
188
|
+
* / length 20, a 20 × 27 = 540 px² cell delivering 6.04 ± 0.12 q/tick.
|
|
189
|
+
*
|
|
190
|
+
* A too-thin mirror does not degrade gracefully — it goes transparent, so a
|
|
191
|
+
* sub-floor gate reads DEAD (no output on any row), not noisy.
|
|
192
|
+
*
|
|
193
|
+
* QUIESCENT CURRENT. The rail runs whether the output is HIGH or LOW (a LOW
|
|
194
|
+
* output is the supply being dumped into the valve), so a gate always draws
|
|
195
|
+
* `supplyVoltage` q/tick. This is static logic; there is no idle state.
|
|
196
|
+
*/
|
|
197
|
+
|
|
198
|
+
import { Tunnel as _Tunnel } from './Tunnel.js';
|
|
199
|
+
import { Wall as _Wall } from './Wall.js';
|
|
200
|
+
import { Trace as _Trace } from './Trace.js';
|
|
201
|
+
import { SphereEmitter as _SphereEmitter } from './SphereEmitter.js';
|
|
202
|
+
import { registerKind } from '../objects/kinds.js';
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The comparator rows. DATA, not geometry: every row builds the identical
|
|
209
|
+
* hardware and differs only in this one function, which is why NAND costs
|
|
210
|
+
* exactly what AND costs on this floor.
|
|
211
|
+
*/
|
|
212
|
+
var TRUTH = {
|
|
213
|
+
AND: function (a, b) { return a && b; },
|
|
214
|
+
OR: function (a, b) { return a || b; },
|
|
215
|
+
XOR: function (a, b) { return a !== b; },
|
|
216
|
+
NAND: function (a, b) { return !(a && b); },
|
|
217
|
+
NOR: function (a, b) { return !(a || b); },
|
|
218
|
+
XNOR: function (a, b) { return a === b; }
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
function LogicGate(universe, opts) {
|
|
222
|
+
opts = opts || {};
|
|
223
|
+
this.universe = universe;
|
|
224
|
+
this._schemaKind = 'LogicGate';
|
|
225
|
+
|
|
226
|
+
var TunnelCls = _Tunnel;
|
|
227
|
+
var WallCls = _Wall;
|
|
228
|
+
var TraceCls = _Trace;
|
|
229
|
+
var EmitterCls = _SphereEmitter;
|
|
230
|
+
|
|
231
|
+
var logic = String(opts.logic || '').toUpperCase();
|
|
232
|
+
if (!TRUTH[logic]) {
|
|
233
|
+
throw new Error('[LogicGate] opts.logic must be one of ' + Object.keys(TRUTH).join(', ') +
|
|
234
|
+
' — got ' + JSON.stringify(opts.logic) + '. (FAIL-LOUD.)');
|
|
235
|
+
}
|
|
236
|
+
var axis = opts.axis || 'x';
|
|
237
|
+
if (axis !== 'x') {
|
|
238
|
+
throw new Error('[LogicGate] only axis "x" (+x flow) is built and MEASURED. A rotated ' +
|
|
239
|
+
'gate would be an unmeasured part wearing a measured datasheet. Rotate the layout ' +
|
|
240
|
+
'instead. (FAIL-LOUD.)');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
var left = opts.left, top = opts.top;
|
|
244
|
+
if (typeof left !== 'number' || typeof top !== 'number') {
|
|
245
|
+
throw new Error('[LogicGate] opts.left and opts.top are required (px). (FAIL-LOUD.)');
|
|
246
|
+
}
|
|
247
|
+
var bore = (opts.bore !== undefined && opts.bore !== null) ? opts.bore : 3;
|
|
248
|
+
var wallThickness = (opts.wallThickness !== undefined && opts.wallThickness !== null)
|
|
249
|
+
? opts.wallThickness : 6;
|
|
250
|
+
if (!(bore > 0) || !(wallThickness > 0)) {
|
|
251
|
+
throw new Error('[LogicGate] bore and wallThickness must both be positive. (FAIL-LOUD.)');
|
|
252
|
+
}
|
|
253
|
+
var length = (opts.length !== undefined) ? opts.length : 48;
|
|
254
|
+
if (length < LogicGate.MIN_LENGTH) {
|
|
255
|
+
throw new Error('[LogicGate] opts.length ' + length + ' is below the measured minimum ' +
|
|
256
|
+
LogicGate.MIN_LENGTH + 'px (the supply inlet and the shunt valve would overlap). ' +
|
|
257
|
+
'(FAIL-LOUD.)');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
var supplyVoltage = (opts.supplyVoltage !== undefined) ? opts.supplyVoltage : 6;
|
|
261
|
+
this._pinnedHigh = (opts.thresholdHigh !== undefined);
|
|
262
|
+
this._pinnedLow = (opts.thresholdLow !== undefined);
|
|
263
|
+
var thresholdHigh = this._pinnedHigh ? opts.thresholdHigh : LogicGate.THRESHOLD_HIGH_FRACTION * supplyVoltage;
|
|
264
|
+
var thresholdLow = this._pinnedLow ? opts.thresholdLow : LogicGate.THRESHOLD_LOW_FRACTION * supplyVoltage;
|
|
265
|
+
if (!(thresholdLow < thresholdHigh)) {
|
|
266
|
+
throw new Error('[LogicGate] thresholdLow (' + thresholdLow + ') must be BELOW ' +
|
|
267
|
+
'thresholdHigh (' + thresholdHigh + ') — the gap IS the hysteresis. (FAIL-LOUD.)');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
var name = opts.name || 'gate';
|
|
271
|
+
var color = opts.color || [200, 170, 70];
|
|
272
|
+
var pitch = TraceCls.lanePitch(bore, wallThickness);
|
|
273
|
+
|
|
274
|
+
this.logic = logic;
|
|
275
|
+
this.left = left; this.top = top;
|
|
276
|
+
this.bore = bore; this.wallThickness = wallThickness; this.length = length;
|
|
277
|
+
this.pitch = pitch;
|
|
278
|
+
this.supplyVoltage = supplyVoltage;
|
|
279
|
+
this.thresholdHigh = thresholdHigh;
|
|
280
|
+
this.thresholdLow = thresholdLow;
|
|
281
|
+
this.windowTicks = (opts.windowTicks !== undefined) ? opts.windowTicks : 20;
|
|
282
|
+
this.name = name;
|
|
283
|
+
this.color = color;
|
|
284
|
+
|
|
285
|
+
// ── THE THREE LANES ──────────────────────────────────────────────────
|
|
286
|
+
// Input A on top, the powered output in the MIDDLE (equidistant from both
|
|
287
|
+
// inputs, so the two crosstalk paths are identical), input B on the bottom.
|
|
288
|
+
// Lanes sit at Trace's published pitch — wall against wall, never sharing
|
|
289
|
+
// mirror pixels (the coincident-mirror leak in the Trace datasheet).
|
|
290
|
+
var yA = top + pitch / 2;
|
|
291
|
+
var yO = top + pitch * 1.5;
|
|
292
|
+
var yB = top + pitch * 2.5;
|
|
293
|
+
this.inPortA = [left, yA];
|
|
294
|
+
this.inPortB = [left, yB];
|
|
295
|
+
this.outPort = [left + length, yO];
|
|
296
|
+
this.footprint = [left, top, length, pitch * 3];
|
|
297
|
+
|
|
298
|
+
var T = LogicGate.TERMINAL_THICKNESS;
|
|
299
|
+
|
|
300
|
+
// ── INPUT TERMINALS: the ammeters. Identical to a Trace's own ground. ─
|
|
301
|
+
this.terminals = [
|
|
302
|
+
new WallCls(universe, {
|
|
303
|
+
rect: [left, yA - pitch / 2, T, pitch],
|
|
304
|
+
collisionBehavior: 'destroy', hooks: ['count'],
|
|
305
|
+
name: name + '_inA', color: [60, 60, 255]
|
|
306
|
+
}),
|
|
307
|
+
new WallCls(universe, {
|
|
308
|
+
rect: [left, yB - pitch / 2, T, pitch],
|
|
309
|
+
collisionBehavior: 'destroy', hooks: ['count'],
|
|
310
|
+
name: name + '_inB', color: [60, 60, 255]
|
|
311
|
+
})
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
// ── OUTPUT CHANNEL: a mirror Tunnel — the same insulation as a Trace run.
|
|
315
|
+
this.channel = new TunnelCls(universe, {
|
|
316
|
+
left: left, top: yO - bore / 2, length: length, bore: bore,
|
|
317
|
+
wallThickness: wallThickness, axis: 'x', mode: 'mirror',
|
|
318
|
+
name: name + '_channel', color: color
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// ── SUPPLY CAP: closes the inlet so the rail cannot spray backwards out
|
|
322
|
+
// of the cell. A destroy slab, not a mirror — a mirror would send the
|
|
323
|
+
// backward fraction forward again and inflate the HIGH level.
|
|
324
|
+
this.supplyCap = new WallCls(universe, {
|
|
325
|
+
rect: [left, yO - pitch / 2, T, pitch],
|
|
326
|
+
collisionBehavior: 'destroy',
|
|
327
|
+
name: name + '_cap', color: [70, 70, 90]
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// ── SHUNT VALVE: the ONE property this gate ever writes. ──────────────
|
|
331
|
+
this.valve = new WallCls(universe, {
|
|
332
|
+
rect: [left + length - LogicGate.VALVE_INSET, yO - pitch / 2, T, pitch],
|
|
333
|
+
collisionBehavior: 'destroy',
|
|
334
|
+
name: name + '_valve', color: [220, 90, 90]
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// ── SUPPLY RAIL: voltage = burst per tick, aimed down the channel. ────
|
|
338
|
+
this.supply = new EmitterCls([left + LogicGate.SUPPLY_INSET, yO], {
|
|
339
|
+
preset: 'laser', is2D: true, R: 24, orientationDeg: 0,
|
|
340
|
+
emissionBoxSide: 0, fundamentalDimension: 0,
|
|
341
|
+
burstSize: Math.max(1, supplyVoltage), emitEveryTicks: 1,
|
|
342
|
+
chargeMode: 'random', renderColor: [255, 120, 60]
|
|
343
|
+
}).addTo(universe);
|
|
344
|
+
|
|
345
|
+
// ── Roll up every sub-part for render wiring. ─────────────────────────
|
|
346
|
+
var parts = this.terminals.concat(this.channel.walls, [this.supplyCap, this.valve]);
|
|
347
|
+
this.walls = parts;
|
|
348
|
+
this.largeObjects = [];
|
|
349
|
+
var self = this;
|
|
350
|
+
parts.forEach(function (w) {
|
|
351
|
+
if (w && w.lo) {
|
|
352
|
+
self.largeObjects.push(w.lo);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
// THE ONE UP-LINK: the gate stamps exactly what IT composed — the terminals, the channel
|
|
356
|
+
// (a Tunnel, which stamps its own plates), the cap, the valve and the supply rail.
|
|
357
|
+
this.terminals.concat([this.channel, this.supplyCap, this.valve, this.supply]).forEach(function (p) {
|
|
358
|
+
if (p) { p._owner = self; }
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// ── Comparator state ─────────────────────────────────────────────────
|
|
362
|
+
this._counts = [[], []]; // raw cumulative ground counts, per terminal
|
|
363
|
+
this._current = [0, 0]; // windowed q/tick, per terminal
|
|
364
|
+
this._states = [false, false];
|
|
365
|
+
this._out = null;
|
|
366
|
+
this._unsub = null;
|
|
367
|
+
this._disposed = false;
|
|
368
|
+
this._applyOutput(TRUTH[logic](false, false));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Terminal / valve / cap slab thickness, px. */
|
|
372
|
+
LogicGate.TERMINAL_THICKNESS = 4;
|
|
373
|
+
/** Supply emitter inset from the channel inlet, px (clear of the inlet cap). */
|
|
374
|
+
LogicGate.SUPPLY_INSET = 8;
|
|
375
|
+
/** Valve inset from the outlet, px. Shorter ⇒ faster fall, less stored current. */
|
|
376
|
+
LogicGate.VALVE_INSET = 8;
|
|
377
|
+
/** Shortest channel that still fits the supply inlet and the valve, px. */
|
|
378
|
+
LogicGate.MIN_LENGTH = LogicGate.SUPPLY_INSET + LogicGate.VALVE_INSET + LogicGate.TERMINAL_THICKNESS;
|
|
379
|
+
/** Default switching thresholds AS A FRACTION OF THE RAIL (see Layer 2). */
|
|
380
|
+
LogicGate.THRESHOLD_HIGH_FRACTION = 0.5;
|
|
381
|
+
LogicGate.THRESHOLD_LOW_FRACTION = 0.25;
|
|
382
|
+
|
|
383
|
+
LogicGate.TRUTH = TRUTH;
|
|
384
|
+
/** The logic rows this part realizes — the ONE vocabulary (schema ferries it). */
|
|
385
|
+
LogicGate.LOGICS = Object.keys(TRUTH);
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* The measured datasheet AS DATA, so a circuit (or a half-adder) can budget
|
|
389
|
+
* levels, margins and settling time without re-running the bench. Every figure
|
|
390
|
+
* is from `docs/internal/physics-lab/logic-gate-bench/` (pkg-node WASM, 2D,
|
|
391
|
+
* fundamental dim 0, inter-quantum collisions OFF, 3 seeds); the README there
|
|
392
|
+
* carries the prediction each one was measured against.
|
|
393
|
+
*/
|
|
394
|
+
LogicGate.MEASURED = {
|
|
395
|
+
/** Terminal current vs source voltage: I = inputGain * V, no knee, I(0)=0 exactly. */
|
|
396
|
+
inputGain: 1.011, inputGainSpread: 0.024,
|
|
397
|
+
/** HIGH output level as a fraction of the rail, and its seed spread (q/tick at rail 6). */
|
|
398
|
+
outputLevelFraction: 1.001, outputLevelSd: 0.036,
|
|
399
|
+
/** LOW output. Not a level — the valve is a destroy slab. */
|
|
400
|
+
outputLowLevel: 0,
|
|
401
|
+
/** sd of the W-tick windowed terminal current ~ noiseCoefficient * sqrt(I/W). */
|
|
402
|
+
noiseCoefficient: 0.75,
|
|
403
|
+
/** Margin in sd from the rail to the threshold, at windowTicks 20, rail 4 / 6 / 8. */
|
|
404
|
+
thresholdMarginSd: { 4: 5.67, 6: 7.21, 8: 8.87 },
|
|
405
|
+
/**
|
|
406
|
+
* How far BELOW `thresholdHigh` an input must stay to be reliably LOW, in sd.
|
|
407
|
+
* The comparator rises on the FIRST excursion and then latches, so the
|
|
408
|
+
* effective switch point is below nominal — design to this, not to 0.5*rail.
|
|
409
|
+
*/
|
|
410
|
+
reliableLowMarginSd: 4,
|
|
411
|
+
/** The part's own latency, ticks. Everything else in a circuit is wire flight. */
|
|
412
|
+
gateLatencyTicks: 8,
|
|
413
|
+
/** Wire flight, px per tick (bench M1) — the other term in any settling budget. */
|
|
414
|
+
flightPxPerTick: 1,
|
|
415
|
+
/** Output level spread across a 2.7x range of INPUT level (level restoration). */
|
|
416
|
+
levelRestorationSd: 0.018,
|
|
417
|
+
/** Output transitions measured across the whole drive sweep. */
|
|
418
|
+
chatterTransitions: 0
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* The smallest cell MEASURED to hold the full truth table (both logics, all
|
|
423
|
+
* four input rows, every seed) — the miniaturization acceptance result, and the
|
|
424
|
+
* palette default. Acceptance is TWO conditions — the table correct AND the HIGH
|
|
425
|
+
* level restored to >= 90% of the rail — because a smaller cell (bore 2 / wall 3,
|
|
426
|
+
* 480 px^2) returns every correct boolean while delivering only 77% of the rail,
|
|
427
|
+
* which is out of margin the moment it drives another gate. Below bore 2 the gate
|
|
428
|
+
* fails outright even where a Trace still conducts: the inlet cap, the shunt valve
|
|
429
|
+
* and the supply do not fit a 1px aperture. A too-thin wall does not degrade — the
|
|
430
|
+
* mirrors go transparent and the gate reads DEAD.
|
|
431
|
+
*/
|
|
432
|
+
LogicGate.MINIMAL = { bore: 3, wallThickness: 3, length: 20, w: 20, h: 27, area: 540 };
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* THE ONE per-tick call — SENSE, DECIDE, ACTUATE.
|
|
436
|
+
* Reads each terminal's collected collisions out of THIS tick's TickData
|
|
437
|
+
* (each TickData must be consumed exactly once — the Wall guard enforces it),
|
|
438
|
+
* windows them into a current, thresholds with hysteresis, and writes the
|
|
439
|
+
* valve's active flag when — and only when — the output boolean changes.
|
|
440
|
+
* @param {Object} tickData — this tick's TickData (from `universe.doTick()`).
|
|
441
|
+
*/
|
|
442
|
+
LogicGate.prototype.onTick = function (tickData) {
|
|
443
|
+
var TraceCls = _Trace;
|
|
444
|
+
var perArrival = TraceCls.GROUND_COUNTS_PER_ARRIVAL;
|
|
445
|
+
for (var i = 0; i < 2; i++) {
|
|
446
|
+
var raw = this.terminals[i].getCollisionHistogram(tickData).total;
|
|
447
|
+
var w = this._counts[i];
|
|
448
|
+
w.push(raw);
|
|
449
|
+
if (w.length > this.windowTicks + 1) { w.shift(); }
|
|
450
|
+
var span = w.length - 1;
|
|
451
|
+
this._current[i] = span > 0 ? ((w[span] - w[0]) / perArrival) / span : 0;
|
|
452
|
+
// Hysteresis: the gap between the two thresholds is what stops a gate
|
|
453
|
+
// sitting on its threshold from chattering on shot noise.
|
|
454
|
+
if (this._states[i] && this._current[i] < this.thresholdLow) { this._states[i] = false; }
|
|
455
|
+
else if (!this._states[i] && this._current[i] >= this.thresholdHigh) { this._states[i] = true; }
|
|
456
|
+
}
|
|
457
|
+
this._applyOutput(TRUTH[this.logic](this._states[0], this._states[1]));
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Write the valve. HIGH ⇒ the valve is INACTIVE (the rail reaches the outlet);
|
|
462
|
+
* LOW ⇒ ACTIVE (the rail is dumped into the destroy slab). Idempotent — the
|
|
463
|
+
* engine write happens only on a transition.
|
|
464
|
+
* @private
|
|
465
|
+
*/
|
|
466
|
+
LogicGate.prototype._applyOutput = function (out) {
|
|
467
|
+
if (out === this._out) { return; }
|
|
468
|
+
this._out = out;
|
|
469
|
+
this.valve.lo.setActive(!out);
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
/** Subscribe `onTick` to a runner's per-tick fan-out. Idempotent. */
|
|
473
|
+
LogicGate.prototype.attachToRunner = function (runner) {
|
|
474
|
+
if (this._unsub || !runner || typeof runner.addTickListener !== 'function') { return this._unsub || null; }
|
|
475
|
+
var self = this;
|
|
476
|
+
this._unsub = runner.addTickListener(function (td) { self.onTick(td); });
|
|
477
|
+
return this._unsub;
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* The gate's live readout.
|
|
482
|
+
* @returns {{a:boolean, b:boolean, out:boolean, currentA:number, currentB:number}}
|
|
483
|
+
*/
|
|
484
|
+
LogicGate.prototype.getState = function () {
|
|
485
|
+
return {
|
|
486
|
+
a: this._states[0], b: this._states[1], out: !!this._out,
|
|
487
|
+
currentA: this._current[0], currentB: this._current[1]
|
|
488
|
+
};
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
/** The nominal output level in q/tick: the rail when HIGH, exactly 0 when LOW. */
|
|
492
|
+
LogicGate.prototype.getOutputLevel = function () {
|
|
493
|
+
return this._out ? this.supplyVoltage : 0;
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* LIVE rail knob. Re-derives the two thresholds from the new rail UNLESS the
|
|
498
|
+
* caller pinned them at construction — a pinned threshold is an explicit
|
|
499
|
+
* design decision and is never silently moved.
|
|
500
|
+
* @param {number} v — burst per tick.
|
|
501
|
+
*/
|
|
502
|
+
LogicGate.prototype.setSupplyVoltage = function (v) {
|
|
503
|
+
this.supplyVoltage = Math.max(0, Math.round(v));
|
|
504
|
+
this.supply.setBurstSize(Math.max(1, this.supplyVoltage));
|
|
505
|
+
this.supply.setActive(this.supplyVoltage > 0);
|
|
506
|
+
if (!this._pinnedHigh) { this.thresholdHigh = LogicGate.THRESHOLD_HIGH_FRACTION * this.supplyVoltage; }
|
|
507
|
+
if (!this._pinnedLow) { this.thresholdLow = LogicGate.THRESHOLD_LOW_FRACTION * this.supplyVoltage; }
|
|
508
|
+
return this.supplyVoltage;
|
|
509
|
+
};
|
|
510
|
+
LogicGate.prototype.getSupplyVoltage = function () { return this.supplyVoltage; };
|
|
511
|
+
|
|
512
|
+
/** LIVE comparator-row swap. The hardware is identical for every row. */
|
|
513
|
+
LogicGate.prototype.setLogic = function (nameIn) {
|
|
514
|
+
var next = String(nameIn || '').toUpperCase();
|
|
515
|
+
if (!TRUTH[next]) {
|
|
516
|
+
throw new Error('[LogicGate.setLogic] unknown logic ' + JSON.stringify(nameIn) +
|
|
517
|
+
' — expected one of ' + LogicGate.LOGICS.join(', ') + '. (FAIL-LOUD.)');
|
|
518
|
+
}
|
|
519
|
+
this.logic = next;
|
|
520
|
+
this._applyOutput(TRUTH[next](this._states[0], this._states[1]));
|
|
521
|
+
return next;
|
|
522
|
+
};
|
|
523
|
+
LogicGate.prototype.getLogic = function () { return this.logic; };
|
|
524
|
+
|
|
525
|
+
/** The miniaturization figure of merit: the cell's area in px². */
|
|
526
|
+
LogicGate.prototype.footprintArea = function () {
|
|
527
|
+
return this.footprint[2] * this.footprint[3];
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
/** Full teardown; delegates to each sub-part's own dispose. Idempotent. */
|
|
531
|
+
LogicGate.prototype.dispose = function () {
|
|
532
|
+
if (this._disposed) { return; }
|
|
533
|
+
this._disposed = true;
|
|
534
|
+
if (this._unsub) { this._unsub(); this._unsub = null; }
|
|
535
|
+
this.terminals.forEach(function (w) { w.dispose(); });
|
|
536
|
+
this.channel.dispose();
|
|
537
|
+
this.supplyCap.dispose();
|
|
538
|
+
this.valve.dispose();
|
|
539
|
+
if (this.supply && typeof this.supply.dispose === 'function') { this.supply.dispose(); }
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Cell size for a given lane geometry, WITHOUT constructing anything — so a
|
|
544
|
+
* layout tool (or a half-adder) can place gates before it builds them.
|
|
545
|
+
* @param {number} bore @param {number} wallThickness @param {number} [length=48]
|
|
546
|
+
* @returns {{w:number, h:number, pitch:number, area:number}}
|
|
547
|
+
*/
|
|
548
|
+
LogicGate.cellSize = function (bore, wallThickness, length) {
|
|
549
|
+
var TraceCls = _Trace;
|
|
550
|
+
var pitch = TraceCls.lanePitch(bore, wallThickness);
|
|
551
|
+
var len = (length !== undefined) ? length : 48;
|
|
552
|
+
return { w: len, h: pitch * 3, pitch: pitch, area: len * pitch * 3 };
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// ── The kind (objects/kinds.js): descriptor, construction defaults, palette, builder ──────────
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* LogicGate — the V2 stock switching part (the successor named in
|
|
559
|
+
* V2_PARTS_CLASSIFICATION.md §4.4 for CircuitGate): two `Wall{destroy,count}`
|
|
560
|
+
* ammeter terminals, a mirror-`Tunnel` output channel carrying a
|
|
561
|
+
* `SphereEmitter` supply rail, and a `Wall{destroy}` shunt valve.
|
|
562
|
+
* Constructor: LogicGate(universe, { logic, left, top, bore, wallThickness,
|
|
563
|
+
* length, supplyVoltage, thresholdHigh, thresholdLow,
|
|
564
|
+
* windowTicks, color, name })
|
|
565
|
+
* Group wrapper: every inner Wall/Tunnel wall and the supply emitter are
|
|
566
|
+
* stamped with the ONE up-link `_owner` = <this>.
|
|
567
|
+
*/
|
|
568
|
+
var LogicGateDescriptor = {
|
|
569
|
+
description: 'A powered logic gate assembled from stock: two counting-Wall ammeter ' +
|
|
570
|
+
'terminals sense the current arriving on two input lanes, a comparator with ' +
|
|
571
|
+
'hysteresis thresholds them into booleans, and one shunt valve either lets the ' +
|
|
572
|
+
'gate\'s own supply rail reach the outlet or dumps it. Because the output is the ' +
|
|
573
|
+
'gate\'s OWN rail, its HIGH level is independent of the input levels, so gates ' +
|
|
574
|
+
'cascade without per-stage retuning. Zero physics of its own.',
|
|
575
|
+
// A gate cell has a real placed extent, but its internal lane geometry is
|
|
576
|
+
// derived — resizing it by dragging would silently break the lane pitch.
|
|
577
|
+
ux: {
|
|
578
|
+
resizable: false,
|
|
579
|
+
draggable: { '2d': true },
|
|
580
|
+
shellRender: 'run'
|
|
581
|
+
},
|
|
582
|
+
fields: [
|
|
583
|
+
{
|
|
584
|
+
name: 'logic',
|
|
585
|
+
type: 'enum',
|
|
586
|
+
// The vocabulary's OWNER is TRUTH (LogicGate.TRUTH / LogicGate.LOGICS) — this
|
|
587
|
+
// descriptor registers from the same module, so it reads the owner.
|
|
588
|
+
constraint: { values: Object.fromEntries(Object.keys(TRUTH).map(function (k) { return [k, k]; })) },
|
|
589
|
+
engineVisible: false,
|
|
590
|
+
live: true,
|
|
591
|
+
description: 'The comparator row applied to the two sensed inputs. DATA, not geometry: every row builds the identical hardware and differs only in this function, so an inverting gate (NAND/NOR/XNOR) costs exactly what a non-inverting one costs here — unlike silicon.'
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
name: 'left',
|
|
595
|
+
type: 'number',
|
|
596
|
+
constraint: { integer: true },
|
|
597
|
+
engineVisible: true,
|
|
598
|
+
unit: 'px',
|
|
599
|
+
description: 'Gate cell left X position (physics pixels). The two input terminal faces and the output-channel inlet all sit on this edge.'
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
name: 'top',
|
|
603
|
+
type: 'number',
|
|
604
|
+
constraint: { integer: true },
|
|
605
|
+
engineVisible: true,
|
|
606
|
+
unit: 'px',
|
|
607
|
+
description: 'Gate cell top Y position (physics pixels). Input A rides the first lane, the powered output the middle lane, input B the third.'
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
name: 'bore',
|
|
611
|
+
type: 'number',
|
|
612
|
+
constraint: { min: 1, integer: true },
|
|
613
|
+
engineVisible: true,
|
|
614
|
+
unit: 'px',
|
|
615
|
+
description: 'Lane aperture in physics pixels — the same bore the Traces wired to this gate should use. Sets the lane pitch (bore + 2 x wallThickness) and therefore the whole cell height.'
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
name: 'wallThickness',
|
|
619
|
+
type: 'number',
|
|
620
|
+
constraint: { min: 1, integer: true },
|
|
621
|
+
engineVisible: true,
|
|
622
|
+
unit: 'px',
|
|
623
|
+
description: 'Lane insulation in physics pixels. This is the knob that sets the shrink floor: too thin and the channel mirrors go transparent, which reads as a dead gate rather than a noisy one.'
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
name: 'length',
|
|
627
|
+
type: 'number',
|
|
628
|
+
constraint: { min: 1, integer: true },
|
|
629
|
+
engineVisible: true,
|
|
630
|
+
unit: 'px',
|
|
631
|
+
description: 'Output-channel X extent in physics pixels — the cell width. The measured floor is 20px (below that the supply inlet and the shunt valve overlap).'
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
name: 'supplyVoltage',
|
|
635
|
+
type: 'number',
|
|
636
|
+
constraint: { min: 0, integer: true },
|
|
637
|
+
engineVisible: false,
|
|
638
|
+
live: true,
|
|
639
|
+
unit: 'quanta/tick',
|
|
640
|
+
description: 'The supply rail: emitter burst per tick, and therefore the gate\'s HIGH output level in quanta per tick. Raising it raises the output level and (unless the thresholds are pinned) the switching thresholds with it, so the gate stays correctly biased at any rail.'
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
name: 'thresholdHigh',
|
|
644
|
+
type: 'number',
|
|
645
|
+
constraint: { min: 0 },
|
|
646
|
+
engineVisible: false,
|
|
647
|
+
live: true,
|
|
648
|
+
unit: 'quanta/tick',
|
|
649
|
+
description: 'Input current at or above which an input is called HIGH. Defaults to half the rail. Because the sensed current is a counting process, an input must sit several standard deviations clear of this value to be reliably read — see the part datasheet for the measured margin.'
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
name: 'thresholdLow',
|
|
653
|
+
type: 'number',
|
|
654
|
+
constraint: { min: 0 },
|
|
655
|
+
engineVisible: false,
|
|
656
|
+
live: true,
|
|
657
|
+
unit: 'quanta/tick',
|
|
658
|
+
description: 'Input current below which a HIGH input reverts to LOW. Defaults to a quarter of the rail. The gap up to thresholdHigh IS the hysteresis — it is what stops an input sitting near its threshold from chattering on shot noise.'
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
name: 'windowTicks',
|
|
662
|
+
type: 'number',
|
|
663
|
+
constraint: { min: 1, integer: true },
|
|
664
|
+
engineVisible: false,
|
|
665
|
+
live: true,
|
|
666
|
+
unit: 'ticks',
|
|
667
|
+
description: 'Sliding window over which the input current is averaged. It sets BOTH the sense noise (which falls as 1/sqrt(window)) and half the switching delay — a longer window is a steadier, slower gate.'
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
name: 'color',
|
|
671
|
+
type: 'rgb',
|
|
672
|
+
engineVisible: false,
|
|
673
|
+
live: true,
|
|
674
|
+
description: 'Render color [r, g, b] for the output-channel mirrors (render-only). A live edit fans out to both mirrors via the `_owner` fan-out in updateObjectProperty.'
|
|
675
|
+
}
|
|
676
|
+
]
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
registerKind(LogicGate, {
|
|
680
|
+
from: import.meta.url,
|
|
681
|
+
descriptor: LogicGateDescriptor,
|
|
682
|
+
defaults: {
|
|
683
|
+
// A palette drop makes an AND gate on the densest lane geometry that was
|
|
684
|
+
// MEASURED to hold the full truth table (bore 3 / wall 3 / length 20 —
|
|
685
|
+
// a 20x27 px cell). `left`/`top` are intentionally absent: the
|
|
686
|
+
// ObjectFactory builder derives them from the drop point (F1 drop idiom).
|
|
687
|
+
logic: 'AND',
|
|
688
|
+
bore: 3,
|
|
689
|
+
wallThickness: 3,
|
|
690
|
+
length: 20,
|
|
691
|
+
supplyVoltage: 6,
|
|
692
|
+
windowTicks: 20,
|
|
693
|
+
color: [200, 170, 70]
|
|
694
|
+
},
|
|
695
|
+
palette: { displayName: 'Logic Gate', category: 'circuit', iconHint: 'gate', rank: 5 },
|
|
696
|
+
build: function (props, ctx, Class) {
|
|
697
|
+
var GateCls = Class; // the class that registered (a composed fork's own class)
|
|
698
|
+
var opts = Object.assign({}, props);
|
|
699
|
+
var bore = (opts.bore != null) ? opts.bore : 3;
|
|
700
|
+
var wt = (opts.wallThickness != null) ? opts.wallThickness : 3;
|
|
701
|
+
var len = (opts.length != null) ? opts.length : 20;
|
|
702
|
+
// Derive the cell origin from the drop point when the recipe declared no
|
|
703
|
+
// placement (F1 drop idiom — same as Wall/Tunnel). The cell is three lanes
|
|
704
|
+
// tall, so centring means backing off half of 3 x (bore + 2 x wallThickness).
|
|
705
|
+
if (opts.left == null) { opts.left = Math.round(ctx.x - len / 2); }
|
|
706
|
+
if (opts.top == null) { opts.top = Math.round(ctx.y - 1.5 * (bore + 2 * wt)); }
|
|
707
|
+
opts.bore = bore; opts.wallThickness = wt; opts.length = len;
|
|
708
|
+
var gate = new GateCls(ctx.runner.universe, opts);
|
|
709
|
+
gate._schemaKind = Class.name;
|
|
710
|
+
// The comparator is a HOST tick-fn: subscribe it to the runner's per-tick
|
|
711
|
+
// fan-out here, off the DECLARED fields, so the switching survives
|
|
712
|
+
// codegen → Restart (a recipe-authored tick-fn would be lost). Idempotent.
|
|
713
|
+
if (typeof gate.attachToRunner === 'function') { gate.attachToRunner(ctx.runner); }
|
|
714
|
+
return gate;
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
// ── Export ───────────────────────────────────────────────────────────────
|
|
719
|
+
export { LogicGate };
|
|
720
|
+
|