Skip to content
Merged
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
8 changes: 5 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ changing the acquisition or hardware layers.

| File | Purpose |
|---|---|
| `src/common/model/RadioactivityModel.ts` | The shared acquisition model — sources, counting cycle, run, derived statistics. Both screens compose it |
| `src/common/model/RadioactivityModel.ts` | The shared acquisition model — sources, counting cycle, run, derived statistics. Each screen's model locks it to one fixed source |
| `src/common/model/RadioactivityScreenModel.ts` | Composes `RadioactivityModel` with the screen-level display state (chart view, curve visibility) both screens share; `SimulationModel`/`DeviceModel` just fix the source and the default chart |
| `src/common/model/CountSource.ts` | The `TCountSource` contract: a monotonic running total. The reason hardware and simulated data share one code path |
| `src/common/model/SimulatedCountSource.ts` | Poisson event generator (the default source, and the only one with a known λ) |
| `src/common/model/GeigerCountSource.ts` | Hardware source: connection lifecycle, polling loop, register interpretation |
Expand All @@ -27,8 +28,9 @@ changing the acquisition or hardware layers.
| `src/common/model/Statistics.ts` | Welford statistics, log-gamma, Poisson and Gaussian distributions |
| `src/common/model/Histogram.ts` | Integer binning, bin-width choice, per-bin expected frequencies |
| `src/common/model/GaussianFit.ts` | Levenberg–Marquardt fit with Poisson weighting |
| `src/common/view/HistogramNode.ts` | Lab centrepiece: bars plus the three model curves |
| `src/common/view/CountRateChartNode.ts` | Intro strip chart: rate against time, with the mean |
| `src/common/view/HistogramNode.ts` | The histogram view: bars plus the three model curves |
| `src/common/view/CountRateChartNode.ts` | The count-rate view: rate against time, with the mean |
| `src/common/view/RadioactivityScreenView.ts` | The view shared by both screens; a chart-view switch chooses which of the above two is shown |
| `src/RadioactivityAndStatisticsColors.ts` | All `ProfileColorProperty` instances, including the validated chart palette |
| `src/RadioactivityAndStatisticsConstants.ts` | Layout, chart sizes, acquisition ranges, timing guards |

Expand Down
33 changes: 21 additions & 12 deletions doc/implementation-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,35 @@ obvious from the code.

## Shape of the sim

Two screens over one shared acquisition model.
Two screens, told apart only by which counting source they are fixed to, over
one shared acquisition model and one shared view.

```
src/
common/
hardware/ PascoProtocol.ts GeigerCounterDevice.ts webBluetoothSupport.ts
model/ RadioactivityModel.ts CountSource.ts SimulatedCountSource.ts
GeigerCountSource.ts Statistics.ts Histogram.ts GaussianFit.ts
CountSample.ts csvExport.ts ConnectionState.ts
view/ SourcePanel AcquisitionPanel DataTableNode StatisticsPanel
HistogramNode CountRateChartNode CountRateDisplayNode
model/ RadioactivityModel.ts RadioactivityScreenModel.ts CountSource.ts
ChartViewType.ts SimulatedCountSource.ts GeigerCountSource.ts
Statistics.ts Histogram.ts GaussianFit.ts CountSample.ts
csvExport.ts ConnectionState.ts
view/ RadioactivityScreenView.ts SourcePanel AcquisitionPanel
ChartViewPanel DataTableNode StatisticsPanel HistogramNode
CountRateChartNode CountRateDisplayNode
DistributionControlsPanel currentDetailsProperty downloadCsv
intro/ model/IntroModel.ts view/IntroScreenView.ts
lab/ model/LabModel.ts view/LabScreenView.ts
simulation/ model/SimulationModel.ts
device/ model/DeviceModel.ts
```

`IntroModel` and `LabModel` **compose** `RadioactivityModel` rather than extend
it. Composition keeps the shared model free of any one screen's assumptions:
the Lab screen adds curve-visibility state without the Intro screen carrying it,
and neither screen can quietly change acquisition semantics for the other.
`RadioactivityModel` **composes**, rather than is extended by, the two count
sources; `RadioactivityScreenModel` in turn composes `RadioactivityModel` and
adds the state both screens need to display it — which chart is shown, and
which theoretical curves are drawn over the histogram. `SimulationModel` and
`DeviceModel` are thin subclasses that only fix which source
`RadioactivityModel` is locked to (`CountSourceType.SIMULATED` or
`GEIGER_COUNTER`) and which chart the screen opens on; `RadioactivityScreenView`
is the one view class both screens use. Composition keeps the shared
acquisition model free of any one screen's assumptions, and a screen can no
longer quietly change acquisition semantics for the other.

## The count-source abstraction

