@bramas/simulant-ui 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.
Files changed (53) hide show
  1. package/LICENCE.txt +515 -0
  2. package/dist/SimulatorApp.jsx +601 -0
  3. package/dist/components/AlgorithmViewer.css +270 -0
  4. package/dist/components/AlgorithmViewer.jsx +218 -0
  5. package/dist/components/BranchPicker.css +96 -0
  6. package/dist/components/BranchPicker.jsx +65 -0
  7. package/dist/components/ConfigGraphGenerator.css +129 -0
  8. package/dist/components/ConfigGraphGenerator.jsx +408 -0
  9. package/dist/components/GenericEditor.jsx +228 -0
  10. package/dist/components/LeanView.jsx +133 -0
  11. package/dist/components/ModelView.css +520 -0
  12. package/dist/components/ModelView.jsx +272 -0
  13. package/dist/components/SettingsLibrary.jsx +61 -0
  14. package/dist/components/StateInspector.css +136 -0
  15. package/dist/components/StateInspector.jsx +103 -0
  16. package/dist/components/highlight.js +99 -0
  17. package/dist/pico.min.css +4 -0
  18. package/dist/renderers/define.js +28 -0
  19. package/dist/renderers/ring.js +103 -0
  20. package/dist/sim/branches.js +28 -0
  21. package/dist/sim/describe.js +408 -0
  22. package/dist/sim/execution.js +42 -0
  23. package/dist/sim/project.js +349 -0
  24. package/dist/sim/snapshot.js +29 -0
  25. package/dist/sim/types.js +1 -0
  26. package/dist/simulator.css +593 -0
  27. package/package.json +50 -3
  28. package/src/SimulatorApp.tsx +661 -0
  29. package/src/components/AlgorithmViewer.css +270 -0
  30. package/src/components/AlgorithmViewer.tsx +256 -0
  31. package/src/components/BranchPicker.css +96 -0
  32. package/src/components/BranchPicker.tsx +90 -0
  33. package/src/components/ConfigGraphGenerator.css +129 -0
  34. package/src/components/ConfigGraphGenerator.tsx +448 -0
  35. package/src/components/GenericEditor.tsx +413 -0
  36. package/src/components/LeanView.tsx +162 -0
  37. package/src/components/ModelView.css +520 -0
  38. package/src/components/ModelView.tsx +317 -0
  39. package/src/components/SettingsLibrary.tsx +84 -0
  40. package/src/components/StateInspector.css +136 -0
  41. package/src/components/StateInspector.tsx +125 -0
  42. package/src/components/highlight.ts +100 -0
  43. package/src/pico.min.css +4 -0
  44. package/src/renderers/define.ts +54 -0
  45. package/src/renderers/ring.ts +112 -0
  46. package/src/sim/branches.ts +30 -0
  47. package/src/sim/describe.ts +416 -0
  48. package/src/sim/execution.ts +65 -0
  49. package/src/sim/project.ts +377 -0
  50. package/src/sim/snapshot.ts +60 -0
  51. package/src/sim/types.ts +28 -0
  52. package/src/simulator.css +593 -0
  53. package/README.md +0 -3
