nomogram 0.1.0__py3-none-any.whl
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.
- nomogram/__init__.py +17 -0
- nomogram/catalog.py +533 -0
- nomogram/comparison.py +305 -0
- nomogram/data/NOTICE.md +25 -0
- nomogram/data/catalog/humidity.toml +534 -0
- nomogram/data/semantics.json +464 -0
- nomogram/expression.py +232 -0
- nomogram/model.py +594 -0
- nomogram/plot.py +176 -0
- nomogram/py.typed +0 -0
- nomogram/semantics.py +333 -0
- nomogram/units.py +118 -0
- nomogram-0.1.0.dist-info/METADATA +125 -0
- nomogram-0.1.0.dist-info/RECORD +17 -0
- nomogram-0.1.0.dist-info/WHEEL +4 -0
- nomogram-0.1.0.dist-info/licenses/LICENSE +28 -0
- nomogram-0.1.0.dist-info/licenses/src/nomogram/data/NOTICE.md +25 -0
nomogram/model.py
ADDED
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Miki
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
|
|
4
|
+
"""Formula model: formulations, their variables, coefficients and sources.
|
|
5
|
+
|
|
6
|
+
A *formulation* is one author's way of computing a quantity kind (the
|
|
7
|
+
Magnus form with Sonntag's 1990 coefficients, for instance). Its
|
|
8
|
+
mathematical content is an expression written once (see
|
|
9
|
+
:mod:`nomogram.expression`); everything around it is traced: variables
|
|
10
|
+
with their quantity kind and unit, empirical coefficients, physical
|
|
11
|
+
constants, objective validity domains, accuracy and references.
|
|
12
|
+
|
|
13
|
+
Two kinds of formulas are told apart by ``fixed_units``:
|
|
14
|
+
|
|
15
|
+
- *fixed units*: the coefficients only hold in the declared units (the
|
|
16
|
+
Magnus form takes t in degC and gives hPa). Coefficients are never
|
|
17
|
+
converted; inputs are converted to the declared unit **and scale**
|
|
18
|
+
before evaluation (a Fahrenheit point becomes a Celsius point), which
|
|
19
|
+
makes the formula usable from any unit;
|
|
20
|
+
- otherwise, a quantity equation whose dimensional homogeneity is checked
|
|
21
|
+
when the catalog is loaded.
|
|
22
|
+
|
|
23
|
+
Validity domains are objective only: physical, or stated by the author or
|
|
24
|
+
the cited publication, each with its criterion and reference. What to do
|
|
25
|
+
outside them is a parameter of :meth:`Formulation.evaluate`; by default
|
|
26
|
+
the result is NaN there. See ``docs/rosetta.md`` for the vocabulary.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import warnings
|
|
32
|
+
from collections.abc import Callable, Mapping
|
|
33
|
+
from dataclasses import dataclass
|
|
34
|
+
from enum import Enum
|
|
35
|
+
from functools import cached_property
|
|
36
|
+
from typing import Any, Literal
|
|
37
|
+
|
|
38
|
+
import numpy as np
|
|
39
|
+
|
|
40
|
+
from nomogram.expression import compile_numeric, parse_expression, to_latex
|
|
41
|
+
from nomogram.semantics import ExternalMatch, semantic_catalog
|
|
42
|
+
from nomogram.units import Q_, convert
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"Coefficient",
|
|
46
|
+
"DomainBasis",
|
|
47
|
+
"Formulation",
|
|
48
|
+
"Notation",
|
|
49
|
+
"OutOfDomainError",
|
|
50
|
+
"PhysicalConstant",
|
|
51
|
+
"Reference",
|
|
52
|
+
"ReferenceStatus",
|
|
53
|
+
"ScaleBasis",
|
|
54
|
+
"TemperatureScale",
|
|
55
|
+
"TradeMeaning",
|
|
56
|
+
"TradeTerm",
|
|
57
|
+
"ValidityDomain",
|
|
58
|
+
"ValidityWarning",
|
|
59
|
+
"Variable",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
#: What :meth:`Formulation.evaluate` does with values outside the domains.
|
|
63
|
+
Outside = Literal["nan", "warn", "raise", "ignore"]
|
|
64
|
+
#: Which validity domains :meth:`Formulation.evaluate` applies.
|
|
65
|
+
Basis = Literal["stated", "physical", "all"]
|
|
66
|
+
|
|
67
|
+
#: Whether :meth:`Formulation.evaluate` converts temperatures between scales.
|
|
68
|
+
ScaleConversion = Literal["convert", "ignore"]
|
|
69
|
+
|
|
70
|
+
#: Relative tolerance at domain bounds, for rounding in unit conversions.
|
|
71
|
+
BOUND_TOLERANCE = 1e-9
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ReferenceStatus(Enum):
|
|
75
|
+
"""Whether a reference has been checked against its source."""
|
|
76
|
+
|
|
77
|
+
VERIFIED = "verified"
|
|
78
|
+
TO_CONFIRM = "to-confirm"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class DomainBasis(Enum):
|
|
82
|
+
"""Ground of a validity domain."""
|
|
83
|
+
|
|
84
|
+
#: Where the underlying physics holds.
|
|
85
|
+
PHYSICAL = "physical"
|
|
86
|
+
#: Range stated by the author or the cited publication.
|
|
87
|
+
STATED = "stated"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ValidityWarning(UserWarning):
|
|
91
|
+
"""A formulation was evaluated outside its validity domain."""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class OutOfDomainError(ValueError):
|
|
95
|
+
"""A formulation was evaluated outside its validity domain."""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class Reference:
|
|
100
|
+
"""A bibliographic source.
|
|
101
|
+
|
|
102
|
+
Attributes
|
|
103
|
+
----------
|
|
104
|
+
key : str
|
|
105
|
+
Catalog key, for example ``"sonntag1990"``.
|
|
106
|
+
authors : str
|
|
107
|
+
Authors, as cited.
|
|
108
|
+
year : int or None
|
|
109
|
+
Year of publication.
|
|
110
|
+
title : str
|
|
111
|
+
Title.
|
|
112
|
+
container : str or None
|
|
113
|
+
Journal, book or series.
|
|
114
|
+
doi : str or None
|
|
115
|
+
DOI, which takes precedence over any other identifier.
|
|
116
|
+
url : str or None
|
|
117
|
+
Address, when there is no DOI.
|
|
118
|
+
status : ReferenceStatus
|
|
119
|
+
Whether the details were checked against the source.
|
|
120
|
+
note : str or None
|
|
121
|
+
What remains to confirm, or any remark.
|
|
122
|
+
matches : tuple of ExternalMatch
|
|
123
|
+
Links to external vocabularies (a Wikidata item...).
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
key: str
|
|
127
|
+
authors: str
|
|
128
|
+
year: int | None
|
|
129
|
+
title: str
|
|
130
|
+
container: str | None
|
|
131
|
+
doi: str | None
|
|
132
|
+
url: str | None
|
|
133
|
+
status: ReferenceStatus
|
|
134
|
+
note: str | None
|
|
135
|
+
matches: tuple[ExternalMatch, ...]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class ScaleBasis(Enum):
|
|
139
|
+
"""How the temperature scale of a formulation is known."""
|
|
140
|
+
|
|
141
|
+
#: Stated by the source.
|
|
142
|
+
STATED = "stated"
|
|
143
|
+
#: Inferred from the publication date only: the scale in force then.
|
|
144
|
+
#: Weak, since new scales took years to be applied (ITS-90 included).
|
|
145
|
+
INFERRED_FROM_DATE = "inferred-from-date"
|
|
146
|
+
#: Inferred from numerical evidence, for instance the agreement with a
|
|
147
|
+
#: formulation on a known scale; the evidence goes in the note.
|
|
148
|
+
INFERRED_FROM_NUMBERS = "inferred-from-numbers"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass(frozen=True)
|
|
152
|
+
class TemperatureScale:
|
|
153
|
+
"""An international temperature scale, and how it differs from ITS-90.
|
|
154
|
+
|
|
155
|
+
Formulations were fitted to temperatures on the scale in force at the
|
|
156
|
+
time (IPTS-68, ITS-48...). Inputs are taken on ITS-90, today's scale,
|
|
157
|
+
and converted to the scale of the formulation before evaluation.
|
|
158
|
+
|
|
159
|
+
Attributes
|
|
160
|
+
----------
|
|
161
|
+
key : str
|
|
162
|
+
Catalog key, for example ``"ipts68"``.
|
|
163
|
+
label : str
|
|
164
|
+
Name of the scale.
|
|
165
|
+
t90 : tuple of float
|
|
166
|
+
Celsius temperatures on ITS-90 where the difference is tabulated;
|
|
167
|
+
empty for ITS-90 itself.
|
|
168
|
+
difference : tuple of float
|
|
169
|
+
``t90 - t_scale`` at those temperatures, in kelvin.
|
|
170
|
+
reference : str or None
|
|
171
|
+
Key of the source of the table.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
key: str
|
|
175
|
+
label: str
|
|
176
|
+
t90: tuple[float, ...]
|
|
177
|
+
difference: tuple[float, ...]
|
|
178
|
+
reference: str | None
|
|
179
|
+
|
|
180
|
+
def from_its90(self, celsius: Any) -> tuple[Any, Any]:
|
|
181
|
+
"""Celsius temperatures on this scale, from ITS-90 ones.
|
|
182
|
+
|
|
183
|
+
Parameters
|
|
184
|
+
----------
|
|
185
|
+
celsius : array_like
|
|
186
|
+
Celsius temperatures on ITS-90.
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
(numpy.ndarray, numpy.ndarray of bool)
|
|
191
|
+
The temperatures on this scale (linear interpolation of the
|
|
192
|
+
table), and where they fall outside the table (NaN there).
|
|
193
|
+
"""
|
|
194
|
+
values = np.asarray(celsius, dtype=float)
|
|
195
|
+
if not self.t90:
|
|
196
|
+
return values, np.zeros(values.shape, dtype=bool)
|
|
197
|
+
outside = (values < self.t90[0]) | (values > self.t90[-1])
|
|
198
|
+
shift = np.interp(values, self.t90, self.difference)
|
|
199
|
+
return np.where(outside, np.nan, values - shift), outside
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass(frozen=True)
|
|
203
|
+
class Variable:
|
|
204
|
+
"""A variable of a formulation: identifier, quantity kind and unit.
|
|
205
|
+
|
|
206
|
+
A variable has no external link of its own: it inherits the links of
|
|
207
|
+
its quantity kind.
|
|
208
|
+
|
|
209
|
+
Attributes
|
|
210
|
+
----------
|
|
211
|
+
identifier : str
|
|
212
|
+
Semantic identifier used in the expression, for example ``"t"``.
|
|
213
|
+
kind : str
|
|
214
|
+
Name of its quantity kind.
|
|
215
|
+
unit : str
|
|
216
|
+
QUDT name of the unit the formulation works in.
|
|
217
|
+
description : str
|
|
218
|
+
What the variable is, in a few words.
|
|
219
|
+
temperature_scale : TemperatureScale or None
|
|
220
|
+
For a temperature, the scale the formulation was fitted on;
|
|
221
|
+
``None`` when unknown or not applicable.
|
|
222
|
+
scale_basis : ScaleBasis or None
|
|
223
|
+
How that scale is known.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
identifier: str
|
|
227
|
+
kind: str
|
|
228
|
+
unit: str
|
|
229
|
+
description: str
|
|
230
|
+
temperature_scale: TemperatureScale | None = None
|
|
231
|
+
scale_basis: ScaleBasis | None = None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@dataclass(frozen=True)
|
|
235
|
+
class Coefficient:
|
|
236
|
+
"""An empirical coefficient of a formulation (the A, B, C of Magnus).
|
|
237
|
+
|
|
238
|
+
Attributes
|
|
239
|
+
----------
|
|
240
|
+
identifier : str
|
|
241
|
+
Identifier used in the expression.
|
|
242
|
+
value : float
|
|
243
|
+
Value, as published.
|
|
244
|
+
unit : str or None
|
|
245
|
+
QUDT name of its unit, for documentation; in a fixed-units
|
|
246
|
+
formulation coefficients are numbers tied to the declared units.
|
|
247
|
+
note : str or None
|
|
248
|
+
Remark.
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
identifier: str
|
|
252
|
+
value: float
|
|
253
|
+
unit: str | None
|
|
254
|
+
note: str | None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@dataclass(frozen=True)
|
|
258
|
+
class PhysicalConstant:
|
|
259
|
+
"""A constant with a metrological status, and its source.
|
|
260
|
+
|
|
261
|
+
Attributes
|
|
262
|
+
----------
|
|
263
|
+
key : str
|
|
264
|
+
Catalog key, for example ``"R_v_iapws95"``: several sources may
|
|
265
|
+
give different values for the same constant.
|
|
266
|
+
identifier : str
|
|
267
|
+
Identifier used in expressions, for example ``"R_v"``.
|
|
268
|
+
value : float
|
|
269
|
+
Value in ``unit``.
|
|
270
|
+
unit : str
|
|
271
|
+
QUDT name of the unit.
|
|
272
|
+
description : str
|
|
273
|
+
What the constant is.
|
|
274
|
+
reference : str
|
|
275
|
+
Key of the source.
|
|
276
|
+
matches : tuple of ExternalMatch
|
|
277
|
+
Links to external vocabularies.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
key: str
|
|
281
|
+
identifier: str
|
|
282
|
+
value: float
|
|
283
|
+
unit: str
|
|
284
|
+
description: str
|
|
285
|
+
reference: str
|
|
286
|
+
matches: tuple[ExternalMatch, ...]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@dataclass(frozen=True)
|
|
290
|
+
class ValidityDomain:
|
|
291
|
+
"""An objective validity domain of a formulation.
|
|
292
|
+
|
|
293
|
+
Attributes
|
|
294
|
+
----------
|
|
295
|
+
basis : DomainBasis
|
|
296
|
+
Physical, or stated by the author or the cited publication.
|
|
297
|
+
bounds : mapping of str to (float, float)
|
|
298
|
+
Closed interval per input identifier, in the input's unit.
|
|
299
|
+
criterion : str or None
|
|
300
|
+
What the domain guarantees, for example a maximum deviation from
|
|
301
|
+
a reference formulation.
|
|
302
|
+
reference : str or None
|
|
303
|
+
Key of the source of the domain.
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
basis: DomainBasis
|
|
307
|
+
bounds: Mapping[str, tuple[float, float]]
|
|
308
|
+
criterion: str | None
|
|
309
|
+
reference: str | None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@dataclass(frozen=True)
|
|
313
|
+
class Notation:
|
|
314
|
+
"""How a convention writes identifiers: identifier to LaTeX symbol.
|
|
315
|
+
|
|
316
|
+
Attributes
|
|
317
|
+
----------
|
|
318
|
+
key : str
|
|
319
|
+
Catalog key, for example ``"wmo"``.
|
|
320
|
+
label : str
|
|
321
|
+
Name of the convention.
|
|
322
|
+
symbols : mapping of str to str
|
|
323
|
+
LaTeX symbol per identifier; identifiers not listed keep their
|
|
324
|
+
default rendering.
|
|
325
|
+
reference : str or None
|
|
326
|
+
Key of the source of the convention.
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
key: str
|
|
330
|
+
label: str
|
|
331
|
+
symbols: Mapping[str, str]
|
|
332
|
+
reference: str | None
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@dataclass(frozen=True)
|
|
336
|
+
class TradeMeaning:
|
|
337
|
+
"""One meaning of an ambiguous trade term.
|
|
338
|
+
|
|
339
|
+
Attributes
|
|
340
|
+
----------
|
|
341
|
+
community : str
|
|
342
|
+
Who uses the term with this meaning.
|
|
343
|
+
kind : str
|
|
344
|
+
Name of the quantity kind it then denotes.
|
|
345
|
+
definition : str
|
|
346
|
+
The meaning, in plain words.
|
|
347
|
+
sources : tuple of str
|
|
348
|
+
Keys of the references documenting this use.
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
community: str
|
|
352
|
+
kind: str
|
|
353
|
+
definition: str
|
|
354
|
+
sources: tuple[str, ...]
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@dataclass(frozen=True)
|
|
358
|
+
class TradeTerm:
|
|
359
|
+
"""A term whose meaning depends on the trade: never a quantity kind itself.
|
|
360
|
+
|
|
361
|
+
Attributes
|
|
362
|
+
----------
|
|
363
|
+
key : str
|
|
364
|
+
Catalog key.
|
|
365
|
+
term : str
|
|
366
|
+
The term, for example ``"absolute humidity"``.
|
|
367
|
+
language : str
|
|
368
|
+
Language code, for example ``"en"``.
|
|
369
|
+
meanings : tuple of TradeMeaning
|
|
370
|
+
Its meanings, each pointing to an unambiguous quantity kind.
|
|
371
|
+
matches : tuple of ExternalMatch
|
|
372
|
+
Links to external vocabularies.
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
key: str
|
|
376
|
+
term: str
|
|
377
|
+
language: str
|
|
378
|
+
meanings: tuple[TradeMeaning, ...]
|
|
379
|
+
matches: tuple[ExternalMatch, ...]
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@dataclass(frozen=True)
|
|
383
|
+
class Formulation:
|
|
384
|
+
"""One author's way of computing a quantity kind.
|
|
385
|
+
|
|
386
|
+
Attributes
|
|
387
|
+
----------
|
|
388
|
+
key : str
|
|
389
|
+
Catalog key, for example ``"sonntag1990"``.
|
|
390
|
+
label : str
|
|
391
|
+
Name of the formulation.
|
|
392
|
+
output : Variable
|
|
393
|
+
The computed variable; its kind groups alternative formulations.
|
|
394
|
+
inputs : tuple of Variable
|
|
395
|
+
Input variables, in the order of the numeric function.
|
|
396
|
+
coefficients : tuple of Coefficient
|
|
397
|
+
Empirical coefficients.
|
|
398
|
+
constants : tuple of PhysicalConstant
|
|
399
|
+
Physical constants used.
|
|
400
|
+
expression : str
|
|
401
|
+
The expression, in restricted Python syntax.
|
|
402
|
+
fixed_units : bool
|
|
403
|
+
Whether the formulation only holds in its declared units.
|
|
404
|
+
domains : tuple of ValidityDomain
|
|
405
|
+
Objective validity domains.
|
|
406
|
+
accuracy : str or None
|
|
407
|
+
Accuracy claimed by the source, as stated.
|
|
408
|
+
references : tuple of str
|
|
409
|
+
Keys of the references.
|
|
410
|
+
note : str or None
|
|
411
|
+
Remark.
|
|
412
|
+
matches : tuple of ExternalMatch
|
|
413
|
+
Links to external vocabularies (Wikidata item of the formula...).
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
key: str
|
|
417
|
+
label: str
|
|
418
|
+
output: Variable
|
|
419
|
+
inputs: tuple[Variable, ...]
|
|
420
|
+
coefficients: tuple[Coefficient, ...]
|
|
421
|
+
constants: tuple[PhysicalConstant, ...]
|
|
422
|
+
expression: str
|
|
423
|
+
fixed_units: bool
|
|
424
|
+
domains: tuple[ValidityDomain, ...]
|
|
425
|
+
accuracy: str | None
|
|
426
|
+
references: tuple[str, ...]
|
|
427
|
+
note: str | None
|
|
428
|
+
matches: tuple[ExternalMatch, ...]
|
|
429
|
+
|
|
430
|
+
@property
|
|
431
|
+
def names(self) -> tuple[str, ...]:
|
|
432
|
+
"""Identifiers of the numeric function's arguments, in order."""
|
|
433
|
+
return (
|
|
434
|
+
tuple(v.identifier for v in self.inputs)
|
|
435
|
+
+ tuple(c.identifier for c in self.coefficients)
|
|
436
|
+
+ tuple(k.identifier for k in self.constants)
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
@cached_property
|
|
440
|
+
def sympy_expression(self) -> Any:
|
|
441
|
+
"""The expression as a SymPy expression."""
|
|
442
|
+
return parse_expression(self.expression, self.names)
|
|
443
|
+
|
|
444
|
+
@cached_property
|
|
445
|
+
def _function(self) -> Callable[..., Any]:
|
|
446
|
+
return compile_numeric(self.sympy_expression, self.names)
|
|
447
|
+
|
|
448
|
+
@cached_property
|
|
449
|
+
def output_factor(self) -> float:
|
|
450
|
+
"""Factor bringing the raw result into the output unit.
|
|
451
|
+
|
|
452
|
+
1 for a fixed-units formulation. For a quantity equation, the
|
|
453
|
+
expression is evaluated once with pint quantities, which checks
|
|
454
|
+
its dimensional homogeneity.
|
|
455
|
+
|
|
456
|
+
Raises
|
|
457
|
+
------
|
|
458
|
+
pint.errors.DimensionalityError
|
|
459
|
+
If the expression does not have the dimension of the output.
|
|
460
|
+
"""
|
|
461
|
+
if self.fixed_units:
|
|
462
|
+
return 1.0
|
|
463
|
+
units = semantic_catalog().units
|
|
464
|
+
ones: list[Any] = [Q_(1.0, units[v.unit].pint) for v in self.inputs]
|
|
465
|
+
ones += [1.0 for _ in self.coefficients]
|
|
466
|
+
ones += [Q_(1.0, units[k.unit].pint) for k in self.constants]
|
|
467
|
+
result = Q_(self._function(*ones))
|
|
468
|
+
return float(result.to(units[self.output.unit].pint).magnitude)
|
|
469
|
+
|
|
470
|
+
def latex(self, notation: Notation | None = None) -> str:
|
|
471
|
+
"""The formulation as a LaTeX equation, in a given notation."""
|
|
472
|
+
import sympy
|
|
473
|
+
|
|
474
|
+
symbols = dict(notation.symbols) if notation else {}
|
|
475
|
+
output = sympy.Symbol(self.output.identifier, real=True)
|
|
476
|
+
left = to_latex(output, symbols)
|
|
477
|
+
return f"{left} = {to_latex(self.sympy_expression, symbols)}"
|
|
478
|
+
|
|
479
|
+
def outside_mask(self, values: Mapping[str, Any], basis: Basis = "stated") -> Any:
|
|
480
|
+
"""Where input values fall outside the selected validity domains.
|
|
481
|
+
|
|
482
|
+
Parameters
|
|
483
|
+
----------
|
|
484
|
+
values : mapping of str to array_like
|
|
485
|
+
Input values, by identifier, in the inputs' units.
|
|
486
|
+
basis : {"stated", "physical", "all"}
|
|
487
|
+
Which domains apply.
|
|
488
|
+
|
|
489
|
+
Returns
|
|
490
|
+
-------
|
|
491
|
+
numpy.ndarray of bool
|
|
492
|
+
``True`` where a value lies outside a selected domain.
|
|
493
|
+
|
|
494
|
+
Notes
|
|
495
|
+
-----
|
|
496
|
+
Bounds are closed and tolerate a relative rounding of
|
|
497
|
+
:data:`BOUND_TOLERANCE`, so that a value converted between units
|
|
498
|
+
(0.01 degC is 273.15999999... K) is not rejected at a bound.
|
|
499
|
+
"""
|
|
500
|
+
arrays = {k: np.asarray(v, dtype=float) for k, v in values.items()}
|
|
501
|
+
shape = np.broadcast_shapes(*(a.shape for a in arrays.values()))
|
|
502
|
+
outside = np.zeros(shape, dtype=bool)
|
|
503
|
+
for domain in self.domains:
|
|
504
|
+
if basis != "all" and domain.basis.value != basis:
|
|
505
|
+
continue
|
|
506
|
+
for identifier, (low, high) in domain.bounds.items():
|
|
507
|
+
value = arrays[identifier]
|
|
508
|
+
margin = BOUND_TOLERANCE * max(abs(low), abs(high), 1.0)
|
|
509
|
+
outside |= (value < low - margin) | (value > high + margin)
|
|
510
|
+
return outside
|
|
511
|
+
|
|
512
|
+
def evaluate(
|
|
513
|
+
self,
|
|
514
|
+
*,
|
|
515
|
+
outside: Outside = "nan",
|
|
516
|
+
basis: Basis = "stated",
|
|
517
|
+
scale: ScaleConversion = "convert",
|
|
518
|
+
**inputs: Any,
|
|
519
|
+
) -> Any:
|
|
520
|
+
"""Evaluate the formulation on pint quantities.
|
|
521
|
+
|
|
522
|
+
Inputs are converted to the unit and scale of each input variable,
|
|
523
|
+
then the numeric function runs, vectorised with numpy.
|
|
524
|
+
|
|
525
|
+
Parameters
|
|
526
|
+
----------
|
|
527
|
+
outside : {"nan", "warn", "raise", "ignore"}
|
|
528
|
+
Behaviour outside the validity domains: NaN there (default),
|
|
529
|
+
compute and warn, raise, or compute silently (comparisons).
|
|
530
|
+
basis : {"stated", "physical", "all"}
|
|
531
|
+
Which domains apply (default: those stated by the sources).
|
|
532
|
+
scale : {"convert", "ignore"}
|
|
533
|
+
Temperatures are taken on ITS-90. With ``"convert"`` (default)
|
|
534
|
+
they are converted to the temperature scale the formulation
|
|
535
|
+
was fitted on, when it is known (NaN beyond the conversion
|
|
536
|
+
table); ``"ignore"`` uses them as they are.
|
|
537
|
+
**inputs : pint.Quantity
|
|
538
|
+
One quantity per input variable, by identifier.
|
|
539
|
+
|
|
540
|
+
Returns
|
|
541
|
+
-------
|
|
542
|
+
pint.Quantity
|
|
543
|
+
The result, in the output unit.
|
|
544
|
+
|
|
545
|
+
Raises
|
|
546
|
+
------
|
|
547
|
+
TypeError
|
|
548
|
+
If an input is missing or unexpected.
|
|
549
|
+
OutOfDomainError
|
|
550
|
+
With ``outside="raise"``, if a value is outside a domain.
|
|
551
|
+
"""
|
|
552
|
+
expected = {v.identifier for v in self.inputs}
|
|
553
|
+
if set(inputs) != expected:
|
|
554
|
+
missing = ", ".join(sorted(expected - set(inputs))) or "none"
|
|
555
|
+
extra = ", ".join(sorted(set(inputs) - expected)) or "none"
|
|
556
|
+
msg = f"{self.key}: missing inputs: {missing}; unexpected: {extra}"
|
|
557
|
+
raise TypeError(msg)
|
|
558
|
+
units = semantic_catalog().units
|
|
559
|
+
values = {
|
|
560
|
+
v.identifier: np.asarray(
|
|
561
|
+
convert(inputs[v.identifier], units[v.unit].pint).magnitude,
|
|
562
|
+
dtype=float,
|
|
563
|
+
)
|
|
564
|
+
for v in self.inputs
|
|
565
|
+
}
|
|
566
|
+
if scale == "convert":
|
|
567
|
+
for v in self.inputs:
|
|
568
|
+
if v.temperature_scale is None or not v.temperature_scale.t90:
|
|
569
|
+
continue
|
|
570
|
+
celsius = convert(inputs[v.identifier], "degC").magnitude
|
|
571
|
+
on_scale, _ = v.temperature_scale.from_its90(celsius)
|
|
572
|
+
values[v.identifier] = np.asarray(
|
|
573
|
+
convert(Q_(on_scale, "degC"), units[v.unit].pint).magnitude,
|
|
574
|
+
dtype=float,
|
|
575
|
+
)
|
|
576
|
+
arguments: list[Any] = [values[v.identifier] for v in self.inputs]
|
|
577
|
+
arguments += [c.value for c in self.coefficients]
|
|
578
|
+
arguments += [k.value for k in self.constants]
|
|
579
|
+
with np.errstate(invalid="ignore", divide="ignore"):
|
|
580
|
+
result = np.asarray(self._function(*arguments), dtype=float)
|
|
581
|
+
result = result * self.output_factor
|
|
582
|
+
mask = self.outside_mask(values, basis)
|
|
583
|
+
if mask.any() and outside != "ignore":
|
|
584
|
+
message = (
|
|
585
|
+
f"{self.key}: {int(mask.sum())} value(s) outside its {basis} domain"
|
|
586
|
+
)
|
|
587
|
+
if outside == "raise":
|
|
588
|
+
raise OutOfDomainError(message)
|
|
589
|
+
if outside == "warn":
|
|
590
|
+
warnings.warn(message, ValidityWarning, stacklevel=2)
|
|
591
|
+
else:
|
|
592
|
+
result = np.where(mask, np.nan, result)
|
|
593
|
+
magnitude = result.item() if result.ndim == 0 else result
|
|
594
|
+
return Q_(magnitude, units[self.output.unit].pint)
|