@holoscript/std 2.1.0 → 6.0.3
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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +197 -0
- package/dist/__tests__/EconomicPrimitives.test.d.ts +2 -0
- package/dist/__tests__/EconomicPrimitives.test.d.ts.map +1 -0
- package/dist/__tests__/EconomicTraits.test.d.ts +2 -0
- package/dist/__tests__/EconomicTraits.test.d.ts.map +1 -0
- package/dist/__tests__/SimulationLabPrimitives.test.d.ts +2 -0
- package/dist/__tests__/SimulationLabPrimitives.test.d.ts.map +1 -0
- package/dist/__tests__/collections.test.d.ts +2 -0
- package/dist/__tests__/collections.test.d.ts.map +1 -0
- package/dist/__tests__/events.test.d.ts +2 -0
- package/dist/__tests__/events.test.d.ts.map +1 -0
- package/dist/__tests__/index.test.d.ts +2 -0
- package/dist/__tests__/index.test.d.ts.map +1 -0
- package/dist/__tests__/materials.test.d.ts +2 -0
- package/dist/__tests__/materials.test.d.ts.map +1 -0
- package/dist/__tests__/math.test.d.ts +2 -0
- package/dist/__tests__/math.test.d.ts.map +1 -0
- package/dist/__tests__/physics.test.d.ts +2 -0
- package/dist/__tests__/physics.test.d.ts.map +1 -0
- package/dist/__tests__/spatial.test.d.ts +2 -0
- package/dist/__tests__/spatial.test.d.ts.map +1 -0
- package/dist/__tests__/standard-library-fundamentals.test.d.ts +2 -0
- package/dist/__tests__/standard-library-fundamentals.test.d.ts.map +1 -0
- package/dist/__tests__/string-and-time-utilities.test.d.ts +2 -0
- package/dist/__tests__/string-and-time-utilities.test.d.ts.map +1 -0
- package/dist/__tests__/string-decoupled.test.d.ts +2 -0
- package/dist/__tests__/string-decoupled.test.d.ts.map +1 -0
- package/dist/__tests__/string.test.d.ts +2 -0
- package/dist/__tests__/string.test.d.ts.map +1 -0
- package/dist/__tests__/time.test.d.ts +2 -0
- package/dist/__tests__/time.test.d.ts.map +1 -0
- package/dist/chunk-7HVUYGPS.js +679 -0
- package/dist/chunk-7HVUYGPS.js.map +1 -0
- package/dist/chunk-P4O3WNG2.js +287 -0
- package/dist/chunk-P4O3WNG2.js.map +1 -0
- package/dist/chunk-PFX2JM4X.js +683 -0
- package/dist/chunk-PFX2JM4X.js.map +1 -0
- package/dist/chunk-W2Q3LUCM.js +348 -0
- package/dist/chunk-W2Q3LUCM.js.map +1 -0
- package/dist/chunk-WS6W35YC.js +443 -0
- package/dist/chunk-WS6W35YC.js.map +1 -0
- package/dist/chunk-ZXVZAQCZ.js +470 -0
- package/dist/chunk-ZXVZAQCZ.js.map +1 -0
- package/dist/collections.cjs +715 -0
- package/dist/collections.cjs.map +1 -0
- package/dist/collections.d.ts.map +1 -1
- package/dist/collections.js +16 -719
- package/dist/collections.js.map +1 -1
- package/dist/events.d.ts +32 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/index.cjs +4101 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +21 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1270 -157
- package/dist/index.js.map +1 -1
- package/dist/materials.d.ts +50 -0
- package/dist/materials.d.ts.map +1 -0
- package/dist/math.cjs +591 -0
- package/dist/math.cjs.map +1 -0
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +51 -537
- package/dist/math.js.map +1 -1
- package/dist/physics.d.ts +59 -0
- package/dist/physics.d.ts.map +1 -0
- package/dist/spatial.d.ts +82 -0
- package/dist/spatial.d.ts.map +1 -0
- package/dist/string.basic.test.d.ts +5 -0
- package/dist/string.basic.test.d.ts.map +1 -0
- package/dist/string.cjs +422 -0
- package/dist/string.cjs.map +1 -0
- package/dist/string.d.ts +5 -1
- package/dist/string.d.ts.map +1 -1
- package/dist/string.js +106 -442
- package/dist/string.js.map +1 -1
- package/dist/string.test.d.ts +5 -0
- package/dist/string.test.d.ts.map +1 -0
- package/dist/time.cjs +512 -0
- package/dist/time.cjs.map +1 -0
- package/dist/time.d.ts.map +1 -1
- package/dist/time.js +41 -528
- package/dist/time.js.map +1 -1
- package/dist/traits/ARTraits.d.ts +14 -0
- package/dist/traits/ARTraits.d.ts.map +1 -0
- package/dist/traits/EconomicPrimitives.cjs +335 -0
- package/dist/traits/EconomicPrimitives.cjs.map +1 -0
- package/dist/traits/EconomicPrimitives.d.ts +345 -0
- package/dist/traits/EconomicPrimitives.d.ts.map +1 -0
- package/dist/traits/EconomicPrimitives.js +55 -0
- package/dist/traits/EconomicPrimitives.js.map +1 -0
- package/dist/traits/EconomicTraits.cjs +471 -0
- package/dist/traits/EconomicTraits.cjs.map +1 -0
- package/dist/traits/EconomicTraits.d.ts +71 -0
- package/dist/traits/EconomicTraits.d.ts.map +1 -0
- package/dist/traits/EconomicTraits.js +17 -0
- package/dist/traits/EconomicTraits.js.map +1 -0
- package/dist/traits/IoTTraits.d.ts +14 -0
- package/dist/traits/IoTTraits.d.ts.map +1 -0
- package/dist/traits/SimulationLabPrimitives.d.ts +187 -0
- package/dist/traits/SimulationLabPrimitives.d.ts.map +1 -0
- package/dist/traits/SimulationLabTraits.d.ts +42 -0
- package/dist/traits/SimulationLabTraits.d.ts.map +1 -0
- package/dist/traits/VRRTraits.d.ts +150 -0
- package/dist/traits/VRRTraits.d.ts.map +1 -0
- package/dist/traits/__tests__/ARTraits.test.d.ts +2 -0
- package/dist/traits/__tests__/ARTraits.test.d.ts.map +1 -0
- package/dist/traits/__tests__/EconomicPrimitives.test.d.ts +2 -0
- package/dist/traits/__tests__/EconomicPrimitives.test.d.ts.map +1 -0
- package/dist/traits/__tests__/EconomicTraits.test.d.ts +2 -0
- package/dist/traits/__tests__/EconomicTraits.test.d.ts.map +1 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/error-utf8.log +6 -0
- package/error.log +0 -0
- package/package.json +42 -19
- package/src/__tests__/EconomicPrimitives.test.ts +690 -0
- package/src/__tests__/EconomicTraits.test.ts +425 -0
- package/src/__tests__/SimulationLabPrimitives.test.ts +415 -0
- package/src/__tests__/collections.test.ts +182 -0
- package/src/__tests__/events.test.ts +135 -0
- package/src/__tests__/index.test.ts +14 -0
- package/src/__tests__/materials.test.ts +84 -0
- package/src/__tests__/math.test.ts +246 -0
- package/src/__tests__/physics.test.ts +83 -0
- package/src/__tests__/spatial.test.ts +333 -0
- package/src/__tests__/standard-library-fundamentals.test.ts +1301 -0
- package/src/__tests__/string-and-time-utilities.test.ts +1070 -0
- package/src/__tests__/string-decoupled.test.ts +14 -0
- package/src/__tests__/string.test.ts +164 -0
- package/src/__tests__/time.test.ts +110 -0
- package/src/collections.ts +8 -2
- package/src/events.ts +88 -0
- package/src/index.ts +161 -11
- package/src/materials.ts +109 -0
- package/src/math.ts +30 -14
- package/src/physics.ts +141 -0
- package/src/spatial.ts +320 -0
- package/src/string.basic.test.ts +14 -0
- package/src/string.test.ts +335 -0
- package/src/string.ts +45 -13
- package/src/time.ts +10 -11
- package/src/traits/ARTraits.ts +103 -0
- package/src/traits/EconomicPrimitives.ts +755 -0
- package/src/traits/EconomicTraits.ts +552 -0
- package/src/traits/IoTTraits.ts +102 -0
- package/src/traits/SimulationLabPrimitives.ts +650 -0
- package/src/traits/SimulationLabTraits.ts +191 -0
- package/src/traits/VRRTraits.ts +326 -0
- package/src/traits/__tests__/ARTraits.test.ts +14 -0
- package/src/traits/__tests__/EconomicPrimitives.test.ts +14 -0
- package/src/traits/__tests__/EconomicTraits.test.ts +14 -0
- package/src/types.ts +47 -12
- package/std_error.log +5 -0
- package/tsup.config.ts +17 -0
- package/vitest.config.ts +10 -0
- package/dist/types.js +0 -198
- package/dist/types.js.map +0 -1
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview SimulationLab Primitives - Runtime math and types for hypothesis testing
|
|
3
|
+
* @module @holoscript/std/simulation
|
|
4
|
+
*
|
|
5
|
+
* Provides the mathematical building blocks for scientific hypothesis testing
|
|
6
|
+
* in HoloScript simulations. Run N epochs with parameter sweeps, collect metrics,
|
|
7
|
+
* and perform statistical analysis to prove or disprove hypotheses.
|
|
8
|
+
*
|
|
9
|
+
* The universe gets one sample. HoloScript gives you infinite.
|
|
10
|
+
*
|
|
11
|
+
* @version 1.0.0
|
|
12
|
+
* @category simulation
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// CORE TYPES
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Direction of expected effect in a hypothesis.
|
|
21
|
+
*/
|
|
22
|
+
export type HypothesisDirection = 'greater' | 'less' | 'different' | 'equal';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Status of a simulation run.
|
|
26
|
+
*/
|
|
27
|
+
export type SimulationStatus = 'pending' | 'running' | 'completed' | 'failed';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A scientific hypothesis to test.
|
|
31
|
+
*/
|
|
32
|
+
export interface Hypothesis {
|
|
33
|
+
/** Human-readable description */
|
|
34
|
+
description: string;
|
|
35
|
+
/** The null hypothesis (what we're trying to reject) */
|
|
36
|
+
null_hypothesis: string;
|
|
37
|
+
/** The alternative hypothesis (what we're testing for) */
|
|
38
|
+
alternative_hypothesis: string;
|
|
39
|
+
/** Expected direction of effect */
|
|
40
|
+
direction: HypothesisDirection;
|
|
41
|
+
/** Significance threshold (default: 0.05) */
|
|
42
|
+
alpha: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A range of parameter values to sweep.
|
|
47
|
+
*/
|
|
48
|
+
export interface ParameterRange {
|
|
49
|
+
/** Parameter name */
|
|
50
|
+
name: string;
|
|
51
|
+
/** Start value */
|
|
52
|
+
min: number;
|
|
53
|
+
/** End value */
|
|
54
|
+
max: number;
|
|
55
|
+
/** Step size */
|
|
56
|
+
step: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A set of discrete parameter values to sweep.
|
|
61
|
+
*/
|
|
62
|
+
export interface ParameterSet {
|
|
63
|
+
/** Parameter name */
|
|
64
|
+
name: string;
|
|
65
|
+
/** Discrete values to test */
|
|
66
|
+
values: number[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Either a range or discrete set of parameter values.
|
|
71
|
+
*/
|
|
72
|
+
export type ParameterSweep = ParameterRange | ParameterSet;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Metrics collected from a single simulation run.
|
|
76
|
+
*/
|
|
77
|
+
export interface SimulationMetrics {
|
|
78
|
+
/** Named metric values */
|
|
79
|
+
values: Record<string, number>;
|
|
80
|
+
/** Seed used for this run */
|
|
81
|
+
seed: number;
|
|
82
|
+
/** Parameter values used */
|
|
83
|
+
params: Record<string, number>;
|
|
84
|
+
/** Wall-clock time in ms */
|
|
85
|
+
durationMs: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Result of a statistical test.
|
|
90
|
+
*/
|
|
91
|
+
export interface StatisticalResult {
|
|
92
|
+
/** Test statistic value */
|
|
93
|
+
statistic: number;
|
|
94
|
+
/** p-value (probability of observing result under null hypothesis) */
|
|
95
|
+
pValue: number;
|
|
96
|
+
/** Whether the null hypothesis is rejected at the given alpha */
|
|
97
|
+
significant: boolean;
|
|
98
|
+
/** Confidence interval [lower, upper] */
|
|
99
|
+
confidenceInterval: [number, number];
|
|
100
|
+
/** Effect size (Cohen's d for t-test) */
|
|
101
|
+
effectSize: number;
|
|
102
|
+
/** Name of the test used */
|
|
103
|
+
testName: string;
|
|
104
|
+
/** Number of samples */
|
|
105
|
+
sampleSize: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Complete simulation experiment result.
|
|
110
|
+
*/
|
|
111
|
+
export interface ExperimentResult {
|
|
112
|
+
/** The hypothesis tested */
|
|
113
|
+
hypothesis: Hypothesis;
|
|
114
|
+
/** Statistical results per metric */
|
|
115
|
+
results: Record<string, StatisticalResult>;
|
|
116
|
+
/** All collected metrics */
|
|
117
|
+
metrics: SimulationMetrics[];
|
|
118
|
+
/** Total epochs run */
|
|
119
|
+
totalEpochs: number;
|
|
120
|
+
/** Total wall-clock time */
|
|
121
|
+
totalDurationMs: number;
|
|
122
|
+
/** Status */
|
|
123
|
+
status: SimulationStatus;
|
|
124
|
+
/** Composition hash for reproducibility */
|
|
125
|
+
compositionHash: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// =============================================================================
|
|
129
|
+
// PARAMETER SWEEP GENERATION
|
|
130
|
+
// =============================================================================
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check if a parameter sweep is a range (vs discrete set).
|
|
134
|
+
*/
|
|
135
|
+
export function isParameterRange(sweep: ParameterSweep): sweep is ParameterRange {
|
|
136
|
+
return 'min' in sweep && 'max' in sweep && 'step' in sweep;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Generate all values from a parameter sweep.
|
|
141
|
+
*/
|
|
142
|
+
export function expandSweep(sweep: ParameterSweep): number[] {
|
|
143
|
+
if (!isParameterRange(sweep)) {
|
|
144
|
+
return (sweep as ParameterSet).values.slice();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const range = sweep as ParameterRange;
|
|
148
|
+
if (range.step <= 0) return [range.min];
|
|
149
|
+
if (range.min > range.max) return [];
|
|
150
|
+
|
|
151
|
+
const values: number[] = [];
|
|
152
|
+
for (let v = range.min; v <= range.max + range.step * 0.001; v += range.step) {
|
|
153
|
+
values.push(Math.round(v * 1e10) / 1e10); // Avoid floating point drift
|
|
154
|
+
}
|
|
155
|
+
return values;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Generate all parameter combinations from multiple sweeps.
|
|
160
|
+
* Returns an array of parameter maps.
|
|
161
|
+
*/
|
|
162
|
+
export function generateSweepCombinations(sweeps: ParameterSweep[]): Record<string, number>[] {
|
|
163
|
+
if (sweeps.length === 0) return [{}];
|
|
164
|
+
|
|
165
|
+
const expanded = sweeps.map((s) => ({
|
|
166
|
+
name: s.name,
|
|
167
|
+
values: expandSweep(s),
|
|
168
|
+
}));
|
|
169
|
+
|
|
170
|
+
// Cartesian product
|
|
171
|
+
let combinations: Record<string, number>[] = [{}];
|
|
172
|
+
|
|
173
|
+
for (const { name, values } of expanded) {
|
|
174
|
+
const newCombinations: Record<string, number>[] = [];
|
|
175
|
+
for (const combo of combinations) {
|
|
176
|
+
for (const val of values) {
|
|
177
|
+
newCombinations.push({ ...combo, [name]: val });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
combinations = newCombinations;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return combinations;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// =============================================================================
|
|
187
|
+
// STATISTICAL FUNCTIONS
|
|
188
|
+
// =============================================================================
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Calculate the arithmetic mean of an array of numbers.
|
|
192
|
+
*/
|
|
193
|
+
export function mean(data: number[]): number {
|
|
194
|
+
if (data.length === 0) return 0;
|
|
195
|
+
return data.reduce((sum, v) => sum + v, 0) / data.length;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Calculate the sample variance (Bessel's correction).
|
|
200
|
+
*/
|
|
201
|
+
export function variance(data: number[]): number {
|
|
202
|
+
if (data.length < 2) return 0;
|
|
203
|
+
const m = mean(data);
|
|
204
|
+
const squaredDiffs = data.reduce((sum, v) => sum + (v - m) ** 2, 0);
|
|
205
|
+
return squaredDiffs / (data.length - 1);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Calculate the sample standard deviation.
|
|
210
|
+
*/
|
|
211
|
+
export function standardDeviation(data: number[]): number {
|
|
212
|
+
return Math.sqrt(variance(data));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Calculate the standard error of the mean.
|
|
217
|
+
*/
|
|
218
|
+
export function standardError(data: number[]): number {
|
|
219
|
+
if (data.length === 0) return 0;
|
|
220
|
+
return standardDeviation(data) / Math.sqrt(data.length);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Calculate Cohen's d effect size between two groups.
|
|
225
|
+
* d = (mean1 - mean2) / pooled_std
|
|
226
|
+
*/
|
|
227
|
+
export function cohensD(group1: number[], group2: number[]): number {
|
|
228
|
+
if (group1.length < 2 || group2.length < 2) return 0;
|
|
229
|
+
|
|
230
|
+
const m1 = mean(group1);
|
|
231
|
+
const m2 = mean(group2);
|
|
232
|
+
const v1 = variance(group1);
|
|
233
|
+
const v2 = variance(group2);
|
|
234
|
+
|
|
235
|
+
// Pooled standard deviation
|
|
236
|
+
const n1 = group1.length;
|
|
237
|
+
const n2 = group2.length;
|
|
238
|
+
const pooledVar = ((n1 - 1) * v1 + (n2 - 1) * v2) / (n1 + n2 - 2);
|
|
239
|
+
const pooledStd = Math.sqrt(pooledVar);
|
|
240
|
+
|
|
241
|
+
if (pooledStd === 0) return 0;
|
|
242
|
+
return (m1 - m2) / pooledStd;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Two-sample independent t-test.
|
|
247
|
+
*
|
|
248
|
+
* Tests whether two groups have significantly different means.
|
|
249
|
+
* Uses Welch's t-test (does not assume equal variances).
|
|
250
|
+
*/
|
|
251
|
+
export function tTest(
|
|
252
|
+
group1: number[],
|
|
253
|
+
group2: number[],
|
|
254
|
+
alpha: number = 0.05,
|
|
255
|
+
direction: HypothesisDirection = 'different'
|
|
256
|
+
): StatisticalResult {
|
|
257
|
+
const n1 = group1.length;
|
|
258
|
+
const n2 = group2.length;
|
|
259
|
+
|
|
260
|
+
if (n1 < 2 || n2 < 2) {
|
|
261
|
+
return {
|
|
262
|
+
statistic: 0,
|
|
263
|
+
pValue: 1,
|
|
264
|
+
significant: false,
|
|
265
|
+
confidenceInterval: [0, 0],
|
|
266
|
+
effectSize: 0,
|
|
267
|
+
testName: 'welch_t_test',
|
|
268
|
+
sampleSize: n1 + n2,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const m1 = mean(group1);
|
|
273
|
+
const m2 = mean(group2);
|
|
274
|
+
const v1 = variance(group1);
|
|
275
|
+
const v2 = variance(group2);
|
|
276
|
+
const se = Math.sqrt(v1 / n1 + v2 / n2);
|
|
277
|
+
|
|
278
|
+
if (se === 0) {
|
|
279
|
+
return {
|
|
280
|
+
statistic: 0,
|
|
281
|
+
pValue: m1 === m2 ? 1 : 0,
|
|
282
|
+
significant: m1 !== m2,
|
|
283
|
+
confidenceInterval: [m1 - m2, m1 - m2],
|
|
284
|
+
effectSize: 0,
|
|
285
|
+
testName: 'welch_t_test',
|
|
286
|
+
sampleSize: n1 + n2,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const t = (m1 - m2) / se;
|
|
291
|
+
|
|
292
|
+
// Welch–Satterthwaite degrees of freedom
|
|
293
|
+
const num = (v1 / n1 + v2 / n2) ** 2;
|
|
294
|
+
const den = (v1 / n1) ** 2 / (n1 - 1) + (v2 / n2) ** 2 / (n2 - 1);
|
|
295
|
+
const df = num / den;
|
|
296
|
+
|
|
297
|
+
// Approximate p-value using the t-distribution CDF approximation
|
|
298
|
+
const pTwoTailed = tDistPValue(Math.abs(t), df);
|
|
299
|
+
let pValue: number;
|
|
300
|
+
|
|
301
|
+
switch (direction) {
|
|
302
|
+
case 'greater':
|
|
303
|
+
pValue = t > 0 ? pTwoTailed / 2 : 1 - pTwoTailed / 2;
|
|
304
|
+
break;
|
|
305
|
+
case 'less':
|
|
306
|
+
pValue = t < 0 ? pTwoTailed / 2 : 1 - pTwoTailed / 2;
|
|
307
|
+
break;
|
|
308
|
+
case 'equal':
|
|
309
|
+
pValue = 1 - pTwoTailed;
|
|
310
|
+
break;
|
|
311
|
+
default: // 'different'
|
|
312
|
+
pValue = pTwoTailed;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Confidence interval for the difference in means
|
|
316
|
+
const tCrit = tDistCritical(alpha, df);
|
|
317
|
+
const diff = m1 - m2;
|
|
318
|
+
const ci: [number, number] = [diff - tCrit * se, diff + tCrit * se];
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
statistic: t,
|
|
322
|
+
pValue,
|
|
323
|
+
significant: pValue < alpha,
|
|
324
|
+
confidenceInterval: ci,
|
|
325
|
+
effectSize: cohensD(group1, group2),
|
|
326
|
+
testName: 'welch_t_test',
|
|
327
|
+
sampleSize: n1 + n2,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* One-sample t-test against a known value.
|
|
333
|
+
*/
|
|
334
|
+
export function oneSampleTTest(
|
|
335
|
+
data: number[],
|
|
336
|
+
populationMean: number,
|
|
337
|
+
alpha: number = 0.05,
|
|
338
|
+
direction: HypothesisDirection = 'different'
|
|
339
|
+
): StatisticalResult {
|
|
340
|
+
const n = data.length;
|
|
341
|
+
|
|
342
|
+
if (n < 2) {
|
|
343
|
+
return {
|
|
344
|
+
statistic: 0,
|
|
345
|
+
pValue: 1,
|
|
346
|
+
significant: false,
|
|
347
|
+
confidenceInterval: [0, 0],
|
|
348
|
+
effectSize: 0,
|
|
349
|
+
testName: 'one_sample_t_test',
|
|
350
|
+
sampleSize: n,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const m = mean(data);
|
|
355
|
+
const se = standardError(data);
|
|
356
|
+
|
|
357
|
+
if (se === 0) {
|
|
358
|
+
return {
|
|
359
|
+
statistic: 0,
|
|
360
|
+
pValue: m === populationMean ? 1 : 0,
|
|
361
|
+
significant: m !== populationMean,
|
|
362
|
+
confidenceInterval: [m, m],
|
|
363
|
+
effectSize: 0,
|
|
364
|
+
testName: 'one_sample_t_test',
|
|
365
|
+
sampleSize: n,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const t = (m - populationMean) / se;
|
|
370
|
+
const df = n - 1;
|
|
371
|
+
|
|
372
|
+
const pTwoTailed = tDistPValue(Math.abs(t), df);
|
|
373
|
+
let pValue: number;
|
|
374
|
+
|
|
375
|
+
switch (direction) {
|
|
376
|
+
case 'greater':
|
|
377
|
+
pValue = t > 0 ? pTwoTailed / 2 : 1 - pTwoTailed / 2;
|
|
378
|
+
break;
|
|
379
|
+
case 'less':
|
|
380
|
+
pValue = t < 0 ? pTwoTailed / 2 : 1 - pTwoTailed / 2;
|
|
381
|
+
break;
|
|
382
|
+
default:
|
|
383
|
+
pValue = pTwoTailed;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const tCrit = tDistCritical(alpha, df);
|
|
387
|
+
const ci: [number, number] = [m - tCrit * se, m + tCrit * se];
|
|
388
|
+
|
|
389
|
+
const sd = standardDeviation(data);
|
|
390
|
+
const effectSize = sd > 0 ? (m - populationMean) / sd : 0;
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
statistic: t,
|
|
394
|
+
pValue,
|
|
395
|
+
significant: pValue < alpha,
|
|
396
|
+
confidenceInterval: ci,
|
|
397
|
+
effectSize,
|
|
398
|
+
testName: 'one_sample_t_test',
|
|
399
|
+
sampleSize: n,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Mann-Whitney U test (non-parametric alternative to t-test).
|
|
405
|
+
* Tests whether one group tends to have larger values than the other.
|
|
406
|
+
*/
|
|
407
|
+
export function mannWhitneyU(
|
|
408
|
+
group1: number[],
|
|
409
|
+
group2: number[],
|
|
410
|
+
alpha: number = 0.05
|
|
411
|
+
): StatisticalResult {
|
|
412
|
+
const n1 = group1.length;
|
|
413
|
+
const n2 = group2.length;
|
|
414
|
+
|
|
415
|
+
if (n1 === 0 || n2 === 0) {
|
|
416
|
+
return {
|
|
417
|
+
statistic: 0,
|
|
418
|
+
pValue: 1,
|
|
419
|
+
significant: false,
|
|
420
|
+
confidenceInterval: [0, 0],
|
|
421
|
+
effectSize: 0,
|
|
422
|
+
testName: 'mann_whitney_u',
|
|
423
|
+
sampleSize: n1 + n2,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Count how many times group1 values exceed group2 values
|
|
428
|
+
let u1 = 0;
|
|
429
|
+
for (const a of group1) {
|
|
430
|
+
for (const b of group2) {
|
|
431
|
+
if (a > b) u1++;
|
|
432
|
+
else if (a === b) u1 += 0.5;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const u2 = n1 * n2 - u1;
|
|
437
|
+
const u = Math.min(u1, u2);
|
|
438
|
+
|
|
439
|
+
// Normal approximation for larger samples
|
|
440
|
+
const meanU = (n1 * n2) / 2;
|
|
441
|
+
const sigmaU = Math.sqrt((n1 * n2 * (n1 + n2 + 1)) / 12);
|
|
442
|
+
|
|
443
|
+
if (sigmaU === 0) {
|
|
444
|
+
return {
|
|
445
|
+
statistic: u,
|
|
446
|
+
pValue: 1,
|
|
447
|
+
significant: false,
|
|
448
|
+
confidenceInterval: [0, 0],
|
|
449
|
+
effectSize: 0,
|
|
450
|
+
testName: 'mann_whitney_u',
|
|
451
|
+
sampleSize: n1 + n2,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const z = (u - meanU) / sigmaU;
|
|
456
|
+
const pValue = 2 * normalCDF(-Math.abs(z)); // Two-tailed
|
|
457
|
+
|
|
458
|
+
// Effect size: rank-biserial correlation r = 1 - 2U/(n1*n2)
|
|
459
|
+
const effectSize = 1 - (2 * u) / (n1 * n2);
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
statistic: u,
|
|
463
|
+
pValue,
|
|
464
|
+
significant: pValue < alpha,
|
|
465
|
+
confidenceInterval: [0, 0], // CI not standard for U-test
|
|
466
|
+
effectSize,
|
|
467
|
+
testName: 'mann_whitney_u',
|
|
468
|
+
sampleSize: n1 + n2,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Chi-squared goodness-of-fit test.
|
|
474
|
+
* Tests whether observed frequencies match expected frequencies.
|
|
475
|
+
*/
|
|
476
|
+
export function chiSquaredTest(
|
|
477
|
+
observed: number[],
|
|
478
|
+
expected: number[],
|
|
479
|
+
alpha: number = 0.05
|
|
480
|
+
): StatisticalResult {
|
|
481
|
+
if (observed.length !== expected.length || observed.length === 0) {
|
|
482
|
+
return {
|
|
483
|
+
statistic: 0,
|
|
484
|
+
pValue: 1,
|
|
485
|
+
significant: false,
|
|
486
|
+
confidenceInterval: [0, 0],
|
|
487
|
+
effectSize: 0,
|
|
488
|
+
testName: 'chi_squared',
|
|
489
|
+
sampleSize: 0,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
let chiSq = 0;
|
|
494
|
+
let totalObserved = 0;
|
|
495
|
+
|
|
496
|
+
for (let i = 0; i < observed.length; i++) {
|
|
497
|
+
if (expected[i] <= 0) continue;
|
|
498
|
+
chiSq += (observed[i] - expected[i]) ** 2 / expected[i];
|
|
499
|
+
totalObserved += observed[i];
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const df = observed.length - 1;
|
|
503
|
+
if (df <= 0) {
|
|
504
|
+
return {
|
|
505
|
+
statistic: chiSq,
|
|
506
|
+
pValue: 1,
|
|
507
|
+
significant: false,
|
|
508
|
+
confidenceInterval: [0, 0],
|
|
509
|
+
effectSize: 0,
|
|
510
|
+
testName: 'chi_squared',
|
|
511
|
+
sampleSize: totalObserved,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Approximate p-value using Wilson-Hilferty approximation
|
|
516
|
+
const pValue = chiSquaredPValue(chiSq, df);
|
|
517
|
+
|
|
518
|
+
// Cramér's V effect size
|
|
519
|
+
const k = observed.length;
|
|
520
|
+
const effectSize = totalObserved > 0 ? Math.sqrt(chiSq / (totalObserved * (k - 1))) : 0;
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
statistic: chiSq,
|
|
524
|
+
pValue,
|
|
525
|
+
significant: pValue < alpha,
|
|
526
|
+
confidenceInterval: [0, 0],
|
|
527
|
+
effectSize,
|
|
528
|
+
testName: 'chi_squared',
|
|
529
|
+
sampleSize: totalObserved,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// =============================================================================
|
|
534
|
+
// METRIC AGGREGATION
|
|
535
|
+
// =============================================================================
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Extract a named metric from an array of simulation results.
|
|
539
|
+
*/
|
|
540
|
+
export function extractMetric(results: SimulationMetrics[], metricName: string): number[] {
|
|
541
|
+
return results.filter((r) => metricName in r.values).map((r) => r.values[metricName]);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Group simulation results by a parameter value.
|
|
546
|
+
*/
|
|
547
|
+
export function groupByParameter(
|
|
548
|
+
results: SimulationMetrics[],
|
|
549
|
+
paramName: string
|
|
550
|
+
): Map<number, SimulationMetrics[]> {
|
|
551
|
+
const groups = new Map<number, SimulationMetrics[]>();
|
|
552
|
+
for (const r of results) {
|
|
553
|
+
const val = r.params[paramName];
|
|
554
|
+
if (val === undefined) continue;
|
|
555
|
+
if (!groups.has(val)) groups.set(val, []);
|
|
556
|
+
groups.get(val)!.push(r);
|
|
557
|
+
}
|
|
558
|
+
return groups;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Generate a summary of metrics across all runs.
|
|
563
|
+
*/
|
|
564
|
+
export function summarizeMetrics(
|
|
565
|
+
results: SimulationMetrics[],
|
|
566
|
+
metricName: string
|
|
567
|
+
): { mean: number; std: number; min: number; max: number; n: number } {
|
|
568
|
+
const values = extractMetric(results, metricName);
|
|
569
|
+
if (values.length === 0) {
|
|
570
|
+
return { mean: 0, std: 0, min: 0, max: 0, n: 0 };
|
|
571
|
+
}
|
|
572
|
+
return {
|
|
573
|
+
mean: mean(values),
|
|
574
|
+
std: standardDeviation(values),
|
|
575
|
+
min: Math.min(...values),
|
|
576
|
+
max: Math.max(...values),
|
|
577
|
+
n: values.length,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// =============================================================================
|
|
582
|
+
// INTERNAL MATH HELPERS
|
|
583
|
+
// =============================================================================
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Standard normal CDF approximation (Abramowitz and Stegun 26.2.17).
|
|
587
|
+
* Accurate to ~1.5e-7.
|
|
588
|
+
*/
|
|
589
|
+
function normalCDF(x: number): number {
|
|
590
|
+
if (x < -8) return 0;
|
|
591
|
+
if (x > 8) return 1;
|
|
592
|
+
|
|
593
|
+
const a1 = 0.254829592;
|
|
594
|
+
const a2 = -0.284496736;
|
|
595
|
+
const a3 = 1.421413741;
|
|
596
|
+
const a4 = -1.453152027;
|
|
597
|
+
const a5 = 1.061405429;
|
|
598
|
+
const p = 0.3275911;
|
|
599
|
+
|
|
600
|
+
const sign = x < 0 ? -1 : 1;
|
|
601
|
+
const absX = Math.abs(x);
|
|
602
|
+
const t = 1.0 / (1.0 + p * absX);
|
|
603
|
+
const y = 1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp((-absX * absX) / 2);
|
|
604
|
+
|
|
605
|
+
return 0.5 * (1.0 + sign * y);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Approximate two-tailed p-value from t-distribution.
|
|
610
|
+
* Uses the Inamdar-Griessammer approximation for large df.
|
|
611
|
+
*/
|
|
612
|
+
function tDistPValue(absT: number, df: number): number {
|
|
613
|
+
if (df <= 0) return 1;
|
|
614
|
+
// Convert t to approximately normal z for large df
|
|
615
|
+
const z = (absT * (1 - 1 / (4 * df))) / Math.sqrt(1 + (absT * absT) / (2 * df));
|
|
616
|
+
return 2 * (1 - normalCDF(z));
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Approximate critical t-value for a given alpha and df.
|
|
621
|
+
*/
|
|
622
|
+
function tDistCritical(alpha: number, df: number): number {
|
|
623
|
+
// Approximate using normal quantile adjusted for df
|
|
624
|
+
const p = 1 - alpha / 2;
|
|
625
|
+
// Rational approximation of the normal quantile (Beasley-Springer-Moro)
|
|
626
|
+
const a = p - 0.5;
|
|
627
|
+
const r = a * a;
|
|
628
|
+
let z =
|
|
629
|
+
(a * (2.50662823884 + r * (-18.61500062529 + r * (41.39119773534 + r * -25.44106049637)))) /
|
|
630
|
+
(1 + r * (-8.4735109309 + r * (23.08336743743 + r * (-21.06224101826 + r * 3.13082909833))));
|
|
631
|
+
|
|
632
|
+
// Cornish-Fisher expansion for t-distribution
|
|
633
|
+
const g1 = (z ** 3 + z) / (4 * df);
|
|
634
|
+
const g2 = (5 * z ** 5 + 16 * z ** 3 + 3 * z) / (96 * df * df);
|
|
635
|
+
z = z + g1 + g2;
|
|
636
|
+
|
|
637
|
+
return Math.abs(z);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Approximate chi-squared p-value using Wilson-Hilferty.
|
|
642
|
+
*/
|
|
643
|
+
function chiSquaredPValue(chiSq: number, df: number): number {
|
|
644
|
+
if (df <= 0) return 1;
|
|
645
|
+
if (chiSq <= 0) return 1;
|
|
646
|
+
// Wilson-Hilferty transformation to normal
|
|
647
|
+
const z = Math.pow(chiSq / df, 1 / 3) - (1 - 2 / (9 * df));
|
|
648
|
+
const denom = Math.sqrt(2 / (9 * df));
|
|
649
|
+
return 1 - normalCDF(z / denom);
|
|
650
|
+
}
|