nomogram 0.4.0__tar.gz → 0.6.0__tar.gz
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-0.4.0 → nomogram-0.6.0}/PKG-INFO +50 -21
- {nomogram-0.4.0 → nomogram-0.6.0}/README.md +48 -19
- {nomogram-0.4.0 → nomogram-0.6.0}/pyproject.toml +19 -3
- {nomogram-0.4.0 → nomogram-0.6.0}/pyproject.toml.orig +13 -4
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/catalog.py +151 -42
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/comparison.py +198 -11
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/data/NOTICE.md +1 -1
- nomogram-0.6.0/src/nomogram/data/catalog/atmosphere.toml +2128 -0
- nomogram-0.6.0/src/nomogram/data/catalog/boundary-layer.toml +850 -0
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/data/catalog/core.toml +40 -2
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/data/catalog/moist-air.toml +713 -12
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/data/catalog/radio.toml +1 -22
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/data/catalog/verifications.json +20 -0
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/data/catalog/water-vapour.toml +288 -14
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/data/semantics.json +610 -75
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/dimensions.py +31 -3
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/display.py +17 -2
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/expression.py +144 -16
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/inversion.py +24 -12
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/model.py +20 -5
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/plot.py +466 -52
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/relations.py +192 -37
- {nomogram-0.4.0 → nomogram-0.6.0}/LICENSE +0 -0
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/__init__.py +0 -0
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/_logo.py +0 -0
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/data/catalog/humidity.toml +0 -0
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/data/catalog/sea-water.toml +0 -0
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/py.typed +0 -0
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/search.py +0 -0
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/semantics.py +0 -0
- {nomogram-0.4.0 → nomogram-0.6.0}/src/nomogram/units.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nomogram
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Repository of scientific formulas: notations, units, validity domains
|
|
5
5
|
Keywords: scientific computing,formulas,units,dimensional analysis,physics,thermodynamics,geoscience,atmospheric science,meteorology,hydrology,psychrometrics
|
|
6
6
|
Author: miki4ia
|
|
@@ -22,7 +22,7 @@ Classifier: Topic :: Scientific/Engineering :: Physics
|
|
|
22
22
|
Classifier: Typing :: Typed
|
|
23
23
|
Requires-Dist: numpy>=2.0
|
|
24
24
|
Requires-Dist: pint>=0.24.4
|
|
25
|
-
Requires-Dist: sympy>=1.
|
|
25
|
+
Requires-Dist: sympy>=1.14
|
|
26
26
|
Requires-Dist: matplotlib>=3.10.7 ; extra == 'viz'
|
|
27
27
|
Requires-Python: >=3.12
|
|
28
28
|
Project-URL: Homepage, https://github.com/miki4iaml/nomogram
|
|
@@ -34,18 +34,18 @@ Description-Content-Type: text/markdown
|
|
|
34
34
|
|
|
35
35
|
<p align="center">
|
|
36
36
|
<picture>
|
|
37
|
-
<source media="(prefers-color-scheme: dark)" srcset="docs/images/banner-dark.svg">
|
|
38
|
-
<img alt="nomogram" src="docs/images/banner-light.svg" width="480">
|
|
37
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/miki4iaml/nomogram/v0.6.0/docs/images/banner-dark.svg">
|
|
38
|
+
<img alt="nomogram" src="https://raw.githubusercontent.com/miki4iaml/nomogram/v0.6.0/docs/images/banner-light.svg" width="480">
|
|
39
39
|
</picture>
|
|
40
40
|
</p>
|
|
41
41
|
|
|
42
|
-
**English** | [Français](README.fr.md)
|
|
42
|
+
**English** | [Français](https://github.com/miki4iaml/nomogram/blob/v0.6.0/README.fr.md)
|
|
43
43
|
|
|
44
44
|
[](https://github.com/miki4iaml/nomogram/actions/workflows/ci.yml)
|
|
45
45
|
[](https://github.com/miki4iaml/nomogram/actions/workflows/github-code-scanning/codeql)
|
|
46
46
|
[](https://codecov.io/gh/miki4iaml/nomogram)
|
|
47
|
-
[](pyproject.toml)
|
|
48
|
-
[](LICENSE)
|
|
47
|
+
[](https://github.com/miki4iaml/nomogram/blob/v0.6.0/pyproject.toml)
|
|
48
|
+
[](https://github.com/miki4iaml/nomogram/blob/v0.6.0/LICENSE)
|
|
49
49
|
[](https://doi.org/10.5281/zenodo.23373134)
|
|
50
50
|
[](https://github.com/astral-sh/uv)
|
|
51
51
|
[](https://github.com/astral-sh/ruff)
|
|
@@ -94,7 +94,7 @@ Python 3.12 or later. Plots need the `viz` extra (matplotlib):
|
|
|
94
94
|
Every saturation vapour pressure formulation of the catalog against the
|
|
95
95
|
IAPWS reference, from 0 to 60 °C and within 1 %; curves stop where a formulation leaves its
|
|
96
96
|
stated validity domain, and the figure takes the option set of nomogram's
|
|
97
|
-
documentation ([script](docs/examples/saturation_vapour_pressure.py)):
|
|
97
|
+
documentation ([script](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/examples/saturation_vapour_pressure.py)):
|
|
98
98
|
|
|
99
99
|
```python
|
|
100
100
|
import numpy as np
|
|
@@ -113,7 +113,7 @@ with nomo.display.display_options(nomo.display.DisplayOptions.DOCUMENTATION):
|
|
|
113
113
|
nomo.plot.plot_deviations(table, xlim=(0, 60), ylim=(-1.0, 1.0)) # extra viz
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-

|
|
117
117
|
|
|
118
118
|
## Usage
|
|
119
119
|
|
|
@@ -129,8 +129,29 @@ ha = nomo.comparison.compose(
|
|
|
129
129
|
ha.evaluate(RH=Q_(100, "percent"), T=Q_(20, "degC")).to("g/m^3") # 17.29
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
+
`compare` gives each value to the input of its quantity kind. A
|
|
133
|
+
formulation with several inputs of one kind, such as a scheme
|
|
134
|
+
interpolating between two levels (the target pressure p and the
|
|
135
|
+
pressures p_1 and p_2 of the levels), compares once all but one of them
|
|
136
|
+
are fixed, which `compare` asks for: `partial` fixes inputs as `functools.partial` fixes arguments,
|
|
137
|
+
here the two levels, and leaves a function of p alone, evaluated,
|
|
138
|
+
compared and plotted like any formulation
|
|
139
|
+
([Humidity between two levels](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/vertical.md#humidity-between-two-levels)).
|
|
140
|
+
It also fixes what is known in advance, such as the levels of a sounding,
|
|
141
|
+
and names the curve in legends:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
levels = {"p_1": Q_(850, "hPa"), "p_2": Q_(500, "hPa"),
|
|
145
|
+
"T_1": Q_(278, "K"), "T_2": Q_(252, "K")}
|
|
146
|
+
in_ln_p = nomo.comparison.partial(
|
|
147
|
+
catalog.formulations["log_pressure_interpolation_temperature"],
|
|
148
|
+
label="T linear in ln p", **levels,
|
|
149
|
+
)
|
|
150
|
+
in_ln_p.evaluate(p=Q_(700, "hPa")).to("K") # 268.49
|
|
151
|
+
```
|
|
152
|
+
|
|
132
153
|
Formulations are found by words, wildcards and fields, whatever their
|
|
133
|
-
notation ([Finding formulations](docs/search.md)):
|
|
154
|
+
notation ([Finding formulations](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/search.md)):
|
|
134
155
|
|
|
135
156
|
```python
|
|
136
157
|
catalog.search(variable="rho*") # ρ in every notation
|
|
@@ -138,20 +159,28 @@ catalog.search(kind="VapourPressure", source="buck*")
|
|
|
138
159
|
```
|
|
139
160
|
|
|
140
161
|
Each formulation keeps its units, validity domain, temperature scale and
|
|
141
|
-
references. The vocabulary is mapped in [docs/rosetta.md](docs/rosetta.md)
|
|
142
|
-
[
|
|
143
|
-
|
|
144
|
-
|
|
162
|
+
references. The vocabulary is mapped in [docs/rosetta.md](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/rosetta.md),
|
|
163
|
+
and [Shared vocabularies](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/shared-vocabularies.md) explains how nomogram
|
|
164
|
+
builds on QUDT, CF and Wikidata and contributes to them;
|
|
165
|
+
[How nomogram earns trust](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/approach.md) explains what is checked and
|
|
166
|
+
how, and [Displaying formulas](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/display.md) shows formulas in notebooks,
|
|
167
|
+
Markdown and plots. [Sea water](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/sea-water.md) shows the salinity
|
|
145
168
|
scales and the vapour pressure of sea water, and what its 2 % lowering
|
|
146
|
-
changes at the sea surface. [
|
|
147
|
-
|
|
169
|
+
changes at the sea surface. [Humidity aloft](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/humidity-aloft.md)
|
|
170
|
+
sets the formula of a radiosonde's producer against its users' along a
|
|
171
|
+
vertical profile, through the dew point, over water or over ice, and
|
|
172
|
+
against WMO's requirements.
|
|
173
|
+
[Security and compliance](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/security-and-compliance.md) gathers, for
|
|
174
|
+
those who approve its use on a laboratory network, how nomogram is
|
|
175
|
+
developed, checked and published, what remains to do, and a checklist.
|
|
176
|
+
[The logo](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/logo.md) is a working nomogram.
|
|
148
177
|
|
|
149
178
|
## Contributing
|
|
150
179
|
|
|
151
180
|
References to integrate, information or corrections about catalog
|
|
152
181
|
entries, new formulas and code are all welcome: see the
|
|
153
|
-
[ways to contribute](.github/CONTRIBUTING.md#ways-to-contribute) and
|
|
154
|
-
the [catalog guide](docs/catalog.md).
|
|
182
|
+
[ways to contribute](https://github.com/miki4iaml/nomogram/blob/v0.6.0/.github/CONTRIBUTING.md#ways-to-contribute) and
|
|
183
|
+
the [catalog guide](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/catalog.md).
|
|
155
184
|
|
|
156
185
|
## Data sources
|
|
157
186
|
|
|
@@ -162,7 +191,7 @@ their canonical units, from the CF standard name table. The package
|
|
|
162
191
|
bundles a subset of each source:
|
|
163
192
|
|
|
164
193
|
- QUDT — Quantities, Units, Dimensions and Types, by QUDT.org
|
|
165
|
-
(<https://qudt.org>), release 3.5.
|
|
194
|
+
(<https://qudt.org>), release 3.5.3, licensed under
|
|
166
195
|
[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/);
|
|
167
196
|
- the [CF standard name table](https://cfconventions.org/Data/cf-standard-names/95/build/cf-standard-name-table.html),
|
|
168
197
|
version 95, [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/);
|
|
@@ -170,9 +199,9 @@ bundles a subset of each source:
|
|
|
170
199
|
Institute, Inc., [UCUM License](https://ucum.org/license).
|
|
171
200
|
|
|
172
201
|
What nomogram selects, changes and adds is listed in
|
|
173
|
-
[NOTICE.md](src/nomogram/data/NOTICE.md).
|
|
202
|
+
[NOTICE.md](https://github.com/miki4iaml/nomogram/blob/v0.6.0/src/nomogram/data/NOTICE.md).
|
|
174
203
|
|
|
175
204
|
## License
|
|
176
205
|
|
|
177
|
-
Code: [BSD-3-Clause](LICENSE). Bundled data: see
|
|
206
|
+
Code: [BSD-3-Clause](https://github.com/miki4iaml/nomogram/blob/v0.6.0/LICENSE). Bundled data: see
|
|
178
207
|
[Data sources](#data-sources).
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<picture>
|
|
3
|
-
<source media="(prefers-color-scheme: dark)" srcset="docs/images/banner-dark.svg">
|
|
4
|
-
<img alt="nomogram" src="docs/images/banner-light.svg" width="480">
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/miki4iaml/nomogram/v0.6.0/docs/images/banner-dark.svg">
|
|
4
|
+
<img alt="nomogram" src="https://raw.githubusercontent.com/miki4iaml/nomogram/v0.6.0/docs/images/banner-light.svg" width="480">
|
|
5
5
|
</picture>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
|
-
**English** | [Français](README.fr.md)
|
|
8
|
+
**English** | [Français](https://github.com/miki4iaml/nomogram/blob/v0.6.0/README.fr.md)
|
|
9
9
|
|
|
10
10
|
[](https://github.com/miki4iaml/nomogram/actions/workflows/ci.yml)
|
|
11
11
|
[](https://github.com/miki4iaml/nomogram/actions/workflows/github-code-scanning/codeql)
|
|
12
12
|
[](https://codecov.io/gh/miki4iaml/nomogram)
|
|
13
|
-
[](pyproject.toml)
|
|
14
|
-
[](LICENSE)
|
|
13
|
+
[](https://github.com/miki4iaml/nomogram/blob/v0.6.0/pyproject.toml)
|
|
14
|
+
[](https://github.com/miki4iaml/nomogram/blob/v0.6.0/LICENSE)
|
|
15
15
|
[](https://doi.org/10.5281/zenodo.23373134)
|
|
16
16
|
[](https://github.com/astral-sh/uv)
|
|
17
17
|
[](https://github.com/astral-sh/ruff)
|
|
@@ -60,7 +60,7 @@ Python 3.12 or later. Plots need the `viz` extra (matplotlib):
|
|
|
60
60
|
Every saturation vapour pressure formulation of the catalog against the
|
|
61
61
|
IAPWS reference, from 0 to 60 °C and within 1 %; curves stop where a formulation leaves its
|
|
62
62
|
stated validity domain, and the figure takes the option set of nomogram's
|
|
63
|
-
documentation ([script](docs/examples/saturation_vapour_pressure.py)):
|
|
63
|
+
documentation ([script](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/examples/saturation_vapour_pressure.py)):
|
|
64
64
|
|
|
65
65
|
```python
|
|
66
66
|
import numpy as np
|
|
@@ -79,7 +79,7 @@ with nomo.display.display_options(nomo.display.DisplayOptions.DOCUMENTATION):
|
|
|
79
79
|
nomo.plot.plot_deviations(table, xlim=(0, 60), ylim=(-1.0, 1.0)) # extra viz
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-

|
|
83
83
|
|
|
84
84
|
## Usage
|
|
85
85
|
|
|
@@ -95,8 +95,29 @@ ha = nomo.comparison.compose(
|
|
|
95
95
|
ha.evaluate(RH=Q_(100, "percent"), T=Q_(20, "degC")).to("g/m^3") # 17.29
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
+
`compare` gives each value to the input of its quantity kind. A
|
|
99
|
+
formulation with several inputs of one kind, such as a scheme
|
|
100
|
+
interpolating between two levels (the target pressure p and the
|
|
101
|
+
pressures p_1 and p_2 of the levels), compares once all but one of them
|
|
102
|
+
are fixed, which `compare` asks for: `partial` fixes inputs as `functools.partial` fixes arguments,
|
|
103
|
+
here the two levels, and leaves a function of p alone, evaluated,
|
|
104
|
+
compared and plotted like any formulation
|
|
105
|
+
([Humidity between two levels](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/vertical.md#humidity-between-two-levels)).
|
|
106
|
+
It also fixes what is known in advance, such as the levels of a sounding,
|
|
107
|
+
and names the curve in legends:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
levels = {"p_1": Q_(850, "hPa"), "p_2": Q_(500, "hPa"),
|
|
111
|
+
"T_1": Q_(278, "K"), "T_2": Q_(252, "K")}
|
|
112
|
+
in_ln_p = nomo.comparison.partial(
|
|
113
|
+
catalog.formulations["log_pressure_interpolation_temperature"],
|
|
114
|
+
label="T linear in ln p", **levels,
|
|
115
|
+
)
|
|
116
|
+
in_ln_p.evaluate(p=Q_(700, "hPa")).to("K") # 268.49
|
|
117
|
+
```
|
|
118
|
+
|
|
98
119
|
Formulations are found by words, wildcards and fields, whatever their
|
|
99
|
-
notation ([Finding formulations](docs/search.md)):
|
|
120
|
+
notation ([Finding formulations](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/search.md)):
|
|
100
121
|
|
|
101
122
|
```python
|
|
102
123
|
catalog.search(variable="rho*") # ρ in every notation
|
|
@@ -104,20 +125,28 @@ catalog.search(kind="VapourPressure", source="buck*")
|
|
|
104
125
|
```
|
|
105
126
|
|
|
106
127
|
Each formulation keeps its units, validity domain, temperature scale and
|
|
107
|
-
references. The vocabulary is mapped in [docs/rosetta.md](docs/rosetta.md)
|
|
108
|
-
[
|
|
109
|
-
|
|
110
|
-
|
|
128
|
+
references. The vocabulary is mapped in [docs/rosetta.md](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/rosetta.md),
|
|
129
|
+
and [Shared vocabularies](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/shared-vocabularies.md) explains how nomogram
|
|
130
|
+
builds on QUDT, CF and Wikidata and contributes to them;
|
|
131
|
+
[How nomogram earns trust](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/approach.md) explains what is checked and
|
|
132
|
+
how, and [Displaying formulas](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/display.md) shows formulas in notebooks,
|
|
133
|
+
Markdown and plots. [Sea water](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/sea-water.md) shows the salinity
|
|
111
134
|
scales and the vapour pressure of sea water, and what its 2 % lowering
|
|
112
|
-
changes at the sea surface. [
|
|
113
|
-
|
|
135
|
+
changes at the sea surface. [Humidity aloft](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/humidity-aloft.md)
|
|
136
|
+
sets the formula of a radiosonde's producer against its users' along a
|
|
137
|
+
vertical profile, through the dew point, over water or over ice, and
|
|
138
|
+
against WMO's requirements.
|
|
139
|
+
[Security and compliance](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/security-and-compliance.md) gathers, for
|
|
140
|
+
those who approve its use on a laboratory network, how nomogram is
|
|
141
|
+
developed, checked and published, what remains to do, and a checklist.
|
|
142
|
+
[The logo](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/logo.md) is a working nomogram.
|
|
114
143
|
|
|
115
144
|
## Contributing
|
|
116
145
|
|
|
117
146
|
References to integrate, information or corrections about catalog
|
|
118
147
|
entries, new formulas and code are all welcome: see the
|
|
119
|
-
[ways to contribute](.github/CONTRIBUTING.md#ways-to-contribute) and
|
|
120
|
-
the [catalog guide](docs/catalog.md).
|
|
148
|
+
[ways to contribute](https://github.com/miki4iaml/nomogram/blob/v0.6.0/.github/CONTRIBUTING.md#ways-to-contribute) and
|
|
149
|
+
the [catalog guide](https://github.com/miki4iaml/nomogram/blob/v0.6.0/docs/catalog.md).
|
|
121
150
|
|
|
122
151
|
## Data sources
|
|
123
152
|
|
|
@@ -128,7 +157,7 @@ their canonical units, from the CF standard name table. The package
|
|
|
128
157
|
bundles a subset of each source:
|
|
129
158
|
|
|
130
159
|
- QUDT — Quantities, Units, Dimensions and Types, by QUDT.org
|
|
131
|
-
(<https://qudt.org>), release 3.5.
|
|
160
|
+
(<https://qudt.org>), release 3.5.3, licensed under
|
|
132
161
|
[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/);
|
|
133
162
|
- the [CF standard name table](https://cfconventions.org/Data/cf-standard-names/95/build/cf-standard-name-table.html),
|
|
134
163
|
version 95, [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/);
|
|
@@ -136,9 +165,9 @@ bundles a subset of each source:
|
|
|
136
165
|
Institute, Inc., [UCUM License](https://ucum.org/license).
|
|
137
166
|
|
|
138
167
|
What nomogram selects, changes and adds is listed in
|
|
139
|
-
[NOTICE.md](src/nomogram/data/NOTICE.md).
|
|
168
|
+
[NOTICE.md](https://github.com/miki4iaml/nomogram/blob/v0.6.0/src/nomogram/data/NOTICE.md).
|
|
140
169
|
|
|
141
170
|
## License
|
|
142
171
|
|
|
143
|
-
Code: [BSD-3-Clause](LICENSE). Bundled data: see
|
|
172
|
+
Code: [BSD-3-Clause](https://github.com/miki4iaml/nomogram/blob/v0.6.0/LICENSE). Bundled data: see
|
|
144
173
|
[Data sources](#data-sources).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nomogram"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "Repository of scientific formulas: notations, units, validity domains"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -39,7 +39,7 @@ classifiers = [
|
|
|
39
39
|
dependencies = [
|
|
40
40
|
"numpy>=2.0",
|
|
41
41
|
"pint>=0.24.4",
|
|
42
|
-
"sympy>=1.
|
|
42
|
+
"sympy>=1.14",
|
|
43
43
|
]
|
|
44
44
|
|
|
45
45
|
[[project.authors]]
|
|
@@ -56,7 +56,7 @@ Changelog = "https://github.com/miki4iaml/nomogram/blob/main/CHANGELOG.md"
|
|
|
56
56
|
viz = ["matplotlib>=3.10.7"]
|
|
57
57
|
|
|
58
58
|
[build-system]
|
|
59
|
-
requires = ["uv_build>=0.12.18,<0.
|
|
59
|
+
requires = ["uv_build>=0.12.18,<0.14.0"]
|
|
60
60
|
build-backend = "uv_build"
|
|
61
61
|
|
|
62
62
|
[dependency-groups]
|
|
@@ -133,8 +133,24 @@ extend-select = [
|
|
|
133
133
|
"PTH",
|
|
134
134
|
"RET",
|
|
135
135
|
"T20",
|
|
136
|
+
"TID251",
|
|
136
137
|
]
|
|
137
138
|
|
|
139
|
+
[tool.ruff.lint.flake8-tidy-imports.banned-api."sympy.sympify"]
|
|
140
|
+
msg = "read formulas with nomogram.expression.parse_expression"
|
|
141
|
+
|
|
142
|
+
[tool.ruff.lint.flake8-tidy-imports.banned-api."sympy.core.sympify"]
|
|
143
|
+
msg = "read formulas with nomogram.expression.parse_expression"
|
|
144
|
+
|
|
145
|
+
[tool.ruff.lint.flake8-tidy-imports.banned-api."sympy.parse_expr"]
|
|
146
|
+
msg = "read formulas with nomogram.expression.parse_expression"
|
|
147
|
+
|
|
148
|
+
[tool.ruff.lint.flake8-tidy-imports.banned-api."sympy.parsing"]
|
|
149
|
+
msg = "read formulas with nomogram.expression.parse_expression"
|
|
150
|
+
|
|
151
|
+
[tool.ruff.lint.flake8-tidy-imports.banned-api."sympy.parsing.sympy_parser"]
|
|
152
|
+
msg = "read formulas with nomogram.expression.parse_expression"
|
|
153
|
+
|
|
138
154
|
[tool.ruff.lint.per-file-ignores]
|
|
139
155
|
"tests/**" = ["ARG001"]
|
|
140
156
|
"scripts/**" = ["T20"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nomogram"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "Repository of scientific formulas: notations, units, validity domains"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -39,7 +39,7 @@ classifiers = [
|
|
|
39
39
|
dependencies = [
|
|
40
40
|
"numpy>=2.0",
|
|
41
41
|
"pint>=0.24.4",
|
|
42
|
-
"sympy>=1.
|
|
42
|
+
"sympy>=1.14",
|
|
43
43
|
]
|
|
44
44
|
|
|
45
45
|
[project.urls]
|
|
@@ -54,7 +54,7 @@ viz = [
|
|
|
54
54
|
]
|
|
55
55
|
|
|
56
56
|
[build-system]
|
|
57
|
-
requires = ["uv_build>=0.12.18,<0.
|
|
57
|
+
requires = ["uv_build>=0.12.18,<0.14.0"]
|
|
58
58
|
build-backend = "uv_build"
|
|
59
59
|
|
|
60
60
|
[dependency-groups]
|
|
@@ -110,7 +110,16 @@ exclude = ["*.md"]
|
|
|
110
110
|
|
|
111
111
|
[tool.ruff.lint]
|
|
112
112
|
# On top of ruff's default selection, which Dependabot bumps surface in CI.
|
|
113
|
-
extend-select = ["ARG", "B", "C4", "PIE", "PT", "PTH", "RET", "T20"]
|
|
113
|
+
extend-select = ["ARG", "B", "C4", "PIE", "PT", "PTH", "RET", "T20", "TID251"]
|
|
114
|
+
|
|
115
|
+
# A formula is read by nomogram.expression.parse_expression, against a
|
|
116
|
+
# whitelist: never by SymPy's parsers, which rely on eval (AGENTS.md).
|
|
117
|
+
[tool.ruff.lint.flake8-tidy-imports.banned-api]
|
|
118
|
+
"sympy.sympify".msg = "read formulas with nomogram.expression.parse_expression"
|
|
119
|
+
"sympy.core.sympify".msg = "read formulas with nomogram.expression.parse_expression"
|
|
120
|
+
"sympy.parse_expr".msg = "read formulas with nomogram.expression.parse_expression"
|
|
121
|
+
"sympy.parsing".msg = "read formulas with nomogram.expression.parse_expression"
|
|
122
|
+
"sympy.parsing.sympy_parser".msg = "read formulas with nomogram.expression.parse_expression"
|
|
114
123
|
|
|
115
124
|
[tool.ruff.lint.per-file-ignores]
|
|
116
125
|
# Fixtures requested for their side effects look like unused arguments.
|
|
@@ -87,6 +87,9 @@ from nomogram.model import (
|
|
|
87
87
|
_origin,
|
|
88
88
|
)
|
|
89
89
|
from nomogram.relations import (
|
|
90
|
+
_alike,
|
|
91
|
+
_head,
|
|
92
|
+
_shape,
|
|
90
93
|
_unit_factor,
|
|
91
94
|
match_inputs,
|
|
92
95
|
record,
|
|
@@ -292,8 +295,8 @@ class Catalog:
|
|
|
292
295
|
>>> c = catalog()
|
|
293
296
|
>>> "formulations.buck1981_magnus" in c.search("buck*", section="formulations")
|
|
294
297
|
True
|
|
295
|
-
>>> c.search(variable="rho*")
|
|
296
|
-
|
|
298
|
+
>>> "formulations.mass_concentration_ideal_gas" in c.search(variable="rho*")
|
|
299
|
+
True
|
|
297
300
|
"""
|
|
298
301
|
from nomogram.search import search
|
|
299
302
|
|
|
@@ -1014,7 +1017,7 @@ class _Builder:
|
|
|
1014
1017
|
names = {
|
|
1015
1018
|
self.constants[n].identifier: n for n in self.constants[key].components
|
|
1016
1019
|
}
|
|
1017
|
-
return expr.
|
|
1020
|
+
return expr.xreplace({s: value(names[s.name]) for s in expr.free_symbols})
|
|
1018
1021
|
|
|
1019
1022
|
def unrounded(self, key: str) -> Any:
|
|
1020
1023
|
"""The exact value of a constant: its definition, recursively, else as printed."""
|
|
@@ -1713,6 +1716,70 @@ class _Builder:
|
|
|
1713
1716
|
raise CatalogError(msg)
|
|
1714
1717
|
return formulations[name]
|
|
1715
1718
|
|
|
1719
|
+
@staticmethod
|
|
1720
|
+
def bindings(
|
|
1721
|
+
place: str, table: Any, inner: Mapping[str, Formulation]
|
|
1722
|
+
) -> dict[str, dict[str, str]]:
|
|
1723
|
+
"""The renamings of the inputs of each inner formulation, by its identifier.
|
|
1724
|
+
|
|
1725
|
+
``bind = { theta = "t" }`` renames an input of every inner
|
|
1726
|
+
formulation; ``bind = { theta_1 = { T = "T_1" } }`` those of the
|
|
1727
|
+
formulation computing ``theta_1`` only, so that one formulation may
|
|
1728
|
+
compute two inputs at different arguments.
|
|
1729
|
+
"""
|
|
1730
|
+
if table is None:
|
|
1731
|
+
table = {}
|
|
1732
|
+
if not isinstance(table, dict):
|
|
1733
|
+
msg = f"{place}.bind: expected a table"
|
|
1734
|
+
raise CatalogError(msg)
|
|
1735
|
+
every: dict[str, str] = {}
|
|
1736
|
+
own: dict[str, dict[str, str]] = {}
|
|
1737
|
+
|
|
1738
|
+
def target(where: str, name: Any) -> str:
|
|
1739
|
+
if not isinstance(name, str) or not name:
|
|
1740
|
+
msg = f"{where}: expected the identifier of an input"
|
|
1741
|
+
raise CatalogError(msg)
|
|
1742
|
+
if name in inner:
|
|
1743
|
+
msg = f"{where}: {name!r} is computed by {inner[name].key}"
|
|
1744
|
+
raise CatalogError(msg)
|
|
1745
|
+
return name
|
|
1746
|
+
|
|
1747
|
+
for source, name in table.items():
|
|
1748
|
+
if isinstance(name, dict):
|
|
1749
|
+
part = inner.get(source)
|
|
1750
|
+
if part is None:
|
|
1751
|
+
msg = f"{place}.bind.{source}: no inner formulation computes {source!r}"
|
|
1752
|
+
raise CatalogError(msg)
|
|
1753
|
+
names = {v.identifier for v in part.inputs}
|
|
1754
|
+
for input_name, renamed in name.items():
|
|
1755
|
+
if input_name not in names:
|
|
1756
|
+
msg = (
|
|
1757
|
+
f"{place}.bind.{source}: {input_name!r} is not an "
|
|
1758
|
+
f"input of {part.key}"
|
|
1759
|
+
)
|
|
1760
|
+
raise CatalogError(msg)
|
|
1761
|
+
own.setdefault(source, {})[input_name] = target(
|
|
1762
|
+
f"{place}.bind.{source}", renamed
|
|
1763
|
+
)
|
|
1764
|
+
continue
|
|
1765
|
+
if not any(
|
|
1766
|
+
source in {v.identifier for v in p.inputs} for p in inner.values()
|
|
1767
|
+
):
|
|
1768
|
+
msg = (
|
|
1769
|
+
f"{place}.bind: {source!r} is not an input of an inner formulation"
|
|
1770
|
+
)
|
|
1771
|
+
raise CatalogError(msg)
|
|
1772
|
+
every[source] = target(f"{place}.bind", name)
|
|
1773
|
+
renames = {}
|
|
1774
|
+
for identifier, part in inner.items():
|
|
1775
|
+
rename = {**every, **own.get(identifier, {})}
|
|
1776
|
+
renames[identifier] = {
|
|
1777
|
+
v.identifier: rename[v.identifier]
|
|
1778
|
+
for v in part.inputs
|
|
1779
|
+
if v.identifier in rename
|
|
1780
|
+
}
|
|
1781
|
+
return renames
|
|
1782
|
+
|
|
1716
1783
|
def composed(
|
|
1717
1784
|
self, key: str, table: Any, formulations: Mapping[str, Formulation]
|
|
1718
1785
|
) -> Formulation:
|
|
@@ -1741,6 +1808,7 @@ class _Builder:
|
|
|
1741
1808
|
if isinstance(node, sympy.Piecewise)
|
|
1742
1809
|
}
|
|
1743
1810
|
inner: dict[str, Formulation] = {}
|
|
1811
|
+
scaled: dict[str, Variable] = {}
|
|
1744
1812
|
for identifier, name in c["inner"].items():
|
|
1745
1813
|
w = f"{place}.inner.{identifier}"
|
|
1746
1814
|
part = self.part(w, name, formulations)
|
|
@@ -1769,33 +1837,26 @@ class _Builder:
|
|
|
1769
1837
|
)
|
|
1770
1838
|
raise CatalogError(msg)
|
|
1771
1839
|
if part.output.scale != target.scale:
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1840
|
+
# A part on no scale (an interpolation, a definition) feeds an
|
|
1841
|
+
# input on the reference scale, where nothing is converted: its
|
|
1842
|
+
# temperatures take that scale. On another scale, converting
|
|
1843
|
+
# its inputs would not convert what it computes.
|
|
1844
|
+
if (
|
|
1845
|
+
part.output.scale is None
|
|
1846
|
+
and target.scale is not None
|
|
1847
|
+
and target.scale.is_reference
|
|
1848
|
+
and not any(v.scale is not None for v in part.inputs)
|
|
1849
|
+
):
|
|
1850
|
+
scaled[identifier] = target
|
|
1851
|
+
else:
|
|
1852
|
+
msg = (
|
|
1853
|
+
f"{w}: {part.key} gives a value on scale "
|
|
1854
|
+
f"{getattr(part.output.scale, 'key', None)}, but {identifier} "
|
|
1855
|
+
f"is on {getattr(target.scale, 'key', None)}"
|
|
1856
|
+
)
|
|
1857
|
+
raise CatalogError(msg)
|
|
1778
1858
|
inner[identifier] = part
|
|
1779
|
-
|
|
1780
|
-
for source, name in bind.items():
|
|
1781
|
-
if not any(
|
|
1782
|
-
source in {v.identifier for v in p.inputs} for p in inner.values()
|
|
1783
|
-
):
|
|
1784
|
-
msg = (
|
|
1785
|
-
f"{place}.bind: {source!r} is not an input of an inner formulation"
|
|
1786
|
-
)
|
|
1787
|
-
raise CatalogError(msg)
|
|
1788
|
-
if name in inner:
|
|
1789
|
-
msg = f"{place}.bind: {name!r} is computed by {inner[name].key}"
|
|
1790
|
-
raise CatalogError(msg)
|
|
1791
|
-
renames = {
|
|
1792
|
-
identifier: {
|
|
1793
|
-
v.identifier: bind[v.identifier]
|
|
1794
|
-
for v in part.inputs
|
|
1795
|
-
if v.identifier in bind
|
|
1796
|
-
}
|
|
1797
|
-
for identifier, part in inner.items()
|
|
1798
|
-
}
|
|
1859
|
+
renames = self.bindings(place, c["bind"], inner)
|
|
1799
1860
|
|
|
1800
1861
|
# Inputs: those of the outer formulation it still needs, then those of
|
|
1801
1862
|
# the inner ones; one identifier is one input, alike in every part.
|
|
@@ -1804,22 +1865,40 @@ class _Builder:
|
|
|
1804
1865
|
pieces = [(outer, v) for v in outer.inputs if v.identifier not in inner]
|
|
1805
1866
|
for identifier, part in inner.items():
|
|
1806
1867
|
rename = renames[identifier]
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
v, identifier=rename.get(v.identifier, v.identifier)
|
|
1812
|
-
),
|
|
1868
|
+
fed = scaled.get(identifier)
|
|
1869
|
+
for v in part.inputs:
|
|
1870
|
+
v = dataclasses.replace(
|
|
1871
|
+
v, identifier=rename.get(v.identifier, v.identifier)
|
|
1813
1872
|
)
|
|
1814
|
-
|
|
1815
|
-
|
|
1873
|
+
if v.identifier in inner:
|
|
1874
|
+
# One identifier would name two quantities: an input of
|
|
1875
|
+
# this part, and what another part computes.
|
|
1876
|
+
msg = (
|
|
1877
|
+
f"{where}: input {v.identifier!r} of {part.key} is "
|
|
1878
|
+
f"computed by {inner[v.identifier].key}; rename it with bind"
|
|
1879
|
+
)
|
|
1880
|
+
raise CatalogError(msg)
|
|
1881
|
+
if (
|
|
1882
|
+
fed is not None
|
|
1883
|
+
and fed.scale is not None
|
|
1884
|
+
and fed.scale.applies_to(v.kind, self.kinds)
|
|
1885
|
+
):
|
|
1886
|
+
v = dataclasses.replace(
|
|
1887
|
+
v, scale=fed.scale, scale_basis=fed.scale_basis
|
|
1888
|
+
)
|
|
1889
|
+
pieces.append((part, v))
|
|
1816
1890
|
for piece, variable in pieces:
|
|
1817
1891
|
came_from.setdefault(variable.identifier, piece.key)
|
|
1818
1892
|
known = inputs.setdefault(variable.identifier, variable)
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1893
|
+
# An input on no scale merges with one on the reference scale
|
|
1894
|
+
# only: on another, its part would receive the converted value.
|
|
1895
|
+
other = variable.scale if known.scale is None else known.scale
|
|
1896
|
+
if (known.kind, known.unit) != (variable.kind, variable.unit) or (
|
|
1897
|
+
known.scale != variable.scale
|
|
1898
|
+
and (
|
|
1899
|
+
None not in (known.scale, variable.scale)
|
|
1900
|
+
or not getattr(other, "is_reference", False)
|
|
1901
|
+
)
|
|
1823
1902
|
):
|
|
1824
1903
|
msg = (
|
|
1825
1904
|
f"{where}: input {variable.identifier} of {piece.key} differs "
|
|
@@ -1827,6 +1906,12 @@ class _Builder:
|
|
|
1827
1906
|
"another part; rename it with bind"
|
|
1828
1907
|
)
|
|
1829
1908
|
raise CatalogError(msg)
|
|
1909
|
+
if known.scale is None and variable.scale is not None:
|
|
1910
|
+
# An input on no scale takes any: that of the other part.
|
|
1911
|
+
inputs[variable.identifier] = dataclasses.replace(
|
|
1912
|
+
known, scale=variable.scale, scale_basis=variable.scale_basis
|
|
1913
|
+
)
|
|
1914
|
+
came_from[variable.identifier] = piece.key
|
|
1830
1915
|
|
|
1831
1916
|
# Part, and formulation that defines it, of each coefficient.
|
|
1832
1917
|
owner: dict[str, tuple[str, str]] = {}
|
|
@@ -2737,6 +2822,11 @@ class _Builder:
|
|
|
2737
2822
|
for name in part.rename.values()
|
|
2738
2823
|
if name not in inputs
|
|
2739
2824
|
}
|
|
2825
|
+
elif formulation.parts:
|
|
2826
|
+
# A composition: checked on the parts substituted as written,
|
|
2827
|
+
# since SymPy may simplify a factor of two calls of a part away.
|
|
2828
|
+
tree = ast.parse(formulation.expression, mode="eval")
|
|
2829
|
+
used = {node.id for node in ast.walk(tree) if isinstance(node, ast.Name)}
|
|
2740
2830
|
unused = sorted(set(names) - used)
|
|
2741
2831
|
if unused:
|
|
2742
2832
|
msg = f"{where}: declared but not used: {', '.join(unused)}"
|
|
@@ -2790,8 +2880,27 @@ class _Builder:
|
|
|
2790
2880
|
built[key] = self.composed(key, tables[key], built)
|
|
2791
2881
|
formulations = {key: built[key] for key in tables}
|
|
2792
2882
|
# Two sources of one formulation make one entry with two citations.
|
|
2883
|
+
# Only formulations of the same head (kinds, units, coefficients),
|
|
2884
|
+
# then of alike shapes, may be one: the costly signature, which
|
|
2885
|
+
# tries every order of the inputs, is computed for those alone.
|
|
2886
|
+
heads: dict[str, list[str]] = {}
|
|
2887
|
+
for key, f in formulations.items():
|
|
2888
|
+
heads.setdefault(_head(f), []).append(key)
|
|
2889
|
+
candidates = []
|
|
2890
|
+
for keys in heads.values():
|
|
2891
|
+
shapes = {key: _shape(formulations[key]) for key in keys if len(keys) > 1}
|
|
2892
|
+
candidates += [
|
|
2893
|
+
key
|
|
2894
|
+
for key in shapes
|
|
2895
|
+
if any(
|
|
2896
|
+
_alike(shapes[key], shapes[other])
|
|
2897
|
+
for other in shapes
|
|
2898
|
+
if other != key
|
|
2899
|
+
)
|
|
2900
|
+
]
|
|
2793
2901
|
seen: dict[str, str] = {}
|
|
2794
|
-
for key
|
|
2902
|
+
for key in candidates:
|
|
2903
|
+
formulation = formulations[key]
|
|
2795
2904
|
fingerprint = signature(formulation)
|
|
2796
2905
|
if fingerprint in seen:
|
|
2797
2906
|
first = seen[fingerprint]
|