feat(mgc): rendezvous planner — same-time landing across all modes
Routes every dispatch through a tick-aware planner so all pumps reach
their setpoint at the same wall-clock instant t* = max(eta_i),
regardless of control strategy or per-pump reaction speed.
Architecture (src/movement/):
- machineProfile.js – pure snapshot of a registered child (state,
position, velocityPctPerS, ladder timings,
flowAt / positionForFlow). Reads timings from
child.state.config.time (the actual storage
location — previous fallback paths silently
produced 0 s, collapsing every eta to ramp-only).
- moveTrajectory.js – seconds-to-target per machine; handles
idle / starting / warmingup / operational / cooling.
- movementScheduler.js – t* = max eta over ALL non-noop moves. Every
command is delayed so its move finishes at t*.
Startup execsequence fires at 0; its flowmovement
is gated by max(ladderS, t* − rampS) so a fast
pump waits before ramping rather than landing
early. useRendezvous=false collapses to all
fireAtTickN=0 (legacy fire-and-forget).
- movementExecutor.js – wall-clock virtual cursor: each tick fires
every command whose fireAtTickN ≤ floor(elapsed/tickS).
tick() no longer awaits pending fireCommand
promises — the synchronous prologue of
handleInput claims the latest-wins gate, which
is what race-favouring relies on.
Shared dispatch path (src/specificClass.js):
- _dispatchFlowDistribution(distribution) — extracted from
_optimalControl. Builds profiles, calls movementScheduler.plan,
replans the executor, ticks once. Reads
config.planner.useRendezvous (default true).
- _optimalControl computes its bestCombination and hands off.
- equalFlowControl (priorityControl mode) computes its
flowDistribution and hands off via ctx.mgc._dispatchFlowDistribution.
Same-time landing now applies in BOTH modes.
Editor toggle (mgc.html + src/nodeClass.js):
- New "Same-time landing" checkbox under Control Strategy.
- nodeClass.buildDomainConfig bridges uiConfig.useRendezvous →
config.planner.useRendezvous. Default ON.
Tests:
- New: planner-convergence.integration.test.js (real-time end-to-end
diagnostic — drives a 3-pump mixed-state dispatch and asserts both
convergence to the demand setpoint AND same-time landing within
one tick).
- New: planner-rendezvous.integration.test.js (schedule-shape
assertions against real pump objects).
- New: movementScheduler.basic.test.js — includes a mixed-speed
multi-startup case proving the fast pumps wait so all three land
together (the regression that prompted this work).
- New: movementExecutor.basic.test.js + moveTrajectory.basic.test.js.
- Updated executor contract test: tick() must NOT await pending fires.
Commands + wiki:
- handlers.js: source/mode allow-list gate moved into a shared _gate()
helper; every command now checks isValidActionForMode +
isValidSourceForMode before dispatching. Status-level commands
(set.mode, set.scaling) are allowed in every mode.
- commands.basic.test.js: coverage for the new gate behaviour.
- wiki regen: Home.md visual-first rewrite + Reference-{Architecture,
Contracts,Examples,Limitations}.md split with _Sidebar.md index.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
34
src/editor/index.js
Normal file
34
src/editor/index.js
Normal file
@@ -0,0 +1,34 @@
|
||||
// machineGroupControl editor — namespace bootstrap.
|
||||
//
|
||||
// Attaches the editor's submodule registry to the shared
|
||||
// window.EVOLV.nodes.machineGroupControl namespace (same one the menuManager
|
||||
// and configManager endpoints populate). Each sibling module in this
|
||||
// directory (mode-cards.js, demand-contract.js, oneditprepare.js) registers
|
||||
// itself by writing additional members onto this namespace.
|
||||
//
|
||||
// Loaded first by mgc.html — must not depend on any other src/editor module.
|
||||
|
||||
(function () {
|
||||
const root = window.EVOLV = window.EVOLV || {};
|
||||
const nodes = root.nodes = root.nodes || {};
|
||||
const ns = nodes.machineGroupControl = nodes.machineGroupControl || {};
|
||||
const editor = ns.editor = ns.editor || {};
|
||||
|
||||
// Pub/sub for mode changes — mode-cards.js fires, anything that wants to
|
||||
// re-render on mode change subscribes. Keep it tiny; no third-party emitter.
|
||||
const modeListeners = [];
|
||||
editor.onModeChange = (cb) => { if (typeof cb === 'function') modeListeners.push(cb); };
|
||||
editor.emitModeChange = (newMode) => {
|
||||
for (const cb of modeListeners) {
|
||||
try { cb(newMode); } catch (e) { /* swallow — UI helper */ }
|
||||
}
|
||||
};
|
||||
|
||||
// Read the currently selected mode from the hidden input that mode-cards.js
|
||||
// keeps in sync with the active card. Falls back to optimalControl if the
|
||||
// input isn't on the page yet (race against oneditprepare).
|
||||
editor.getMode = () => {
|
||||
const el = document.getElementById('node-input-mode');
|
||||
return (el && el.value) || 'optimalControl';
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user