@hestia-earth/engine-models 0.81.6 → 0.82.1

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/README.md CHANGED
@@ -142,3 +142,22 @@ The `ecoinventV3` model requires a valid [license](https://ecoinvent.org/offerin
142
142
 
143
143
  To amortise the impacts of land use change using linear discounting across IPCC (2019) models:
144
144
  1. Set the env variable `USE_LINEAR_DISCOUNTING` to `true`
145
+
146
+ ## TypeScript / JavaScript package
147
+
148
+ The model metadata is also published to npm as [`@hestia-earth/engine-models`](https://www.npmjs.com/package/@hestia-earth/engine-models):
149
+
150
+ ```bash
151
+ npm install @hestia-earth/engine-models
152
+ ```
153
+
154
+ It exposes the links between models, terms and their documentation (`model-links.json`), and the KaTeX **formulas** extracted from each model, with bindings that map every symbol to a value logged at runtime (the "jlog"):
155
+
156
+ ```ts
157
+ import { getFormulas } from '@hestia-earth/engine-models';
158
+
159
+ getFormulas({ model: 'ipcc2019', term: 'ch4ToAirEntericFermentation' });
160
+ // => [ { formula: 'E_{CH4} = (GE \\times Y_m) / 55.65', bindings: [...] } ]
161
+ ```
162
+
163
+ See [`formulas/README.md`](./formulas/README.md) for the full guide on getting a model's formulas, rendering them with KaTeX, and substituting the symbols with the values from a given execution.
@@ -0,0 +1,99 @@
1
+ import { IModel } from './models';
2
+ /**
3
+ * A single symbol in a formula, optionally bound to a value logged in the jlog.
4
+ */
5
+ export interface IFormulaBinding {
6
+ /**
7
+ * The KaTeX symbol as written in the formula, e.g. `E_{CH4}`, `GE`.
8
+ */
9
+ symbol: string;
10
+ /**
11
+ * Human-readable description from the formula's "Where:" list.
12
+ */
13
+ description?: string;
14
+ /**
15
+ * The jlog field this symbol resolves to. `value` is the result (left-hand
16
+ * side); any other key is looked up on the model's jlog entry. Absent for
17
+ * constants / display-only symbols. When `column` is set, this is the log key
18
+ * of a packed `log_as_table` string rather than a scalar.
19
+ */
20
+ key?: string;
21
+ /**
22
+ * A value inside a packed `log_as_table` string: the column to read.
23
+ * - with no `match`, the symbol sits under a `\sum` and is expanded once per
24
+ * row (e.g. `\sum_i M_i \times EF_i` → `(32 × 0.166) + (13 × 0.495)`);
25
+ * - with `match`, it resolves to a single row (row selection, see `match`).
26
+ */
27
+ column?: string;
28
+ /**
29
+ * Row selection: pick the one row of the `key` table whose id (first column)
30
+ * starts with this value, then read `column` from it. Resolves to a single
31
+ * value, e.g. `NH_3\text{-}N` → the `emission-value` of the row `nh3…`.
32
+ */
33
+ match?: string;
34
+ /**
35
+ * A fixed constant (not logged): the symbol always substitutes to this literal
36
+ * (e.g. `ER` → `2`) and is excluded from coverage. Empty string marks a
37
+ * constant with no display value.
38
+ */
39
+ constant?: string;
40
+ }
41
+ /**
42
+ * A KaTeX formula extracted from a model's documentation, with the bindings
43
+ * needed to substitute its symbols with values logged at runtime.
44
+ */
45
+ export interface IFormula {
46
+ /**
47
+ * The KaTeX source (without the `$$` delimiters).
48
+ */
49
+ formula: string;
50
+ /**
51
+ * The symbols in the formula, in "Where:" list order.
52
+ */
53
+ bindings: IFormulaBinding[];
54
+ }
55
+ /**
56
+ * Get the formulas (if any) for a model-links entry. A documentation file may
57
+ * define several formulas, so this always returns an array (empty when none).
58
+ */
59
+ export declare const getFormulasForLink: (link: Pick<IModel, "docPath">) => IFormula[];
60
+ /**
61
+ * Get the formulas for a model, addressed by the same parameters used to look
62
+ * up a model in model-links.json (e.g. `{ model, term }` or `{ model, modelKey }`).
63
+ * Returns an empty array when the model is unknown or has no formulas.
64
+ */
65
+ export declare const getFormulas: (model: Partial<IModel>) => IFormula[];
66
+ /**
67
+ * The result of rendering a formula against a set of values, as KaTeX source
68
+ * strings (without the `$$` delimiters). Feed these to `katex.render`.
69
+ */
70
+ export interface IRenderedFormula {
71
+ /**
72
+ * The formula as authored, e.g. `E_{CH4} = (GE \times Y_m) / 55.65`.
73
+ */
74
+ symbolic: string;
75
+ /**
76
+ * Symbols replaced by their values, e.g. `59.24 = (52340 \times 0.063) / 55.65`.
77
+ * A symbol with no value is left as the symbol.
78
+ */
79
+ substituted: string;
80
+ /**
81
+ * Symbols kept, each wrapped in `\htmlData{key=..., value=...}{symbol}` so the
82
+ * rendered DOM node carries `data-key` / `data-value` for tooltips or toggling.
83
+ * Requires `trust: (ctx) => ctx.command === '\\htmlData'` in the KaTeX options.
84
+ */
85
+ annotated: string;
86
+ }
87
+ /**
88
+ * Render a formula against a set of values, producing the KaTeX strings needed
89
+ * to display it symbolically, with substituted values, or annotated for
90
+ * interaction. `values` maps each binding `key` to its value for one execution
91
+ * (typically pulled from the model's jlog entry, plus `value` for the result);
92
+ * the caller decides where those values come from, so this stays agnostic.
93
+ * Symbols without a value in `values` are left symbolic.
94
+ *
95
+ * A `\sum` whose summand symbols are bound to table columns (`key`+`column`) is
96
+ * expanded once per row, provided `values[key]` holds the packed table string
97
+ * (or an array of row objects). Without the table it stays symbolic.
98
+ */
99
+ export declare const renderFormula: (formula: IFormula, values: Record<string, unknown>) => IRenderedFormula;
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ var __read = (this && this.__read) || function (o, n) {
3
+ var m = typeof Symbol === "function" && o[Symbol.iterator];
4
+ if (!m) return o;
5
+ var i = m.call(o), r, ar = [], e;
6
+ try {
7
+ while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
8
+ }
9
+ catch (error) { e = { error: error }; }
10
+ finally {
11
+ try {
12
+ if (r && !r.done && (m = i["return"])) m.call(i);
13
+ }
14
+ finally { if (e) throw e.error; }
15
+ }
16
+ return ar;
17
+ };
18
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
19
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
20
+ if (ar || !(i in from)) {
21
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
22
+ ar[i] = from[i];
23
+ }
24
+ }
25
+ return to.concat(ar || Array.prototype.slice.call(from));
26
+ };
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.renderFormula = exports.getFormulas = exports.getFormulasForLink = void 0;
29
+ var data = require("../formulas/index.json");
30
+ var utils_1 = require("./utils");
31
+ // keyed by the same `docPath` found on model-links.json entries
32
+ var formulas = data;
33
+ /**
34
+ * Get the formulas (if any) for a model-links entry. A documentation file may
35
+ * define several formulas, so this always returns an array (empty when none).
36
+ */
37
+ var getFormulasForLink = function (link) {
38
+ return (link && formulas[link.docPath]) || [];
39
+ };
40
+ exports.getFormulasForLink = getFormulasForLink;
41
+ /**
42
+ * Get the formulas for a model, addressed by the same parameters used to look
43
+ * up a model in model-links.json (e.g. `{ model, term }` or `{ model, modelKey }`).
44
+ * Returns an empty array when the model is unknown or has no formulas.
45
+ */
46
+ var getFormulas = function (model) {
47
+ var link = (0, utils_1.findMatchingModel)(model);
48
+ return link ? (0, exports.getFormulasForLink)(link) : [];
49
+ };
50
+ exports.getFormulas = getFormulas;
51
+ var isAlpha = function (char) { return /[a-zA-Z]/.test(char || ''); };
52
+ // Split a KaTeX string on the declared symbols only (longest-first), so
53
+ // `\times`, subscripts and constants are never touched.
54
+ var splitOnSymbols = function (formula, symbols) {
55
+ var ordered = __spreadArray([], __read(symbols), false).sort(function (a, b) { return b.length - a.length; });
56
+ var segments = [];
57
+ var i = 0;
58
+ var _loop_1 = function () {
59
+ var symbol = ordered.find(function (s) {
60
+ return formula.startsWith(s, i) &&
61
+ !isAlpha(formula[i - 1]) &&
62
+ !isAlpha(formula[i + s.length]) &&
63
+ formula[i - 1] !== '\\';
64
+ });
65
+ if (symbol) {
66
+ segments.push({ symbol: symbol });
67
+ i += symbol.length;
68
+ }
69
+ else {
70
+ var j_1 = i + 1;
71
+ while (j_1 < formula.length && !ordered.some(function (s) { return formula.startsWith(s, j_1); }))
72
+ j_1++;
73
+ segments.push({ text: formula.slice(i, j_1) });
74
+ i = j_1;
75
+ }
76
+ };
77
+ while (i < formula.length) {
78
+ _loop_1();
79
+ }
80
+ return segments;
81
+ };
82
+ var round4 = function (n) { return String(+n.toPrecision(4)); };
83
+ // Round numeric-looking strings, leave everything else (e.g. "kg FPCM") as-is.
84
+ var formatNumericString = function (value) {
85
+ var asNumber = Number(value);
86
+ return value !== '' && Number.isFinite(asNumber) ? round4(asNumber) : value;
87
+ };
88
+ var formatValue = function (value) {
89
+ if (value === null || value === undefined)
90
+ return null;
91
+ if (typeof value === 'number')
92
+ return round4(value);
93
+ return formatNumericString(String(value));
94
+ };
95
+ // `\sum` with an optional index subscript (`_i`, `_{s=1}`, ...) and upper bound
96
+ // superscript (`^S`, `^{S}`), and trailing space.
97
+ var SUM_RE = /\\sum(?:_\{[^}]*\}|_[^\s{])?(?:\^\{[^}]*\}|\^[^\s{])?\s*/;
98
+ // Parse a packed `log_as_table` string into rows: rows split on `;`, columns on
99
+ // `_`, each column a `key:value` pair (values never contain `_` or `:`).
100
+ var parseTable = function (packed) {
101
+ return packed
102
+ .split(';')
103
+ .filter(Boolean)
104
+ .map(function (row) {
105
+ return row.split('_').reduce(function (cols, pair) {
106
+ var at = pair.indexOf(':');
107
+ if (at !== -1)
108
+ cols[pair.slice(0, at)] = pair.slice(at + 1);
109
+ return cols;
110
+ }, {});
111
+ });
112
+ };
113
+ /**
114
+ * Render a formula against a set of values, producing the KaTeX strings needed
115
+ * to display it symbolically, with substituted values, or annotated for
116
+ * interaction. `values` maps each binding `key` to its value for one execution
117
+ * (typically pulled from the model's jlog entry, plus `value` for the result);
118
+ * the caller decides where those values come from, so this stays agnostic.
119
+ * Symbols without a value in `values` are left symbolic.
120
+ *
121
+ * A `\sum` whose summand symbols are bound to table columns (`key`+`column`) is
122
+ * expanded once per row, provided `values[key]` holds the packed table string
123
+ * (or an array of row objects). Without the table it stays symbolic.
124
+ */
125
+ var renderFormula = function (formula, values) {
126
+ // sum: a per-row column under a \sum. point: a single value — a scalar, a row
127
+ // selection (`match` + `column`), or a fixed constant.
128
+ var sumBindings = formula.bindings.filter(function (b) { return b.key && b.column && !b.match; });
129
+ var pointBindings = formula.bindings.filter(function (b) { return (b.key && !(b.column && !b.match)) || b.constant; });
130
+ var toRows = function (source) {
131
+ return Array.isArray(source)
132
+ ? source
133
+ : typeof source === 'string'
134
+ ? parseTable(source)
135
+ : [];
136
+ };
137
+ var sumMatch = sumBindings.length ? SUM_RE.exec(formula.formula) : null;
138
+ // The summand may draw from several parallel tables (e.g. one for values, one
139
+ // for factors); parse each once and align them by row index.
140
+ var tableRows = {};
141
+ if (sumMatch) {
142
+ sumBindings.forEach(function (b) {
143
+ var key = b.key;
144
+ if (!(key in tableRows))
145
+ tableRows[key] = toRows(values[key]);
146
+ });
147
+ }
148
+ var rowCount = sumMatch
149
+ ? Math.max.apply(Math, __spreadArray([0], __read(sumBindings.map(function (b) { return tableRows[b.key].length; })), false)) : 0;
150
+ // Resolve a point binding to a single value: a fixed constant, a scalar lookup,
151
+ // or the selected row's column for a row selection (`match` = id prefix).
152
+ var pointValue = function (b) {
153
+ if (b.constant !== undefined)
154
+ return b.constant || undefined;
155
+ if (!b.match)
156
+ return values[b.key];
157
+ var row = toRows(values[b.key]).find(function (r) { var _a; return String((_a = Object.values(r)[0]) !== null && _a !== void 0 ? _a : '').startsWith(b.match); });
158
+ return row ? row[b.column] : undefined;
159
+ };
160
+ // How a binding is referenced in the `data-key` attribute.
161
+ var ref = function (b) {
162
+ return b.constant !== undefined
163
+ ? 'const'
164
+ : "".concat(b.key).concat(b.match ? '@' + b.match : '').concat(b.column ? ':' + b.column : '');
165
+ };
166
+ // Expand the `\sum` inline, wrapping each row value with `wrapRow`. Returns the
167
+ // original formula when there is no table to expand over.
168
+ var expand = function (wrapRow) {
169
+ if (!sumMatch || !rowCount)
170
+ return formula.formula;
171
+ var before = formula.formula.slice(0, sumMatch.index);
172
+ var summand = formula.formula.slice(sumMatch.index + sumMatch[0].length);
173
+ var symbols = sumBindings.map(function (b) { return b.symbol; });
174
+ var terms = Array.from({ length: rowCount }, function (_, i) {
175
+ return '(' +
176
+ splitOnSymbols(summand, symbols)
177
+ .map(function (seg) {
178
+ if (seg.text !== undefined)
179
+ return seg.text;
180
+ var b = sumBindings.find(function (x) { return x.symbol === seg.symbol; });
181
+ var row = tableRows[b.key][i];
182
+ var value = row ? formatValue(row[b.column]) : null;
183
+ return value === null ? seg.symbol : wrapRow(b, value);
184
+ })
185
+ .join('') +
186
+ ')';
187
+ });
188
+ return before + terms.join(' + ');
189
+ };
190
+ // Substitute the point (single-value) symbols over a working string.
191
+ var substitutePoints = function (working, wrap) {
192
+ return splitOnSymbols(working, pointBindings.map(function (b) { return b.symbol; }))
193
+ .map(function (seg) {
194
+ if (seg.text !== undefined)
195
+ return seg.text;
196
+ var b = pointBindings.find(function (x) { return x.symbol === seg.symbol; });
197
+ return wrap(b, seg.symbol, formatValue(pointValue(b)));
198
+ })
199
+ .join('');
200
+ };
201
+ var substituted = substitutePoints(expand(function (b, v) { return "\\htmlData{key=".concat(ref(b), "}{").concat(v, "}"); }), function (b, symbol, value) {
202
+ return value === null ? symbol : "\\htmlData{key=".concat(ref(b), "}{").concat(value, "}");
203
+ });
204
+ var annotated = substitutePoints(expand(function (b, v) { return "\\htmlData{key=".concat(ref(b), ", value=").concat(v, "}{").concat(v, "}"); }), function (b, symbol, value) {
205
+ return "\\htmlData{key=".concat(ref(b), ", value=").concat(value === null ? 'na' : value, "}{").concat(symbol, "}");
206
+ });
207
+ return { symbolic: formula.formula, substituted: substituted, annotated: annotated };
208
+ };
209
+ exports.renderFormula = renderFormula;
package/cjs/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './config';
2
+ export * from './formulas';
2
3
  export * from './models';
