@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
|
@@ -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.
|