Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/connected-optimizer-source.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@hashintel/petrinaut": patch
"@hashintel/ds-components": patch
---

A connected optimization source runs studies in this browser behind the experimental In-browser optimization setting. The optimization form gains Runs per step and the experiments' Backend switch, which stays on the CPU because the GPU backend cannot compute an expression objective. A connected study's drawer streams the objective's metrics for the step being evaluated, and for whichever point the navigator or the surface picks once the study is over. The connected study's Surface draws only the study's steps — each a dot the field interpolates between, the best emphasized, pruned steps hollow — and fills in as the step in flight streams; it becomes navigable once the study is over or Follow steps is off, as do the Parameters band sliders. `Slider` accepts `disabled`. A connected study can be stopped and continued with more steps on the same sampler, settles its controls on the best step when it ends, stops refining a picked point that cannot beat the best after its first runs, evaluates up to four steps at once with a Parallel steps field, and lists every batch computing under the summary's progress bar. The connected study's drawer lays everything out in view at once: one summary strip (status, steps, best, backend), the Parameters band, the Surface beside the objective's chart, and the steps table filling the rest, the best step starred. The create form seeds each study with a fresh random **Seed**, editable for reproducibility.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export interface SliderProps {
defaultValue?: number;
label?: string;
showValueText?: boolean;
/** Shows the value without letting the pointer or keyboard move it. */
disabled?: boolean;
onChange?: (value: number) => void;
/** Fires once when a drag or keyboard interaction settles. */
onChangeEnd?: (value: number) => void;
Expand All @@ -43,19 +45,25 @@ export const Slider: React.FC<SliderProps> = ({
defaultValue,
label,
showValueText = false,
disabled,
onChange,
onChangeEnd,
}) => {
return (
<BaseSlider.Root
min={min}
step={step}
disabled={disabled}
className={cx(
css({
position: "relative",
display: "flex",
flexDirection: "column",
gap: "2",
"&[data-disabled]": {
opacity: "[0.5]",
cursor: "not-allowed",
},
}),
className,
)}
Expand Down
2 changes: 1 addition & 1 deletion libs/@hashintel/petrinaut-core/src/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export const petrinautDocSummaries: Record<PetrinautDocName, string> = {
"compilation-output":
"The Compilation bottom-panel tab: enabling it, the GPU verdict line, structural blockers, shader emission failures, per-item GPU/CPU/untested/no-HIR/unused status, and HIR node counts.",
examples:
"Walkthroughs of the built-in examples and the scenarios/metrics each ships with: SIR, Supply Chain with Disruption, Supply Chain Profit, Deployment Pipeline, Production with Machine Failure, Probabilistic Satellite Launcher, Café Queue, Drone Patrol.",
"Walkthroughs of the built-in examples and the scenarios/metrics each ships with: SIR, Vaccination Campaign, Supply Chain with Disruption, Supply Chain Profit, Deployment Pipeline, Production with Machine Failure, Probabilistic Satellite Launcher, Café Queue, Drone Patrol.",
};

const getLatestNetDefinitionToolInputSchema = z
Expand Down
2 changes: 2 additions & 0 deletions libs/@hashintel/petrinaut-core/src/examples/examples.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
sirModel,
supplyChainProfit,
supplyChainWithDisruption,
vaccinationCampaign,
} from "./index";

const EXAMPLES = [
Expand All @@ -19,6 +20,7 @@ const EXAMPLES = [
sirModel,
supplyChainProfit,
supplyChainWithDisruption,
vaccinationCampaign,
];

describe.each(EXAMPLES.map((example) => [example.title, example] as const))(
Expand Down
1 change: 1 addition & 0 deletions libs/@hashintel/petrinaut-core/src/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export { cafeQueue } from "./cafe-queue";
export { dronePatrol } from "./drone-patrol";
export { supplyChainWithDisruption } from "./supply-chain-with-disruption";
export { supplyChainProfit } from "./supply-chain-profit";
export { vaccinationCampaign } from "./vaccination-campaign";
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { describe, expect, it } from "vitest";

import { compileHirArtifacts } from "../hir";
import { lowerScenarioToHir } from "../hir/scenario";
import { compileScenario } from "../simulation/authoring/scenario/compile-scenario";
import {
createMonteCarloExperiment,
runExperimentToCompletion,
} from "../simulation/monte-carlo";
import { analyzeCompilation } from "../webgpu/compilation-report";
import { assessGpuEligibility } from "../webgpu/eligibility";
import { vaccinationCampaign } from "./vaccination-campaign";

import type { CompiledScenarioResult } from "../simulation/authoring/scenario/compile-scenario";

const { petriNetDefinition } = vaccinationCampaign;

const winterWave = petriNetDefinition.scenarios!.find(
(scenario) => scenario.id === "scenario__winter_wave",
)!;
const totalCost = petriNetDefinition.metrics!.find(
(metric) => metric.id === "metric__total_cost",
)!;
const infectedPlace = petriNetDefinition.places.find(
(place) => place.name === "Infected",
)!;

const { artifacts } = compileHirArtifacts(petriNetDefinition, undefined, {
includeHir: true,
});
const winterWaveHir = lowerScenarioToHir(winterWave);

/** The two levers the optimization stories range over. */
type Levers = { vaccination_coverage: number; contact_reduction: number };

const compile = (levers?: Levers): CompiledScenarioResult => {
const outcome = compileScenario(
winterWave,
winterWaveHir,
petriNetDefinition.parameters,
petriNetDefinition.places,
petriNetDefinition.types,
levers ? { scenarioParameterValues: levers } : undefined,
);
if (!outcome.ok) {
throw new Error(
`scenario failed to compile: ${outcome.errors
.map((error) => error.message)
.join("; ")}`,
);
}
return outcome.result;
};

/** Mean Total cost on the final state over eight seeded 60-day runs. */
const meanTotalCost = async (levers: Levers): Promise<number> => {
const compiled = compile(levers);
const runCount = 8;
const handle = await createMonteCarloExperiment({
sdcpn: petriNetDefinition,
hirArtifacts: artifacts,
initialMarking: compiled.initialState,
parameterValues: compiled.parameterValues,
seed: 1,
dt: 0.1,
maxTime: 60,
runCount,
runs: Array.from({ length: runCount }, (_, index) => ({
seed: 1000 + index,
})),
metricSpecs: [
{
kind: "expression",
id: totalCost.id,
label: totalCost.name,
sampleRuns: "all",
code: totalCost.code,
artifact: artifacts.metrics[totalCost.id]!,
},
],
});
const completion = await runExperimentToCompletion(handle);
if (completion.event.type !== "complete") {
throw new Error(`experiment ended with ${completion.event.type}`);
}
let sum = 0;
for (const result of completion.runResults.values()) {
sum += result[totalCost.id]!;
}
return sum / completion.runResults.size;
};

describe("Vaccination Campaign", () => {
it("is GPU-eligible as an uncoloured net", () => {
const result = assessGpuEligibility(petriNetDefinition);

expect(result.eligible).toBe(true);
if (!result.eligible) return;
expect(result.profile.uncolouredOnly).toBe(true);
// Four counts, two firing counts, rng, status = 8 words.
expect(result.profile.bytesPerRun).toBe(32);
});

it("compiles to a GPU shader with a place-count objective", () => {
const report = analyzeCompilation({
sdcpn: petriNetDefinition,
artifacts,
metricSpecs: [
{
id: "infected",
label: "Infected",
kind: "placeTokenCountMean",
placeId: infectedPlace.id,
},
],
});

expect(report.gpuReady).toBe(true);
expect(report.eligibilityReasons).toStrictEqual([]);
expect(report.shaderFailure).toBeNull();
expect(report.metricFailure).toBeNull();
expect(
report.items
.filter((item) => item.kind === "lambda")
.map((item) => item.status),
).toStrictEqual(["gpu-ready", "gpu-ready"]);
});

it("seeds the Winter wave from the coverage and the initial cases", () => {
const result = compile();

expect(result.initialState).toEqual({
place__susceptible: 686,
place__infected: 20,
place__recovered: 0,
place__vaccinated: 294,
});
expect(Number(result.parameterValues.vaccination_coverage)).toBeCloseTo(
0.3,
);
expect(Number(result.parameterValues.contact_reduction)).toBeCloseTo(0.2);
});

it("prices the cheapest response inside the levers' domain", async () => {
const floor = await meanTotalCost({
vaccination_coverage: 0.45,
contact_reduction: 0.4,
});
const boundary: Levers[] = [
{ vaccination_coverage: 0, contact_reduction: 0 },
{ vaccination_coverage: 0, contact_reduction: 0.4 },
{ vaccination_coverage: 0, contact_reduction: 0.8 },
{ vaccination_coverage: 0.45, contact_reduction: 0 },
{ vaccination_coverage: 0.9, contact_reduction: 0 },
{ vaccination_coverage: 0.9, contact_reduction: 0.8 },
];

for (const point of boundary) {
expect(
await meanTotalCost(point),
`coverage ${point.vaccination_coverage}, contact reduction ${point.contact_reduction} costs more than the valley floor`,
).toBeGreaterThan(floor);
}
});
});
Loading
Loading