3
4
  export * from './utils';
4
5
  export * from './validate-config';
package/cjs/index.js CHANGED
@@ -15,6 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./config"), exports);
18
+ __exportStar(require("./formulas"), exports);
18
19
  __exportStar(require("./models"), exports);
19
20
  __exportStar(require("./utils"), exports);
20
21
  __exportStar(require("./validate-config"), exports);
package/cjs/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const ENGINE_VERSION = "0.81.6";
1
+ export declare const ENGINE_VERSION = "0.82.1";
package/cjs/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ENGINE_VERSION = void 0;
4
- exports.ENGINE_VERSION = '0.81.6';
4
+ exports.ENGINE_VERSION = '0.82.1';
@@ -0,0 +1,99 @@
1
+ import { IModel } from './models';
2
+ /**
3
+ * A single symbol in a formula, optionally bound to a value logged in the jlog.
4
+ */
5
+ export interface IFormulaBinding {
6
+ /**
7
+ * The KaTeX symbol as written in the formula, e.g. `E_{CH4}`, `GE`.
8
+ */
9
+ symbol: string;
10
+ /**
11
+ * Human-readable description from the formula's "Where:" list.
12
+ */
13
+ description?: string;
14
+ /**
15
+ * The jlog field this symbol resolves to. `value` is the result (left-hand
16
+ * side); any other key is looked up on the model's jlog entry. Absent for
17
+ * constants / display-only symbols. When `column` is set, this is the log key
18
+ * of a packed `log_as_table` string rather than a scalar.
19
+ */
20
+ key?: string;
21
+ /**
22
+ * A value inside a packed `log_as_table` string: the column to read.
23
+ * - with no `match`, the symbol sits under a `\sum` and is expanded once per
24
+ * row (e.g. `\sum_i M_i \times EF_i` → `(32 × 0.166) + (13 × 0.495)`);
25
+ * - with `match`, it resolves to a single row (row selection, see `match`).
26
+ */
27
+ column?: string;
28
+ /**
29
+ * Row selection: pick the one row of the `key` table whose id (first column)
30
+ * starts with this value, then read `column` from it. Resolves to a single
31
+ * value, e.g. `NH_3\text{-}N` → the `emission-value` of the row `nh3…`.
32
+ */
33
+ match?: string;
34
+ /**
35
+ * A fixed constant (not logged): the symbol always substitutes to this literal
36
+ * (e.g. `ER` → `2`) and is excluded from coverage. Empty string marks a
37
+ * constant with no display value.
38
+ */
39
+ constant?: string;
40
+ }
41
+ /**
42
+ * A KaTeX formula extracted from a model's documentation, with the bindings
43
+ * needed to substitute its symbols with values logged at runtime.
44
+ */
45
+ export interface IFormula {
46
+ /**
47
+ * The KaTeX source (without the `$$` delimiters).
48
+ */
49
+ formula: string;
50
+ /**
51
+ * The symbols in the formula, in "Where:" list order.
52
+ */
53
+ bindings: IFormulaBinding[];
54
+ }
55
+ /**
56
+ * Get the formulas (if any) for a model-links entry. A documentation file may
57
+ * define several formulas, so this always returns an array (empty when none).
58
+ */
59
+ export declare const getFormulasForLink: (link: Pick<IModel, "docPath">) => IFormula[];
60
+ /**
61
+ * Get the formulas for a model, addressed by the same parameters used to look
62
+ * up a model in model-links.json (e.g. `{ model, term }` or `{ model, modelKey }`).
63
+ * Returns an empty array when the model is unknown or has no formulas.
64
+ */
65
+ export declare const getFormulas: (model: Partial<IModel>) => IFormula[];
66
+ /**
67
+ * The result of rendering a formula against a set of values, as KaTeX source
68
+ * strings (without the `$$` delimiters). Feed these to `katex.render`.
69
+ */
70
+ export interface IRenderedFormula {
71
+ /**
72
+ * The formula as authored, e.g. `E_{CH4} = (GE \times Y_m) / 55.65`.
73
+ */
74
+ symbolic: string;
75
+ /**
76
+ * Symbols replaced by their values, e.g. `59.24 = (52340 \times 0.063) / 55.65`.
77
+ * A symbol with no value is left as the symbol.
78
+ */
79
+ substituted: string;
80
+ /**
81
+ * Symbols kept, each wrapped in `\htmlData{key=..., value=...}{symbol}` so the
82
+ * rendered DOM node carries `data-key` / `data-value` for tooltips or toggling.
83
+ * Requires `trust: (ctx) => ctx.command === '\\htmlData'` in the KaTeX options.
84
+ */
85
+ annotated: string;
86
+ }
87
+ /**
88
+ * Render a formula against a set of values, producing the KaTeX strings needed
89
+ * to display it symbolically, with substituted values, or annotated for
90
+ * interaction. `values` maps each binding `key` to its value for one execution
91
+ * (typically pulled from the model's jlog entry, plus `value` for the result);
92
+ * the caller decides where those values come from, so this stays agnostic.
93
+ * Symbols without a value in `values` are left symbolic.
94
+ *
95
+ * A `\sum` whose summand symbols are bound to table columns (`key`+`column`) is
96
+ * expanded once per row, provided `values[key]` holds the packed table string
97
+ * (or an array of row objects). Without the table it stays symbolic.
98
+ */
99
+ export declare const renderFormula: (formula: IFormula, values: Record<string, unknown>) => IRenderedFormula;
@@ -0,0 +1,156 @@
1
+ import * as data from '../formulas/index.json';
2
+ import { findMatchingModel } from './utils';
3
+ // keyed by the same `docPath` found on model-links.json entries
4
+ const formulas = data;
5
+ /**
6
+ * Get the formulas (if any) for a model-links entry. A documentation file may
7
+ * define several formulas, so this always returns an array (empty when none).
8
+ */
9
+ export const getFormulasForLink = (link) => (link && formulas[link.docPath]) || [];
10
+ /**
11
+ * Get the formulas for a model, addressed by the same parameters used to look
12
+ * up a model in model-links.json (e.g. `{ model, term }` or `{ model, modelKey }`).
13
+ * Returns an empty array when the model is unknown or has no formulas.
14
+ */
15
+ export const getFormulas = (model) => {
16
+ const link = findMatchingModel(model);
17
+ return link ? getFormulasForLink(link) : [];
18
+ };
19
+ const isAlpha = (char) => /[a-zA-Z]/.test(char || '');
20
+ // Split a KaTeX string on the declared symbols only (longest-first), so
21
+ // `\times`, subscripts and constants are never touched.
22
+ const splitOnSymbols = (formula, symbols) => {
23
+ const ordered = [...symbols].sort((a, b) => b.length - a.length);
24
+ const segments = [];
25
+ let i = 0;
26
+ while (i < formula.length) {
27
+ const symbol = ordered.find((s) => formula.startsWith(s, i) &&
28
+ !isAlpha(formula[i - 1]) &&
29
+ !isAlpha(formula[i + s.length]) &&
30
+ formula[i - 1] !== '\\');
31
+ if (symbol) {
32
+ segments.push({ symbol });
33
+ i += symbol.length;
34
+ }
35
+ else {
36
+ let j = i + 1;
37
+ while (j < formula.length && !ordered.some((s) => formula.startsWith(s, j)))
38
+ j++;
39
+ segments.push({ text: formula.slice(i, j) });
40
+ i = j;
41
+ }
42
+ }
43
+ return segments;
44
+ };
45
+ const round4 = (n) => String(+n.toPrecision(4));
46
+ // Round numeric-looking strings, leave everything else (e.g. "kg FPCM") as-is.
47
+ const formatNumericString = (value) => {
48
+ const asNumber = Number(value);
49
+ return value !== '' && Number.isFinite(asNumber) ? round4(asNumber) : value;
50
+ };
51
+ const formatValue = (value) => {
52
+ if (value === null || value === undefined)
53
+ return null;
54
+ if (typeof value === 'number')
55
+ return round4(value);
56
+ return formatNumericString(String(value));
57
+ };
58
+ // `\sum` with an optional index subscript (`_i`, `_{s=1}`, ...) and upper bound
59
+ // superscript (`^S`, `^{S}`), and trailing space.
60
+ const SUM_RE = /\\sum(?:_\{[^}]*\}|_[^\s{])?(?:\^\{[^}]*\}|\^[^\s{])?\s*/;
61
+ // Parse a packed `log_as_table` string into rows: rows split on `;`, columns on
62
+ // `_`, each column a `key:value` pair (values never contain `_` or `:`).
63
+ const parseTable = (packed) => packed
64
+ .split(';')
65
+ .filter(Boolean)
66
+ .map((row) => row.split('_').reduce((cols, pair) => {
67
+ const at = pair.indexOf(':');
68
+ if (at !== -1)
69
+ cols[pair.slice(0, at)] = pair.slice(at + 1);
70
+ return cols;
71
+ }, {}));
72
+ /**
73
+ * Render a formula against a set of values, producing the KaTeX strings needed
74
+ * to display it symbolically, with substituted values, or annotated for
75
+ * interaction. `values` maps each binding `key` to its value for one execution
76
+ * (typically pulled from the model's jlog entry, plus `value` for the result);
77
+ * the caller decides where those values come from, so this stays agnostic.
78
+ * Symbols without a value in `values` are left symbolic.
79
+ *
80
+ * A `\sum` whose summand symbols are bound to table columns (`key`+`column`) is
81
+ * expanded once per row, provided `values[key]` holds the packed table string
82
+ * (or an array of row objects). Without the table it stays symbolic.
83
+ */
84
+ export const renderFormula = (formula, values) => {
85
+ // sum: a per-row column under a \sum. point: a single value — a scalar, a row
86
+ // selection (`match` + `column`), or a fixed constant.
87
+ const sumBindings = formula.bindings.filter((b) => b.key && b.column && !b.match);
88
+ const pointBindings = formula.bindings.filter((b) => (b.key && !(b.column && !b.match)) || b.constant);
89
+ const toRows = (source) => Array.isArray(source)
90
+ ? source
91
+ : typeof source === 'string'
92
+ ? parseTable(source)
93
+ : [];
94
+ const sumMatch = sumBindings.length ? SUM_RE.exec(formula.formula) : null;
95
+ // The summand may draw from several parallel tables (e.g. one for values, one
96
+ // for factors); parse each once and align them by row index.
97
+ const tableRows = {};
98
+ if (sumMatch) {
99
+ sumBindings.forEach((b) => {
100
+ const key = b.key;
101
+ if (!(key in tableRows))
102
+ tableRows[key] = toRows(values[key]);
103
+ });
104
+ }
105
+ const rowCount = sumMatch
106
+ ? Math.max(0, ...sumBindings.map((b) => tableRows[b.key].length))
107
+ : 0;
108
+ // Resolve a point binding to a single value: a fixed constant, a scalar lookup,
109
+ // or the selected row's column for a row selection (`match` = id prefix).
110
+ const pointValue = (b) => {
111
+ if (b.constant !== undefined)
112
+ return b.constant || undefined;
113
+ if (!b.match)
114
+ return values[b.key];
115
+ const row = toRows(values[b.key]).find((r) => { var _a; return String((_a = Object.values(r)[0]) !== null && _a !== void 0 ? _a : '').startsWith(b.match); });
116
+ return row ? row[b.column] : undefined;
117
+ };
118
+ // How a binding is referenced in the `data-key` attribute.
119
+ const ref = (b) => b.constant !== undefined
120
+ ? 'const'
121
+ : `${b.key}${b.match ? '@' + b.match : ''}${b.column ? ':' + b.column : ''}`;
122
+ // Expand the `\sum` inline, wrapping each row value with `wrapRow`. Returns the
123
+ // original formula when there is no table to expand over.
124
+ const expand = (wrapRow) => {
125
+ if (!sumMatch || !rowCount)
126
+ return formula.formula;
127
+ const before = formula.formula.slice(0, sumMatch.index);
128
+ const summand = formula.formula.slice(sumMatch.index + sumMatch[0].length);
129
+ const symbols = sumBindings.map((b) => b.symbol);
130
+ const terms = Array.from({ length: rowCount }, (_, i) => '(' +
131
+ splitOnSymbols(summand, symbols)
132
+ .map((seg) => {
133
+ if (seg.text !== undefined)
134
+ return seg.text;
135
+ const b = sumBindings.find((x) => x.symbol === seg.symbol);
136
+ const row = tableRows[b.key][i];
137
+ const value = row ? formatValue(row[b.column]) : null;
138
+ return value === null ? seg.symbol : wrapRow(b, value);
139
+ })
140
+ .join('') +
141
+ ')');
142
+ return before + terms.join(' + ');
143
+ };
144
+ // Substitute the point (single-value) symbols over a working string.
145
+ const substitutePoints = (working, wrap) => splitOnSymbols(working, pointBindings.map((b) => b.symbol))
146
+ .map((seg) => {
147
+ if (seg.text !== undefined)
148
+ return seg.text;
149
+ const b = pointBindings.find((x) => x.symbol === seg.symbol);
150
+ return wrap(b, seg.symbol, formatValue(pointValue(b)));
151
+ })
152
+ .join('');
153
+ const substituted = substitutePoints(expand((b, v) => `\\htmlData{key=${ref(b)}}{${v}}`), (b, symbol, value) => value === null ? symbol : `\\htmlData{key=${ref(b)}}{${value}}`);
154
+ const annotated = substitutePoints(expand((b, v) => `\\htmlData{key=${ref(b)}, value=${v}}{${v}}`), (b, symbol, value) => `\\htmlData{key=${ref(b)}, value=${value === null ? 'na' : value}}{${symbol}}`);
155
+ return { symbolic: formula.formula, substituted, annotated };
156
+ };
package/esm/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './config';
2
+ export * from './formulas';
2
3
  export * from './models';
3
4
  export * from './utils';
4
5
  export * from './validate-config';
package/esm/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './config';
2
+ export * from './formulas';
2
3
  export * from './models';
3
4
  export * from './utils';
4
5
  export * from './validate-config';
package/esm/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const ENGINE_VERSION = "0.81.6";
1
+ export declare const ENGINE_VERSION = "0.82.1";
package/esm/version.js CHANGED
@@ -1 +1 @@
1
- export const ENGINE_VERSION = '0.81.6';
1
+ export const ENGINE_VERSION = '0.82.1';