@idlekit/core 0.2.0 → 0.3.0
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/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/scenario/compile.d.ts +25 -0
- package/dist/scenario/compile.js +60 -0
- package/dist/scenario/compile.js.map +1 -1
- package/dist/scenario/scenarioContract.d.ts +152 -0
- package/dist/scenario/scenarioContract.js +2815 -0
- package/dist/scenario/scenarioContract.js.map +1 -0
- package/dist/sim/analysis/analyzers.d.ts +245 -0
- package/dist/sim/analysis/analyzers.js +1076 -0
- package/dist/sim/analysis/analyzers.js.map +1 -0
- package/dist/sim/analysis/contracts.d.ts +147 -0
- package/dist/sim/analysis/contracts.js +1230 -0
- package/dist/sim/analysis/contracts.js.map +1 -0
- package/dist/sim/analysis/distribution.d.ts +199 -0
- package/dist/sim/analysis/distribution.js +1246 -0
- package/dist/sim/analysis/distribution.js.map +1 -0
- package/dist/sim/analysis/eta.d.ts +118 -0
- package/dist/sim/analysis/eta.js +968 -70
- package/dist/sim/analysis/eta.js.map +1 -1
- package/dist/sim/analysis/experiment.d.ts +300 -0
- package/dist/sim/analysis/experiment.js +2206 -0
- package/dist/sim/analysis/experiment.js.map +1 -0
- package/dist/sim/analysis/growth.d.ts +2 -0
- package/dist/sim/analysis/growth.js +5 -1
- package/dist/sim/analysis/growth.js.map +1 -1
- package/dist/sim/analysis/growthFit.d.ts +183 -0
- package/dist/sim/analysis/growthFit.js +1798 -0
- package/dist/sim/analysis/growthFit.js.map +1 -0
- package/dist/sim/analysis/inspect.d.ts +236 -0
- package/dist/sim/analysis/inspect.js +388 -0
- package/dist/sim/analysis/inspect.js.map +1 -0
- package/dist/sim/analysis/metrics.d.ts +91 -0
- package/dist/sim/analysis/metrics.js +266 -0
- package/dist/sim/analysis/metrics.js.map +1 -0
- package/dist/sim/analysis/prestigeCycle.d.ts +6 -0
- package/dist/sim/analysis/prestigeCycle.js +10 -1
- package/dist/sim/analysis/prestigeCycle.js.map +1 -1
- package/dist/sim/analysis/registry.d.ts +41 -0
- package/dist/sim/analysis/registry.js +54 -0
- package/dist/sim/analysis/registry.js.map +1 -0
- package/dist/sim/analysis/resetActions.d.ts +7 -0
- package/dist/sim/analysis/resetActions.js +45 -0
- package/dist/sim/analysis/resetActions.js.map +1 -0
- package/dist/sim/analysis/resetCycle.d.ts +175 -0
- package/dist/sim/analysis/resetCycle.js +1009 -0
- package/dist/sim/analysis/resetCycle.js.map +1 -0
- package/dist/sim/analysis/resetOptimization.d.ts +198 -0
- package/dist/sim/analysis/resetOptimization.js +2187 -0
- package/dist/sim/analysis/resetOptimization.js.map +1 -0
- package/dist/sim/analysis/sensitivity.d.ts +73 -0
- package/dist/sim/analysis/sensitivity.js +160 -0
- package/dist/sim/analysis/sensitivity.js.map +1 -0
- package/dist/sim/monteCarlo.d.ts +12 -0
- package/dist/sim/monteCarlo.js +33 -4
- package/dist/sim/monteCarlo.js.map +1 -1
- package/dist/sim/observation.d.ts +22 -0
- package/dist/sim/observation.js +33 -0
- package/dist/sim/observation.js.map +1 -1
- package/dist/sim/offline.js +9 -2
- package/dist/sim/offline.js.map +1 -1
- package/dist/sim/random.d.ts +15 -0
- package/dist/sim/random.js +28 -0
- package/dist/sim/random.js.map +1 -1
- package/dist/sim/session.d.ts +7 -0
- package/dist/sim/session.js +13 -0
- package/dist/sim/session.js.map +1 -1
- package/dist/sim/simulator.js +7 -1
- package/dist/sim/simulator.js.map +1 -1
- package/dist/sim/step.d.ts +67 -1
- package/dist/sim/step.js +100 -18
- package/dist/sim/step.js.map +1 -1
- package/dist/sim/strategy/opt/freshRun.d.ts +3 -0
- package/dist/sim/strategy/opt/freshRun.js +6 -0
- package/dist/sim/strategy/opt/freshRun.js.map +1 -0
- package/dist/sim/strategy/opt/runner.js +2 -2
- package/dist/sim/strategy/opt/runner.js.map +1 -1
- package/dist/sim/strategy/planner.d.ts +26 -0
- package/dist/sim/strategy/planner.js +30 -0
- package/dist/sim/strategy/planner.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,1798 @@
|
|
|
1
|
+
import * as _validateReport_1 from "typia/lib/internal/_validateReport";
|
|
2
|
+
import * as _createStandardSchema_1 from "typia/lib/internal/_createStandardSchema";
|
|
3
|
+
import typia, {} from "typia";
|
|
4
|
+
import { legacyGrowthSpeedBucket } from "./growth";
|
|
5
|
+
/**
|
|
6
|
+
* Growth fit v1.
|
|
7
|
+
* `analyzeGrowth` stays the legacy speed-bucket report.
|
|
8
|
+
* This artifact separates shape from log slope and does not prove an asymptote.
|
|
9
|
+
*/
|
|
10
|
+
const GROWTH_FIT_ALGORITHM_VERSION = "growth-fit-v1";
|
|
11
|
+
const GROWTH_FIT_SCHEMA_ID = "https://idlekit.dev/schemas/growth.fit.v1.schema.json";
|
|
12
|
+
const MIN_SAMPLES = 8;
|
|
13
|
+
const MIN_SPAN_SEC = 30;
|
|
14
|
+
const LOG_RMSE_OK = 1e-4;
|
|
15
|
+
const VALUE_REL_OK = 1e-4;
|
|
16
|
+
/** A value-space fit this close is exact for a line or parabola, not an approximation. */
|
|
17
|
+
const VALUE_EXACT = 1e-9;
|
|
18
|
+
const SLOPE_TOLERANCE = 1e-4;
|
|
19
|
+
const DROP_LOG10 = 1;
|
|
20
|
+
/**
|
|
21
|
+
* @evidence docs/requirements/active/growth-model-classification.md#req-pr12-shape-speed Slow and fast exponentials share a shape. Other formulas stay distinct, and a short trace stays unknown.
|
|
22
|
+
* @evidenceReview docs/requirements/active/growth-model-classification.md#req-pr12-shape-speed #acbd473 Re-read the section: exp(0.001 t) and exp(0.2 t) are both exponential, and the slopes differ.
|
|
23
|
+
*/
|
|
24
|
+
export const growthShapeIndependentOfSpeed = "shape-independent-of-log-slope";
|
|
25
|
+
/**
|
|
26
|
+
* @evidence docs/requirements/active/growth-model-classification.md#req-pr12-scale-domain A rescaled huge series keeps its shape. Zero, negative, a drop, irregular time, and one point are not a speed claim.
|
|
27
|
+
* @evidenceReview docs/requirements/active/growth-model-classification.md#req-pr12-scale-domain #38e5c42 Re-read the section: 1e400 and a constant multiple match, and abs is not applied to a negative sample.
|
|
28
|
+
*/
|
|
29
|
+
export const growthDomainNotDisguised = "non-positive-and-discontinuity-are-not-speed";
|
|
30
|
+
/**
|
|
31
|
+
* @evidence docs/requirements/active/growth-model-classification.md#req-pr12-legacy-preserved The old report still names speed buckets. netWorthFallback stays on both results.
|
|
32
|
+
* @evidenceReview docs/requirements/active/growth-model-classification.md#req-pr12-legacy-preserved #1355aa0 Re-read the section: the legacy regime is still a slope bucket, and the fit does not replace it.
|
|
33
|
+
*/
|
|
34
|
+
export const growthLegacyObservationPreserved = "legacy-report-and-net-worth-fallback";
|
|
35
|
+
export function growthFitSchemaId() {
|
|
36
|
+
return GROWTH_FIT_SCHEMA_ID;
|
|
37
|
+
}
|
|
38
|
+
export function growthFitAlgorithmVersion() {
|
|
39
|
+
return GROWTH_FIT_ALGORITHM_VERSION;
|
|
40
|
+
}
|
|
41
|
+
export function growthFitSlopeTolerance() {
|
|
42
|
+
return SLOPE_TOLERANCE;
|
|
43
|
+
}
|
|
44
|
+
export class GrowthFitRejected extends Error {
|
|
45
|
+
constructor(message) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = "GrowthFitRejected";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/*
|
|
51
|
+
* typia runs on first call, not at module load. A plugin that the bundled CLI loads
|
|
52
|
+
* imports core source without the transform, and a module-load call would throw there.
|
|
53
|
+
*/
|
|
54
|
+
let growthFitValidator;
|
|
55
|
+
function buildGrowthFitJsonSchema() {
|
|
56
|
+
return {
|
|
57
|
+
components: {
|
|
58
|
+
schemas: {
|
|
59
|
+
"ReadonlyschemaVersion1algorithmVersiongrowth-fit-v1methodheuristic-fitstatusAnalysisStatusseriesRequestedmoneynetWorthvalueSourcemoneynetWorthnetWorthFallback---15more---windowsreadonlyReadonly---": {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
schemaVersion: {
|
|
63
|
+
"const": 1
|
|
64
|
+
},
|
|
65
|
+
algorithmVersion: {
|
|
66
|
+
"const": "growth-fit-v1"
|
|
67
|
+
},
|
|
68
|
+
method: {
|
|
69
|
+
"const": "heuristic-fit"
|
|
70
|
+
},
|
|
71
|
+
status: {
|
|
72
|
+
$ref: "#/components/schemas/AnalysisStatus"
|
|
73
|
+
},
|
|
74
|
+
seriesRequested: {
|
|
75
|
+
oneOf: [
|
|
76
|
+
{
|
|
77
|
+
"const": "money"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"const": "netWorth"
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
valueSource: {
|
|
85
|
+
oneOf: [
|
|
86
|
+
{
|
|
87
|
+
"const": "money"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"const": "netWorth"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"const": "netWorthFallback"
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
windowSec: {
|
|
98
|
+
type: "number"
|
|
99
|
+
},
|
|
100
|
+
samplingBudget: {
|
|
101
|
+
type: "number"
|
|
102
|
+
},
|
|
103
|
+
sampleCount: {
|
|
104
|
+
type: "number"
|
|
105
|
+
},
|
|
106
|
+
inputSampleCount: {
|
|
107
|
+
type: "number"
|
|
108
|
+
},
|
|
109
|
+
slopeTolerance: {
|
|
110
|
+
type: "number"
|
|
111
|
+
},
|
|
112
|
+
fitWindow: {
|
|
113
|
+
$ref: "#/components/schemas/ReadonlywindowSecnumberstartSecnumberendSecnumber"
|
|
114
|
+
},
|
|
115
|
+
irregular: {
|
|
116
|
+
type: "boolean"
|
|
117
|
+
},
|
|
118
|
+
claimsResourceBottleneck: {
|
|
119
|
+
"const": false
|
|
120
|
+
},
|
|
121
|
+
softcap: {
|
|
122
|
+
oneOf: [
|
|
123
|
+
{
|
|
124
|
+
"const": "absent"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"const": "configured"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"const": "unknown"
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
cadenceSec: {
|
|
135
|
+
type: "number"
|
|
136
|
+
},
|
|
137
|
+
cadenceCoarserThanWindow: {
|
|
138
|
+
type: "boolean"
|
|
139
|
+
},
|
|
140
|
+
downsampled: {
|
|
141
|
+
$ref: "#/components/schemas/Readonlyseennumberkeptnumber"
|
|
142
|
+
},
|
|
143
|
+
retention: {
|
|
144
|
+
$ref: "#/components/schemas/ReadonlytotalSeennumberretainednumberdroppednumber"
|
|
145
|
+
},
|
|
146
|
+
assumptions: {
|
|
147
|
+
type: "array",
|
|
148
|
+
items: {
|
|
149
|
+
type: "string"
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
warnings: {
|
|
153
|
+
type: "array",
|
|
154
|
+
items: {
|
|
155
|
+
type: "string"
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
windows: {
|
|
159
|
+
type: "array",
|
|
160
|
+
items: {
|
|
161
|
+
$ref: "#/components/schemas/ReadonlystartSecnumberendSecnumbersampleCountnumberdomainGrowthDomainstatusAnalysisStatusshapeGrowthShapediscontinuityGrowthDiscontinuityKindspeedReadonly---legacySpeedBucketGrowthLegacyBucketnullresidualReadonly---qualitystring"
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
required: [
|
|
166
|
+
"schemaVersion",
|
|
167
|
+
"algorithmVersion",
|
|
168
|
+
"method",
|
|
169
|
+
"status",
|
|
170
|
+
"seriesRequested",
|
|
171
|
+
"valueSource",
|
|
172
|
+
"windowSec",
|
|
173
|
+
"samplingBudget",
|
|
174
|
+
"sampleCount",
|
|
175
|
+
"inputSampleCount",
|
|
176
|
+
"slopeTolerance",
|
|
177
|
+
"fitWindow",
|
|
178
|
+
"irregular",
|
|
179
|
+
"claimsResourceBottleneck",
|
|
180
|
+
"softcap",
|
|
181
|
+
"cadenceCoarserThanWindow",
|
|
182
|
+
"assumptions",
|
|
183
|
+
"warnings",
|
|
184
|
+
"windows"
|
|
185
|
+
],
|
|
186
|
+
additionalProperties: false
|
|
187
|
+
},
|
|
188
|
+
AnalysisStatus: {
|
|
189
|
+
oneOf: [
|
|
190
|
+
{
|
|
191
|
+
"const": "censored"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"const": "error"
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"const": "insufficient-data"
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"const": "invalid"
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"const": "ok"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"const": "unknown"
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"const": "unsupported"
|
|
210
|
+
}
|
|
211
|
+
]
|
|
212
|
+
},
|
|
213
|
+
ReadonlywindowSecnumberstartSecnumberendSecnumber: {
|
|
214
|
+
type: "object",
|
|
215
|
+
properties: {
|
|
216
|
+
windowSec: {
|
|
217
|
+
type: "number"
|
|
218
|
+
},
|
|
219
|
+
startSec: {
|
|
220
|
+
type: "number"
|
|
221
|
+
},
|
|
222
|
+
endSec: {
|
|
223
|
+
type: "number"
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
required: [
|
|
227
|
+
"windowSec",
|
|
228
|
+
"startSec",
|
|
229
|
+
"endSec"
|
|
230
|
+
],
|
|
231
|
+
additionalProperties: false
|
|
232
|
+
},
|
|
233
|
+
Readonlyseennumberkeptnumber: {
|
|
234
|
+
type: "object",
|
|
235
|
+
properties: {
|
|
236
|
+
seen: {
|
|
237
|
+
type: "number"
|
|
238
|
+
},
|
|
239
|
+
kept: {
|
|
240
|
+
type: "number"
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
required: [
|
|
244
|
+
"seen",
|
|
245
|
+
"kept"
|
|
246
|
+
],
|
|
247
|
+
additionalProperties: false
|
|
248
|
+
},
|
|
249
|
+
ReadonlytotalSeennumberretainednumberdroppednumber: {
|
|
250
|
+
type: "object",
|
|
251
|
+
properties: {
|
|
252
|
+
totalSeen: {
|
|
253
|
+
type: "number"
|
|
254
|
+
},
|
|
255
|
+
retained: {
|
|
256
|
+
type: "number"
|
|
257
|
+
},
|
|
258
|
+
dropped: {
|
|
259
|
+
type: "number"
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
required: [
|
|
263
|
+
"totalSeen",
|
|
264
|
+
"retained",
|
|
265
|
+
"dropped"
|
|
266
|
+
],
|
|
267
|
+
additionalProperties: false
|
|
268
|
+
},
|
|
269
|
+
"ReadonlystartSecnumberendSecnumbersampleCountnumberdomainGrowthDomainstatusAnalysisStatusshapeGrowthShapediscontinuityGrowthDiscontinuityKindspeedReadonly---legacySpeedBucketGrowthLegacyBucketnullresidualReadonly---qualitystring": {
|
|
270
|
+
type: "object",
|
|
271
|
+
properties: {
|
|
272
|
+
startSec: {
|
|
273
|
+
type: "number"
|
|
274
|
+
},
|
|
275
|
+
endSec: {
|
|
276
|
+
type: "number"
|
|
277
|
+
},
|
|
278
|
+
sampleCount: {
|
|
279
|
+
type: "number"
|
|
280
|
+
},
|
|
281
|
+
domain: {
|
|
282
|
+
$ref: "#/components/schemas/GrowthDomain"
|
|
283
|
+
},
|
|
284
|
+
status: {
|
|
285
|
+
$ref: "#/components/schemas/AnalysisStatus"
|
|
286
|
+
},
|
|
287
|
+
shape: {
|
|
288
|
+
$ref: "#/components/schemas/GrowthShape"
|
|
289
|
+
},
|
|
290
|
+
discontinuity: {
|
|
291
|
+
$ref: "#/components/schemas/GrowthDiscontinuityKind"
|
|
292
|
+
},
|
|
293
|
+
speed: {
|
|
294
|
+
$ref: "#/components/schemas/ReadonlylogSlopePerSecnumbernulldoublingTimeSecnumbernull"
|
|
295
|
+
},
|
|
296
|
+
legacySpeedBucket: {
|
|
297
|
+
oneOf: [
|
|
298
|
+
{
|
|
299
|
+
type: "null"
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
"const": "exp"
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
"const": "softcap"
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
"const": "stall"
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"const": "super-exp"
|
|
312
|
+
}
|
|
313
|
+
]
|
|
314
|
+
},
|
|
315
|
+
residual: {
|
|
316
|
+
$ref: "#/components/schemas/Readonlyspacelog-ratelog10nonevaluermsenumbernull"
|
|
317
|
+
},
|
|
318
|
+
quality: {
|
|
319
|
+
type: "string"
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
required: [
|
|
323
|
+
"startSec",
|
|
324
|
+
"endSec",
|
|
325
|
+
"sampleCount",
|
|
326
|
+
"domain",
|
|
327
|
+
"status",
|
|
328
|
+
"shape",
|
|
329
|
+
"discontinuity",
|
|
330
|
+
"speed",
|
|
331
|
+
"legacySpeedBucket",
|
|
332
|
+
"residual",
|
|
333
|
+
"quality"
|
|
334
|
+
],
|
|
335
|
+
additionalProperties: false
|
|
336
|
+
},
|
|
337
|
+
GrowthDomain: {
|
|
338
|
+
oneOf: [
|
|
339
|
+
{
|
|
340
|
+
"const": "empty"
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
"const": "mixed"
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
"const": "negative"
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
"const": "nonfinite"
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
"const": "positive"
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
"const": "zero"
|
|
356
|
+
}
|
|
357
|
+
]
|
|
358
|
+
},
|
|
359
|
+
GrowthShape: {
|
|
360
|
+
oneOf: [
|
|
361
|
+
{
|
|
362
|
+
"const": "accelerating-log"
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
"const": "constant"
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
"const": "exponential"
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
"const": "polynomial"
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
"const": "saturating"
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
"const": "unknown"
|
|
378
|
+
}
|
|
379
|
+
]
|
|
380
|
+
},
|
|
381
|
+
GrowthDiscontinuityKind: {
|
|
382
|
+
oneOf: [
|
|
383
|
+
{
|
|
384
|
+
"const": "drop"
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
"const": "none"
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
"const": "purchase"
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
"const": "reset"
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
"const": "sign"
|
|
397
|
+
}
|
|
398
|
+
]
|
|
399
|
+
},
|
|
400
|
+
ReadonlylogSlopePerSecnumbernulldoublingTimeSecnumbernull: {
|
|
401
|
+
type: "object",
|
|
402
|
+
properties: {
|
|
403
|
+
logSlopePerSec: {
|
|
404
|
+
oneOf: [
|
|
405
|
+
{
|
|
406
|
+
type: "null"
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
type: "number"
|
|
410
|
+
}
|
|
411
|
+
]
|
|
412
|
+
},
|
|
413
|
+
doublingTimeSec: {
|
|
414
|
+
oneOf: [
|
|
415
|
+
{
|
|
416
|
+
type: "null"
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
type: "number"
|
|
420
|
+
}
|
|
421
|
+
]
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
required: [
|
|
425
|
+
"logSlopePerSec",
|
|
426
|
+
"doublingTimeSec"
|
|
427
|
+
],
|
|
428
|
+
additionalProperties: false
|
|
429
|
+
},
|
|
430
|
+
"Readonlyspacelog-ratelog10nonevaluermsenumbernull": {
|
|
431
|
+
type: "object",
|
|
432
|
+
properties: {
|
|
433
|
+
space: {
|
|
434
|
+
oneOf: [
|
|
435
|
+
{
|
|
436
|
+
"const": "log-rate"
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
"const": "log10"
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
"const": "none"
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
"const": "value"
|
|
446
|
+
}
|
|
447
|
+
],
|
|
448
|
+
description: "\"log-rate\" is the natural log of the per-second growth rate, the space the saturation fit runs in."
|
|
449
|
+
},
|
|
450
|
+
rmse: {
|
|
451
|
+
oneOf: [
|
|
452
|
+
{
|
|
453
|
+
type: "null"
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
type: "number"
|
|
457
|
+
}
|
|
458
|
+
]
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
required: [
|
|
462
|
+
"space",
|
|
463
|
+
"rmse"
|
|
464
|
+
],
|
|
465
|
+
additionalProperties: false
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
schema: {
|
|
470
|
+
type: "object",
|
|
471
|
+
properties: {
|
|
472
|
+
schemaVersion: {
|
|
473
|
+
"const": 1
|
|
474
|
+
},
|
|
475
|
+
algorithmVersion: {
|
|
476
|
+
"const": "growth-fit-v1"
|
|
477
|
+
},
|
|
478
|
+
method: {
|
|
479
|
+
"const": "heuristic-fit"
|
|
480
|
+
},
|
|
481
|
+
status: {
|
|
482
|
+
$ref: "#/components/schemas/AnalysisStatus"
|
|
483
|
+
},
|
|
484
|
+
seriesRequested: {
|
|
485
|
+
oneOf: [
|
|
486
|
+
{
|
|
487
|
+
"const": "money"
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
"const": "netWorth"
|
|
491
|
+
}
|
|
492
|
+
]
|
|
493
|
+
},
|
|
494
|
+
valueSource: {
|
|
495
|
+
oneOf: [
|
|
496
|
+
{
|
|
497
|
+
"const": "money"
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
"const": "netWorth"
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
"const": "netWorthFallback"
|
|
504
|
+
}
|
|
505
|
+
]
|
|
506
|
+
},
|
|
507
|
+
windowSec: {
|
|
508
|
+
type: "number"
|
|
509
|
+
},
|
|
510
|
+
samplingBudget: {
|
|
511
|
+
type: "number"
|
|
512
|
+
},
|
|
513
|
+
sampleCount: {
|
|
514
|
+
type: "number"
|
|
515
|
+
},
|
|
516
|
+
inputSampleCount: {
|
|
517
|
+
type: "number"
|
|
518
|
+
},
|
|
519
|
+
slopeTolerance: {
|
|
520
|
+
type: "number"
|
|
521
|
+
},
|
|
522
|
+
fitWindow: {
|
|
523
|
+
$ref: "#/components/schemas/ReadonlywindowSecnumberstartSecnumberendSecnumber"
|
|
524
|
+
},
|
|
525
|
+
irregular: {
|
|
526
|
+
type: "boolean"
|
|
527
|
+
},
|
|
528
|
+
claimsResourceBottleneck: {
|
|
529
|
+
"const": false
|
|
530
|
+
},
|
|
531
|
+
softcap: {
|
|
532
|
+
oneOf: [
|
|
533
|
+
{
|
|
534
|
+
"const": "absent"
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
"const": "configured"
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
"const": "unknown"
|
|
541
|
+
}
|
|
542
|
+
]
|
|
543
|
+
},
|
|
544
|
+
cadenceSec: {
|
|
545
|
+
type: "number"
|
|
546
|
+
},
|
|
547
|
+
cadenceCoarserThanWindow: {
|
|
548
|
+
type: "boolean"
|
|
549
|
+
},
|
|
550
|
+
downsampled: {
|
|
551
|
+
$ref: "#/components/schemas/Readonlyseennumberkeptnumber"
|
|
552
|
+
},
|
|
553
|
+
retention: {
|
|
554
|
+
$ref: "#/components/schemas/ReadonlytotalSeennumberretainednumberdroppednumber"
|
|
555
|
+
},
|
|
556
|
+
assumptions: {
|
|
557
|
+
type: "array",
|
|
558
|
+
items: {
|
|
559
|
+
type: "string"
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
warnings: {
|
|
563
|
+
type: "array",
|
|
564
|
+
items: {
|
|
565
|
+
type: "string"
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
windows: {
|
|
569
|
+
type: "array",
|
|
570
|
+
items: {
|
|
571
|
+
$ref: "#/components/schemas/ReadonlystartSecnumberendSecnumbersampleCountnumberdomainGrowthDomainstatusAnalysisStatusshapeGrowthShapediscontinuityGrowthDiscontinuityKindspeedReadonly---legacySpeedBucketGrowthLegacyBucketnullresidualReadonly---qualitystring"
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
required: [
|
|
576
|
+
"schemaVersion",
|
|
577
|
+
"algorithmVersion",
|
|
578
|
+
"method",
|
|
579
|
+
"status",
|
|
580
|
+
"seriesRequested",
|
|
581
|
+
"valueSource",
|
|
582
|
+
"windowSec",
|
|
583
|
+
"samplingBudget",
|
|
584
|
+
"sampleCount",
|
|
585
|
+
"inputSampleCount",
|
|
586
|
+
"slopeTolerance",
|
|
587
|
+
"fitWindow",
|
|
588
|
+
"irregular",
|
|
589
|
+
"claimsResourceBottleneck",
|
|
590
|
+
"softcap",
|
|
591
|
+
"cadenceCoarserThanWindow",
|
|
592
|
+
"assumptions",
|
|
593
|
+
"warnings",
|
|
594
|
+
"windows"
|
|
595
|
+
],
|
|
596
|
+
additionalProperties: false
|
|
597
|
+
},
|
|
598
|
+
version: "3.1"
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
let growthFitJsonSchemaUnit;
|
|
602
|
+
/**
|
|
603
|
+
* @evidence docs/requirements/active/growth-model-classification.md#req-pr12-shape-speed The generated validator accepts this artifact and rejects a missing status.
|
|
604
|
+
* @evidenceReview docs/requirements/active/growth-model-classification.md#req-pr12-shape-speed #acbd473 Re-read the section: the wire object is growth fit v1.
|
|
605
|
+
*/
|
|
606
|
+
export const validateGrowthFitArtifact = (input) => (growthFitValidator ??= (() => {
|
|
607
|
+
const _ip0 = input => 1 === input["schemaVersion"];
|
|
608
|
+
const _ip1 = input => "growth-fit-v1" === input["algorithmVersion"];
|
|
609
|
+
const _ip2 = input => "heuristic-fit" === input["method"];
|
|
610
|
+
const _ip3 = input => "censored" === input["status"] || "error" === input["status"] || "insufficient-data" === input["status"] || "invalid" === input["status"] || "ok" === input["status"] || "unknown" === input["status"] || "unsupported" === input["status"];
|
|
611
|
+
const _ip4 = input => "money" === input["seriesRequested"] || "netWorth" === input["seriesRequested"];
|
|
612
|
+
const _ip5 = input => "money" === input["valueSource"] || "netWorth" === input["valueSource"] || "netWorthFallback" === input["valueSource"];
|
|
613
|
+
const _ip6 = input => "number" === typeof input["windowSec"];
|
|
614
|
+
const _ip7 = input => "number" === typeof input["samplingBudget"];
|
|
615
|
+
const _ip8 = input => "number" === typeof input["sampleCount"];
|
|
616
|
+
const _ip9 = input => "number" === typeof input["inputSampleCount"];
|
|
617
|
+
const _ip10 = input => "number" === typeof input["slopeTolerance"];
|
|
618
|
+
const _ip11 = input => "object" === typeof input["fitWindow"] && null !== input["fitWindow"] && _io1(input["fitWindow"]);
|
|
619
|
+
const _ip12 = input => "boolean" === typeof input["irregular"];
|
|
620
|
+
const _ip13 = input => false === input["claimsResourceBottleneck"];
|
|
621
|
+
const _ip14 = input => "absent" === input["softcap"] || "configured" === input["softcap"] || "unknown" === input["softcap"];
|
|
622
|
+
const _ip15 = input => undefined === input["cadenceSec"] || "number" === typeof input["cadenceSec"];
|
|
623
|
+
const _ip16 = input => "boolean" === typeof input["cadenceCoarserThanWindow"];
|
|
624
|
+
const _ip17 = input => undefined === input["downsampled"] || "object" === typeof input["downsampled"] && null !== input["downsampled"] && _io2(input["downsampled"]);
|
|
625
|
+
const _ip18 = input => undefined === input["retention"] || "object" === typeof input["retention"] && null !== input["retention"] && _io3(input["retention"]);
|
|
626
|
+
const _ip19 = input => Array.isArray(input["assumptions"]) && input["assumptions"].every(elem => "string" === typeof elem);
|
|
627
|
+
const _ip20 = input => Array.isArray(input["warnings"]) && input["warnings"].every(elem => "string" === typeof elem);
|
|
628
|
+
const _ip21 = input => Array.isArray(input["windows"]) && input["windows"].every(elem => "object" === typeof elem && null !== elem && _io4(elem));
|
|
629
|
+
const _ip22 = input => "number" === typeof input["startSec"];
|
|
630
|
+
const _ip23 = input => "number" === typeof input["endSec"];
|
|
631
|
+
const _ve0 = "Readonly<{ schemaVersion: 1; algorithmVersion: \"growth-fit-v1\"; method: \"heuristic-fit\"; status: AnalysisStatus; seriesRequested: \"money\" | \"netWorth\"; valueSource: \"money\" | \"netWorth\" | \"netWorthFallback\"; ... 15 more ...; windows: readonly Readonly<...>[]; }>";
|
|
632
|
+
const _vp0 = (input, _path, _exceptionable = true) => 1 === input["schemaVersion"] || _report(_exceptionable, {
|
|
633
|
+
path: _path + ".schemaVersion",
|
|
634
|
+
expected: "1",
|
|
635
|
+
value: input["schemaVersion"]
|
|
636
|
+
});
|
|
637
|
+
const _vp1 = (input, _path, _exceptionable = true) => "growth-fit-v1" === input["algorithmVersion"] || _report(_exceptionable, {
|
|
638
|
+
path: _path + ".algorithmVersion",
|
|
639
|
+
expected: "\"growth-fit-v1\"",
|
|
640
|
+
value: input["algorithmVersion"]
|
|
641
|
+
});
|
|
642
|
+
const _vp2 = (input, _path, _exceptionable = true) => "heuristic-fit" === input["method"] || _report(_exceptionable, {
|
|
643
|
+
path: _path + ".method",
|
|
644
|
+
expected: "\"heuristic-fit\"",
|
|
645
|
+
value: input["method"]
|
|
646
|
+
});
|
|
647
|
+
const _vp3 = (input, _path, _exceptionable = true) => "censored" === input["status"] || "error" === input["status"] || "insufficient-data" === input["status"] || "invalid" === input["status"] || "ok" === input["status"] || "unknown" === input["status"] || "unsupported" === input["status"] || _report(_exceptionable, {
|
|
648
|
+
path: _path + ".status",
|
|
649
|
+
expected: "(\"censored\" | \"error\" | \"insufficient-data\" | \"invalid\" | \"ok\" | \"unknown\" | \"unsupported\")",
|
|
650
|
+
value: input["status"]
|
|
651
|
+
});
|
|
652
|
+
const _vp4 = (input, _path, _exceptionable = true) => "money" === input["seriesRequested"] || "netWorth" === input["seriesRequested"] || _report(_exceptionable, {
|
|
653
|
+
path: _path + ".seriesRequested",
|
|
654
|
+
expected: "(\"money\" | \"netWorth\")",
|
|
655
|
+
value: input["seriesRequested"]
|
|
656
|
+
});
|
|
657
|
+
const _vp5 = (input, _path, _exceptionable = true) => "money" === input["valueSource"] || "netWorth" === input["valueSource"] || "netWorthFallback" === input["valueSource"] || _report(_exceptionable, {
|
|
658
|
+
path: _path + ".valueSource",
|
|
659
|
+
expected: "(\"money\" | \"netWorth\" | \"netWorthFallback\")",
|
|
660
|
+
value: input["valueSource"]
|
|
661
|
+
});
|
|
662
|
+
const _vp6 = (input, _path, _exceptionable = true) => "number" === typeof input["windowSec"] || _report(_exceptionable, {
|
|
663
|
+
path: _path + ".windowSec",
|
|
664
|
+
expected: "number",
|
|
665
|
+
value: input["windowSec"]
|
|
666
|
+
});
|
|
667
|
+
const _vp7 = (input, _path, _exceptionable = true) => "number" === typeof input["samplingBudget"] || _report(_exceptionable, {
|
|
668
|
+
path: _path + ".samplingBudget",
|
|
669
|
+
expected: "number",
|
|
670
|
+
value: input["samplingBudget"]
|
|
671
|
+
});
|
|
672
|
+
const _vp8 = (input, _path, _exceptionable = true) => "number" === typeof input["sampleCount"] || _report(_exceptionable, {
|
|
673
|
+
path: _path + ".sampleCount",
|
|
674
|
+
expected: "number",
|
|
675
|
+
value: input["sampleCount"]
|
|
676
|
+
});
|
|
677
|
+
const _vp9 = (input, _path, _exceptionable = true) => "number" === typeof input["inputSampleCount"] || _report(_exceptionable, {
|
|
678
|
+
path: _path + ".inputSampleCount",
|
|
679
|
+
expected: "number",
|
|
680
|
+
value: input["inputSampleCount"]
|
|
681
|
+
});
|
|
682
|
+
const _vp10 = (input, _path, _exceptionable = true) => "number" === typeof input["slopeTolerance"] || _report(_exceptionable, {
|
|
683
|
+
path: _path + ".slopeTolerance",
|
|
684
|
+
expected: "number",
|
|
685
|
+
value: input["slopeTolerance"]
|
|
686
|
+
});
|
|
687
|
+
const _vp11 = (input, _path, _exceptionable = true) => ("object" === typeof input["fitWindow"] && null !== input["fitWindow"] || _report(_exceptionable, {
|
|
688
|
+
path: _path + ".fitWindow",
|
|
689
|
+
expected: "Readonly<{ windowSec: number; startSec: number; endSec: number; }>",
|
|
690
|
+
value: input["fitWindow"]
|
|
691
|
+
})) && _vo1(input["fitWindow"], _path + ".fitWindow", true && _exceptionable) || _report(_exceptionable, {
|
|
692
|
+
path: _path + ".fitWindow",
|
|
693
|
+
expected: "Readonly<{ windowSec: number; startSec: number; endSec: number; }>",
|
|
694
|
+
value: input["fitWindow"]
|
|
695
|
+
});
|
|
696
|
+
const _vp12 = (input, _path, _exceptionable = true) => "boolean" === typeof input["irregular"] || _report(_exceptionable, {
|
|
697
|
+
path: _path + ".irregular",
|
|
698
|
+
expected: "boolean",
|
|
699
|
+
value: input["irregular"]
|
|
700
|
+
});
|
|
701
|
+
const _vp13 = (input, _path, _exceptionable = true) => false === input["claimsResourceBottleneck"] || _report(_exceptionable, {
|
|
702
|
+
path: _path + ".claimsResourceBottleneck",
|
|
703
|
+
expected: "false",
|
|
704
|
+
value: input["claimsResourceBottleneck"]
|
|
705
|
+
});
|
|
706
|
+
const _vp14 = (input, _path, _exceptionable = true) => "absent" === input["softcap"] || "configured" === input["softcap"] || "unknown" === input["softcap"] || _report(_exceptionable, {
|
|
707
|
+
path: _path + ".softcap",
|
|
708
|
+
expected: "(\"absent\" | \"configured\" | \"unknown\")",
|
|
709
|
+
value: input["softcap"]
|
|
710
|
+
});
|
|
711
|
+
const _vp15 = (input, _path, _exceptionable = true) => undefined === input["cadenceSec"] || "number" === typeof input["cadenceSec"] || _report(_exceptionable, {
|
|
712
|
+
path: _path + ".cadenceSec",
|
|
713
|
+
expected: "(number | undefined)",
|
|
714
|
+
value: input["cadenceSec"]
|
|
715
|
+
});
|
|
716
|
+
const _vp16 = (input, _path, _exceptionable = true) => "boolean" === typeof input["cadenceCoarserThanWindow"] || _report(_exceptionable, {
|
|
717
|
+
path: _path + ".cadenceCoarserThanWindow",
|
|
718
|
+
expected: "boolean",
|
|
719
|
+
value: input["cadenceCoarserThanWindow"]
|
|
720
|
+
});
|
|
721
|
+
const _vp17 = (input, _path, _exceptionable = true) => undefined === input["downsampled"] || ("object" === typeof input["downsampled"] && null !== input["downsampled"] || _report(_exceptionable, {
|
|
722
|
+
path: _path + ".downsampled",
|
|
723
|
+
expected: "(Readonly<{ seen: number; kept: number; }> | undefined)",
|
|
724
|
+
value: input["downsampled"]
|
|
725
|
+
})) && _vo2(input["downsampled"], _path + ".downsampled", true && _exceptionable) || _report(_exceptionable, {
|
|
726
|
+
path: _path + ".downsampled",
|
|
727
|
+
expected: "(Readonly<{ seen: number; kept: number; }> | undefined)",
|
|
728
|
+
value: input["downsampled"]
|
|
729
|
+
});
|
|
730
|
+
const _vp18 = (input, _path, _exceptionable = true) => undefined === input["retention"] || ("object" === typeof input["retention"] && null !== input["retention"] || _report(_exceptionable, {
|
|
731
|
+
path: _path + ".retention",
|
|
732
|
+
expected: "(Readonly<{ totalSeen: number; retained: number; dropped: number; }> | undefined)",
|
|
733
|
+
value: input["retention"]
|
|
734
|
+
})) && _vo3(input["retention"], _path + ".retention", true && _exceptionable) || _report(_exceptionable, {
|
|
735
|
+
path: _path + ".retention",
|
|
736
|
+
expected: "(Readonly<{ totalSeen: number; retained: number; dropped: number; }> | undefined)",
|
|
737
|
+
value: input["retention"]
|
|
738
|
+
});
|
|
739
|
+
const _vp19 = (input, _path, _exceptionable = true) => (Array.isArray(input["assumptions"]) || _report(_exceptionable, {
|
|
740
|
+
path: _path + ".assumptions",
|
|
741
|
+
expected: "ReadonlyArray<string>",
|
|
742
|
+
value: input["assumptions"]
|
|
743
|
+
})) && input["assumptions"].map((elem, _index4) => "string" === typeof elem || _report(_exceptionable, {
|
|
744
|
+
path: _path + ".assumptions[" + _index4 + "]",
|
|
745
|
+
expected: "string",
|
|
746
|
+
value: elem
|
|
747
|
+
})).every(flag => flag) || _report(_exceptionable, {
|
|
748
|
+
path: _path + ".assumptions",
|
|
749
|
+
expected: "ReadonlyArray<string>",
|
|
750
|
+
value: input["assumptions"]
|
|
751
|
+
});
|
|
752
|
+
const _vp20 = (input, _path, _exceptionable = true) => (Array.isArray(input["warnings"]) || _report(_exceptionable, {
|
|
753
|
+
path: _path + ".warnings",
|
|
754
|
+
expected: "ReadonlyArray<string>",
|
|
755
|
+
value: input["warnings"]
|
|
756
|
+
})) && input["warnings"].map((elem, _index5) => "string" === typeof elem || _report(_exceptionable, {
|
|
757
|
+
path: _path + ".warnings[" + _index5 + "]",
|
|
758
|
+
expected: "string",
|
|
759
|
+
value: elem
|
|
760
|
+
})).every(flag => flag) || _report(_exceptionable, {
|
|
761
|
+
path: _path + ".warnings",
|
|
762
|
+
expected: "ReadonlyArray<string>",
|
|
763
|
+
value: input["warnings"]
|
|
764
|
+
});
|
|
765
|
+
const _vp21 = (input, _path, _exceptionable = true) => (Array.isArray(input["windows"]) || _report(_exceptionable, {
|
|
766
|
+
path: _path + ".windows",
|
|
767
|
+
expected: _ve2,
|
|
768
|
+
value: input["windows"]
|
|
769
|
+
})) && input["windows"].map((elem, _index6) => ("object" === typeof elem && null !== elem || _report(_exceptionable, {
|
|
770
|
+
path: _path + ".windows[" + _index6 + "]",
|
|
771
|
+
expected: _ve1,
|
|
772
|
+
value: elem
|
|
773
|
+
})) && _vo4(elem, _path + ".windows[" + _index6 + "]", true && _exceptionable) || _report(_exceptionable, {
|
|
774
|
+
path: _path + ".windows[" + _index6 + "]",
|
|
775
|
+
expected: _ve1,
|
|
776
|
+
value: elem
|
|
777
|
+
})).every(flag => flag) || _report(_exceptionable, {
|
|
778
|
+
path: _path + ".windows",
|
|
779
|
+
expected: _ve2,
|
|
780
|
+
value: input["windows"]
|
|
781
|
+
});
|
|
782
|
+
const _ve1 = "Readonly<{ startSec: number; endSec: number; sampleCount: number; domain: GrowthDomain; status: AnalysisStatus; shape: GrowthShape; discontinuity: GrowthDiscontinuityKind; speed: Readonly<...>; legacySpeedBucket: GrowthLegacyBucket | null; residual: Readonly<...>; quality: string; }>";
|
|
783
|
+
const _ve2 = "ReadonlyArray<Readonly<{ startSec: number; endSec: number; sampleCount: number; domain: GrowthDomain; status: AnalysisStatus; shape: GrowthShape; discontinuity: GrowthDiscontinuityKind; speed: Readonly<...>; legacySpeedBucket: GrowthLegacyBucket | null; residual: Readonly<...>; quality: string; }>>";
|
|
784
|
+
const _vp22 = (input, _path, _exceptionable = true) => "number" === typeof input["startSec"] || _report(_exceptionable, {
|
|
785
|
+
path: _path + ".startSec",
|
|
786
|
+
expected: "number",
|
|
787
|
+
value: input["startSec"]
|
|
788
|
+
});
|
|
789
|
+
const _vp23 = (input, _path, _exceptionable = true) => "number" === typeof input["endSec"] || _report(_exceptionable, {
|
|
790
|
+
path: _path + ".endSec",
|
|
791
|
+
expected: "number",
|
|
792
|
+
value: input["endSec"]
|
|
793
|
+
});
|
|
794
|
+
const _io0 = input => _ip0(input) && _ip1(input) && _ip2(input) && _ip3(input) && _ip4(input) && _ip5(input) && _ip6(input) && _ip7(input) && _ip8(input) && _ip9(input) && _ip10(input) && _ip11(input) && _ip12(input) && _ip13(input) && _ip14(input) && _ip15(input) && _ip16(input) && _ip17(input) && _ip18(input) && _ip19(input) && _ip20(input) && _ip21(input);
|
|
795
|
+
const _io1 = input => _ip6(input) && _ip22(input) && _ip23(input);
|
|
796
|
+
const _io2 = input => "number" === typeof input.seen && "number" === typeof input.kept;
|
|
797
|
+
const _io3 = input => "number" === typeof input.totalSeen && "number" === typeof input.retained && "number" === typeof input.dropped;
|
|
798
|
+
const _io4 = input => _ip22(input) && _ip23(input) && _ip8(input) && ("empty" === input.domain || "mixed" === input.domain || "negative" === input.domain || "nonfinite" === input.domain || "positive" === input.domain || "zero" === input.domain) && _ip3(input) && ("accelerating-log" === input.shape || "constant" === input.shape || "exponential" === input.shape || "polynomial" === input.shape || "saturating" === input.shape || "unknown" === input.shape) && ("drop" === input.discontinuity || "none" === input.discontinuity || "purchase" === input.discontinuity || "reset" === input.discontinuity || "sign" === input.discontinuity) && ("object" === typeof input.speed && null !== input.speed && _io5(input.speed)) && (null === input.legacySpeedBucket || "exp" === input.legacySpeedBucket || "softcap" === input.legacySpeedBucket || "stall" === input.legacySpeedBucket || "super-exp" === input.legacySpeedBucket) && ("object" === typeof input.residual && null !== input.residual && _io6(input.residual)) && "string" === typeof input.quality;
|
|
799
|
+
const _io5 = input => (null === input.logSlopePerSec || "number" === typeof input.logSlopePerSec) && (null === input.doublingTimeSec || "number" === typeof input.doublingTimeSec);
|
|
800
|
+
const _io6 = input => ("log-rate" === input.space || "log10" === input.space || "none" === input.space || "value" === input.space) && (null === input.rmse || "number" === typeof input.rmse);
|
|
801
|
+
const _vo0 = (input, _path, _exceptionable = true) => [_vp0(input, _path, true && _exceptionable), _vp1(input, _path, true && _exceptionable), _vp2(input, _path, true && _exceptionable), _vp3(input, _path, true && _exceptionable), _vp4(input, _path, true && _exceptionable), _vp5(input, _path, true && _exceptionable), _vp6(input, _path, true && _exceptionable), _vp7(input, _path, true && _exceptionable), _vp8(input, _path, true && _exceptionable), _vp9(input, _path, true && _exceptionable), _vp10(input, _path, true && _exceptionable), _vp11(input, _path, true && _exceptionable), _vp12(input, _path, true && _exceptionable), _vp13(input, _path, true && _exceptionable), _vp14(input, _path, true && _exceptionable), _vp15(input, _path, true && _exceptionable), _vp16(input, _path, true && _exceptionable), _vp17(input, _path, true && _exceptionable), _vp18(input, _path, true && _exceptionable), _vp19(input, _path, true && _exceptionable), _vp20(input, _path, true && _exceptionable), _vp21(input, _path, true && _exceptionable)].every(flag => flag);
|
|
802
|
+
const _vo1 = (input, _path, _exceptionable = true) => [_vp6(input, _path, true && _exceptionable), _vp22(input, _path, true && _exceptionable), _vp23(input, _path, true && _exceptionable)].every(flag => flag);
|
|
803
|
+
const _vo2 = (input, _path, _exceptionable = true) => ["number" === typeof input.seen || _report(_exceptionable, {
|
|
804
|
+
path: _path + ".seen",
|
|
805
|
+
expected: "number",
|
|
806
|
+
value: input.seen
|
|
807
|
+
}), "number" === typeof input.kept || _report(_exceptionable, {
|
|
808
|
+
path: _path + ".kept",
|
|
809
|
+
expected: "number",
|
|
810
|
+
value: input.kept
|
|
811
|
+
})].every(flag => flag);
|
|
812
|
+
const _vo3 = (input, _path, _exceptionable = true) => ["number" === typeof input.totalSeen || _report(_exceptionable, {
|
|
813
|
+
path: _path + ".totalSeen",
|
|
814
|
+
expected: "number",
|
|
815
|
+
value: input.totalSeen
|
|
816
|
+
}), "number" === typeof input.retained || _report(_exceptionable, {
|
|
817
|
+
path: _path + ".retained",
|
|
818
|
+
expected: "number",
|
|
819
|
+
value: input.retained
|
|
820
|
+
}), "number" === typeof input.dropped || _report(_exceptionable, {
|
|
821
|
+
path: _path + ".dropped",
|
|
822
|
+
expected: "number",
|
|
823
|
+
value: input.dropped
|
|
824
|
+
})].every(flag => flag);
|
|
825
|
+
const _vo4 = (input, _path, _exceptionable = true) => [_vp22(input, _path, true && _exceptionable), _vp23(input, _path, true && _exceptionable), _vp8(input, _path, true && _exceptionable), "empty" === input.domain || "mixed" === input.domain || "negative" === input.domain || "nonfinite" === input.domain || "positive" === input.domain || "zero" === input.domain || _report(_exceptionable, {
|
|
826
|
+
path: _path + ".domain",
|
|
827
|
+
expected: "(\"empty\" | \"mixed\" | \"negative\" | \"nonfinite\" | \"positive\" | \"zero\")",
|
|
828
|
+
value: input.domain
|
|
829
|
+
}), _vp3(input, _path, true && _exceptionable), "accelerating-log" === input.shape || "constant" === input.shape || "exponential" === input.shape || "polynomial" === input.shape || "saturating" === input.shape || "unknown" === input.shape || _report(_exceptionable, {
|
|
830
|
+
path: _path + ".shape",
|
|
831
|
+
expected: "(\"accelerating-log\" | \"constant\" | \"exponential\" | \"polynomial\" | \"saturating\" | \"unknown\")",
|
|
832
|
+
value: input.shape
|
|
833
|
+
}), "drop" === input.discontinuity || "none" === input.discontinuity || "purchase" === input.discontinuity || "reset" === input.discontinuity || "sign" === input.discontinuity || _report(_exceptionable, {
|
|
834
|
+
path: _path + ".discontinuity",
|
|
835
|
+
expected: "(\"drop\" | \"none\" | \"purchase\" | \"reset\" | \"sign\")",
|
|
836
|
+
value: input.discontinuity
|
|
837
|
+
}), ("object" === typeof input.speed && null !== input.speed || _report(_exceptionable, {
|
|
838
|
+
path: _path + ".speed",
|
|
839
|
+
expected: "Readonly<{ logSlopePerSec: number | null; doublingTimeSec: number | null; }>",
|
|
840
|
+
value: input.speed
|
|
841
|
+
})) && _vo5(input.speed, _path + ".speed", true && _exceptionable) || _report(_exceptionable, {
|
|
842
|
+
path: _path + ".speed",
|
|
843
|
+
expected: "Readonly<{ logSlopePerSec: number | null; doublingTimeSec: number | null; }>",
|
|
844
|
+
value: input.speed
|
|
845
|
+
}), null === input.legacySpeedBucket || "exp" === input.legacySpeedBucket || "softcap" === input.legacySpeedBucket || "stall" === input.legacySpeedBucket || "super-exp" === input.legacySpeedBucket || _report(_exceptionable, {
|
|
846
|
+
path: _path + ".legacySpeedBucket",
|
|
847
|
+
expected: "(\"exp\" | \"softcap\" | \"stall\" | \"super-exp\" | null)",
|
|
848
|
+
value: input.legacySpeedBucket
|
|
849
|
+
}), ("object" === typeof input.residual && null !== input.residual || _report(_exceptionable, {
|
|
850
|
+
path: _path + ".residual",
|
|
851
|
+
expected: "Readonly<{ space: \"log-rate\" | \"log10\" | \"none\" | \"value\"; rmse: number | null; }>",
|
|
852
|
+
value: input.residual
|
|
853
|
+
})) && _vo6(input.residual, _path + ".residual", true && _exceptionable) || _report(_exceptionable, {
|
|
854
|
+
path: _path + ".residual",
|
|
855
|
+
expected: "Readonly<{ space: \"log-rate\" | \"log10\" | \"none\" | \"value\"; rmse: number | null; }>",
|
|
856
|
+
value: input.residual
|
|
857
|
+
}), "string" === typeof input.quality || _report(_exceptionable, {
|
|
858
|
+
path: _path + ".quality",
|
|
859
|
+
expected: "string",
|
|
860
|
+
value: input.quality
|
|
861
|
+
})].every(flag => flag);
|
|
862
|
+
const _vo5 = (input, _path, _exceptionable = true) => [null === input.logSlopePerSec || "number" === typeof input.logSlopePerSec || _report(_exceptionable, {
|
|
863
|
+
path: _path + ".logSlopePerSec",
|
|
864
|
+
expected: "(null | number)",
|
|
865
|
+
value: input.logSlopePerSec
|
|
866
|
+
}), null === input.doublingTimeSec || "number" === typeof input.doublingTimeSec || _report(_exceptionable, {
|
|
867
|
+
path: _path + ".doublingTimeSec",
|
|
868
|
+
expected: "(null | number)",
|
|
869
|
+
value: input.doublingTimeSec
|
|
870
|
+
})].every(flag => flag);
|
|
871
|
+
const _vo6 = (input, _path, _exceptionable = true) => ["log-rate" === input.space || "log10" === input.space || "none" === input.space || "value" === input.space || _report(_exceptionable, {
|
|
872
|
+
path: _path + ".space",
|
|
873
|
+
expected: "(\"log-rate\" | \"log10\" | \"none\" | \"value\")",
|
|
874
|
+
value: input.space
|
|
875
|
+
}), null === input.rmse || "number" === typeof input.rmse || _report(_exceptionable, {
|
|
876
|
+
path: _path + ".rmse",
|
|
877
|
+
expected: "(null | number)",
|
|
878
|
+
value: input.rmse
|
|
879
|
+
})].every(flag => flag);
|
|
880
|
+
const __is = input => "object" === typeof input && null !== input && _io0(input);
|
|
881
|
+
let errors;
|
|
882
|
+
let _report;
|
|
883
|
+
return _createStandardSchema_1._createStandardSchema(input => {
|
|
884
|
+
if (false === __is(input)) {
|
|
885
|
+
errors = [];
|
|
886
|
+
_report = _validateReport_1._validateReport(errors);
|
|
887
|
+
((input, _path, _exceptionable = true) => ("object" === typeof input && null !== input || _report(true, {
|
|
888
|
+
path: _path + "",
|
|
889
|
+
expected: _ve0,
|
|
890
|
+
value: input
|
|
891
|
+
})) && _vo0(input, _path + "", true) || _report(true, {
|
|
892
|
+
path: _path + "",
|
|
893
|
+
expected: _ve0,
|
|
894
|
+
value: input
|
|
895
|
+
}))(input, "$input", true);
|
|
896
|
+
const success = 0 === errors.length;
|
|
897
|
+
return success ? {
|
|
898
|
+
success,
|
|
899
|
+
data: input
|
|
900
|
+
} : {
|
|
901
|
+
success,
|
|
902
|
+
errors,
|
|
903
|
+
data: input
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
return {
|
|
907
|
+
success: true,
|
|
908
|
+
data: input
|
|
909
|
+
};
|
|
910
|
+
});
|
|
911
|
+
})())(input);
|
|
912
|
+
/**
|
|
913
|
+
* @evidence docs/requirements/active/growth-model-classification.md#req-pr12-shape-speed The structural schema is OpenAPI 3.1 from typia.
|
|
914
|
+
* @evidenceReview docs/requirements/active/growth-model-classification.md#req-pr12-shape-speed #acbd473 Re-read the section: schema version is 3.1.
|
|
915
|
+
*/
|
|
916
|
+
export function growthFitJsonSchema() {
|
|
917
|
+
growthFitJsonSchemaUnit ??= buildGrowthFitJsonSchema();
|
|
918
|
+
return growthFitJsonSchemaUnit;
|
|
919
|
+
}
|
|
920
|
+
const ASSUMPTIONS = [
|
|
921
|
+
"log slope is the least-squares slope of absLog10 per second",
|
|
922
|
+
"shape and speed are separate fields",
|
|
923
|
+
"a candidate fit does not prove an asymptotic regime",
|
|
924
|
+
"negative and zero samples are not passed through abs",
|
|
925
|
+
];
|
|
926
|
+
function assertSampling(sampling) {
|
|
927
|
+
if (sampling.method !== GROWTH_FIT_ALGORITHM_VERSION || sampling.methodVersion !== 1) {
|
|
928
|
+
throw new GrowthFitRejected("growth fit requires method growth-fit-v1 version 1");
|
|
929
|
+
}
|
|
930
|
+
if (!Number.isInteger(sampling.samplingBudget) || sampling.samplingBudget < 1) {
|
|
931
|
+
throw new GrowthFitRejected("samplingBudget must be a positive integer");
|
|
932
|
+
}
|
|
933
|
+
if (typeof sampling.windowSec !== "number" || !Number.isFinite(sampling.windowSec) || !(sampling.windowSec > 0)) {
|
|
934
|
+
throw new GrowthFitRejected("windowSec must be a finite number > 0");
|
|
935
|
+
}
|
|
936
|
+
if (sampling.cadenceSec !== undefined &&
|
|
937
|
+
(typeof sampling.cadenceSec !== "number" || !Number.isFinite(sampling.cadenceSec) || !(sampling.cadenceSec > 0))) {
|
|
938
|
+
throw new GrowthFitRejected("cadenceSec must be a finite number > 0");
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
function readPoint(E, sample) {
|
|
942
|
+
if (typeof sample.t !== "number" || !Number.isFinite(sample.t)) {
|
|
943
|
+
throw new GrowthFitRejected("sample time must be a finite number");
|
|
944
|
+
}
|
|
945
|
+
if (!E.isFinite(sample.amount)) {
|
|
946
|
+
return { t: sample.t, sign: "nonfinite", log10: null, numeric: null };
|
|
947
|
+
}
|
|
948
|
+
const cmp = E.cmp(sample.amount, E.zero());
|
|
949
|
+
if (cmp === 0)
|
|
950
|
+
return { t: sample.t, sign: "zero", log10: null, numeric: 0 };
|
|
951
|
+
if (cmp < 0)
|
|
952
|
+
return { t: sample.t, sign: "negative", log10: null, numeric: null };
|
|
953
|
+
const log10 = E.absLog10(sample.amount);
|
|
954
|
+
if (!Number.isFinite(log10))
|
|
955
|
+
return { t: sample.t, sign: "nonfinite", log10: null, numeric: null };
|
|
956
|
+
let numeric = null;
|
|
957
|
+
if (log10 <= 300 && log10 >= -300) {
|
|
958
|
+
const narrowed = E.toNumber(sample.amount);
|
|
959
|
+
if (Number.isFinite(narrowed) && narrowed > 0)
|
|
960
|
+
numeric = narrowed;
|
|
961
|
+
}
|
|
962
|
+
return { t: sample.t, sign: "positive", log10, numeric };
|
|
963
|
+
}
|
|
964
|
+
function preparePoints(E, samples) {
|
|
965
|
+
const read = samples.map((sample) => readPoint(E, sample)).sort((left, right) => left.t - right.t);
|
|
966
|
+
const unique = [];
|
|
967
|
+
for (const point of read) {
|
|
968
|
+
const previous = unique[unique.length - 1];
|
|
969
|
+
if (previous && previous.t === point.t)
|
|
970
|
+
unique[unique.length - 1] = point;
|
|
971
|
+
else
|
|
972
|
+
unique.push(point);
|
|
973
|
+
}
|
|
974
|
+
return unique;
|
|
975
|
+
}
|
|
976
|
+
function evenKeep(items, budget) {
|
|
977
|
+
if (items.length <= budget)
|
|
978
|
+
return [...items];
|
|
979
|
+
// A one-point budget keeps the first observation instead of dividing by budget - 1.
|
|
980
|
+
if (budget <= 1)
|
|
981
|
+
return items.slice(0, Math.max(0, budget));
|
|
982
|
+
const last = items.length - 1;
|
|
983
|
+
const kept = [];
|
|
984
|
+
for (let index = 0; index < budget; index += 1) {
|
|
985
|
+
const at = Math.round((index * last) / (budget - 1));
|
|
986
|
+
const item = items[at];
|
|
987
|
+
if (item !== undefined && kept[kept.length - 1] !== item)
|
|
988
|
+
kept.push(item);
|
|
989
|
+
}
|
|
990
|
+
return kept;
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Keep samples per fit window, so the budget leaves analyzable windows rather than one point per window. Windows
|
|
994
|
+
* split the same way slicesOf splits them. When the budget cannot give every window MIN_SAMPLES points, an evenly
|
|
995
|
+
* spaced subset of windows keeps MIN_SAMPLES each and the rest are left unfitted.
|
|
996
|
+
*/
|
|
997
|
+
/** Two samples hold the same value to within a few ULPs. */
|
|
998
|
+
function sameValue(a, b) {
|
|
999
|
+
if (a.sign !== b.sign)
|
|
1000
|
+
return false;
|
|
1001
|
+
if (a.numeric !== null && b.numeric !== null) {
|
|
1002
|
+
return Math.abs(b.numeric - a.numeric) <= 4 * Number.EPSILON * Math.max(Math.abs(a.numeric), Math.abs(b.numeric));
|
|
1003
|
+
}
|
|
1004
|
+
return a.log10 !== null && a.log10 === b.log10;
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Thin one window to the budget. A window that settles on its final value keeps its samples for the part before it
|
|
1008
|
+
* settles, plus the settled point and the last one, so a long plateau does not take the samples a saturation fit needs.
|
|
1009
|
+
*/
|
|
1010
|
+
function thinWindow(window, budget) {
|
|
1011
|
+
if (window.length <= budget)
|
|
1012
|
+
return [...window];
|
|
1013
|
+
const last = window[window.length - 1];
|
|
1014
|
+
let settled = window.length - 1;
|
|
1015
|
+
while (settled > 0 && sameValue(window[settled - 1], last))
|
|
1016
|
+
settled -= 1;
|
|
1017
|
+
// No interior settled samples, or a window that never moves: thin evenly.
|
|
1018
|
+
if (window.length - 1 - settled < 2 || settled === 0)
|
|
1019
|
+
return evenKeep(window, budget);
|
|
1020
|
+
const before = window.slice(0, settled + 1);
|
|
1021
|
+
if (before.length >= budget - 1)
|
|
1022
|
+
return [...evenKeep(before, budget - 1), last];
|
|
1023
|
+
return [...before, ...evenKeep(window.slice(settled + 1), budget - before.length)];
|
|
1024
|
+
}
|
|
1025
|
+
function windowKeep(items, budget, windowSec, breakTimes) {
|
|
1026
|
+
// Split at breaks first, as slicesOf does, so each side of a break gets its own share of the budget.
|
|
1027
|
+
const opens = new Set();
|
|
1028
|
+
for (const t of breakTimes) {
|
|
1029
|
+
const index = firstIndexAtOrAfter(items, t, false);
|
|
1030
|
+
if (index > 0)
|
|
1031
|
+
opens.add(index);
|
|
1032
|
+
}
|
|
1033
|
+
const segments = [];
|
|
1034
|
+
let segmentStart = 0;
|
|
1035
|
+
for (let index = 1; index <= items.length; index += 1) {
|
|
1036
|
+
if (index === items.length || opens.has(index)) {
|
|
1037
|
+
segments.push(items.slice(segmentStart, index));
|
|
1038
|
+
segmentStart = index;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
const windows = [];
|
|
1042
|
+
for (const segment of segments) {
|
|
1043
|
+
let start = 0;
|
|
1044
|
+
while (start < segment.length) {
|
|
1045
|
+
const endTime = segment[start].t + windowSec;
|
|
1046
|
+
let end = start + 1;
|
|
1047
|
+
while (end < segment.length && segment[end].t <= endTime + 1e-9)
|
|
1048
|
+
end += 1;
|
|
1049
|
+
windows.push(segment.slice(start, end));
|
|
1050
|
+
start = end;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (items.length <= budget)
|
|
1054
|
+
return { kept: [...items], windows: windows.length, fittedWindows: windows.length };
|
|
1055
|
+
const perWindow = Math.floor(budget / windows.length);
|
|
1056
|
+
if (perWindow >= MIN_SAMPLES) {
|
|
1057
|
+
return {
|
|
1058
|
+
kept: windows.flatMap((window) => thinWindow(window, perWindow)),
|
|
1059
|
+
windows: windows.length,
|
|
1060
|
+
fittedWindows: windows.length,
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
const count = Math.max(1, Math.floor(budget / MIN_SAMPLES));
|
|
1064
|
+
const each = Math.max(1, Math.floor(budget / count));
|
|
1065
|
+
// Spend the budget on windows that can be fitted: enough samples and span. Only when none can is any window used.
|
|
1066
|
+
const fittable = windows
|
|
1067
|
+
.map((window, index) => ({ window, index }))
|
|
1068
|
+
.filter(({ window }) => window.length >= MIN_SAMPLES && window[window.length - 1].t - window[0].t >= MIN_SPAN_SEC)
|
|
1069
|
+
.map(({ index }) => index);
|
|
1070
|
+
const pool = fittable.length > 0 ? fittable : windows.map((_, index) => index);
|
|
1071
|
+
const chosen = evenKeep(pool, count);
|
|
1072
|
+
return {
|
|
1073
|
+
kept: chosen.flatMap((index) => thinWindow(windows[index], each)),
|
|
1074
|
+
windows: windows.length,
|
|
1075
|
+
fittedWindows: chosen.length,
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
function domainOf(points) {
|
|
1079
|
+
if (points.length === 0)
|
|
1080
|
+
return "empty";
|
|
1081
|
+
if (points.some((point) => point.sign === "nonfinite"))
|
|
1082
|
+
return "nonfinite";
|
|
1083
|
+
const signs = new Set(points.map((point) => point.sign));
|
|
1084
|
+
if (signs.size === 1 && signs.has("positive"))
|
|
1085
|
+
return "positive";
|
|
1086
|
+
if (signs.size === 1 && signs.has("negative"))
|
|
1087
|
+
return "negative";
|
|
1088
|
+
if (signs.size === 1 && signs.has("zero"))
|
|
1089
|
+
return "zero";
|
|
1090
|
+
return "mixed";
|
|
1091
|
+
}
|
|
1092
|
+
function mean(values) {
|
|
1093
|
+
let sum = 0;
|
|
1094
|
+
for (const value of values)
|
|
1095
|
+
sum += value;
|
|
1096
|
+
return sum / values.length;
|
|
1097
|
+
}
|
|
1098
|
+
function rmseAround(predicted, actual) {
|
|
1099
|
+
let sum = 0;
|
|
1100
|
+
for (let index = 0; index < actual.length; index += 1) {
|
|
1101
|
+
const error = actual[index] - predicted[index];
|
|
1102
|
+
sum += error * error;
|
|
1103
|
+
}
|
|
1104
|
+
return Math.sqrt(sum / actual.length);
|
|
1105
|
+
}
|
|
1106
|
+
function solve(matrix, rhs) {
|
|
1107
|
+
const size = rhs.length;
|
|
1108
|
+
const rows = matrix.map((row, index) => [...row, rhs[index]]);
|
|
1109
|
+
for (let col = 0; col < size; col += 1) {
|
|
1110
|
+
let pivot = col;
|
|
1111
|
+
for (let row = col + 1; row < size; row += 1) {
|
|
1112
|
+
if (Math.abs(rows[row][col]) > Math.abs(rows[pivot][col]))
|
|
1113
|
+
pivot = row;
|
|
1114
|
+
}
|
|
1115
|
+
const swapped = rows[col];
|
|
1116
|
+
rows[col] = rows[pivot];
|
|
1117
|
+
rows[pivot] = swapped;
|
|
1118
|
+
const diagonal = rows[col][col];
|
|
1119
|
+
if (Math.abs(diagonal) < 1e-18)
|
|
1120
|
+
return null;
|
|
1121
|
+
for (let column = col; column <= size; column += 1)
|
|
1122
|
+
rows[col][column] = rows[col][column] / diagonal;
|
|
1123
|
+
for (let row = 0; row < size; row += 1) {
|
|
1124
|
+
if (row === col)
|
|
1125
|
+
continue;
|
|
1126
|
+
const factor = rows[row][col];
|
|
1127
|
+
for (let column = col; column <= size; column += 1) {
|
|
1128
|
+
rows[row][column] = rows[row][column] - factor * rows[col][column];
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
return rows.map((row) => row[size]);
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Largest |x|, so a fit runs on x / scale. Rebased times can still be wide enough that x * x overflows;
|
|
1136
|
+
* on the scaled axis the moments stay finite, and the coefficients are mapped back.
|
|
1137
|
+
*/
|
|
1138
|
+
function axisScale(xs) {
|
|
1139
|
+
let scale = 0;
|
|
1140
|
+
for (const x of xs)
|
|
1141
|
+
scale = Math.max(scale, Math.abs(x));
|
|
1142
|
+
return Number.isFinite(scale) && scale > 0 ? scale : 1;
|
|
1143
|
+
}
|
|
1144
|
+
function fitLinear(xs, ys) {
|
|
1145
|
+
if (xs.length !== ys.length || xs.length < 2)
|
|
1146
|
+
return null;
|
|
1147
|
+
const scale = axisScale(xs);
|
|
1148
|
+
const fit = fitLinearScaled(xs.map((x) => x / scale), ys);
|
|
1149
|
+
if (!fit)
|
|
1150
|
+
return null;
|
|
1151
|
+
const b = fit.b / scale;
|
|
1152
|
+
if (!Number.isFinite(fit.a) || !Number.isFinite(b) || !Number.isFinite(fit.rmse))
|
|
1153
|
+
return null;
|
|
1154
|
+
return { a: fit.a, b, rmse: fit.rmse };
|
|
1155
|
+
}
|
|
1156
|
+
function fitLinearScaled(xs, ys) {
|
|
1157
|
+
const n = xs.length;
|
|
1158
|
+
let sx = 0;
|
|
1159
|
+
let sy = 0;
|
|
1160
|
+
let sxx = 0;
|
|
1161
|
+
let sxy = 0;
|
|
1162
|
+
for (let index = 0; index < n; index += 1) {
|
|
1163
|
+
const x = xs[index];
|
|
1164
|
+
const y = ys[index];
|
|
1165
|
+
sx += x;
|
|
1166
|
+
sy += y;
|
|
1167
|
+
sxx += x * x;
|
|
1168
|
+
sxy += x * y;
|
|
1169
|
+
}
|
|
1170
|
+
const denom = n * sxx - sx * sx;
|
|
1171
|
+
const b = Math.abs(denom) < 1e-18 ? 0 : (n * sxy - sx * sy) / denom;
|
|
1172
|
+
const a = (sy - b * sx) / n;
|
|
1173
|
+
const predicted = xs.map((x) => a + b * x);
|
|
1174
|
+
return { a, b, rmse: rmseAround(predicted, ys) };
|
|
1175
|
+
}
|
|
1176
|
+
function fitQuadratic(xs, ys) {
|
|
1177
|
+
if (xs.length !== ys.length || xs.length < 3)
|
|
1178
|
+
return null;
|
|
1179
|
+
const scale = axisScale(xs);
|
|
1180
|
+
const fit = fitQuadraticScaled(xs.map((x) => x / scale), ys);
|
|
1181
|
+
if (!fit)
|
|
1182
|
+
return null;
|
|
1183
|
+
const b = fit.b / scale;
|
|
1184
|
+
const c = fit.c / scale / scale;
|
|
1185
|
+
if (!Number.isFinite(fit.a) || !Number.isFinite(b) || !Number.isFinite(c) || !Number.isFinite(fit.rmse))
|
|
1186
|
+
return null;
|
|
1187
|
+
return { a: fit.a, b, c, rmse: fit.rmse };
|
|
1188
|
+
}
|
|
1189
|
+
function fitQuadraticScaled(xs, ys) {
|
|
1190
|
+
let sx = 0;
|
|
1191
|
+
let sx2 = 0;
|
|
1192
|
+
let sx3 = 0;
|
|
1193
|
+
let sx4 = 0;
|
|
1194
|
+
let sy = 0;
|
|
1195
|
+
let sxy = 0;
|
|
1196
|
+
let sx2y = 0;
|
|
1197
|
+
for (let index = 0; index < xs.length; index += 1) {
|
|
1198
|
+
const x = xs[index];
|
|
1199
|
+
const y = ys[index];
|
|
1200
|
+
const x2 = x * x;
|
|
1201
|
+
sx += x;
|
|
1202
|
+
sx2 += x2;
|
|
1203
|
+
sx3 += x2 * x;
|
|
1204
|
+
sx4 += x2 * x2;
|
|
1205
|
+
sy += y;
|
|
1206
|
+
sxy += x * y;
|
|
1207
|
+
sx2y += x2 * y;
|
|
1208
|
+
}
|
|
1209
|
+
const solved = solve([
|
|
1210
|
+
[xs.length, sx, sx2],
|
|
1211
|
+
[sx, sx2, sx3],
|
|
1212
|
+
[sx2, sx3, sx4],
|
|
1213
|
+
], [sy, sxy, sx2y]);
|
|
1214
|
+
if (!solved)
|
|
1215
|
+
return null;
|
|
1216
|
+
const a = solved[0];
|
|
1217
|
+
const b = solved[1];
|
|
1218
|
+
const c = solved[2];
|
|
1219
|
+
const predicted = xs.map((x) => a + b * x + c * x * x);
|
|
1220
|
+
return { a, b, c, rmse: rmseAround(predicted, ys) };
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* |mean| of the series, computed on halves so a series near the number range does not overflow the sum.
|
|
1224
|
+
* It is not floored at 1, so a sub-unit series keeps a relative residual and a unit change does not change shape.
|
|
1225
|
+
*/
|
|
1226
|
+
function seriesScale(values) {
|
|
1227
|
+
let sum = 0;
|
|
1228
|
+
for (const value of values)
|
|
1229
|
+
sum += value / 2;
|
|
1230
|
+
const scale = Math.abs((sum / values.length) * 2);
|
|
1231
|
+
return Number.isFinite(scale) && scale > 0 ? scale : 1;
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Saturation: the growth rate per second decays geometrically. Rates are normalized by each gap, so an
|
|
1235
|
+
* evenly downsampled trace with gaps of 11 and 12 is read the same as the full-resolution trace.
|
|
1236
|
+
*/
|
|
1237
|
+
/**
|
|
1238
|
+
* The saturation fit's residual in log-rate space, or null when the series is not saturating. A flat tail, where the
|
|
1239
|
+
* series has reached its plateau to the number's precision, is allowed: zero steps are skipped, decreases reject.
|
|
1240
|
+
*/
|
|
1241
|
+
function saturating(ts, values) {
|
|
1242
|
+
if (values.length < MIN_SAMPLES)
|
|
1243
|
+
return null;
|
|
1244
|
+
const mids = [];
|
|
1245
|
+
const logRates = [];
|
|
1246
|
+
let flat = false;
|
|
1247
|
+
for (let index = 1; index < values.length; index += 1) {
|
|
1248
|
+
const gap = ts[index] - ts[index - 1];
|
|
1249
|
+
const step = values[index] - values[index - 1];
|
|
1250
|
+
if (!(gap > 0) || step < 0 || Number.isNaN(step))
|
|
1251
|
+
return null;
|
|
1252
|
+
// A step within a few ULPs of the values is rounding at the plateau, not growth.
|
|
1253
|
+
const rounding = 4 * Number.EPSILON * Math.max(Math.abs(values[index]), Math.abs(values[index - 1]));
|
|
1254
|
+
if (step <= rounding) {
|
|
1255
|
+
flat = true;
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
// Real growth after the plateau means the series did not saturate there.
|
|
1259
|
+
if (flat)
|
|
1260
|
+
return null;
|
|
1261
|
+
// previous + gap / 2, not (previous + next) / 2: two finite times near the number limit can sum to Infinity.
|
|
1262
|
+
mids.push(ts[index - 1] + gap / 2);
|
|
1263
|
+
// log(step) - log(gap): the quotient of finite operands can underflow even when its log is representable.
|
|
1264
|
+
logRates.push(Math.log(step) - Math.log(gap));
|
|
1265
|
+
}
|
|
1266
|
+
if (logRates.length < 4)
|
|
1267
|
+
return null;
|
|
1268
|
+
const fit = fitLinear(mids, logRates);
|
|
1269
|
+
if (!fit || !(fit.b < 0) || fit.rmse > 0.1)
|
|
1270
|
+
return null;
|
|
1271
|
+
// The decay is judged over the whole span, not per retained gap, so downsampling cannot change the verdict.
|
|
1272
|
+
return Math.exp(logRates[logRates.length - 1] - logRates[0]) < 0.25 ? fit.rmse : null;
|
|
1273
|
+
}
|
|
1274
|
+
function irregularSpacing(points) {
|
|
1275
|
+
const gaps = [];
|
|
1276
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
1277
|
+
gaps.push(points[index].t - points[index - 1].t);
|
|
1278
|
+
}
|
|
1279
|
+
if (gaps.length < 2)
|
|
1280
|
+
return false;
|
|
1281
|
+
// A loop, not Math.min(...gaps): a million-point trace exceeds the argument limit of a spread call.
|
|
1282
|
+
let count = 0;
|
|
1283
|
+
let smallest = Number.POSITIVE_INFINITY;
|
|
1284
|
+
let largest = 0;
|
|
1285
|
+
for (const gap of gaps) {
|
|
1286
|
+
if (!(gap > 0))
|
|
1287
|
+
continue;
|
|
1288
|
+
count += 1;
|
|
1289
|
+
if (gap < smallest)
|
|
1290
|
+
smallest = gap;
|
|
1291
|
+
if (gap > largest)
|
|
1292
|
+
largest = gap;
|
|
1293
|
+
}
|
|
1294
|
+
if (count < 2)
|
|
1295
|
+
return false;
|
|
1296
|
+
return largest / smallest > 3;
|
|
1297
|
+
}
|
|
1298
|
+
function blankWindow(points, discontinuity, status, quality) {
|
|
1299
|
+
const first = points[0];
|
|
1300
|
+
const last = points[points.length - 1];
|
|
1301
|
+
return {
|
|
1302
|
+
startSec: first?.t ?? 0,
|
|
1303
|
+
endSec: last?.t ?? 0,
|
|
1304
|
+
sampleCount: points.length,
|
|
1305
|
+
domain: domainOf(points),
|
|
1306
|
+
status,
|
|
1307
|
+
shape: "unknown",
|
|
1308
|
+
discontinuity,
|
|
1309
|
+
speed: { logSlopePerSec: null, doublingTimeSec: null },
|
|
1310
|
+
legacySpeedBucket: null,
|
|
1311
|
+
residual: { space: "none", rmse: null },
|
|
1312
|
+
quality,
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
function classifyPositive(points, discontinuity, originSec = points[0]?.t ?? 0) {
|
|
1316
|
+
const first = points[0];
|
|
1317
|
+
const last = points[points.length - 1];
|
|
1318
|
+
const span = last.t - first.t;
|
|
1319
|
+
const logs = points.map((point) => point.log10);
|
|
1320
|
+
if (points.length < MIN_SAMPLES || span < MIN_SPAN_SEC || logs.some((value) => value === null)) {
|
|
1321
|
+
return blankWindow(points, discontinuity, "insufficient-data", `samples ${points.length} span ${span}`);
|
|
1322
|
+
}
|
|
1323
|
+
// Fit log values relative to the first one. A large log offset, such as 1e12, otherwise drowns the low-order
|
|
1324
|
+
// digits of the regression sums, and the shape would depend on the series scale.
|
|
1325
|
+
const logOrigin = logs[0];
|
|
1326
|
+
const ys = logs.map((value) => value - logOrigin);
|
|
1327
|
+
// Fit on time since the window start. Raw moments of a large absolute t cancel catastrophically.
|
|
1328
|
+
const ts = points.map((point) => point.t - first.t);
|
|
1329
|
+
// A loop, not Math.max(...ys): a large sampling budget can exceed the argument limit of a spread call.
|
|
1330
|
+
let logLow = Number.POSITIVE_INFINITY;
|
|
1331
|
+
let logHigh = Number.NEGATIVE_INFINITY;
|
|
1332
|
+
for (const y of ys) {
|
|
1333
|
+
if (y < logLow)
|
|
1334
|
+
logLow = y;
|
|
1335
|
+
if (y > logHigh)
|
|
1336
|
+
logHigh = y;
|
|
1337
|
+
}
|
|
1338
|
+
const logSpan = logHigh - logLow;
|
|
1339
|
+
const linear = fitLinear(ts, ys);
|
|
1340
|
+
const quadratic = fitQuadratic(ts, ys);
|
|
1341
|
+
// A power law is fitted from the window start and from the start of its break segment, and the closer fit is kept.
|
|
1342
|
+
// Window partitioning alone then does not change a power law's shape.
|
|
1343
|
+
const localPower = fitLinear(ts.map((t) => Math.log10(t + 1)), ys);
|
|
1344
|
+
const segmentPower = originSec < first.t ? fitLinear(points.map((point) => Math.log10(point.t - originSec + 1)), ys) : null;
|
|
1345
|
+
// The absolute time origin too, when every time is past it, so a trace that starts late keeps the law's basis.
|
|
1346
|
+
const absolutePower = first.t > 0 && Number.isFinite(last.t + 1) ? fitLinear(points.map((point) => Math.log10(point.t + 1)), ys) : null;
|
|
1347
|
+
let power = localPower;
|
|
1348
|
+
for (const candidate of [segmentPower, absolutePower]) {
|
|
1349
|
+
if (candidate !== null && (power === null || candidate.rmse < power.rmse))
|
|
1350
|
+
power = candidate;
|
|
1351
|
+
}
|
|
1352
|
+
// Value-space fits read each amount relative to the window's largest, through its log, when the amount itself is
|
|
1353
|
+
// outside the Number range. A unit change, such as a factor of 1e400, does not switch value-space shapes off.
|
|
1354
|
+
const direct = points.map((point) => point.numeric);
|
|
1355
|
+
const numbers = direct.every((value) => value !== null)
|
|
1356
|
+
? direct
|
|
1357
|
+
: ys.map((y) => 10 ** (y - logHigh));
|
|
1358
|
+
const valueReady = numbers.every((value) => value !== null && Number.isFinite(value));
|
|
1359
|
+
// Value-space fits run in units of the series scale, so a large series cannot overflow the squared error.
|
|
1360
|
+
const valueScale = valueReady ? seriesScale(numbers) : 1;
|
|
1361
|
+
const scaled = valueReady ? numbers.map((value) => value / valueScale) : [];
|
|
1362
|
+
const valueLinear = valueReady ? fitLinear(ts, scaled) : null;
|
|
1363
|
+
const valueQuadratic = valueReady ? fitQuadratic(ts, scaled) : null;
|
|
1364
|
+
const valueLinearRel = valueLinear ? valueLinear.rmse : null;
|
|
1365
|
+
const valueQuadraticRel = valueQuadratic ? valueQuadratic.rmse : null;
|
|
1366
|
+
const valueRmse = valueLinearRel === null
|
|
1367
|
+
? null
|
|
1368
|
+
: valueQuadraticRel === null
|
|
1369
|
+
? valueLinearRel
|
|
1370
|
+
: Math.min(valueLinearRel, valueQuadraticRel);
|
|
1371
|
+
const numericValues = valueReady ? numbers.map((value) => value ?? 0) : [];
|
|
1372
|
+
const expOk = linear !== null && linear.rmse <= LOG_RMSE_OK;
|
|
1373
|
+
// Acceleration is the quadratic term standing well above its residual across the window, with the quadratic fit a
|
|
1374
|
+
// clear improvement on the line. A fixed minimum line residual would make the shape depend on acceleration strength.
|
|
1375
|
+
const accelOk = quadratic !== null &&
|
|
1376
|
+
linear !== null &&
|
|
1377
|
+
quadratic.c > 0 &&
|
|
1378
|
+
quadratic.c * span * span > 1e3 * Math.max(quadratic.rmse, Number.EPSILON) &&
|
|
1379
|
+
quadratic.rmse <= LOG_RMSE_OK &&
|
|
1380
|
+
linear.rmse > quadratic.rmse * 4;
|
|
1381
|
+
const powerOk = power !== null && linear !== null && power.rmse <= 1e-3 && power.rmse * 4 < linear.rmse;
|
|
1382
|
+
const valuePolyOk = valueRmse !== null && valueRmse <= VALUE_REL_OK;
|
|
1383
|
+
const satRmse = valueReady ? saturating(ts, numericValues) : null;
|
|
1384
|
+
const satOk = satRmse !== null;
|
|
1385
|
+
let shape = "unknown";
|
|
1386
|
+
let residual = {
|
|
1387
|
+
space: "log10",
|
|
1388
|
+
rmse: linear?.rmse ?? null,
|
|
1389
|
+
};
|
|
1390
|
+
// An exact value-space line or parabola is polynomial whatever its intercept. A large intercept can make
|
|
1391
|
+
// the same series look nearly straight in log space.
|
|
1392
|
+
// An exact log fit keeps precedence: a value fit wins only when it is closer than the log fit's
|
|
1393
|
+
// relative error (log10 residual times ln 10), so a slow exponential is not read as a parabola.
|
|
1394
|
+
const valueExact = valueRmse !== null &&
|
|
1395
|
+
valueRmse <= VALUE_EXACT &&
|
|
1396
|
+
!(linear !== null && linear.rmse * Math.LN10 < valueRmse) &&
|
|
1397
|
+
!(accelOk && quadratic !== null && quadratic.rmse * Math.LN10 < valueRmse) &&
|
|
1398
|
+
!(powerOk && power !== null && power.rmse * Math.LN10 < valueRmse);
|
|
1399
|
+
// An exact value fit whose change across the window stands well above its residual is a moving series, even
|
|
1400
|
+
// when a large intercept leaves almost no log span. A constant series fits exactly too, but does not move.
|
|
1401
|
+
const valueMoves = valueExact &&
|
|
1402
|
+
((valueLinear !== null && Math.abs(valueLinear.b * span) > 1e3 * Math.max(valueLinear.rmse, Number.EPSILON)) ||
|
|
1403
|
+
(valueQuadratic !== null &&
|
|
1404
|
+
Math.abs(valueQuadratic.c * span * span) > 1e3 * Math.max(valueQuadratic.rmse, Number.EPSILON)));
|
|
1405
|
+
// The same holds in log space: an exact log-linear fit with a real slope is a slow exponential, not a constant.
|
|
1406
|
+
const logMoves = linear !== null && Math.abs(linear.b * span) > 1e3 * Math.max(linear.rmse, Number.EPSILON);
|
|
1407
|
+
// Detected saturation is not constant, and its decaying rates rule out exponential, accelerating, and power
|
|
1408
|
+
// growth. A value fit exact only against the series scale, not against its movement, does not outrank it either,
|
|
1409
|
+
// so a large baseline under the same curve keeps the shape.
|
|
1410
|
+
if (logSpan < 1e-6 && !valueMoves && !logMoves && !satOk) {
|
|
1411
|
+
shape = "constant";
|
|
1412
|
+
residual = { space: "log10", rmse: rmseAround(ys.map(() => mean(ys)), ys) };
|
|
1413
|
+
}
|
|
1414
|
+
else if (valueExact && (valueMoves || !satOk)) {
|
|
1415
|
+
shape = "polynomial";
|
|
1416
|
+
residual = { space: "value", rmse: valueRmse };
|
|
1417
|
+
}
|
|
1418
|
+
else if (satOk) {
|
|
1419
|
+
shape = "saturating";
|
|
1420
|
+
// The saturation fit's own error, not the unrelated value-space line or parabola.
|
|
1421
|
+
residual = { space: "log-rate", rmse: satRmse };
|
|
1422
|
+
}
|
|
1423
|
+
else if (expOk && !accelOk && !(powerOk && power !== null && power.rmse * 100 < linear.rmse)) {
|
|
1424
|
+
// A power fit far closer than the log-linear one is a power law that only looks exponential over this window.
|
|
1425
|
+
shape = "exponential";
|
|
1426
|
+
residual = { space: "log10", rmse: linear.rmse };
|
|
1427
|
+
}
|
|
1428
|
+
else if (accelOk && quadratic) {
|
|
1429
|
+
shape = "accelerating-log";
|
|
1430
|
+
residual = { space: "log10", rmse: quadratic.rmse };
|
|
1431
|
+
}
|
|
1432
|
+
else if (powerOk && power) {
|
|
1433
|
+
shape = "polynomial";
|
|
1434
|
+
residual = { space: "log10", rmse: power.rmse };
|
|
1435
|
+
}
|
|
1436
|
+
else if (valuePolyOk) {
|
|
1437
|
+
shape = "polynomial";
|
|
1438
|
+
residual = { space: "value", rmse: valueRmse };
|
|
1439
|
+
}
|
|
1440
|
+
const slope = linear && Number.isFinite(linear.b) ? linear.b : null;
|
|
1441
|
+
const status = shape === "unknown" ? "unknown" : "ok";
|
|
1442
|
+
const legacy = status === "ok" && slope !== null ? legacyGrowthSpeedBucket(slope) : null;
|
|
1443
|
+
return {
|
|
1444
|
+
startSec: first.t,
|
|
1445
|
+
endSec: last.t,
|
|
1446
|
+
sampleCount: points.length,
|
|
1447
|
+
domain: "positive",
|
|
1448
|
+
status,
|
|
1449
|
+
shape,
|
|
1450
|
+
discontinuity,
|
|
1451
|
+
speed: {
|
|
1452
|
+
logSlopePerSec: slope,
|
|
1453
|
+
// Any positive slope has a doubling time while the quotient is finite.
|
|
1454
|
+
doublingTimeSec: slope !== null && slope > 0 && Number.isFinite(Math.log10(2) / slope) ? Math.log10(2) / slope : null,
|
|
1455
|
+
},
|
|
1456
|
+
legacySpeedBucket: legacy,
|
|
1457
|
+
residual,
|
|
1458
|
+
quality: `shape=${shape}; logSpan=${logSpan}; exp=${linear?.rmse ?? "none"}; quad=${quadratic?.rmse ?? "none"}; quadC=${quadratic?.c ?? "none"}; power=${power?.rmse ?? "none"}; value=${valueRmse ?? "none"}; sat=${satOk}`,
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
function classifySlice(slice) {
|
|
1462
|
+
const domain = domainOf(slice.points);
|
|
1463
|
+
if (domain !== "positive") {
|
|
1464
|
+
const status = domain === "empty" ? "insufficient-data" : "invalid";
|
|
1465
|
+
return blankWindow(slice.points, slice.discontinuity, status, `domain ${domain}`);
|
|
1466
|
+
}
|
|
1467
|
+
return classifyPositive(slice.points, slice.discontinuity, slice.originSec);
|
|
1468
|
+
}
|
|
1469
|
+
const BREAK_RANK = { reset: 4, drop: 3, sign: 2, purchase: 1 };
|
|
1470
|
+
/**
|
|
1471
|
+
* Breaks keyed by time. One time keeps the highest-ranked kind: reset, then drop, then sign, then purchase.
|
|
1472
|
+
* The map keeps a lookup O(1), so a trace that alternates sign on every point stays linear.
|
|
1473
|
+
*/
|
|
1474
|
+
function pushBreak(breaks, next) {
|
|
1475
|
+
const existing = breaks.get(next.t);
|
|
1476
|
+
if (!existing || BREAK_RANK[next.kind] > BREAK_RANK[existing.kind])
|
|
1477
|
+
breaks.set(next.t, next);
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Index of the first time-ordered item at or after `t` (strictly after when `strict`), or -1.
|
|
1481
|
+
* Binary search keeps break and action lookups at O(log n) per item.
|
|
1482
|
+
*/
|
|
1483
|
+
function firstIndexAtOrAfter(items, t, strict) {
|
|
1484
|
+
let low = 0;
|
|
1485
|
+
let high = items.length;
|
|
1486
|
+
while (low < high) {
|
|
1487
|
+
const mid = (low + high) >>> 1;
|
|
1488
|
+
const at = items[mid].t;
|
|
1489
|
+
if (strict ? at > t : at >= t)
|
|
1490
|
+
high = mid;
|
|
1491
|
+
else
|
|
1492
|
+
low = mid + 1;
|
|
1493
|
+
}
|
|
1494
|
+
return low < items.length ? low : -1;
|
|
1495
|
+
}
|
|
1496
|
+
function slicesOf(points, breaks, windowSec) {
|
|
1497
|
+
const opening = points.map(() => "none");
|
|
1498
|
+
for (const item of breaks) {
|
|
1499
|
+
const index = firstIndexAtOrAfter(points, item.t, false);
|
|
1500
|
+
if (index <= 0)
|
|
1501
|
+
continue;
|
|
1502
|
+
// Breaks that open at one kept point keep the same precedence as breaks at one time.
|
|
1503
|
+
const current = opening[index];
|
|
1504
|
+
if (current === "none" || BREAK_RANK[item.kind] > BREAK_RANK[current])
|
|
1505
|
+
opening[index] = item.kind;
|
|
1506
|
+
}
|
|
1507
|
+
const coarse = [];
|
|
1508
|
+
let start = 0;
|
|
1509
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
1510
|
+
if (opening[index] === "none")
|
|
1511
|
+
continue;
|
|
1512
|
+
coarse.push({ start, end: index, discontinuity: opening[start] ?? "none" });
|
|
1513
|
+
start = index;
|
|
1514
|
+
}
|
|
1515
|
+
coarse.push({ start, end: points.length, discontinuity: opening[start] ?? "none" });
|
|
1516
|
+
const slices = [];
|
|
1517
|
+
for (const segment of coarse) {
|
|
1518
|
+
let cursor = segment.start;
|
|
1519
|
+
let first = true;
|
|
1520
|
+
while (cursor < segment.end) {
|
|
1521
|
+
const endTime = points[cursor].t + windowSec;
|
|
1522
|
+
let next = cursor + 1;
|
|
1523
|
+
while (next < segment.end && points[next].t <= endTime + 1e-9)
|
|
1524
|
+
next += 1;
|
|
1525
|
+
if (next === cursor)
|
|
1526
|
+
next = cursor + 1;
|
|
1527
|
+
slices.push({
|
|
1528
|
+
points: points.slice(cursor, next),
|
|
1529
|
+
discontinuity: first ? segment.discontinuity : "none",
|
|
1530
|
+
originSec: points[segment.start].t,
|
|
1531
|
+
});
|
|
1532
|
+
first = false;
|
|
1533
|
+
cursor = next;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
return slices;
|
|
1537
|
+
}
|
|
1538
|
+
function rollup(windows) {
|
|
1539
|
+
if (windows.length === 0)
|
|
1540
|
+
return "insufficient-data";
|
|
1541
|
+
const statuses = new Set(windows.map((window) => window.status));
|
|
1542
|
+
if (statuses.size === 1)
|
|
1543
|
+
return windows[0].status;
|
|
1544
|
+
if (windows.some((window) => window.status === "ok"))
|
|
1545
|
+
return "ok";
|
|
1546
|
+
return "unknown";
|
|
1547
|
+
}
|
|
1548
|
+
function baseArtifact(args) {
|
|
1549
|
+
const first = args.points[0];
|
|
1550
|
+
const last = args.points[args.points.length - 1];
|
|
1551
|
+
return {
|
|
1552
|
+
schemaVersion: 1,
|
|
1553
|
+
algorithmVersion: GROWTH_FIT_ALGORITHM_VERSION,
|
|
1554
|
+
method: "heuristic-fit",
|
|
1555
|
+
status: rollup(args.windows),
|
|
1556
|
+
seriesRequested: args.seriesRequested,
|
|
1557
|
+
valueSource: args.valueSource,
|
|
1558
|
+
windowSec: args.sampling.windowSec,
|
|
1559
|
+
samplingBudget: args.sampling.samplingBudget,
|
|
1560
|
+
sampleCount: args.sampleCount,
|
|
1561
|
+
inputSampleCount: args.inputSampleCount,
|
|
1562
|
+
slopeTolerance: SLOPE_TOLERANCE,
|
|
1563
|
+
fitWindow: {
|
|
1564
|
+
windowSec: args.sampling.windowSec,
|
|
1565
|
+
startSec: first?.t ?? 0,
|
|
1566
|
+
endSec: last?.t ?? 0,
|
|
1567
|
+
},
|
|
1568
|
+
irregular: args.irregular,
|
|
1569
|
+
claimsResourceBottleneck: false,
|
|
1570
|
+
softcap: args.softcap,
|
|
1571
|
+
...(args.sampling.cadenceSec !== undefined ? { cadenceSec: args.sampling.cadenceSec } : {}),
|
|
1572
|
+
cadenceCoarserThanWindow: args.sampling.cadenceSec !== undefined && args.sampling.cadenceSec >= args.sampling.windowSec,
|
|
1573
|
+
...(args.downsampled ? { downsampled: args.downsampled } : {}),
|
|
1574
|
+
...(args.retention ? { retention: args.retention } : {}),
|
|
1575
|
+
assumptions: ASSUMPTIONS,
|
|
1576
|
+
warnings: args.warnings,
|
|
1577
|
+
windows: args.windows,
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Fit one observed series.
|
|
1582
|
+
* Amounts stay in the Engine until a finite log10, or a safe number, is required.
|
|
1583
|
+
*/
|
|
1584
|
+
export function fitGrowth(args) {
|
|
1585
|
+
assertSampling(args.sampling);
|
|
1586
|
+
const prepared = preparePoints(args.E, args.samples);
|
|
1587
|
+
// Find sign changes and drops on every prepared point, then keep both sides of each break through the
|
|
1588
|
+
// sampling budget, so downsampling cannot hide an invalid domain or a discontinuity.
|
|
1589
|
+
// Supplied breaks go through the same precedence as detected ones, so a purchase cannot replace a reset.
|
|
1590
|
+
const breakMap = new Map();
|
|
1591
|
+
for (const item of args.discontinuities ?? [])
|
|
1592
|
+
pushBreak(breakMap, item);
|
|
1593
|
+
const boundary = new Set();
|
|
1594
|
+
for (let index = 1; index < prepared.length; index += 1) {
|
|
1595
|
+
const previous = prepared[index - 1];
|
|
1596
|
+
const current = prepared[index];
|
|
1597
|
+
let found = false;
|
|
1598
|
+
if (previous.sign !== current.sign) {
|
|
1599
|
+
pushBreak(breakMap, { t: current.t, kind: "sign" });
|
|
1600
|
+
found = true;
|
|
1601
|
+
}
|
|
1602
|
+
if (previous.sign === "positive" &&
|
|
1603
|
+
current.sign === "positive" &&
|
|
1604
|
+
previous.log10 !== null &&
|
|
1605
|
+
current.log10 !== null &&
|
|
1606
|
+
// An exact tenfold drop is a drop. The tolerance absorbs log10 rounding.
|
|
1607
|
+
previous.log10 - current.log10 >= DROP_LOG10 - 1e-9) {
|
|
1608
|
+
pushBreak(breakMap, { t: current.t, kind: "drop" });
|
|
1609
|
+
found = true;
|
|
1610
|
+
}
|
|
1611
|
+
if (found) {
|
|
1612
|
+
boundary.add(index - 1);
|
|
1613
|
+
boundary.add(index);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
const breaks = [...breakMap.values()].sort((left, right) => left.t - right.t);
|
|
1617
|
+
for (const item of breaks) {
|
|
1618
|
+
const index = firstIndexAtOrAfter(prepared, item.t, false);
|
|
1619
|
+
if (index >= 0) {
|
|
1620
|
+
boundary.add(index);
|
|
1621
|
+
if (index > 0)
|
|
1622
|
+
boundary.add(index - 1);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
const breakTimes = [...breakMap.values()].map((item) => item.t);
|
|
1626
|
+
const windowed = windowKeep(prepared, args.sampling.samplingBudget, args.sampling.windowSec, breakTimes);
|
|
1627
|
+
const keptSet = new Set(windowed.kept);
|
|
1628
|
+
for (const index of boundary)
|
|
1629
|
+
keptSet.add(prepared[index]);
|
|
1630
|
+
const kept = prepared.filter((point) => keptSet.has(point));
|
|
1631
|
+
const downsampled = kept.length === prepared.length ? undefined : { seen: prepared.length, kept: kept.length };
|
|
1632
|
+
const warnings = [
|
|
1633
|
+
"A candidate fit is not a proof of the asymptotic regime.",
|
|
1634
|
+
"legacySpeedBucket is the old slope threshold. It is not the shape and it is not a cause.",
|
|
1635
|
+
];
|
|
1636
|
+
if (args.softcap === "configured") {
|
|
1637
|
+
warnings.push("A configured softcap is recorded separately. It does not choose the shape.");
|
|
1638
|
+
}
|
|
1639
|
+
if (downsampled) {
|
|
1640
|
+
warnings.push("Samples were evenly reduced to the sampling budget. windowSec is still the time window.");
|
|
1641
|
+
}
|
|
1642
|
+
if (windowed.fittedWindows < windowed.windows) {
|
|
1643
|
+
warnings.push(`The sampling budget fits ${windowed.fittedWindows} of ${windowed.windows} windows with enough samples. The rest are not fitted.`);
|
|
1644
|
+
}
|
|
1645
|
+
if (args.retention && args.retention.dropped > 0) {
|
|
1646
|
+
warnings.push("The fit sees the retained trace only.");
|
|
1647
|
+
}
|
|
1648
|
+
// Irregular spacing is a property of the input cadence; the sampling budget must not hide it.
|
|
1649
|
+
const irregular = irregularSpacing(prepared);
|
|
1650
|
+
if (irregular)
|
|
1651
|
+
warnings.push("Sample spacing is irregular. Observed times are not rewritten onto a grid.");
|
|
1652
|
+
const cadenceCoarse = args.sampling.cadenceSec !== undefined && args.sampling.cadenceSec >= args.sampling.windowSec;
|
|
1653
|
+
if (cadenceCoarse) {
|
|
1654
|
+
warnings.push("Cadence is coarser than windowSec, so the window is not identified.");
|
|
1655
|
+
return baseArtifact({
|
|
1656
|
+
seriesRequested: args.seriesRequested,
|
|
1657
|
+
valueSource: args.valueSource,
|
|
1658
|
+
sampling: args.sampling,
|
|
1659
|
+
inputSampleCount: prepared.length,
|
|
1660
|
+
sampleCount: kept.length,
|
|
1661
|
+
points: kept,
|
|
1662
|
+
softcap: args.softcap ?? "unknown",
|
|
1663
|
+
retention: args.retention,
|
|
1664
|
+
downsampled,
|
|
1665
|
+
irregular,
|
|
1666
|
+
warnings,
|
|
1667
|
+
// Coarse cadence hides the window, not the observed domain. Non-positive or nonfinite values stay invalid.
|
|
1668
|
+
windows: [
|
|
1669
|
+
blankWindow(kept, "none", domainOf(kept) === "positive" || domainOf(kept) === "empty" ? "insufficient-data" : "invalid", "cadence coarser than window"),
|
|
1670
|
+
],
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
const fitted = kept.length === 0
|
|
1674
|
+
? [blankWindow(kept, "none", "insufficient-data", "no retained samples")]
|
|
1675
|
+
: slicesOf(kept, breaks, args.sampling.windowSec).map((slice) => classifySlice(slice));
|
|
1676
|
+
if (kept.length > args.sampling.samplingBudget) {
|
|
1677
|
+
warnings.push("Break boundary points were kept beyond the sampling budget.");
|
|
1678
|
+
}
|
|
1679
|
+
const incomplete = args.actionHistory === "incomplete";
|
|
1680
|
+
if (incomplete) {
|
|
1681
|
+
warnings.push("The action history is incomplete, so a purchase break can be missing. Shape claims are withheld.");
|
|
1682
|
+
}
|
|
1683
|
+
const windows = incomplete
|
|
1684
|
+
? fitted.map((window) =>
|
|
1685
|
+
// A fitted window, classified or not, read samples a missing purchase break may have split.
|
|
1686
|
+
window.status === "ok" || window.status === "unknown"
|
|
1687
|
+
? {
|
|
1688
|
+
...window,
|
|
1689
|
+
status: "insufficient-data",
|
|
1690
|
+
shape: "unknown",
|
|
1691
|
+
// The fitted shape's residual, speed, and diagnostics would restate the withheld claim.
|
|
1692
|
+
speed: { logSlopePerSec: null, doublingTimeSec: null },
|
|
1693
|
+
legacySpeedBucket: null,
|
|
1694
|
+
residual: { space: "none", rmse: null },
|
|
1695
|
+
quality: `action history incomplete; samples ${window.sampleCount}`,
|
|
1696
|
+
}
|
|
1697
|
+
: window)
|
|
1698
|
+
: fitted;
|
|
1699
|
+
return baseArtifact({
|
|
1700
|
+
seriesRequested: args.seriesRequested,
|
|
1701
|
+
valueSource: args.valueSource,
|
|
1702
|
+
sampling: args.sampling,
|
|
1703
|
+
inputSampleCount: prepared.length,
|
|
1704
|
+
sampleCount: kept.length,
|
|
1705
|
+
points: kept,
|
|
1706
|
+
softcap: args.softcap ?? "unknown",
|
|
1707
|
+
retention: args.retention,
|
|
1708
|
+
downsampled,
|
|
1709
|
+
irregular,
|
|
1710
|
+
warnings,
|
|
1711
|
+
windows,
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
function softcapOf(scenario, explicit, state) {
|
|
1715
|
+
if (explicit)
|
|
1716
|
+
return explicit;
|
|
1717
|
+
if (!scenario?.model.analytic)
|
|
1718
|
+
return scenario ? "absent" : "unknown";
|
|
1719
|
+
// The hint is read at the analyzed run's start, not the scenario's opening state, so a resumed run is judged
|
|
1720
|
+
// by the state it fitted.
|
|
1721
|
+
const hint = scenario.model.analytic(scenario.ctx, state ?? scenario.initial);
|
|
1722
|
+
return hint?.softcap ? "configured" : "absent";
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* The break an action row opens. Action.kind is the source of truth: a prestige action is a reset, and every other
|
|
1726
|
+
* action is a purchase whatever its id says, unless a prestige count change confirms a reset.
|
|
1727
|
+
*/
|
|
1728
|
+
function actionBreakKind(scenario, before, actionId) {
|
|
1729
|
+
const matches = before ? scenario.model.actions(scenario.ctx, before).filter((action) => action.id === actionId) : [];
|
|
1730
|
+
// The log keeps only the id. When actions of different kinds share it, the applied kind is unknown, so the row is
|
|
1731
|
+
// a purchase unless a prestige count change confirms a reset.
|
|
1732
|
+
if (new Set(matches.map((action) => action.kind)).size > 1)
|
|
1733
|
+
return "purchase";
|
|
1734
|
+
const listed = matches[0];
|
|
1735
|
+
if (listed?.kind === "prestige")
|
|
1736
|
+
return "reset";
|
|
1737
|
+
// A custom action, or one the model did not list at the traced state, such as one unlocked between trace points, is
|
|
1738
|
+
// a purchase unless a prestige count change confirms a reset; the count loop below adds those. An id is not
|
|
1739
|
+
// evidence of what the action does.
|
|
1740
|
+
return "purchase";
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Read a run without converting the whole amount to Number first.
|
|
1744
|
+
* `netWorthFallback` matches `analyzeGrowth` when the model has no net worth.
|
|
1745
|
+
*/
|
|
1746
|
+
export function fitGrowthFromRun(args) {
|
|
1747
|
+
if (args.series === "netWorth" && !args.scenario) {
|
|
1748
|
+
throw new Error("analyzeGrowth requires compiled scenario when series='netWorth'");
|
|
1749
|
+
}
|
|
1750
|
+
const hasNetWorth = args.scenario.model.netWorth !== undefined;
|
|
1751
|
+
const valueSource = args.series === "money" ? "money" : hasNetWorth ? "netWorth" : "netWorthFallback";
|
|
1752
|
+
// A present but empty trace was emptied by retention. It is not replaced by start and end.
|
|
1753
|
+
const states = args.run.trace !== undefined ? args.run.trace : [args.run.start, args.run.end];
|
|
1754
|
+
const samples = states.map((state) => ({
|
|
1755
|
+
t: state.t,
|
|
1756
|
+
amount: valueSource === "netWorth"
|
|
1757
|
+
? args.scenario.model.netWorth(args.scenario.ctx, state).amount
|
|
1758
|
+
: state.wallet.money.amount,
|
|
1759
|
+
}));
|
|
1760
|
+
const discontinuities = [];
|
|
1761
|
+
for (const row of args.run.actionsLog ?? []) {
|
|
1762
|
+
// An action row is stamped at decision time; the trace point at that t is the state before it.
|
|
1763
|
+
// The break opens at the first trace state strictly after the action, found by binary search.
|
|
1764
|
+
const afterIndex = firstIndexAtOrAfter(states, row.t, true);
|
|
1765
|
+
const after = states[afterIndex];
|
|
1766
|
+
if (!after)
|
|
1767
|
+
continue;
|
|
1768
|
+
discontinuities.push({ t: after.t, kind: actionBreakKind(args.scenario, states[afterIndex - 1], row.actionId) });
|
|
1769
|
+
}
|
|
1770
|
+
const loggedEveryAction = args.run.actionsLog !== undefined && (args.run.actionsLogMeta?.dropped ?? 0) === 0;
|
|
1771
|
+
// A zero count proves no action ran only when the counter was observed. A disabled observation reports 0 too.
|
|
1772
|
+
const noActionApplied = args.run.stats?.actions.status === "observed" && args.run.stats.actions.applied === 0;
|
|
1773
|
+
let previousCount = states[0]?.prestige.count;
|
|
1774
|
+
for (const state of states.slice(1)) {
|
|
1775
|
+
if (previousCount !== undefined && state.prestige.count !== previousCount) {
|
|
1776
|
+
discontinuities.push({ t: state.t, kind: "reset" });
|
|
1777
|
+
}
|
|
1778
|
+
previousCount = state.prestige.count;
|
|
1779
|
+
}
|
|
1780
|
+
return fitGrowth({
|
|
1781
|
+
E: args.scenario.ctx.E,
|
|
1782
|
+
samples,
|
|
1783
|
+
seriesRequested: args.series,
|
|
1784
|
+
valueSource,
|
|
1785
|
+
sampling: args.sampling,
|
|
1786
|
+
discontinuities,
|
|
1787
|
+
actionHistory: loggedEveryAction || noActionApplied ? "complete" : "incomplete",
|
|
1788
|
+
softcap: softcapOf(args.scenario, args.softcap, args.run.start),
|
|
1789
|
+
retention: args.run.traceLog
|
|
1790
|
+
? {
|
|
1791
|
+
totalSeen: args.run.traceLog.totalSeen,
|
|
1792
|
+
retained: args.run.traceLog.retained,
|
|
1793
|
+
dropped: args.run.traceLog.dropped,
|
|
1794
|
+
}
|
|
1795
|
+
: undefined,
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
//# sourceMappingURL=growthFit.js.map
|