@hestia-earth/engine-models 0.81.5 → 0.82.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.
Files changed (77) hide show
  1. package/README.md +19 -0
  2. package/cjs/formulas.d.ts +86 -0
  3. package/cjs/formulas.js +180 -0
  4. package/cjs/index.d.ts +1 -0
  5. package/cjs/index.js +1 -0
  6. package/cjs/version.d.ts +1 -1
  7. package/cjs/version.js +1 -1
  8. package/config/Cycle.json +2 -2
  9. package/config/ImpactAssessment.json +12 -0
  10. package/esm/formulas.d.ts +86 -0
  11. package/esm/formulas.js +130 -0
  12. package/esm/index.d.ts +1 -0
  13. package/esm/index.js +1 -0
  14. package/esm/version.d.ts +1 -1
  15. package/esm/version.js +1 -1
  16. package/formulas/README.md +190 -0
  17. package/formulas/deRuijterEtAl2010/nh3ToAirCropResidueDecomposition.json +30 -0
  18. package/formulas/emepEea2019/co2ToAirFuelCombustion.json +23 -0
  19. package/formulas/emepEea2019/nh3ToAirExcreta.json +27 -0
  20. package/formulas/emepEea2019/pm10ToAirAnimalHousing.json +28 -0
  21. package/formulas/emepEea2019/pm25ToAirAnimalHousing.json +28 -0
  22. package/formulas/emepEea2019/tspToAirAnimalHousing.json +28 -0
  23. package/formulas/hestia/feedConversionRatio/feedConversionRatioCarbon.json +23 -0
  24. package/formulas/index.json +2464 -0
  25. package/formulas/ipcc2019/belowGroundCropResidue.json +31 -0
  26. package/formulas/ipcc2019/ch4ToAirAquacultureSystems.json +29 -0
  27. package/formulas/ipcc2019/ch4ToAirEntericFermentation.json +29 -0
  28. package/formulas/ipcc2019/ch4ToAirExcreta.json +36 -0
  29. package/formulas/ipcc2019/ch4ToAirFloodedRice.json +31 -0
  30. package/formulas/ipcc2019/ch4ToAirOrganicSoilCultivation.json +40 -0
  31. package/formulas/ipcc2019/co2ToAirUreaHydrolysis.json +27 -0
  32. package/formulas/ipcc2019/emissionsToAirOrganicSoilBurning.json +49 -0
  33. package/formulas/ipcc2019/n2OToAirAquacultureSystemsIndirect.json +31 -0
  34. package/formulas/ipcc2019/n2OToAirCropResidueBurningDirect.json +25 -0
  35. package/formulas/ipcc2019/n2OToAirCropResidueDecompositionDirect.json +28 -0
  36. package/formulas/ipcc2019/n2OToAirCropResidueDecompositionIndirect.json +35 -0
  37. package/formulas/ipcc2019/n2OToAirExcretaDirect.json +25 -0
  38. package/formulas/ipcc2019/n2OToAirExcretaIndirect.json +39 -0
  39. package/formulas/ipcc2019/n2OToAirFuelCombustionIndirect.json +31 -0
  40. package/formulas/ipcc2019/n2OToAirInorganicFertiliserDirect.json +28 -0
  41. package/formulas/ipcc2019/n2OToAirInorganicFertiliserIndirect.json +39 -0
  42. package/formulas/ipcc2019/n2OToAirNaturalVegetationBurningIndirect.json +31 -0
  43. package/formulas/ipcc2019/n2OToAirOrganicFertiliserDirect.json +28 -0
  44. package/formulas/ipcc2019/n2OToAirOrganicFertiliserIndirect.json +39 -0
  45. package/formulas/ipcc2019/n2OToAirOrganicSoilBurningIndirect.json +31 -0
  46. package/formulas/ipcc2019/n2OToAirOrganicSoilCultivationIndirect.json +31 -0
  47. package/formulas/ipcc2019/nh3ToAirInorganicFertiliser.json +31 -0
  48. package/formulas/ipcc2019/nh3ToAirOrganicFertiliser.json +31 -0
  49. package/formulas/ipcc2019/no3ToGroundwaterCropResidueDecomposition.json +28 -0
  50. package/formulas/ipcc2019/no3ToGroundwaterExcreta.json +28 -0
  51. package/formulas/ipcc2019/no3ToGroundwaterInorganicFertiliser.json +28 -0
  52. package/formulas/ipcc2019/no3ToGroundwaterOrganicFertiliser.json +28 -0
  53. package/formulas/ipcc2019/nonCo2EmissionsToAirNaturalVegetationBurning.json +13 -0
  54. package/formulas/ipcc2019/noxToAirInorganicFertiliser.json +31 -0
  55. package/formulas/ipcc2019/noxToAirOrganicFertiliser.json +31 -0
  56. package/formulas/ipcc2019/pastureGrass.json +556 -0
  57. package/formulas/pooreNemecek2018/ch4ToAirAquacultureSystems.json +88 -0
  58. package/formulas/pooreNemecek2018/n2OToAirAquacultureSystemsDirect.json +32 -0
  59. package/formulas/pooreNemecek2018/n2ToAirAquacultureSystems.json +32 -0
  60. package/formulas/pooreNemecek2018/nh3ToAirAquacultureSystems.json +40 -0
  61. package/formulas/pooreNemecek2018/no3ToGroundwaterCropResidueDecomposition.json +68 -0
  62. package/formulas/pooreNemecek2018/no3ToGroundwaterExcreta.json +68 -0
  63. package/formulas/pooreNemecek2018/no3ToGroundwaterInorganicFertiliser.json +68 -0
  64. package/formulas/pooreNemecek2018/no3ToGroundwaterOrganicFertiliser.json +68 -0
  65. package/formulas/pooreNemecek2018/noxToAirAquacultureSystems.json +32 -0
  66. package/formulas/schererPfister2015/nErosionSoilFlux.json +34 -0
  67. package/formulas/schererPfister2015/pErosionSoilFlux.json +109 -0
  68. package/formulas/stehfestBouwman2006/n2OToAirCropResidueDecompositionDirect.json +37 -0
  69. package/formulas/stehfestBouwman2006/n2OToAirExcretaDirect.json +37 -0
  70. package/formulas/stehfestBouwman2006/n2OToAirInorganicFertiliserDirect.json +37 -0
  71. package/formulas/stehfestBouwman2006/n2OToAirOrganicFertiliserDirect.json +37 -0
  72. package/formulas/stehfestBouwman2006/noxToAirCropResidueDecomposition.json +36 -0
  73. package/formulas/stehfestBouwman2006/noxToAirExcreta.json +36 -0
  74. package/formulas/stehfestBouwman2006/noxToAirInorganicFertiliser.json +36 -0
  75. package/formulas/stehfestBouwman2006/noxToAirOrganicFertiliser.json +36 -0
  76. package/model-links.json +700 -9
  77. 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,86 @@
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
+ * For a per-row value inside a packed `log_as_table` string: the column to
23
+ * read from each row. When set, the symbol sits under a `\sum` and is expanded
24
+ * once per row (e.g. `\sum_i M_i \times EF_i` → `(32 × 0.166) + (13 × 0.495)`).
25
+ */
26
+ column?: string;
27
+ }
28
+ /**
29
+ * A KaTeX formula extracted from a model's documentation, with the bindings
30
+ * needed to substitute its symbols with values logged at runtime.
31
+ */
32
+ export interface IFormula {
33
+ /**
34
+ * The KaTeX source (without the `$$` delimiters).
35
+ */
36
+ formula: string;
37
+ /**
38
+ * The symbols in the formula, in "Where:" list order.
39
+ */
40
+ bindings: IFormulaBinding[];
41
+ }
42
+ /**
43
+ * Get the formulas (if any) for a model-links entry. A documentation file may
44
+ * define several formulas, so this always returns an array (empty when none).
45
+ */
46
+ export declare const getFormulasForLink: (link: Pick<IModel, "docPath">) => IFormula[];
47
+ /**
48
+ * Get the formulas for a model, addressed by the same parameters used to look
49
+ * up a model in model-links.json (e.g. `{ model, term }` or `{ model, modelKey }`).
50
+ * Returns an empty array when the model is unknown or has no formulas.
51
+ */
52
+ export declare const getFormulas: (model: Partial<IModel>) => IFormula[];
53
+ /**
54
+ * The result of rendering a formula against a set of values, as KaTeX source
55
+ * strings (without the `$$` delimiters). Feed these to `katex.render`.
56
+ */
57
+ export interface IRenderedFormula {
58
+ /**
59
+ * The formula as authored, e.g. `E_{CH4} = (GE \times Y_m) / 55.65`.
60
+ */
61
+ symbolic: string;
62
+ /**
63
+ * Symbols replaced by their values, e.g. `59.24 = (52340 \times 0.063) / 55.65`.
64
+ * A symbol with no value is left as the symbol.
65
+ */
66
+ substituted: string;
67
+ /**
68
+ * Symbols kept, each wrapped in `\htmlData{key=..., value=...}{symbol}` so the
69
+ * rendered DOM node carries `data-key` / `data-value` for tooltips or toggling.
70
+ * Requires `trust: (ctx) => ctx.command === '\\htmlData'` in the KaTeX options.
71
+ */
72
+ annotated: string;
73
+ }
74
+ /**
75
+ * Render a formula against a set of values, producing the KaTeX strings needed
76
+ * to display it symbolically, with substituted values, or annotated for
77
+ * interaction. `values` maps each binding `key` to its value for one execution
78
+ * (typically pulled from the model's jlog entry, plus `value` for the result);
79
+ * the caller decides where those values come from, so this stays agnostic.
80
+ * Symbols without a value in `values` are left symbolic.
81
+ *
82
+ * A `\sum` whose summand symbols are bound to table columns (`key`+`column`) is
83
+ * expanded once per row, provided `values[key]` holds the packed table string
84
+ * (or an array of row objects). Without the table it stays symbolic.
85
+ */
86
+ export declare const renderFormula: (formula: IFormula, values: Record<string, unknown>) => IRenderedFormula;
@@ -0,0 +1,180 @@
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`, `_{i}`, ...) and trailing space.
96
+ var SUM_RE = /\\sum(?:_\{[^}]*\}|_[^\s{])?\s*/;
97
+ // Parse a packed `log_as_table` string into rows: rows split on `;`, columns on
98
+ // `_`, each column a `key:value` pair (values never contain `_` or `:`).
99
+ var parseTable = function (packed) {
100
+ return packed
101
+ .split(';')
102
+ .filter(Boolean)
103
+ .map(function (row) {
104
+ return row.split('_').reduce(function (cols, pair) {
105
+ var at = pair.indexOf(':');
106
+ if (at !== -1)
107
+ cols[pair.slice(0, at)] = pair.slice(at + 1);
108
+ return cols;
109
+ }, {});
110
+ });
111
+ };
112
+ /**
113
+ * Render a formula against a set of values, producing the KaTeX strings needed
114
+ * to display it symbolically, with substituted values, or annotated for
115
+ * interaction. `values` maps each binding `key` to its value for one execution
116
+ * (typically pulled from the model's jlog entry, plus `value` for the result);
117
+ * the caller decides where those values come from, so this stays agnostic.
118
+ * Symbols without a value in `values` are left symbolic.
119
+ *
120
+ * A `\sum` whose summand symbols are bound to table columns (`key`+`column`) is
121
+ * expanded once per row, provided `values[key]` holds the packed table string
122
+ * (or an array of row objects). Without the table it stays symbolic.
123
+ */
124
+ var renderFormula = function (formula, values) {
125
+ var _a;
126
+ var columnBindings = formula.bindings.filter(function (b) { return b.key && b.column; });
127
+ var scalarBindings = formula.bindings.filter(function (b) { return b.key && !b.column; });
128
+ var sumMatch = columnBindings.length ? SUM_RE.exec(formula.formula) : null;
129
+ var tableKey = (_a = columnBindings[0]) === null || _a === void 0 ? void 0 : _a.key;
130
+ var raw = values[tableKey];
131
+ var rows = !sumMatch
132
+ ? null
133
+ : Array.isArray(raw)
134
+ ? raw
135
+ : typeof raw === 'string'
136
+ ? parseTable(raw)
137
+ : null;
138
+ // Expand the `\sum` inline, wrapping each row value with `wrapRow`. Returns the
139
+ // original formula when there is no table to expand over.
140
+ var expand = function (wrapRow) {
141
+ if (!sumMatch || !rows || !rows.length)
142
+ return formula.formula;
143
+ var before = formula.formula.slice(0, sumMatch.index);
144
+ var summand = formula.formula.slice(sumMatch.index + sumMatch[0].length);
145
+ var symbols = columnBindings.map(function (b) { return b.symbol; });
146
+ var terms = rows.map(function (row) {
147
+ return '(' +
148
+ splitOnSymbols(summand, symbols)
149
+ .map(function (seg) {
150
+ if (seg.text !== undefined)
151
+ return seg.text;
152
+ var b = columnBindings.find(function (x) { return x.symbol === seg.symbol; });
153
+ var value = formatValue(row[b.column]);
154
+ return value === null ? seg.symbol : wrapRow(b, value);
155
+ })
156
+ .join('') +
157
+ ')';
158
+ });
159
+ return before + terms.join(' + ');
160
+ };
161
+ // Substitute the scalar (non-column) symbols over a working string.
162
+ var substituteScalars = function (working, wrap) {
163
+ return splitOnSymbols(working, scalarBindings.map(function (b) { return b.symbol; }))
164
+ .map(function (seg) {
165
+ if (seg.text !== undefined)
166
+ return seg.text;
167
+ var b = scalarBindings.find(function (x) { return x.symbol === seg.symbol; });
168
+ return wrap(b, seg.symbol, formatValue(values[b.key]));
169
+ })
170
+ .join('');
171
+ };
172
+ var substituted = substituteScalars(expand(function (b, v) { return "\\htmlData{key=".concat(b.key, ":").concat(b.column, "}{").concat(v, "}"); }), function (b, symbol, value) {
173
+ return value === null ? symbol : "\\htmlData{key=".concat(b.key, "}{").concat(value, "}");
174
+ });
175
+ var annotated = substituteScalars(expand(function (b, v) { return "\\htmlData{key=".concat(b.key, ":").concat(b.column, ", value=").concat(v, "}{").concat(v, "}"); }), function (b, symbol, value) {
176
+ return "\\htmlData{key=".concat(b.key, ", value=").concat(value === null ? 'na' : value, "}{").concat(symbol, "}");
177
+ });
178
+ return { symbolic: formula.formula, substituted: substituted, annotated: annotated };
179
+ };
180
+ 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.5";
1
+ export declare const ENGINE_VERSION = "0.82.0";
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.5';
4
+ exports.ENGINE_VERSION = '0.82.0';
package/config/Cycle.json CHANGED
@@ -996,7 +996,7 @@
996
996
  },
997
997
  {
998
998
  "key": "emissions",
999
- "model": "bafu2025",
999
+ "model": "bafu2025AndEmberClimate",
1000
1000
  "value": "cycle",
1001
1001
  "runStrategy": "always",
1002
1002
  "mergeStrategy": "list",
@@ -1007,7 +1007,7 @@
1007
1007
  },
1008
1008
  {
1009
1009
  "key": "emissions",
1010
- "model": "ecoinventV3AndEmberClimate",
1010
+ "model": "bafu2025",
1011
1011
  "value": "cycle",
1012
1012
  "runStrategy": "always",
1013
1013
  "mergeStrategy": "list",
@@ -205,6 +205,18 @@
205
205
  },
206
206
  "stage": 1
207
207
  },
208
+ {
209
+ "key": "emissionsResourceUse",
210
+ "model": "bafu2025AndEmberClimate",
211
+ "value": "impact_assessment",
212
+ "runStrategy": "always",
213
+ "mergeStrategy": "list",
214
+ "mergeArgs": {
215
+ "replaceThreshold": ["value", 0.01],
216
+ "skipSameTerm": true
217
+ },
218
+ "stage": 1
219
+ },
208
220
  {
209
221
  "key": "emissionsResourceUse",
210
222
  "model": "bafu2025",
@@ -0,0 +1,86 @@
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
+ * For a per-row value inside a packed `log_as_table` string: the column to
23
+ * read from each row. When set, the symbol sits under a `\sum` and is expanded
24
+ * once per row (e.g. `\sum_i M_i \times EF_i` → `(32 × 0.166) + (13 × 0.495)`).
25
+ */
26
+ column?: string;
27
+ }
28
+ /**
29
+ * A KaTeX formula extracted from a model's documentation, with the bindings
30
+ * needed to substitute its symbols with values logged at runtime.
31
+ */
32
+ export interface IFormula {
33
+ /**
34
+ * The KaTeX source (without the `$$` delimiters).
35
+ */
36
+ formula: string;
37
+ /**
38
+ * The symbols in the formula, in "Where:" list order.
39
+ */
40
+ bindings: IFormulaBinding[];
41
+ }
42
+ /**
43
+ * Get the formulas (if any) for a model-links entry. A documentation file may
44
+ * define several formulas, so this always returns an array (empty when none).
45
+ */
46
+ export declare const getFormulasForLink: (link: Pick<IModel, "docPath">) => IFormula[];
47
+ /**
48
+ * Get the formulas for a model, addressed by the same parameters used to look
49
+ * up a model in model-links.json (e.g. `{ model, term }` or `{ model, modelKey }`).
50
+ * Returns an empty array when the model is unknown or has no formulas.
51
+ */
52
+ export declare const getFormulas: (model: Partial<IModel>) => IFormula[];
53
+ /**
54
+ * The result of rendering a formula against a set of values, as KaTeX source
55
+ * strings (without the `$$` delimiters). Feed these to `katex.render`.
56
+ */
57
+ export interface IRenderedFormula {
58
+ /**
59
+ * The formula as authored, e.g. `E_{CH4} = (GE \times Y_m) / 55.65`.
60
+ */
61
+ symbolic: string;
62
+ /**
63
+ * Symbols replaced by their values, e.g. `59.24 = (52340 \times 0.063) / 55.65`.
64
+ * A symbol with no value is left as the symbol.
65
+ */
66
+ substituted: string;
67
+ /**
68
+ * Symbols kept, each wrapped in `\htmlData{key=..., value=...}{symbol}` so the
69
+ * rendered DOM node carries `data-key` / `data-value` for tooltips or toggling.
70
+ * Requires `trust: (ctx) => ctx.command === '\\htmlData'` in the KaTeX options.
71
+ */
72
+ annotated: string;
73
+ }
74
+ /**
75
+ * Render a formula against a set of values, producing the KaTeX strings needed
76
+ * to display it symbolically, with substituted values, or annotated for
77
+ * interaction. `values` maps each binding `key` to its value for one execution
78
+ * (typically pulled from the model's jlog entry, plus `value` for the result);
79
+ * the caller decides where those values come from, so this stays agnostic.
80
+ * Symbols without a value in `values` are left symbolic.
81
+ *
82
+ * A `\sum` whose summand symbols are bound to table columns (`key`+`column`) is
83
+ * expanded once per row, provided `values[key]` holds the packed table string
84
+ * (or an array of row objects). Without the table it stays symbolic.
85
+ */
86
+ export declare const renderFormula: (formula: IFormula, values: Record<string, unknown>) => IRenderedFormula;
@@ -0,0 +1,130 @@
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`, `_{i}`, ...) and trailing space.
59
+ const SUM_RE = /\\sum(?:_\{[^}]*\}|_[^\s{])?\s*/;
60
+ // Parse a packed `log_as_table` string into rows: rows split on `;`, columns on
61
+ // `_`, each column a `key:value` pair (values never contain `_` or `:`).
62
+ const parseTable = (packed) => packed
63
+ .split(';')
64
+ .filter(Boolean)
65
+ .map((row) => row.split('_').reduce((cols, pair) => {
66
+ const at = pair.indexOf(':');
67
+ if (at !== -1)
68
+ cols[pair.slice(0, at)] = pair.slice(at + 1);
69
+ return cols;
70
+ }, {}));
71
+ /**
72
+ * Render a formula against a set of values, producing the KaTeX strings needed
73
+ * to display it symbolically, with substituted values, or annotated for
74
+ * interaction. `values` maps each binding `key` to its value for one execution
75
+ * (typically pulled from the model's jlog entry, plus `value` for the result);
76
+ * the caller decides where those values come from, so this stays agnostic.
77
+ * Symbols without a value in `values` are left symbolic.
78
+ *
79
+ * A `\sum` whose summand symbols are bound to table columns (`key`+`column`) is
80
+ * expanded once per row, provided `values[key]` holds the packed table string
81
+ * (or an array of row objects). Without the table it stays symbolic.
82
+ */
83
+ export const renderFormula = (formula, values) => {
84
+ var _a;
85
+ const columnBindings = formula.bindings.filter((b) => b.key && b.column);
86
+ const scalarBindings = formula.bindings.filter((b) => b.key && !b.column);
87
+ const sumMatch = columnBindings.length ? SUM_RE.exec(formula.formula) : null;
88
+ const tableKey = (_a = columnBindings[0]) === null || _a === void 0 ? void 0 : _a.key;
89
+ const raw = values[tableKey];
90
+ const rows = !sumMatch
91
+ ? null
92
+ : Array.isArray(raw)
93
+ ? raw
94
+ : typeof raw === 'string'
95
+ ? parseTable(raw)
96
+ : null;
97
+ // Expand the `\sum` inline, wrapping each row value with `wrapRow`. Returns the
98
+ // original formula when there is no table to expand over.
99
+ const expand = (wrapRow) => {
100
+ if (!sumMatch || !rows || !rows.length)
101
+ return formula.formula;
102
+ const before = formula.formula.slice(0, sumMatch.index);
103
+ const summand = formula.formula.slice(sumMatch.index + sumMatch[0].length);
104
+ const symbols = columnBindings.map((b) => b.symbol);
105
+ const terms = rows.map((row) => '(' +
106
+ splitOnSymbols(summand, symbols)
107
+ .map((seg) => {
108
+ if (seg.text !== undefined)
109
+ return seg.text;
110
+ const b = columnBindings.find((x) => x.symbol === seg.symbol);
111
+ const value = formatValue(row[b.column]);
112
+ return value === null ? seg.symbol : wrapRow(b, value);
113
+ })
114
+ .join('') +
115
+ ')');
116
+ return before + terms.join(' + ');
117
+ };
118
+ // Substitute the scalar (non-column) symbols over a working string.
119
+ const substituteScalars = (working, wrap) => splitOnSymbols(working, scalarBindings.map((b) => b.symbol))
120
+ .map((seg) => {
121
+ if (seg.text !== undefined)
122
+ return seg.text;
123
+ const b = scalarBindings.find((x) => x.symbol === seg.symbol);
124
+ return wrap(b, seg.symbol, formatValue(values[b.key]));
125
+ })
126
+ .join('');
127
+ const substituted = substituteScalars(expand((b, v) => `\\htmlData{key=${b.key}:${b.column}}{${v}}`), (b, symbol, value) => value === null ? symbol : `\\htmlData{key=${b.key}}{${value}}`);
128
+ const annotated = substituteScalars(expand((b, v) => `\\htmlData{key=${b.key}:${b.column}, value=${v}}{${v}}`), (b, symbol, value) => `\\htmlData{key=${b.key}, value=${value === null ? 'na' : value}}{${symbol}}`);
129
+ return { symbolic: formula.formula, substituted, annotated };
130
+ };
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.5";
1
+ export declare const ENGINE_VERSION = "0.82.0";
package/esm/version.js CHANGED
@@ -1 +1 @@
1
- export const ENGINE_VERSION = '0.81.5';
1
+ export const ENGINE_VERSION = '0.82.0';