@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.
@@ -0,0 +1,213 @@
1
+ # Model formulas
2
+
3
+ This folder contains the KaTeX formulas extracted from each model's
4
+ documentation, together with the **bindings** that tie every symbol in a formula
5
+ to a value logged at runtime (the "jlog"). With these you can render a model's
6
+ formula and *substitute* its symbols with the actual values from a given
7
+ execution — so a user can see exactly where a result came from.
8
+
9
+ `index.json` is generated by `scripts/generate-formulas.js` (run as part of
10
+ `npm run build:models`). Do not edit it by hand — edit the model's `.md`
11
+ documentation instead (see [Authoring bindings](#authoring-bindings)).
12
+
13
+ ## Contents
14
+
15
+ `index.json` holds every formula, keyed by the same `docPath` used in
16
+ `model-links.json`: `{ [docPath]: IFormula[] }`. It is the single file the
17
+ package imports.
18
+
19
+ ## Data shape
20
+
21
+ ```ts
22
+ interface IFormulaBinding {
23
+ symbol: string; // the KaTeX symbol as written, e.g. "E_{CH4}", "GE"
24
+ description?: string; // the "Where:" list description text
25
+ key?: string; // the jlog field this symbol resolves to (absent for constants)
26
+ column?: string; // a column of a packed table (per-row under a \sum, or with `match`)
27
+ match?: string; // row selection: the row whose id starts with this, read `column`
28
+ constant?: string; // a fixed constant (not logged), substituted as-is
29
+ }
30
+
31
+ interface IFormula {
32
+ formula: string; // KaTeX source, without the `$$` delimiters
33
+ bindings: IFormulaBinding[];
34
+ }
35
+ ```
36
+
37
+ `key` is special-cased: `"value"` is the **result** (the left-hand side of the
38
+ equation), and any other key is a field to read from the model's jlog entry. A
39
+ binding with no `key` is a display-only constant (e.g. `55.65`).
40
+
41
+ When a symbol sits under a `\sum` and its value comes from a per-input **table**
42
+ (a packed `log_as_table` string), the binding also carries a `column`: `key` is
43
+ the table's log key and `column` is the field to read from each row.
44
+
45
+ ## Getting the formulas for a model
46
+
47
+ The package exposes two helpers (re-exported from the package root):
48
+
49
+ ```ts
50
+ import { getFormulas, getFormulasForLink } from '@hestia-earth/engine-models';
51
+
52
+ // By the same parameters used to look up a model in model-links.json:
53
+ getFormulas({ model: 'ipcc2019', term: 'ch4ToAirEntericFermentation' });
54
+ // => [ { formula: 'E_{CH4} = (GE \\times Y_m) / 55.65', bindings: [...] } ]
55
+
56
+ getFormulas({ model: 'ipcc2019', modelKey: 'pastureGrass' });
57
+ // => [ ...20 formulas... ]
58
+
59
+ // Or, if you already have a model-links entry (an IModel):
60
+ import { findMatchingModel } from '@hestia-earth/engine-models';
61
+ const link = findMatchingModel({ model: 'ipcc2019', term: 'ch4ToAirEntericFermentation' });
62
+ getFormulasForLink(link);
63
+ ```
64
+
65
+ Both always return an **array** (a documentation page may define several
66
+ formulas), and an **empty array** when the model is unknown or has no formulas.
67
+
68
+ ## Rendering & substituting
69
+
70
+ `renderFormula(formula, values)` turns one `IFormula` plus a map of values into
71
+ three KaTeX strings (without the `$$` delimiters):
72
+
73
+ - **symbolic** — the formula as authored (`E_{CH4} = (GE \times Y_m) / 55.65`)
74
+ - **substituted** — symbols replaced by their values (`59.24 = (52340 \times 0.063) / 55.65`)
75
+ - **annotated** — symbols kept, each wrapped in `\htmlData{key=..., value=...}{symbol}`
76
+ so the DOM node carries `data-key` / `data-value` for a tooltip or toggle
77
+
78
+ It only substitutes *declared* symbols (longest-first), so `\times`, subscripts
79
+ and constants are never touched, and a symbol with no value is left symbolic.
80
+
81
+ ```ts
82
+ import { getFormulas, renderFormula } from '@hestia-earth/engine-models';
83
+
84
+ const [formula] = getFormulas({ model: 'ipcc2019', term: 'ch4ToAirEntericFermentation' });
85
+
86
+ // `values` maps each binding key to its value for ONE execution. You supply it,
87
+ // so you decide where each value comes from (see "Where the values come from").
88
+ const values = { value: 59.24, total_feed_in_MJ: 52340, enteric_factor: 0.063 };
89
+
90
+ const { symbolic, substituted, annotated } = renderFormula(formula, values);
91
+ ```
92
+
93
+ ### Where the values come from
94
+
95
+ `renderFormula` is agnostic — it just reads `values[key]` for each binding. The
96
+ values for one execution come from that model's **jlog** entry (the fields logged
97
+ via `debugValues` / `logRequirements`, keyed exactly as in the bindings). The
98
+ result symbol (`key: "value"`) is a special case: an emission's numeric value may
99
+ be logged, or read from the recalculated node it belongs to — supply whichever
100
+ your platform has. A minimal join over a jlog node's `logs[]`:
101
+
102
+ ```ts
103
+ const values = Object.assign({}, ...(jlogNode.logs || []));
104
+ // then set values.value from the node's result if it isn't logged:
105
+ values.value ??= recalculatedEmission?.value;
106
+ ```
107
+
108
+ ### Expanding a `\sum`
109
+
110
+ When a formula sums over inputs (`CO_2 = \sum_i M_{urea,i} \times EF_i`) and its
111
+ summand symbols are bound to table columns, `renderFormula` expands the sum once
112
+ per row — provided `values[<table key>]` holds the packed `log_as_table` string
113
+ (or an array of row objects). Each row's columns fill the summand:
114
+
115
+ ```ts
116
+ const [formula] = getFormulas({ model: 'ipcc2019', term: 'co2ToAirUreaHydrolysis' });
117
+ const { substituted } = renderFormula(formula, {
118
+ value: 24.4,
119
+ urea_values: 'id:urea_value:100_factor:0.2;id:ureaKgN_value:50_factor:0.088'
120
+ });
121
+ // 24.4 = (100 \times 0.2) + (50 \times 0.088)
122
+ ```
123
+
124
+ Without the table string the `\sum` is left symbolic.
125
+
126
+ ### Rendering with KaTeX
127
+
128
+ `\htmlData` is **disabled by default** — opt in with a narrowly-scoped `trust`
129
+ callback:
130
+
131
+ ```ts
132
+ import katex from 'katex';
133
+ import 'katex/dist/katex.min.css';
134
+
135
+ katex.render(substituted, element, {
136
+ displayMode: true,
137
+ trust: (ctx) => ctx.command === '\\htmlData',
138
+ strict: false,
139
+ });
140
+ ```
141
+
142
+ Because every substituted symbol is a real DOM node carrying `data-key` /
143
+ `data-value`, the symbol ↔ value toggle and hover tooltips are pure DOM — no
144
+ re-render required:
145
+
146
+ ```ts
147
+ element.querySelectorAll('[data-key]').forEach((span) => {
148
+ const el = span as HTMLElement;
149
+ el.title = `${el.dataset.key} = ${el.dataset.value}`;
150
+ });
151
+ ```
152
+
153
+ ## Authoring bindings
154
+
155
+ Bindings live in the model's `.md` documentation. Add a backticked `` `[logkey]` ``
156
+ token to each symbol in the "Where:" list that follows a formula:
157
+
158
+ ```markdown
159
+ $$E_{CH4} = (GE \times Y_m) / 55.65$$
160
+
161
+ Where:
162
+
163
+ - $E_{CH4}$ = Methane emissions (in kg $CH_4$) `[value]`
164
+ - $GE$ = Gross Energy intake (in MJ). `[total_feed_in_MJ]`
165
+ - $Y_m$ = Methane conversion factor. `[enteric_factor]`
166
+ - $55.65$ = The energy content of methane. `[const]`
167
+ ```
168
+
169
+ - Bind the result (left-hand side) to `` `[value]` ``.
170
+ - Bind every other symbol to the jlog field it corresponds to — the `key` passed
171
+ to `debugValues` / `logRequirements` in the model's `.py` (e.g. `enteric_factor`).
172
+ - **Every symbol in the `Where:` list must be annotated.** Tag display-only
173
+ constants (conversion factors, physical constants, e.g. `55.65`, `\frac{17}{14}`)
174
+ with `` `[const]` ``: they render as-is and are excluded from coverage. An
175
+ untagged, unbound symbol is treated as a *genuine missing binding*, not a
176
+ constant — this is what lets the coverage report tell the two apart.
177
+ - Tag a **named** constant with `` `[const:<value>]` `` (e.g. `ER` → `` `[const:2]` ``):
178
+ it substitutes to the literal value (rather than rendering as-is) without being
179
+ logged, and is excluded from coverage.
180
+
181
+ For a `\sum` over inputs, bind each summand symbol to a **table column** with
182
+ `` `[<table key>:<column>]` `` — the log key of the packed `log_as_table` value
183
+ and the per-row field:
184
+
185
+ ```markdown
186
+ $$CO_2 = \sum_i M_{urea,i} \times EF_i$$
187
+
188
+ Where:
189
+
190
+ - $CO_2$ = carbon dioxide emission from urea hydrolysis (kg) `[value]`
191
+ - $M_{urea,i}$ = mass of each urea fertiliser applied (kg) `[urea_values:value]`
192
+ - $EF_i$ = product-specific CO<sub>2</sub> emission factor `[urea_values:factor]`
193
+ ```
194
+
195
+ When a symbol is a single value living in a specific row of a packed table (not a
196
+ `\sum`), select the row with `` `[<table key>@<id prefix>:<column>]` `` — the row
197
+ whose id (first column) starts with `<id prefix>`, reading `<column>`:
198
+
199
+ ```markdown
200
+ $$N_2O_{indirect} = [(NH_3\text{-}N + NO_x\text{-}N) \times EF_4] \times \frac{44}{28}$$
201
+
202
+ Where:
203
+
204
+ - $N_2O_{indirect}$ = indirect N<sub>2</sub>O emission `[value]`
205
+ - $NH_3\text{-}N$ = nitrogen volatilised as ammonia (kg N) `[values@nh3:emission-value]`
206
+ - $NO_x\text{-}N$ = nitrogen volatilised as nitrogen oxides (kg N) `[values@nox:emission-value]`
207
+ - $EF_4$ = emission factor for deposition of volatilised N `[values@nh3:ef4-factor]`
208
+ ```
209
+
210
+ `scripts/validate-documentation.js` validates these on every build: it errors if a
211
+ bound symbol is not in the formula or a symbol is bound twice. It warns (without
212
+ failing) when there is no `` `[value]` `` result, several results, or a key is not
213
+ found in the sibling `.py` — a hint that the value is logged via a shared util.