@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 +19 -0
- package/cjs/formulas.d.ts +99 -0
- package/cjs/formulas.js +209 -0
- package/cjs/index.d.ts +1 -0
- package/cjs/index.js +1 -0
- package/cjs/version.d.ts +1 -1
- package/cjs/version.js +1 -1
- package/esm/formulas.d.ts +99 -0
- package/esm/formulas.js +156 -0
- package/esm/index.d.ts +1 -0
- package/esm/index.js +1 -0
- package/esm/version.d.ts +1 -1
- package/esm/version.js +1 -1
- package/formulas/README.md +213 -0
- package/formulas/index.json +5440 -0
- package/model-links.json +656 -5
- package/package.json +9 -8
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;
|
package/cjs/formulas.js
ADDED
|
@@ -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
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.
|
|
1
|
+
export declare const ENGINE_VERSION = "0.82.1";
|
package/cjs/version.js
CHANGED
|
@@ -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;
|
package/esm/formulas.js
ADDED
|
@@ -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
package/esm/index.js
CHANGED
package/esm/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const ENGINE_VERSION = "0.
|
|
1
|
+
export declare const ENGINE_VERSION = "0.82.1";
|
package/esm/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const ENGINE_VERSION = '0.
|
|
1
|
+
export const ENGINE_VERSION = '0.82.1';
|