@bramas/simulant-engine 0.0.0-stage → 0.1.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/dist/system.js ADDED
@@ -0,0 +1,607 @@
1
+ /**
2
+ * An experiment with concrete parameters: its initial configurations, the
3
+ * successors of a configuration under the model's round, and the task's
4
+ * verdict (plan, Sections 3.7 and 6.4).
5
+ */
6
+ import { check } from "@bramas/simulant-lang/check";
7
+ import { T } from "@bramas/simulant-lang/types";
8
+ import { Compiler, Runtime, } from "./compile.js";
9
+ import { DynamicRing, Ring } from "./topology.js";
10
+ import { encoder, equality, show } from "./values.js";
11
+ export class IRError extends Error {
12
+ errors;
13
+ constructor(errors) {
14
+ super(errors.join('\n'));
15
+ this.errors = errors;
16
+ }
17
+ }
18
+ function frameOf(fn, args) {
19
+ const fr = new Array(fn.nslots);
20
+ for (let i = 0; i < args.length; i++)
21
+ fr[i] = args[i];
22
+ return fr;
23
+ }
24
+ export function runDet(fn, args) {
25
+ if (!fn.det)
26
+ throw new Error('expected a deterministic expression');
27
+ return fn.det(frameOf(fn, args));
28
+ }
29
+ export function runND(fn, args, k) {
30
+ const fr = frameOf(fn, args);
31
+ if (fn.det)
32
+ k(fn.det(fr));
33
+ else
34
+ fn.nd(fr, k);
35
+ }
36
+ export function makeTopology(exp, size) {
37
+ const t = exp.model.topology;
38
+ return t.kind === 'ring' ? new Ring(size) : new DynamicRing(size, t.k);
39
+ }
40
+ const MOVES = ['stay', 'cw', 'ccw'];
41
+ export class System {
42
+ exp;
43
+ params;
44
+ rt;
45
+ lay;
46
+ topo;
47
+ /** Anonymous agents are stored as a sorted multiset; under SSYNC (or when asked) they keep their identity. */
48
+ anonymous;
49
+ compiler;
50
+ /** Compiled roots, with the values of their component's parameters (passed after the root's own variables). */
51
+ sensors;
52
+ sensorNames;
53
+ hooks;
54
+ step;
55
+ verdictFn;
56
+ lineFn;
57
+ agentKey;
58
+ worldKey;
59
+ decisionKey;
60
+ actionDefaults;
61
+ showFrame;
62
+ /** While set, successors record their agents' inputs (for `explain`). */
63
+ recording = false;
64
+ traced;
65
+ constructor(exp, params, opts = {}) {
66
+ this.exp = exp;
67
+ this.params = params;
68
+ const checked = check(exp);
69
+ if (checked.errors.length)
70
+ throw new IRError(checked.errors);
71
+ const lay = this.lay = checked.layout;
72
+ const rt = this.rt = new Runtime(lay);
73
+ for (const p of exp.params) {
74
+ if (!(p.name in params))
75
+ throw new Error(`missing parameter ${p.name}`);
76
+ }
77
+ rt.params = params;
78
+ const paramTypes = new Map(exp.params.map(p => [p.name, p.type]));
79
+ const compiler = this.compiler = new Compiler(rt, lay, paramTypes);
80
+ const { model, algorithm, task } = exp;
81
+ const defsOf = (...lists) => new Map(lists.flatMap(l => l ?? []).map(d => [d.name, d]));
82
+ this.topo = rt.topo = makeTopology(exp, runDet(compiler.root(model.topology.size, [], defsOf(model.defs)), []));
83
+ this.anonymous = opts.sortAgents ?? model.round === 'fsync';
84
+ const sensorVars = [['C', T.config], ['me', T.agent], ['frame', T.frame]];
85
+ const sensorDefs = new Map(lay.sensors.map(s => {
86
+ const owner = model.components.find(c => c.name === s.owner);
87
+ const [vars, args] = componentParams(owner);
88
+ return [s.name, { fn: compiler.root(s.body, [...sensorVars, ...vars], defsOf(model.defs, owner?.defs)), args }];
89
+ }));
90
+ this.sensorNames = algorithm.uses;
91
+ this.sensors = algorithm.uses.map(name => sensorDefs.get(name));
92
+ this.hooks = { beforeLook: [], beforeMove: [], afterMove: [] };
93
+ for (const c of model.components) {
94
+ const [vars, args] = componentParams(c);
95
+ for (const hook of ['beforeLook', 'beforeMove', 'afterMove']) {
96
+ const body = c[hook];
97
+ if (body)
98
+ this.hooks[hook].push({ fn: compiler.root(body, [['C', T.config], ...vars], defsOf(model.defs, c.defs)), args });
99
+ }
100
+ }
101
+ this.actionDefaults = Object.fromEntries(lay.actions.map(a => [a.name, a.default]));
102
+ if (algorithm.native) {
103
+ const native = opts.natives?.[algorithm.native];
104
+ if (!native)
105
+ throw new Error(`native algorithm ${algorithm.native} is not registered`);
106
+ this.step = native;
107
+ }
108
+ else {
109
+ const stepFn = compiler.root(algorithm.step, [['mem', algorithm.mem], ['pub', algorithm.pub], ['view', lay.view]], defsOf(model.defs, algorithm.defs));
110
+ const step = (mem, pub, view) => runDet(stepFn, [mem, pub, view]);
111
+ if (opts.memoStep === false)
112
+ this.step = step;
113
+ else {
114
+ // Steps are deterministic: an algorithm that computes much (views of a whole ring) runs once per input.
115
+ const [memKey, pubKey, viewKey] = [encoder(algorithm.mem), encoder(lay.pub), encoder(lay.view)];
116
+ const memo = new Map();
117
+ this.step = (mem, pub, view) => {
118
+ const key = `${memKey(mem)}|${pubKey(pub)}|${viewKey(view)}`;
119
+ let out = memo.get(key);
120
+ if (!out) {
121
+ if (memo.size >= 1 << 17)
122
+ memo.clear();
123
+ out = step(mem, pub, view);
124
+ memo.set(key, out);
125
+ }
126
+ return out;
127
+ };
128
+ }
129
+ }
130
+ if (algorithm.line)
131
+ this.lineFn = compiler.root(algorithm.line, [['mem', algorithm.mem]], defsOf(model.defs, algorithm.defs));
132
+ this.verdictFn = compiler.root(task.verdict, [['C', T.config]], defsOf(model.defs, task.defs));
133
+ const pos = encoder(T.node);
134
+ const fields = [
135
+ ...lay.flags.map(f => (a) => a.f[f.name] ? '1' : '0'),
136
+ ...lay.agentFields.map(f => { const e = encoder(f.type); return (a) => e(a.f[f.name]); }),
137
+ ];
138
+ const pubKey = encoder(lay.pub);
139
+ const memKey = encoder(lay.mem);
140
+ this.agentKey = a => {
141
+ let s = pos(a.pos) + '|';
142
+ for (const f of fields)
143
+ s += f(a);
144
+ return s + '|' + pubKey(a.pub) + '|' + memKey(a.mem);
145
+ };
146
+ this.worldKey = encoder(lay.worldType);
147
+ this.decisionKey = encoder(lay.decision);
148
+ this.showFrame = show(T.frame);
149
+ }
150
+ // -------------------------------------------------------------------------
151
+ // Configurations
152
+ key(C) {
153
+ let s = '';
154
+ for (const a of C.agents)
155
+ s += this.agentKey(a) + ';';
156
+ return s + '#' + this.worldKey(C.world);
157
+ }
158
+ /** Sorts anonymous agents, so that no order leaks into state equality. */
159
+ canonical(C) {
160
+ return this.canonicalOrder(C).config;
161
+ }
162
+ /** `canonical`, with the index in `C` of each agent of the result. */
163
+ canonicalOrder(C) {
164
+ const identity = C.agents.map((_, i) => i);
165
+ if (!this.anonymous || C.agents.length < 2)
166
+ return { config: C, order: identity };
167
+ const keyed = C.agents.map((a, i) => [this.agentKey(a), a, i]);
168
+ keyed.sort((x, y) => x[0] < y[0] ? -1 : x[0] > y[0] ? 1 : 0);
169
+ return { config: { agents: keyed.map(([, a]) => a), world: C.world }, order: keyed.map(([, , i]) => i) };
170
+ }
171
+ /** Why the agent at `index` in the successor of `C` with key `key` took its decision (see `explainAll`). */
172
+ explain(C, key, index) {
173
+ return this.explainAll(C, key)[index];
174
+ }
175
+ /**
176
+ * Why each agent of the successor of `C` with key `key` (by index in it) took its decision: the
177
+ * step is replayed with the same inputs, by a traced compilation of the algorithm. Undefined for
178
+ * agents that were not activated (or did not decide) in that step.
179
+ */
180
+ explainAll(C, key) {
181
+ this.recording = true;
182
+ let succ;
183
+ try {
184
+ succ = this.successors(C).find(s => s.key === key);
185
+ }
186
+ finally {
187
+ this.recording = false;
188
+ }
189
+ if (!succ?.order || !succ.inputs)
190
+ return [];
191
+ return succ.order.map(from => {
192
+ const input = succ.inputs[from];
193
+ return input && this.explainStep(C.agents[from], input);
194
+ });
195
+ }
196
+ explainStep(a, input) {
197
+ const { rt, exp, lay } = this;
198
+ const view = Object.fromEntries(lay.view.k === 'record'
199
+ ? lay.view.fields.map(f => [f.name, show(f.type, true)(input.view[f.name])])
200
+ : Object.entries(input.view).map(([k, v]) => [k, String(v)]));
201
+ if (exp.algorithm.native) {
202
+ const local = this.step(a.mem, a.pub, input.view);
203
+ return { frame: this.showFrame(input.frame), view, move: String(local.move), calls: [] };
204
+ }
205
+ if (!this.traced) {
206
+ const { model, algorithm } = exp;
207
+ const compiler = new Compiler(rt, lay, new Map(exp.params.map(p => [p.name, p.type])), true);
208
+ const defs = new Map([...(model.defs ?? []), ...(algorithm.defs ?? [])].map(d => [d.name, d]));
209
+ const fn = compiler.root(algorithm.step, [['mem', algorithm.mem], ['pub', algorithm.pub], ['view', lay.view]], defs);
210
+ this.traced = (mem, pub, v) => runDet(fn, [mem, pub, v]);
211
+ }
212
+ rt.trace = { stack: [[]], count: 0 };
213
+ try {
214
+ const local = this.traced(a.mem, a.pub, input.view);
215
+ return { line: rt.trace.line, frame: this.showFrame(input.frame), view, move: String(local.move), calls: rt.trace.stack[0] };
216
+ }
217
+ finally {
218
+ rt.trace = undefined;
219
+ }
220
+ }
221
+ verdict(C) {
222
+ return runDet(this.verdictFn, [C]);
223
+ }
224
+ line(a) {
225
+ return this.lineFn && runDet(this.lineFn, [a.mem]);
226
+ }
227
+ // -------------------------------------------------------------------------
228
+ // Initial configurations
229
+ /** The ring size under other parameter values. */
230
+ sizeWith(params) {
231
+ const saved = this.rt.params;
232
+ this.rt.params = params;
233
+ try {
234
+ const defs = new Map((this.exp.model.defs ?? []).map(d => [d.name, d]));
235
+ return runDet(this.compiler.root(this.exp.model.topology.size, [], defs), []);
236
+ }
237
+ finally {
238
+ this.rt.params = saved;
239
+ }
240
+ }
241
+ /**
242
+ * The initial configurations under other parameter values with the same ring size. Parameters
243
+ * reach only the initialisation and the topology size (the checker enforces it), so this system
244
+ * explores them exactly as `new System(exp, params)` would.
245
+ */
246
+ initialWith(params) {
247
+ if (this.sizeWith(params) !== this.topo.size)
248
+ throw new Error('initialWith: another ring size needs another system');
249
+ const saved = this.rt.params;
250
+ this.rt.params = params;
251
+ try {
252
+ return this.initial();
253
+ }
254
+ finally {
255
+ this.rt.params = saved;
256
+ }
257
+ }
258
+ initial() {
259
+ const { exp, compiler, lay, rt } = this;
260
+ const defs = new Map((exp.model.defs ?? []).map(d => [d.name, d]));
261
+ const algDefs = new Map([...(exp.model.defs ?? []), ...(exp.algorithm.defs ?? [])].map(d => [d.name, d]));
262
+ const k = runDet(compiler.root(exp.agents.count, [], defs), []);
263
+ const posFn = compiler.root(exp.agents.pos, [['i', T.nat]], defs);
264
+ const pubFn = compiler.root(exp.algorithm.init.pub, [['i', T.nat]], algDefs);
265
+ const memFn = compiler.root(exp.algorithm.init.mem, [['i', T.nat]], algDefs);
266
+ const flags = Object.fromEntries(lay.flags.map(f => [f.name, false]));
267
+ const base = Array.from({ length: k }, (_, i) => ({
268
+ pos: runDet(posFn, [i]) % this.topo.size, pub: runDet(pubFn, [i]), mem: runDet(memFn, [i]), f: { ...flags },
269
+ }));
270
+ const slots = [];
271
+ for (const c of exp.model.components) {
272
+ const cdefs = new Map([...(exp.model.defs ?? []), ...(c.defs ?? [])].map(d => [d.name, d]));
273
+ const [vars, args] = componentParams(c);
274
+ for (const f of c.agent ?? []) {
275
+ const fn = compiler.root(c.init.agent[f.name], [['i', T.nat], ...vars], cdefs);
276
+ for (let i = 0; i < k; i++)
277
+ slots.push({ fn, args: [i, ...args], agent: i, set: (agents, _w, v) => { agents[i].f[f.name] = v; } });
278
+ }
279
+ }
280
+ for (const c of exp.model.components) {
281
+ const cdefs = new Map([...(exp.model.defs ?? []), ...(c.defs ?? [])].map(d => [d.name, d]));
282
+ const [vars, args] = componentParams(c);
283
+ for (const f of c.world ?? []) {
284
+ const fn = compiler.root(c.init.world[f.name], vars, cdefs);
285
+ slots.push({ fn, args, set: (_a, world, v) => { world[f.name] = v; } });
286
+ }
287
+ }
288
+ const out = new Map();
289
+ const values = [];
290
+ rt.witness = [];
291
+ const go = (j) => {
292
+ if (j === slots.length) {
293
+ const agents = base.map(a => ({ ...a, f: { ...a.f } }));
294
+ const world = {};
295
+ slots.forEach((s, i) => s.set(agents, world, values[i]));
296
+ const config = this.canonical({ agents, world });
297
+ const key = this.key(config);
298
+ if (!out.has(key))
299
+ out.set(key, { config, key, witness: rt.witness.slice(), activated: new Set() });
300
+ return;
301
+ }
302
+ const s = slots[j];
303
+ const depth = rt.witness.length;
304
+ runND(s.fn, s.args, v => {
305
+ values[j] = v;
306
+ if (s.agent !== undefined)
307
+ for (let i = depth; i < rt.witness.length; i++)
308
+ rt.witness[i] = prefix(rt.witness[i], s.agent);
309
+ go(j + 1);
310
+ });
311
+ };
312
+ go(0);
313
+ return [...out.values()];
314
+ }
315
+ // -------------------------------------------------------------------------
316
+ // The standard round (plan 3.7)
317
+ successors(C) {
318
+ const { rt, topo } = this;
319
+ const out = new Map();
320
+ const states = topo.states();
321
+ rt.witness = [];
322
+ states.forEach(({ state, label }, si) => {
323
+ // 1. The topology state, then beforeLook.
324
+ rt.topoState = state;
325
+ if (states.length > 1)
326
+ rt.witness.push({ site: 'topology', index: si, label });
327
+ this.parallel(C, this.hooks.beforeLook, eff => {
328
+ const C1 = this.apply(C, eff);
329
+ // 2. Activation.
330
+ this.activate(C1, active => {
331
+ // 3. Frames, look and compute.
332
+ this.decide(C1, active, (decisions, inputs) => {
333
+ rt.decisions = decisions;
334
+ // 4. beforeMove, with the decisions.
335
+ this.parallel(C1, this.hooks.beforeMove, eff2 => {
336
+ // 5. Move.
337
+ const C3 = this.move(this.apply(C1, eff2), decisions);
338
+ // 6. afterMove.
339
+ this.parallel(C3, this.hooks.afterMove, eff3 => {
340
+ const { config, order } = this.canonicalOrder(this.apply(C3, eff3));
341
+ const key = this.key(config);
342
+ const known = out.get(key);
343
+ if (known)
344
+ for (const i of active)
345
+ known.activated.add(i);
346
+ else
347
+ out.set(key, {
348
+ config, key, witness: rt.witness.slice(), activated: new Set(active),
349
+ ...(this.recording ? { inputs: inputs.slice(), order } : {}),
350
+ });
351
+ });
352
+ });
353
+ });
354
+ });
355
+ });
356
+ if (states.length > 1)
357
+ rt.witness.pop();
358
+ });
359
+ return [...out.values()];
360
+ }
361
+ /** All hooks of a hook point read the same snapshot; their effects are merged. */
362
+ parallel(C, hooks, k) {
363
+ if (!hooks.length)
364
+ return k(undefined);
365
+ const effects = new Array(hooks.length);
366
+ const go = (i) => {
367
+ if (i === hooks.length)
368
+ return k(merge(effects));
369
+ runND(hooks[i].fn, [C, ...hooks[i].args], e => { effects[i] = e; go(i + 1); });
370
+ };
371
+ go(0);
372
+ }
373
+ apply(C, eff) {
374
+ if (!eff)
375
+ return C;
376
+ const worldNames = Object.keys(eff.world);
377
+ const world = worldNames.length ? { ...C.world, ...eff.world } : C.world;
378
+ let agents = C.agents;
379
+ for (const [flag, marks] of Object.entries(eff.raise)) {
380
+ for (let i = 0; i < agents.length; i++) {
381
+ if (!marks[i] || agents[i].f[flag])
382
+ continue;
383
+ if (agents === C.agents)
384
+ agents = agents.slice();
385
+ agents[i] = { ...agents[i], f: { ...agents[i].f, [flag]: true } };
386
+ }
387
+ }
388
+ return agents === C.agents && world === C.world ? C : { agents, world };
389
+ }
390
+ /** FSYNC: all eligible agents; SSYNC: any nonempty subset; sequential: any singleton. */
391
+ activate(C, k) {
392
+ const eligible = C.agents.flatMap((a, i) => this.rt.acts(a) === 'never' ? [] : [i]);
393
+ if (this.exp.model.round === 'fsync' || eligible.length === 0)
394
+ return k(eligible);
395
+ const w = this.rt.witness;
396
+ let index = 0;
397
+ const maxSize = this.exp.model.round === 'sequential' ? 1 : eligible.length;
398
+ for (let size = 1; size <= maxSize; size++) {
399
+ const pick = (from, current) => {
400
+ if (current.length === size) {
401
+ w.push({ site: 'activation', index: index++, label: `activates ${current.map(i => `a${i}`).join(', ')}` });
402
+ k(current.slice());
403
+ w.pop();
404
+ return;
405
+ }
406
+ for (let i = from; i < eligible.length; i++) {
407
+ current.push(eligible[i]);
408
+ pick(i + 1, current);
409
+ current.pop();
410
+ }
411
+ };
412
+ pick(0, []);
413
+ }
414
+ }
415
+ idle(a) {
416
+ return { decided: false, move: 'stay', pub: a.pub, mem: a.mem, ...this.actionDefaults };
417
+ }
418
+ /** The product, over the activated agents, of their possible decisions (with the inputs of each, by agent). */
419
+ decide(C, active, k) {
420
+ const options = active.map(i => this.options(C, i));
421
+ const decisions = C.agents.map(a => this.idle(a));
422
+ const inputs = C.agents.map(() => undefined);
423
+ const w = this.rt.witness;
424
+ const go = (j) => {
425
+ if (j === active.length)
426
+ return k(decisions.slice(), inputs);
427
+ for (const { d, w: parts, input } of options[j]) {
428
+ decisions[active[j]] = d;
429
+ inputs[active[j]] = input;
430
+ for (const p of parts)
431
+ w.push(p);
432
+ go(j + 1);
433
+ w.length -= parts.length;
434
+ }
435
+ };
436
+ go(0);
437
+ }
438
+ frameChoices(a) {
439
+ const fm = this.exp.model.frame;
440
+ if (fm.kind === 'common')
441
+ return [this.topo.identityFrame];
442
+ if (fm.kind === 'dynamicReflection')
443
+ return this.topo.frames;
444
+ return [a.f[fm.field]];
445
+ }
446
+ /**
447
+ * The decisions an agent may take: one per frame and per outcome of its
448
+ * sensors' choices, deduplicated by their effect (global move, new state,
449
+ * actions), keeping the first witness.
450
+ */
451
+ options(C, i) {
452
+ const { rt } = this;
453
+ const a = C.agents[i];
454
+ if (rt.acts(a) === 'adversary') {
455
+ return MOVES.map((move, index) => ({
456
+ d: { ...this.idle(a), decided: true, move },
457
+ w: [{ site: `adversary a${i}`, index, label: `a${i} (adversary): ${move}` }],
458
+ }));
459
+ }
460
+ const saved = rt.witness;
461
+ const found = new Map();
462
+ const frames = this.frameChoices(a);
463
+ frames.forEach((frame, fi) => {
464
+ rt.witness = frames.length > 1 ? [{ site: `frame a${i}`, index: fi, label: `a${i}: ${this.showFrame(frame)}` }] : [];
465
+ const depth = rt.witness.length;
466
+ this.view(C, i, frame, view => {
467
+ const local = this.step(a.mem, a.pub, view);
468
+ const d = {
469
+ ...this.actionDefaults, ...local, decided: true,
470
+ move: this.topo.toGlobal(frame, local.move), pub: local.pub ?? a.pub, mem: local.mem ?? a.mem,
471
+ };
472
+ const key = this.decisionKey(d);
473
+ if (!found.has(key)) {
474
+ found.set(key, { d, w: rt.witness.map((c, j) => j < depth ? c : prefix(c, i)), ...(this.recording ? { input: { frame, view } } : {}) });
475
+ }
476
+ });
477
+ });
478
+ rt.witness = saved;
479
+ return [...found.values()];
480
+ }
481
+ /** The sensors of an activation, evaluated once each (with their choices). */
482
+ view(C, i, frame, k) {
483
+ const { sensors, sensorNames } = this;
484
+ const view = {};
485
+ const go = (j) => {
486
+ if (j === sensors.length)
487
+ return k({ ...view });
488
+ runND(sensors[j].fn, [C, i, frame, ...sensors[j].args], v => { view[sensorNames[j]] = v; go(j + 1); });
489
+ };
490
+ go(0);
491
+ }
492
+ /** Rigid movement along the edges present in this round; only agents that still act apply their decision. */
493
+ move(C, decisions) {
494
+ const { rt, topo } = this;
495
+ let changed = false;
496
+ const agents = C.agents.map((a, i) => {
497
+ const d = decisions[i];
498
+ if (!d.decided || rt.acts(a) === 'never')
499
+ return a;
500
+ const pos = d.move !== 'stay' && topo.canMove(rt.topoState, a.pos, d.move) ? topo.target(a.pos, d.move) : a.pos;
501
+ if (pos === a.pos && d.pub === a.pub && d.mem === a.mem)
502
+ return a;
503
+ changed = true;
504
+ return { ...a, pos, pub: d.pub, mem: d.mem };
505
+ });
506
+ return changed ? { agents, world: C.world } : C;
507
+ }
508
+ }
509
+ /** A component's parameters, as extra root variables, and their values. */
510
+ function componentParams(c) {
511
+ const params = c?.params ?? [];
512
+ return [params.map(p => [p.name, p.type]), params.map(p => c.args[p.name])];
513
+ }
514
+ function prefix(c, agent) {
515
+ return { ...c, label: `a${agent}: ${c.label}` };
516
+ }
517
+ function merge(effects) {
518
+ if (effects.length === 1)
519
+ return effects[0];
520
+ const eff = { world: {}, raise: {} };
521
+ for (const e of effects) {
522
+ Object.assign(eff.world, e.world);
523
+ for (const [flag, marks] of Object.entries(e.raise)) {
524
+ const into = eff.raise[flag] ??= [];
525
+ marks.forEach((m, i) => { if (m)
526
+ into[i] = true; });
527
+ }
528
+ }
529
+ return eff;
530
+ }
531
+ /**
532
+ * The values of the experiment's parameters, for `check-all`: each domain
533
+ * is evaluated with the earlier parameters fixed.
534
+ */
535
+ export function* parameterValues(exp) {
536
+ const checked = check(exp);
537
+ if (checked.errors.length)
538
+ throw new IRError(checked.errors);
539
+ const rt = new Runtime(checked.layout);
540
+ const compiler = new Compiler(rt, checked.layout, new Map(exp.params.map(p => [p.name, p.type])));
541
+ const defs = new Map((exp.model.defs ?? []).map(d => [d.name, d]));
542
+ const size = compiler.root(exp.model.topology.size, [], defs);
543
+ const domains = exp.params.map(p => p.domain && compiler.root(p.domain, [], defs));
544
+ function* go(i, params) {
545
+ if (i === exp.params.length) {
546
+ yield { ...params };
547
+ return;
548
+ }
549
+ const domain = domains[i];
550
+ if (!domain)
551
+ throw new Error(`parameter ${exp.params[i].name} has no domain`);
552
+ rt.params = params;
553
+ try {
554
+ rt.topo = makeTopology(exp, runDet(size, []));
555
+ }
556
+ catch {
557
+ // The size depends on a later parameter: domains cannot use the topology yet.
558
+ }
559
+ for (const v of runDet(domain, []))
560
+ yield* go(i + 1, { ...params, [exp.params[i].name]: v });
561
+ }
562
+ yield* go(0, {});
563
+ }
564
+ /** Parameters outside their declared domains, without enumerating the settings product. */
565
+ export function parameterDomainViolations(exp, params) {
566
+ const checked = check(exp);
567
+ if (checked.errors.length)
568
+ throw new IRError(checked.errors);
569
+ const rt = new Runtime(checked.layout);
570
+ rt.params = params;
571
+ const compiler = new Compiler(rt, checked.layout, new Map(exp.params.map(p => [p.name, p.type])));
572
+ const defs = new Map((exp.model.defs ?? []).map(d => [d.name, d]));
573
+ rt.topo = makeTopology(exp, runDet(compiler.root(exp.model.topology.size, [], defs), []));
574
+ return exp.params.filter(p => {
575
+ // Parameters without a domain are supplied by the user and impose no domain restriction.
576
+ if (!p.domain)
577
+ return false;
578
+ const values = runDet(compiler.root(p.domain, [], defs), []);
579
+ const equal = equality(p.type);
580
+ return !values.some(value => equal(value, params[p.name]));
581
+ }).map(p => p.name);
582
+ }
583
+ const definitionCache = new WeakMap();
584
+ /**
585
+ * A definition of an experiment (`def f (x : T) : R := e` in `.sim`), as a function on values:
586
+ * renderers and tests reuse the algorithm's own definitions. Deterministic definitions only.
587
+ */
588
+ export function definition(exp, name) {
589
+ let cache = definitionCache.get(exp);
590
+ if (!cache)
591
+ definitionCache.set(exp, cache = new Map());
592
+ let f = cache.get(name);
593
+ if (!f) {
594
+ const checked = check(exp);
595
+ if (checked.errors.length)
596
+ throw new IRError(checked.errors);
597
+ const compiler = new Compiler(new Runtime(checked.layout), checked.layout, new Map(exp.params.map(p => [p.name, p.type])));
598
+ const defs = new Map([...(exp.model.defs ?? []), ...(exp.algorithm.defs ?? []), ...(exp.task.defs ?? [])].map(d => [d.name, d]));
599
+ const def = defs.get(name);
600
+ if (!def)
601
+ throw new Error(`${exp.name} has no definition ${name}`);
602
+ const fn = compiler.root(def.body, def.params.map(p => [p.name, p.type]), defs);
603
+ f = (...args) => runDet(fn, args);
604
+ cache.set(name, f);
605
+ }
606
+ return f;
607
+ }
@@ -0,0 +1,82 @@
1
+ const flip = (m) => m === 'cw' ? 'ccw' : m === 'ccw' ? 'cw' : 'stay';
2
+ export function opposite(m) {
3
+ return flip(m);
4
+ }
5
+ export class Ring {
6
+ size;
7
+ kind = 'ring';
8
+ frames = [false, true];
9
+ identityFrame = false;
10
+ constructor(size) {
11
+ this.size = size;
12
+ if (!Number.isInteger(size) || size < 2)
13
+ throw new Error(`a ring needs at least 2 nodes, got ${size}`);
14
+ }
15
+ states() {
16
+ return [{ state: null, label: '' }];
17
+ }
18
+ target(n, m) {
19
+ const N = this.size;
20
+ return m === 'cw' ? (n + 1) % N : m === 'ccw' ? (n + N - 1) % N : n;
21
+ }
22
+ canMove(_state, _n, _m) {
23
+ return true;
24
+ }
25
+ offset(n, f, i) {
26
+ const N = this.size;
27
+ return f ? ((n - i) % N + N) % N : (n + i) % N;
28
+ }
29
+ toGlobal(f, m) {
30
+ return f ? flip(m) : m;
31
+ }
32
+ automorphisms() {
33
+ const N = this.size;
34
+ const make = (k, reflects) => ({
35
+ label: reflects ? `reflection ${k}` : `rotation ${k}`,
36
+ reflects,
37
+ node: reflects ? n => ((k - n) % N + N) % N : n => (n + k) % N,
38
+ frame: reflects ? f => !f : f => f,
39
+ inverse: () => reflects ? make(k, true) : make((N - k) % N, false),
40
+ });
41
+ return [...Array.from({ length: N }, (_, k) => make(k, false)), ...Array.from({ length: N }, (_, k) => make(k, true))];
42
+ }
43
+ }
44
+ /**
45
+ * A ring where, at the start of every round, the adversary removes up to `k`
46
+ * edges. Edge `i` joins nodes `i` and `i + 1`. The state is the sorted list
47
+ * of missing edges; it is not kept between rounds.
48
+ */
49
+ export class DynamicRing extends Ring {
50
+ k;
51
+ kind = 'dynamicRing';
52
+ choices;
53
+ constructor(size, k) {
54
+ super(size);
55
+ this.k = k;
56
+ const N = size;
57
+ const sets = [[]];
58
+ const extend = (from, current) => {
59
+ for (let e = from; e < N; e++) {
60
+ const next = [...current, e];
61
+ if (next.length > k)
62
+ continue;
63
+ sets.push(next);
64
+ extend(e + 1, next);
65
+ }
66
+ };
67
+ extend(0, []);
68
+ this.choices = sets.map(state => ({
69
+ state,
70
+ label: state.length ? `missing ${state.map(e => `u${e}–u${(e + 1) % N}`).join(', ')}` : 'all edges present',
71
+ }));
72
+ }
73
+ states() {
74
+ return this.choices;
75
+ }
76
+ canMove(state, n, m) {
77
+ if (m === 'stay' || !state.length)
78
+ return true;
79
+ const edge = m === 'cw' ? n : (n + this.size - 1) % this.size;
80
+ return !state.includes(edge);
81
+ }
82
+ }