Expand Down
2 changes: 1 addition & 1 deletion doc/model.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ as a run of 10 and a standard error about three times smaller.
## The Gaussian limit

For large λ the Poisson distribution approaches a Gaussian with the same mean
and σ = √λ. The Lab screen can draw that Gaussian on top of the histogram.
and σ = √λ. Either screen can draw that Gaussian on top of the histogram.

The approximation is not uniformly good, and the sim lets that be seen. At the
peak the two agree to a fraction of a percent even at λ = 100; one standard
Expand Down
2 changes: 1 addition & 1 deletion src/RadioactivityAndStatisticsColors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ const RadioactivityAndStatisticsColors = {
projector: "#5b6b8c",
}),

/** Count-rate trace on the Intro screen's strip chart (a single series). */
/** Count-rate trace on the count-rate strip chart (a single series). */
countRateTraceColorProperty: new ProfileColorProperty(RadioactivityAndStatisticsNamespace, "countRateTrace", {
default: "#3987e5",
projector: "#2a78d6",
Expand Down
4 changes: 2 additions & 2 deletions src/RadioactivityAndStatisticsConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ export const CONTROL_PANEL_WIDTH = 225;

// ── Charts (screen pixels) ────────────────────────────────────────────────────

/** Plot area of the Lab screen's histogram. */
/** Plot area of the histogram, shown on either screen. */
export const HISTOGRAM_CHART_SIZE = { width: 380, height: 440 } as const;

/** Plot area of the Intro screen's count-rate strip chart. */
/** Plot area of the count-rate strip chart, shown on either screen. */
export const RATE_CHART_SIZE = { width: 420, height: 330 } as const;

/** Stroke width of plotted model curves. */
Expand Down
19 changes: 11 additions & 8 deletions src/common/RadioactivityAndStatisticsScreenIcons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
* Programmatic home-screen and navigation-bar icons, drawn on the standard PhET
* 548 × 373 canvas using the sim's own colors so they follow the active profile.
*
* Each icon is a miniature of what its screen is about: the Intro icon is a
* fluctuating count-rate trace about its mean, the Lab icon is a histogram with
* a bell curve over it. They use the same colours as the real charts, so the
* home screen previews what the screen actually looks like.
* Both screens can show either chart now, so the icons no longer distinguish
* "fluctuation" from "distribution" — instead each is a miniature of its
* screen's fixed source: the Simulation icon is a fluctuating count-rate
* trace, evoking the mock-up source's adjustable activity; the Device icon is
* a histogram with a bell curve, evoking the real counter's collected run.
* They use the same colours as the real charts, so the home screen previews
* what the screen actually looks like.
*/
import { Shape } from "scenerystack/kite";
import { Circle, Line, Node, Path, Rectangle } from "scenerystack/scenery";
Expand All @@ -33,13 +36,13 @@ function iconFrom(content: Node): ScreenIcon {
}

/**
* Intro: a count rate scattering about its mean.
* Simulation: a count rate scattering about its mean.
*
* The sample heights are fixed rather than random so the icon is identical on
* every launch — an icon that changed shape between sessions would read as a
* different screen.
*/
export function createIntroIcon(): ScreenIcon {
export function createSimulationIcon(): ScreenIcon {
const samples = [0.55, 0.78, 0.34, 0.62, 0.45, 0.86, 0.5, 0.28, 0.7, 0.4];
const left = INSET;
const right = W - INSET;
Expand Down Expand Up @@ -87,8 +90,8 @@ export function createIntroIcon(): ScreenIcon {
);
}

/** Lab: a histogram of counts with the Poisson curve drawn over it. */
export function createLabIcon(): ScreenIcon {
/** Device: a histogram of counts with the Poisson curve drawn over it. */
export function createDeviceIcon(): ScreenIcon {
const bars = [0.12, 0.3, 0.62, 0.92, 0.78, 0.45, 0.2, 0.08];
const left = INSET;
const right = W - INSET;
Expand Down
21 changes: 21 additions & 0 deletions src/common/model/ChartViewType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* ChartViewType.ts
*
* Which of the two model charts a screen is currently showing. Both screens
* carry the same acquisition machinery and can display either chart — only the
* counting source is fixed per screen — so the choice between them is a single
* enum rather than two screen-specific booleans.
*/

/** Which chart is drawn in the centre of the screen. */
export const ChartViewType = {
/** Distribution of counts per interval, with the theoretical curves. */
HISTOGRAM: "histogram",
/** Count rate against time, with the running mean. */
COUNT_RATE: "countRate",
} as const;

export type ChartViewTypeValue = (typeof ChartViewType)[keyof typeof ChartViewType];

/** Ordered for the view-choice radio button group. */
export const CHART_VIEW_TYPES: readonly ChartViewTypeValue[] = [ChartViewType.HISTOGRAM, ChartViewType.COUNT_RATE];
11 changes: 10 additions & 1 deletion src/common/model/RadioactivityModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ import { computeStatistics, type SampleStatistics } from "./Statistics.js";
export type RadioactivityModelOptions = {
/** Host-side Geiger controls from Preferences → Simulation. */
readonly geigerControls?: GeigerDeviceControls;

/**
* Locks {@link sourceTypeProperty} to one source for the lifetime of the
* model (reset returns to this value too, since it is the Property's
* initial value). Each screen now has a fixed counting source, so there is
* no UI that could ever change it away from this. Defaults to
* {@link CountSourceType.SIMULATED}.
*/
readonly fixedSourceType?: CountSourceTypeValue;
};

export class RadioactivityModel implements TModel {
Expand Down Expand Up @@ -132,7 +141,7 @@ export class RadioactivityModel implements TModel {
this.simulatedSource = new SimulatedCountSource(DEFAULT_ACTIVITY);
this.geigerSource = new GeigerCountSource(options?.geigerControls ?? null);

this.sourceTypeProperty = new Property<CountSourceTypeValue>(CountSourceType.SIMULATED);
this.sourceTypeProperty = new Property<CountSourceTypeValue>(options?.fixedSourceType ?? CountSourceType.SIMULATED);
this.activeSourceProperty = new DerivedProperty(
[this.sourceTypeProperty],
(sourceType): TCountSource =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
/**
* LabModel.ts
* RadioactivityScreenModel.ts
*
* Model for the Lab screen: the distribution of the measurements, and how it
* compares with theory.
*
* Adds only what the Intro screen has no use for — which theoretical curves are
* drawn — on top of the shared {@link RadioactivityModel}. Everything about
* collecting data, including the histogram and the Gaussian fit, is shared;
* the Lab screen simply chooses to display it.
* Model shared by both screens: a {@link RadioactivityModel} locked to one
* counting source, plus the display choices that used to live on the Lab
* screen alone — which chart is shown, and which theoretical curves are drawn
* over the histogram. Both screens can show either chart now, so both need
* this state; only the fixed source tells the two screens apart.
*/

import { BooleanProperty } from "scenerystack/axon";
import { BooleanProperty, Property } from "scenerystack/axon";
import type { TModel } from "scenerystack/joist";
import { RadioactivityModel } from "../../common/model/RadioactivityModel.js";
import type { RadioactivityAndStatisticsPreferencesModel } from "../../preferences/RadioactivityAndStatisticsPreferencesModel.js";
import type { ChartViewTypeValue } from "./ChartViewType.js";
import type { CountSourceTypeValue } from "./CountSource.js";
import { RadioactivityModel } from "./RadioactivityModel.js";

export type RadioactivityScreenModelOptions = {
/** The one source this screen ever counts from. */
readonly fixedSourceType: CountSourceTypeValue;

export class LabModel implements TModel {
/** Which chart the screen opens on. */
readonly initialChartView: ChartViewTypeValue;
};

export class RadioactivityScreenModel implements TModel {
/** Sources, counting cycle, collected run, and derived statistics. */
public readonly acquisition: RadioactivityModel;

/** Which chart is currently shown: the histogram, or the count-rate trace. */
public readonly chartViewProperty: Property<ChartViewTypeValue>;

/**
* Whether the Poisson prediction is drawn, using λ = the measured mean.
*
Expand All @@ -38,17 +49,23 @@ export class LabModel implements TModel {
/** Whether the least-squares best-fit Gaussian is drawn. */
public readonly gaussianFitVisibleProperty = new BooleanProperty(false);

public constructor(preferences: RadioactivityAndStatisticsPreferencesModel) {
public constructor(
preferences: RadioactivityAndStatisticsPreferencesModel,
options: RadioactivityScreenModelOptions,
) {
this.acquisition = new RadioactivityModel({
fixedSourceType: options.fixedSourceType,
geigerControls: {
beepEnabledProperty: preferences.beepEnabledProperty,
tubeVoltageProperty: preferences.tubeVoltageProperty,
},
});
this.chartViewProperty = new Property<ChartViewTypeValue>(options.initialChartView);
}

public reset(): void {
this.acquisition.reset();
this.chartViewProperty.reset();
this.poissonVisibleProperty.reset();
this.gaussianPredictionVisibleProperty.reset();
this.gaussianFitVisibleProperty.reset();
Expand Down
4 changes: 2 additions & 2 deletions src/common/view/AcquisitionPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { DerivedProperty, PatternStringProperty } from "scenerystack/axon";
import { HBox, Node, Text, VBox } from "scenerystack/scenery";
import { NumberControl, PhetFont } from "scenerystack/scenery-phet";
import { Checkbox, RectangularPushButton } from "scenerystack/sun";
import type { SharedControlA11yStrings } from "../../i18n/StringManager.js";
import type { ScreenControlA11yStrings } from "../../i18n/StringManager.js";
import { StringManager } from "../../i18n/StringManager.js";
import RadioactivityAndStatisticsColors from "../../RadioactivityAndStatisticsColors.js";
import {
Expand All @@ -31,7 +31,7 @@ import { downloadCsv } from "./downloadCsv.js";
export class AcquisitionPanel extends RadioactivityAndStatisticsPanel {
private readonly disposeAcquisitionPanel: () => void;

public constructor(model: RadioactivityModel, a11y: SharedControlA11yStrings) {
public constructor(model: RadioactivityModel, a11y: ScreenControlA11yStrings) {
const stringManager = StringManager.getInstance();
const strings = stringManager.getAcquisitionStrings();
const readoutStrings = stringManager.getReadoutStrings();
Expand Down
69 changes: 69 additions & 0 deletions src/common/view/ChartViewPanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* ChartViewPanel.ts
*
* Chooses which of the two model charts is drawn: the histogram, or the count
* rate over time. Every screen now carries both charts, so this is the one
* control that decides which one is on screen.
*/

import type { Property } from "scenerystack/axon";
import { Text, VBox } from "scenerystack/scenery";
import { PhetFont } from "scenerystack/scenery-phet";
import { AquaRadioButtonGroup } from "scenerystack/sun";
import type { ScreenControlA11yStrings } from "../../i18n/StringManager.js";
import { StringManager } from "../../i18n/StringManager.js";
import RadioactivityAndStatisticsColors from "../../RadioactivityAndStatisticsColors.js";
import { CONTROL_PANEL_WIDTH } from "../../RadioactivityAndStatisticsConstants.js";
import { ChartViewType, type ChartViewTypeValue } from "../model/ChartViewType.js";
import { RadioactivityAndStatisticsPanel } from "../RadioactivityAndStatisticsPanel.js";

export class ChartViewPanel extends RadioactivityAndStatisticsPanel {
public constructor(chartViewProperty: Property<ChartViewTypeValue>, a11y: ScreenControlA11yStrings) {
const strings = StringManager.getInstance().getChartViewStrings();

const title = new Text(strings.titleStringProperty, {
font: new PhetFont({ size: 15, weight: "bold" }),
fill: RadioactivityAndStatisticsColors.textColorProperty,
});

const radioGroup = new AquaRadioButtonGroup(
chartViewProperty,
[
{
value: ChartViewType.HISTOGRAM,
createNode: () =>
new Text(strings.histogramStringProperty, {
font: new PhetFont(13),
fill: RadioactivityAndStatisticsColors.textColorProperty,
maxWidth: 170,
}),
},
{
value: ChartViewType.COUNT_RATE,
createNode: () =>
new Text(strings.countRateStringProperty, {
font: new PhetFont(13),
fill: RadioactivityAndStatisticsColors.textColorProperty,
maxWidth: 170,
}),
},
],
{
spacing: 6,
radioButtonOptions: { radius: 7 },
accessibleName: a11y.chartViewRadioGroupStringProperty,
},
);

super(
new VBox({
align: "left",
spacing: 8,
preferredWidth: CONTROL_PANEL_WIDTH - 24,
stretch: true,
children: [title, radioGroup],
}),
{ minWidth: CONTROL_PANEL_WIDTH },
);
}
}
4 changes: 2 additions & 2 deletions src/common/view/DistributionControlsPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { type BooleanProperty, DerivedProperty, type TReadOnlyProperty } from "s
import { Text, VBox } from "scenerystack/scenery";
import { NumberControl, PhetFont } from "scenerystack/scenery-phet";
import { Checkbox } from "scenerystack/sun";
import type { LabControlA11yStrings } from "../../i18n/StringManager.js";
import type { ScreenControlA11yStrings } from "../../i18n/StringManager.js";
import { StringManager } from "../../i18n/StringManager.js";
import RadioactivityAndStatisticsColors from "../../RadioactivityAndStatisticsColors.js";
import { BIN_WIDTH_RANGE, CONTROL_PANEL_WIDTH } from "../../RadioactivityAndStatisticsConstants.js";
Expand All @@ -39,7 +39,7 @@ export type CurveVisibilityControls = {
export class DistributionControlsPanel extends RadioactivityAndStatisticsPanel {
private readonly disposeDistributionControlsPanel: () => void;

public constructor(model: RadioactivityModel, curves: CurveVisibilityControls, a11y: LabControlA11yStrings) {
public constructor(model: RadioactivityModel, curves: CurveVisibilityControls, a11y: ScreenControlA11yStrings) {
const strings = StringManager.getInstance().getHistogramStrings();

const title = new Text(strings.titleStringProperty, {
Expand Down
Loading
Loading