@@ -0,0 +1,349 @@
1
+ import { parameterDomainViolations, parameterValues, System } from "@bramas/simulant-engine/system";
2
+ import { checkFirstBranch, checkSetting, checkSettings, stateGraph } from "@bramas/simulant-engine/check";
3
+ import { formatDiagnostic } from "@bramas/simulant-vite-plugin/module";
4
+ import { isRenderer } from "../renderers/define.js";
5
+ import { agentColor, drawRing, ringLayout } from "../renderers/ring.js";
6
+ import { describeExperiment } from "./describe.js";
7
+ import { simExecution } from "./execution.js";
8
+ import { Snapshot } from "./snapshot.js";
9
+ /** Settings with older parameter names renamed (see `SimProjectOptions.aliases`). */
10
+ export function renameSettings(settings, aliases = {}) {
11
+ if (!settings || typeof settings !== 'object')
12
+ return settings;
13
+ const out = {};
14
+ for (const [k, v] of Object.entries(settings)) {
15
+ const name = aliases[k];
16
+ if (name === undefined)
17
+ out[k] = v;
18
+ else if (!(name in settings))
19
+ out[name] = v;
20
+ }
21
+ return out;
22
+ }
23
+ /** Settings from the editor (or a URL) to parameter values of the right types. */
24
+ function coerce(value, t) {
25
+ switch (t.k) {
26
+ case 'nat':
27
+ case 'int':
28
+ case 'node': return typeof value === 'string' ? Number(value) : value;
29
+ case 'bool': return value === true || value === 'true';
30
+ case 'list':
31
+ case 'multiset':
32
+ case 'record':
33
+ case 'option': return typeof value === 'string' ? JSON.parse(value) : value;
34
+ default: return value;
35
+ }
36
+ }
37
+ /** Whether a setting is a value of the declared type (after coercion). */
38
+ function fits(value, t) {
39
+ let v;
40
+ try {
41
+ v = coerce(value, t);
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ switch (t.k) {
47
+ case 'nat':
48
+ case 'node': return Number.isInteger(v) && v >= 0;
49
+ case 'int': return Number.isInteger(v);
50
+ case 'bool': return typeof value === 'boolean' || value === 'true' || value === 'false';
51
+ case 'enum': return t.ctors.includes(v);
52
+ case 'string': return typeof v === 'string';
53
+ case 'list':
54
+ case 'multiset': return Array.isArray(v);
55
+ case 'option': return true;
56
+ case 'record': return typeof v === 'object' && v !== null && !Array.isArray(v);
57
+ default: return true;
58
+ }
59
+ }
60
+ /** The first parameter values of an experiment's domains (the form's defaults). */
61
+ function firstValues(exp) {
62
+ try {
63
+ return parameterValues(exp).next().value;
64
+ }
65
+ catch {
66
+ return undefined;
67
+ }
68
+ }
69
+ /**
70
+ * Settings for a new definition of the experiment (plan, Section 6.7): values
71
+ * of parameters that still exist with a compatible type are kept; the others
72
+ * are reset to their defaults, and each reset is noted.
73
+ */
74
+ export function adjustSettings(exp, settings) {
75
+ if (!settings)
76
+ return { settings, notes: [] };
77
+ const first = firstValues(exp) ?? {};
78
+ const notes = [];
79
+ const out = {};
80
+ for (const p of exp.params) {
81
+ if (!(p.name in settings)) {
82
+ out[p.name] = first[p.name];
83
+ notes.push(`new parameter ${p.name} = ${JSON.stringify(first[p.name])}`);
84
+ }
85
+ else if (fits(settings[p.name], p.type)) {
86
+ out[p.name] = settings[p.name];
87
+ }
88
+ else {
89
+ out[p.name] = first[p.name];
90
+ notes.push(`${p.name} reset from ${JSON.stringify(settings[p.name])} to ${JSON.stringify(first[p.name])}`);
91
+ }
92
+ }
93
+ for (const k of Object.keys(settings)) {
94
+ if (!exp.params.some(p => p.name === k))
95
+ notes.push(`parameter ${k} no longer exists`);
96
+ }
97
+ return { settings: out, notes };
98
+ }
99
+ function field(name, t, value) {
100
+ switch (t.k) {
101
+ case 'nat':
102
+ case 'int':
103
+ case 'node': return { type: 'number', key: name, label: name, defaultValue: Number(value ?? 0), ...(t.k === 'int' ? {} : { min: 0 }) };
104
+ case 'bool': return { type: 'boolean', key: name, label: name, defaultValue: !!value };
105
+ case 'enum': return { type: 'select', key: name, label: name, defaultValue: String(value ?? t.ctors[0]), options: t.ctors.map(c => ({ value: c, label: c })) };
106
+ default: return { type: 'text', key: name, label: `${name} (JSON)`, defaultValue: JSON.stringify(value ?? null) };
107
+ }
108
+ }
109
+ /** The algorithm's definition as written in its file (the viewer shows it as is, with its line numbers). */
110
+ export function algorithmSource(exp, sources) {
111
+ const src = exp.algorithm.source;
112
+ const text = src && sources[src.file];
113
+ const linesOf = (file, from, to) => {
114
+ const all = (sources[file] ?? '').split('\n');
115
+ const lines = [];
116
+ for (let n = from; n <= to; n++)
117
+ lines.push({ number: n, text: (all[n - 1] ?? '').trimEnd() });
118
+ while (lines.length && !lines[lines.length - 1].text)
119
+ lines.pop();
120
+ return lines;
121
+ };
122
+ const lines = src && text ? linesOf(src.file, src.line, src.endLine) : [];
123
+ // The definitions the algorithm uses, in file order.
124
+ const definitions = (src?.defs ?? [])
125
+ .filter(d => sources[d.file] !== undefined)
126
+ .sort((a, b) => a.file === b.file ? a.line - b.line : a.file < b.file ? -1 : 1)
127
+ .map(d => ({ name: d.name, file: d.file, lines: linesOf(d.file, d.line, d.endLine) }));
128
+ return { name: exp.algorithm.name, file: src?.file, lines, ...(definitions.length ? { definitions } : {}) };
129
+ }
130
+ export function simProject(module, experiment, opts = {}) {
131
+ const draw = opts.draw ?? drawRing;
132
+ const lazy = (f) => {
133
+ let done = false, value;
134
+ return () => { if (!done) {
135
+ value = f();
136
+ done = true;
137
+ } return value; };
138
+ };
139
+ const load = (m) => {
140
+ const exp = m.experiments[experiment];
141
+ if (!exp)
142
+ return undefined;
143
+ const first = lazy(() => firstValues(exp));
144
+ // Line information only affects the algorithm and model views: an edit that only moves lines redraws.
145
+ const { line: _line, source: _source, ...algorithm } = exp.algorithm;
146
+ const { source: _expSource, ...rest } = exp;
147
+ return {
148
+ // Lines of expressions (`at`) are display only, like the line table.
149
+ exp, key: JSON.stringify({ ...rest, algorithm }, (k, v) => k === 'at' ? undefined : v), first,
150
+ source: algorithmSource(exp, m.sources ?? {}),
151
+ description: lazy(() => describeExperiment(exp, m.sources ?? {}, m.revision, m.lean?.[experiment])),
152
+ editorSchema: lazy(() => ({ fields: exp.params.map(p => field(p.name, p.type, first()?.[p.name])) })),
153
+ };
154
+ };
155
+ const errors = (m) => {
156
+ const own = (m.diagnostics ?? []).filter(d => !d.experiment || d.experiment === experiment).map(formatDiagnostic);
157
+ return own.length ? own : [`no experiment ${experiment} in the module`];
158
+ };
159
+ let current = load(module);
160
+ let initialErrors = current ? [] : errors(module);
161
+ let revision = module.revision ?? '';
162
+ const listeners = new Set();
163
+ const emit = (u) => { for (const l of listeners)
164
+ l(u); };
165
+ module.subscribe?.(next => {
166
+ revision = next.revision ?? revision;
167
+ const loaded = load(next);
168
+ if (!loaded) {
169
+ emit({ kind: 'failed', revision, diagnostics: errors(next) });
170
+ return;
171
+ }
172
+ const before = current;
173
+ initialErrors = [];
174
+ current = loaded;
175
+ // Same semantics (at most lines moved): the execution goes on; the algorithm view takes the new lines.
176
+ if (before && before.key === loaded.key) {
177
+ emit({ kind: 'redraw' });
178
+ return;
179
+ }
180
+ else
181
+ emit({ kind: 'replaced', revision, adjust: settings => adjustSettings(loaded.exp, renameSettings(settings, opts.aliases)) });
182
+ });
183
+ if (isRenderer(draw))
184
+ draw.subscribe(() => emit({ kind: 'redraw' }));
185
+ const need = () => {
186
+ if (!current)
187
+ throw new Error(initialErrors.join('\n'));
188
+ return current;
189
+ };
190
+ const layouts = new WeakMap();
191
+ // Lines of agents of a system built before a line-only edit, from the current line table.
192
+ const lineSystems = new Map();
193
+ const lineOf = (snapshot, index) => {
194
+ const cur = need();
195
+ if (snapshot.system.exp === cur.exp)
196
+ return snapshot.agents[index].line;
197
+ const key = `${cur.key.length}:${JSON.stringify(snapshot.system.params)}`;
198
+ let sys = lineSystems.get(key);
199
+ if (!sys || sys.exp !== cur.exp) {
200
+ lineSystems.clear();
201
+ sys = new System(cur.exp, snapshot.system.params);
202
+ lineSystems.set(key, sys);
203
+ }
204
+ return sys.line(snapshot.config.agents[index]);
205
+ };
206
+ const inSource = (line) => {
207
+ const src = need().exp.algorithm.source;
208
+ return line !== undefined && (!src || (line >= src.line && line <= src.endLine)) ? line : undefined;
209
+ };
210
+ const normalizeSettings = (settings) => renameSettings(settings, opts.aliases);
211
+ const paramsOf = (given) => {
212
+ const { exp, first } = need();
213
+ const settings = normalizeSettings(given);
214
+ return Object.fromEntries(exp.params.map(p => [p.name, coerce(settings?.[p.name] ?? first()?.[p.name], p.type)]));
215
+ };
216
+ // Results carry the revision they were computed for, and their traces adapter paths.
217
+ const report = (r, rev) => ({
218
+ status: r.status, reason: r.reason, stats: r.stats, revision: rev,
219
+ ...(typeof r.settings === 'number' ? { settings: r.settings, graphs: r.graphs } : {}),
220
+ trace: r.trace && {
221
+ kind: r.trace.kind, settings: r.trace.settings, path: r.trace.path, cycleStart: r.trace.cycleStart,
222
+ steps: r.trace.steps.map(s => ({ label: s.label, verdict: s.verdict })),
223
+ },
224
+ });
225
+ const limits = (o = {}) => ({ maxDepth: o.maxDepth, maxStates: o.maxStates });
226
+ const setup = (settings) => {
227
+ const { exp } = need();
228
+ const params = paramsOf(settings);
229
+ const system = new System(exp, params);
230
+ return {
231
+ system, params,
232
+ info: {
233
+ experiment, algorithm: exp.algorithm.name, size: system.topo.size, params,
234
+ components: exp.model.components.map(c => c.name),
235
+ },
236
+ };
237
+ };
238
+ return {
239
+ setup: setup,
240
+ execute: (model) => simExecution(model.system),
241
+ normalizeSettings,
242
+ settingsWarnings: settings => {
243
+ const params = paramsOf(settings);
244
+ return parameterDomainViolations(need().exp, params)
245
+ .map(name => `${name} = ${JSON.stringify(params[name])} is outside its declared parameter domain.`);
246
+ },
247
+ subscribe: listener => {
248
+ listeners.add(listener);
249
+ return () => { listeners.delete(listener); };
250
+ },
251
+ revision: () => revision,
252
+ experiment: () => ({ name: experiment, ir: need().exp }),
253
+ check: async (settings, o = {}) => {
254
+ const rev = revision;
255
+ return report(await checkSetting(need().exp, paramsOf(settings), limits(o), o.signal && { signal: o.signal }), rev);
256
+ },
257
+ checkAll: async (o = {}, onProgress) => {
258
+ const rev = revision;
259
+ const { exp } = need();
260
+ return report(await checkSettings(exp, parameterValues(exp), {
261
+ ...limits(o), shared: !o.perSetting, maxConfigs: o.maxConfigs,
262
+ }, o.signal && { signal: o.signal }, onProgress), rev);
263
+ },
264
+ checkFirstBranch: (settings, o = {}) => report(checkFirstBranch(need().exp, paramsOf(settings), limits(o)), revision),
265
+ stateGraph: (settings, o = {}) => {
266
+ const { exp } = need();
267
+ const params = paramsOf(settings);
268
+ const g = stateGraph(exp, params, limits(o));
269
+ // States as snapshots, so that `debugConfig` and renderers take them.
270
+ const system = new System(exp, params);
271
+ return { ...g, states: g.states.map(s => ({ ...s, config: new Snapshot(s.config, system) })) };
272
+ },
273
+ drawConfig: (ctx, snapshot, model, options) => {
274
+ // The viewer owns the canvas: draw within the given placement, restoring its state.
275
+ ctx.save();
276
+ try {
277
+ const layout = draw(ctx, snapshot, model.info, options);
278
+ if (layout)
279
+ layouts.set(snapshot, layout);
280
+ }
281
+ finally {
282
+ ctx.restore();
283
+ }
284
+ },
285
+ getAgentsRenderInfo: (snapshot, _model, centerX, centerY, radius) => opts.draw ? layouts.get(snapshot)?.agents ?? [] : ringLayout(snapshot, { centerX, centerY, radius }).agents,
286
+ debugConfig: (snapshot) => {
287
+ const nodes = Array.from({ length: snapshot.size }, (_, n) => {
288
+ const here = snapshot.get(n).agents.map(a => `${a.id}${a.correct ? '' : '!'}`);
289
+ return here.length ? here.join(',') : '.';
290
+ });
291
+ return `${nodes.join('|')} ${JSON.stringify(snapshot.world)}`;
292
+ },
293
+ // The current UI stops at `success` and `failure`. Only under the goal `eventually` is a
294
+ // success final; under the other goals, executions go on (the explorer decides them).
295
+ stateKey: (snapshot) => snapshot.system.key(snapshot.config),
296
+ verdict: (snapshot, model) => ({ verdict: model.system.verdict(snapshot.config), goal: model.system.exp.task.goal }),
297
+ executionStatusChecker: (snapshot, model) => {
298
+ const v = model.system.verdict(snapshot.config);
299
+ return v === 'success' && model.system.exp.task.goal !== 'eventually' ? 'inProgress' : v;
300
+ },
301
+ settingsGenerator: function* () {
302
+ if (current)
303
+ yield* parameterValues(current.exp);
304
+ },
305
+ get editorSchema() {
306
+ return current?.editorSchema() ?? { fields: [] };
307
+ },
308
+ inspect: (snapshot, model) => ({
309
+ world: { ...snapshot.world },
310
+ params: { ...model.params },
311
+ agents: snapshot.agents
312
+ .map((a, i) => ({ a, i }))
313
+ .sort((x, y) => x.a.id - y.a.id)
314
+ .map(({ a, i }) => ({
315
+ id: a.id,
316
+ name: opts.agentName?.(a.id) ?? `a${a.id}`,
317
+ color: agentColor(a.id),
318
+ status: !a.present ? 'hidden' : a.correct ? 'correct' : 'faulty',
319
+ value: {
320
+ position: a.pos,
321
+ // Outside the algorithm's definition (the idling of a lag), the agent is idle; without a
322
+ // line table (no memory), the algorithm view shows where the last step decided.
323
+ ...(a.present && lineOf(snapshot, i) !== undefined ? { line: inSource(lineOf(snapshot, i)) ?? 'idle' } : {}),
324
+ flags: a.flags,
325
+ ...a.fields,
326
+ pub: a.pub,
327
+ mem: a.mem,
328
+ },
329
+ })),
330
+ }),
331
+ describe: () => need().description(),
332
+ getAlgorithmViewInfo: (snapshot) => ({
333
+ source: need().source,
334
+ // Hidden agents (such as those the black hole destroyed) run no line; the others by id.
335
+ agents: snapshot.agents
336
+ .map((a, i) => ({ a, i }))
337
+ .filter(({ a }) => a.present)
338
+ .sort((x, y) => x.a.id - y.a.id)
339
+ .map(({ a, i }) => ({
340
+ agentId: opts.agentName?.(a.id) ?? `a${a.id}`,
341
+ index: i,
342
+ line: lineOf(snapshot, i) ?? 0,
343
+ // From the current definition: program-counter values survive an edit that only moves lines.
344
+ calls: need().exp.algorithm.source?.calls?.[snapshot.config.agents[i].mem?.pc ?? 'start'] ?? [],
345
+ color: agentColor(a.id),
346
+ })),
347
+ }),
348
+ };
349
+ }
@@ -0,0 +1,29 @@
1
+ export class Snapshot {
2
+ config;
3
+ system;
4
+ agents;
5
+ world;
6
+ size;
7
+ constructor(config, system) {
8
+ this.config = config;
9
+ this.system = system;
10
+ const flagNames = system.lay.flags.map(f => f.name);
11
+ this.size = system.topo.size;
12
+ this.world = config.world;
13
+ this.agents = config.agents.map((a, index) => {
14
+ const flags = Object.fromEntries(flagNames.map(f => [f, !!a.f[f]]));
15
+ const id = typeof a.pub?.id === 'number' ? a.pub.id : index;
16
+ return {
17
+ index, id, pos: a.pos, flags,
18
+ fields: Object.fromEntries(Object.entries(a.f).filter(([k]) => !flagNames.includes(k))),
19
+ pub: a.pub, mem: a.mem, line: system.line(a),
20
+ correct: system.rt.correct(a), present: system.rt.present(a),
21
+ _id: id, _pos: a.pos,
22
+ };
23
+ });
24
+ }
25
+ /** The agents on a node (the former engine's configuration interface, which renderers still use). */
26
+ get(node) {
27
+ return { state: null, agents: this.agents.filter(a => a.pos === node) };
28
+ }
29
+ }
@@ -0,0 +1 @@
1
+ export {};