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
5 changes: 5 additions & 0 deletions .changeset/ds-range-slider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/ds-components": patch
---

Add `RangeSlider`: a two-thumb slider selecting an inclusive range, whose thumbs may coincide for single-point selections.
5 changes: 5 additions & 0 deletions .changeset/interval-sweeps-range-slider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/petrinaut": patch
---

Parameter sweeps declare an interval per swept parameter instead of a value count. The navigator becomes a range slider per parameter β€” the whole interval selected by default, resizable, collapsible to a point β€” with the interval quantized into ~50 positions so revisited positions restore their cached runs. A range selection samples points across the region in a low-discrepancy order and streams the merged distribution over the region; a point behaves as before.
128 changes: 128 additions & 0 deletions libs/@hashintel/ds-components/src/components/Slider/range-slider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { Slider as BaseSlider } from "@ark-ui/react/slider";

import { css, cx } from "@hashintel/ds-helpers/css";

const THUMB_SIZE = 12;

const thumbStyles = css({
outline: "none",
display: "block",
width: `[${THUMB_SIZE}px]`,
height: `[${THUMB_SIZE}px]`,
borderRadius: "full",
border: "[1px solid rgba(255,255,255,0.45)]",
background:
"[linear-gradient(180deg, rgba(59,130,246,0.95) 0%, rgba(37,99,235,0.98) 100%)]",
boxShadow: "[0 1px 6px rgba(37,99,235,0.28)]",
transition: "[transform 0.15s ease]",
"&[data-dragging]": {
transform: "scale(1.3)",
},
"&[data-focus]": {
boxShadow: "[0 0 0 3px rgba(59,130,246,0.3)]",
},
});

export interface RangeSliderProps {
className?: string;
style?: React.CSSProperties;
min: number;
max: number;
step?: number;
/** Both ends of the selected range; they may coincide (a point). */
value: [number, number];
"aria-label"?: string;
disabled?: boolean;
onChange?: (value: [number, number]) => void;
/** Fires once when a drag or keyboard interaction settles. */
onChangeEnd?: (value: [number, number]) => void;
}

