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