Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | 1x 1x 1x 1x 125x 125x 125x 125x 125x 2x 2x 125x 2x 2x 125x 1x 1x 125x 25x 25x 125x 125x 1x 1x 1x 1x 1x 1x 125x 1x 1x 1x 1x 123x 123x 123x 123x 123x 123x 1x 74x 74x 74x 74x 74x 74x 1x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 1x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 1x 1x 9x 9x 7x 7x | /**
* ForecastConfig management and validation.
*
* Mirrors the Python `ForecastConfig` frozen-dataclass semantics plus the
* auto-adjustment logic in `compile()` that rounds `maxContext` and
* `maxHorizon` to patch-size multiples.
*/
import {
DEFAULT_FORECAST_CONFIG,
TIMESFM_25_CONFIG,
type ForecastConfig,
type ModelConfig,
} from './types';
import { ConfigValidationError } from './errors';
import * as os from 'node:os';
/**
* Validate and normalise a ForecastConfig against a ModelConfig.
*
* Returns a *new* object; the input is never mutated.
*
* Adjustments applied (matching the Python `compile()` logic):
* - `maxContext` rounded up to the next multiple of `inputPatchLen`.
* - `maxHorizon` rounded up to the next multiple of `outputPatchLen`.
* - If `useContinuousQuantileHead` is true, `maxHorizon` must be ≤
* `outputQuantileLen`.
*
* @throws {RangeError} if context + horizon exceeds the model's limit.
*/
export function validateAndNormalizeConfig(
fc: ForecastConfig,
mc: ModelConfig = TIMESFM_25_CONFIG,
): ForecastConfig {
// --- Copy to avoid mutating the caller's object. ---
let { maxContext, maxHorizon } = fc;
// --- Ensure sane defaults (must run before validation & rounding) ---
// NaN and Infinity are NOT caught by <= 0 checks, so guard them explicitly.
if (!Number.isFinite(maxContext) || maxContext <= 0) {
maxContext = mc.inputPatchLen;
}
if (!Number.isFinite(maxHorizon) || maxHorizon <= 0) {
maxHorizon = mc.outputPatchLen;
}
// --- Round context to patch boundary ---
if (maxContext % mc.inputPatchLen !== 0) {
maxContext = Math.ceil(maxContext / mc.inputPatchLen) * mc.inputPatchLen;
}
// --- Round horizon to output-patch boundary ---
if (maxHorizon % mc.outputPatchLen !== 0) {
maxHorizon = Math.ceil(maxHorizon / mc.outputPatchLen) * mc.outputPatchLen;
}
// --- Validate perCoreBatchSize ---
// NaN, 0, or negative would cause division by zero or NaN in model.forecast()
if (!Number.isFinite(fc.perCoreBatchSize) || fc.perCoreBatchSize < 1) {
throw new ConfigValidationError(`perCoreBatchSize must be ≥ 1, got ${fc.perCoreBatchSize}.`);
}
// --- Hard limits ---
if (maxContext + maxHorizon > mc.contextLimit) {
throw new ConfigValidationError(
`Context + horizon (${maxContext} + ${maxHorizon} = ${
maxContext + maxHorizon
}) exceeds the model's context limit (${mc.contextLimit}).`,
);
}
if (fc.useContinuousQuantileHead && maxHorizon > mc.outputQuantileLen) {
throw new ConfigValidationError(
`Continuous quantile head requires maxHorizon ≤ ${mc.outputQuantileLen}, got ${maxHorizon}.`,
);
}
return {
...fc,
maxContext,
maxHorizon,
};
}
/**
* Create a ForecastConfig by merging user overrides into the defaults,
* then normalising.
*/
export function createForecastConfig(
overrides: Partial<ForecastConfig> = {},
mc: ModelConfig = TIMESFM_25_CONFIG,
): ForecastConfig {
const merged: ForecastConfig = { ...DEFAULT_FORECAST_CONFIG, ...overrides };
return validateAndNormalizeConfig(merged, mc);
}
/**
* Check whether two ForecastConfigs are equivalent.
*
* Compares all fields that affect forecasting behaviour. Useful for
* detecting config drift in long-running pipelines and for test assertions
* that verify config round-tripping through validateAndNormalizeConfig.
*/
export function configsEqual(a: ForecastConfig, b: ForecastConfig): boolean {
const keys: Array<keyof ForecastConfig> = [
'maxContext',
'maxHorizon',
'normalizeInputs',
'useContinuousQuantileHead',
'forceFlipInvariance',
'inferIsPositive',
'fixQuantileCrossing',
'returnBackcast',
'perCoreBatchSize',
];
return keys.every((k) => a[k] === b[k]);
}
// ---------------------------------------------------------------------------
// Batch-size suggestion
// ---------------------------------------------------------------------------
/**
* Options for {@link suggestBatchSize}.
*/
export interface SuggestBatchSizeOptions {
/** Available RAM in GB (auto-detected via `os.freemem()` if omitted). */
freeMemoryGB?: number;
/** Fraction of free RAM to use (0–1, default 0.5). */
memoryFraction?: number;
}
/**
* Suggest a `perCoreBatchSize` based on available system memory.
*
* TimesFM 2.5 200M loads ~1.5 GB into RAM (model weights + activations).
* Each additional batch element adds ~200 MB for intermediate tensors.
* This function computes a safe batch size that fits within a configurable
* fraction of free memory (default: 50 %).
*
* The result is clamped to [1, 16] — the model processes elements
* concurrently via `Promise.all`, so diminishing returns apply beyond 8.
*
* ```typescript
* import { suggestBatchSize, createForecastConfig } from '@agentix-e/timesfm-core';
*
* const bs = suggestBatchSize();
* // or with explicit options:
* const bs = suggestBatchSize({ freeMemoryGB: 8, memoryFraction: 0.5 });
* const fc = createForecastConfig({ perCoreBatchSize: bs });
* ```
*
* @param freeMemoryGBOrOptions Available RAM in GB, or an options object.
* @param memoryFractionArg Fraction of free RAM to use (0–1, default 0.5).
* Only used when the first argument is a number.
* @returns Suggested batch size (1–16).
*/
export function suggestBatchSize(
freeMemoryGBOrOptions?: number | SuggestBatchSizeOptions,
memoryFractionArg: number = 0.5,
): number {
// ~1.5 GB for model + ONNX Runtime overhead
const MODEL_OVERHEAD_GB = 1.5;
// ~0.2 GB per additional batch element (intermediate tensors)
const PER_BATCH_GB = 0.2;
let freeMemoryGB: number | undefined;
let memoryFraction: number = memoryFractionArg;
if (typeof freeMemoryGBOrOptions === 'object' && freeMemoryGBOrOptions !== null) {
freeMemoryGB = freeMemoryGBOrOptions.freeMemoryGB;
memoryFraction = freeMemoryGBOrOptions.memoryFraction ?? 0.5;
} else {
freeMemoryGB = freeMemoryGBOrOptions;
memoryFraction = memoryFractionArg;
}
if (freeMemoryGB === undefined) {
freeMemoryGB = os.freemem() / 1024 ** 3;
}
const usableGB = freeMemoryGB * memoryFraction - MODEL_OVERHEAD_GB;
if (usableGB <= 0) return 1;
return Math.max(1, Math.min(16, Math.floor(usableGB / PER_BATCH_GB)));
}
|