/**
* A two-thumb slider selecting an inclusive range. The thumbs may occupy the
* same position, which callers treat as a single-point selection.
*/
export const RangeSlider: React.FC<RangeSliderProps> = ({
className,
style,
min,
max,
step,
value,
"aria-label": ariaLabel,
disabled,
onChange,
onChangeEnd,
}) => {
const emit = (values: number[]): [number, number] => {
const [start = min, end = max] = values;
return start <= end ? [start, end] : [end, start];
};

return (
<BaseSlider.Root
min={min}
max={max}
step={step}
value={[value[0], value[1]]}
minStepsBetweenThumbs={0}
disabled={disabled}
aria-label={ariaLabel ? [ariaLabel, ariaLabel] : undefined}
className={cx(
css({
position: "relative",
display: "flex",
flexDirection: "column",
flex: "1",
minWidth: "[120px]",
}),
className,
)}
style={style}
onValueChange={(details) => {
onChange?.(emit(details.value));
}}
onValueChangeEnd={(details) => {
onChangeEnd?.(emit(details.value));
}}
>
<BaseSlider.Control
className={css({
position: "relative",
display: "flex",
alignItems: "center",
height: `[${THUMB_SIZE + 4}px]`,
})}
>
<BaseSlider.Track
className={css({
flex: "1",
position: "relative",
height: "[4px]",
alignItems: "center",
borderRadius: "full",
backgroundColor: "neutral.s40",
})}
>
<BaseSlider.Range
className={css({
top: "[0px]",
bottom: "[0px]",
backgroundColor: "blue.s70",
borderRadius: "full",
// A collapsed range (point selection) still reads as present.
minWidth: "[2px]",
})}
/>
</BaseSlider.Track>

<BaseSlider.Thumb index={0} className={thumbStyles}>
<BaseSlider.HiddenInput />
</BaseSlider.Thumb>
<BaseSlider.Thumb index={1} className={thumbStyles}>
<BaseSlider.HiddenInput />
</BaseSlider.Thumb>
</BaseSlider.Control>
</BaseSlider.Root>
);
};
4 changes: 4 additions & 0 deletions libs/@hashintel/ds-components/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ export {
type SegmentedControlProps,
} from "./components/SegmentedControl/segmented-control";
export { Select, type SelectItem } from "./components/Select/select";
export {
RangeSlider,
type RangeSliderProps,
} from "./components/Slider/range-slider";
export { Slider, type SliderProps } from "./components/Slider/slider";
export { TextArea } from "./components/TextArea/text-area";
export { TextInput } from "./components/TextInput/text-input";
Expand Down
17 changes: 11 additions & 6 deletions libs/@hashintel/petrinaut/docs/experiments.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Experiments live under the **Simulate** [global mode](drawing-a-net.md#global-mo
| **Name** | `Experiment` | Free text. |
| **Scenario** | `(Default)` | Either `(Default)` (no scenario; uses each place's manually-set initial marking and net-level parameter defaults) or one of your saved [scenarios](scenarios.md). An experiment runs against exactly one scenario. |
| **Scenario parameters** | each scenario parameter's default | When a scenario is selected, you can override its scenario parameters per experiment. Expressions are evaluated once at start. Each numeric parameter also has a **Sweep** toggle β€” see [Parameter sweeps](#parameter-sweeps). |
| **Runs** | `1000` | Positive integer; how many independent simulations to run. For a sweep, this is the run budget **per combination**. |
| **Runs** | `1000` | Positive integer; how many independent simulations to run. For a sweep, this is the run budget **per sampled point**. |
| **Time step (dt)** | `0.1` | Same meaning as in single-run simulations (see [Simulation](simulation.md#time-step-dt)). |
| **Max time (seconds)** | `180` | Each run advances until simulation time reaches this value, then completes. |
| **Run on GPU** | off | Only shown when **WebGPU** is on under **Settings β†’ Simulation**. Greyed out with the reason on hover when this model cannot run on the GPU. See [Compute backend](#compute-backend-experimental). |
Expand All @@ -35,7 +35,7 @@ Experiments progress through these status labels:
| ---------------- | ------------------------------------------------------------------------------------------------- |
| **Initializing** | The experiment has been created and its workers are starting up. |
| **Running** | Runs are in progress. |
| **Idle** | A sweep whose selected combination is fully sampled. Moving a parameter control resumes running. |
| **Idle** | A sweep whose selected region is fully sampled. Moving a parameter control resumes running. |
| **Complete** | All runs finished without error. |
| **Error** | The experiment failed to start or hit an unrecoverable error. The drawer shows the error message. |
| **Cancelled** | You clicked **Cancel**, or the experiment was cancelled. |
Expand All @@ -55,15 +55,20 @@ Two consequences worth knowing:

### Parameter sweeps

Flip **Sweep** on any numeric scenario parameter to explore a range of values instead of one. Set the minimum, the maximum, and how many evenly spaced values to take; several swept parameters form a grid of combinations (capped at 200), and the form shows the grid size before you run.
Flip **Sweep** on any numeric scenario parameter to explore an interval of values instead of one. Set the minimum and the maximum β€” that is all a sweep declares. Petrinaut quantizes the interval finely (about fifty steps; integer parameters step by whole numbers) so results can be cached and restored per position.

A sweep never computes its whole grid up front. It computes **the combination you are looking at**: the results drawer grows a **Parameters** strip β€” pinned while you scroll β€” with one control per swept parameter. Runs for the selected combination accumulate in escalating batches (8, 25, 100, … up to your run budget), and the metric charts below sharpen as they stream in. Move a control and compute immediately restarts on the new combination, like a raytracer dropping its rays when the camera moves. Combinations you have visited keep their results, so stepping back is instant and refinement resumes where it left off.
A sweep computes **what you have selected**. The results drawer grows a **Parameters** strip β€” pinned while you scroll β€” with one slider per swept parameter. Each slider selects a range on its interval, and starts spanning the whole of it:

Every combination samples the same seed sequence (common random numbers), so differences you see between combinations come from the parameters, not from sampling luck. The GPU backend works for sweeps the same way it does for a plain experiment: the choice is made on the first batch and each later combination reuses it.
- **Range** (the default): Petrinaut samples points spread across the selected region, a small batch at a time, and the metric charts below show the distribution **over the region** β€” it takes shape after the first few points and keeps sharpening while you stay. Resize the range from either end to focus; the status line counts sampled points and runs.
- **Point**: switch a parameter's control to Point and its slider collapses to a single value. A point refines in escalating batches (8, 25, 100, … up to your run budget), exactly like a plain experiment at that value.

Move a slider and compute immediately restarts on the new selection, like a raytracer dropping its rays when the camera moves. Every position you have visited keeps its results: narrowing a range, collapsing to a point, or sliding back to an earlier value restores its runs and distributions instantly, and refinement resumes where it left off.

Every sampled point uses the same seed sequence (common random numbers), so differences you see across the interval come from the parameters, not from sampling luck. The GPU backend works for sweeps the same way it does for a plain experiment: the choice is made on the first batch and each later batch reuses it.

#### The surface view

A sweep with two or more swept parameters grows a **Surface** section under the metrics: a contour plot of one metric's final value over two parameters you pick, with every other parameter held at its navigator value. The plot fills in live β€” combinations are sampled a few at a time (8 runs each), coarse shape first β€” and **clicking the surface moves the navigator** to the nearest combination, which then refines it with more runs. Changing the fixed parameters, the axes, or the metric restarts the fill for the new slice.
A sweep with two or more swept parameters grows a **Surface** section under the metrics: a contour plot of one metric's final value over two parameters you pick, with every other parameter held at the middle of its selected range. The plot fills in live β€” points are sampled a few runs at a time (8 runs each), coarse shape first β€” and **clicking the surface moves the navigator**: both shown parameters collapse to a point at the clicked position, which then refines with more runs. Changing the fixed parameters, the axes, or the metric restarts the fill for the new slice.

### Compute backend (experimental)

Expand Down
14 changes: 9 additions & 5 deletions libs/@hashintel/petrinaut/src/react/experiments/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,19 @@ export type ExperimentRecord = {

/** Navigator-facing state of a sweep experiment. */
export type ExperimentSweepState = {
/** Value index per swept parameter identifier. */
/** Inclusive position range per swept parameter identifier. */
selection: SweepSelection;
/** Concrete swept values for `selection`. */
parameterValues: Readonly<Record<string, number>>;
/** Finished runs for the selected combination. */
/** Concrete values of the cell currently being computed, or null. */
activeCellValues: Readonly<Record<string, number>> | null;
/** Finished runs across the selected region. */
runsCompleted: number;
/** Runs contributing to the shown frames, including the in-flight batch. */
runsSampled: number;
/** Ladder target the in-flight batch climbs to; null when saturated. */
/** Cells of the region with at least one finished batch. */
cellsSampled: number;
/** Cells inside the selected region. */
cellsInRegion: number;
/** Ladder target the in-flight batch climbs its cell to; null when done. */
runTarget: number | null;
computing: boolean;
};
Expand Down
Loading
Loading