nomogram 0.4.0__tar.gz → 0.5.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.5.0}/PKG-INFO +39 -18
- {nomogram-0.4.0 → nomogram-0.5.0}/README.md +38 -17
- {nomogram-0.4.0 → nomogram-0.5.0}/pyproject.toml +1 -1
- {nomogram-0.4.0 → nomogram-0.5.0}/pyproject.toml.orig +1 -1
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/catalog.py +150 -41
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/comparison.py +161 -6
- nomogram-0.5.0/src/nomogram/data/catalog/atmosphere.toml +1590 -0
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/data/catalog/core.toml +40 -2
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/data/catalog/moist-air.toml +341 -1
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/data/catalog/radio.toml +1 -22
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/data/semantics.json +343 -74
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/dimensions.py +24 -2
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/display.py +6 -2
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/expression.py +19 -3
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/inversion.py +3 -1
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/model.py +14 -3
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/plot.py +279 -19
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/relations.py +129 -30
- {nomogram-0.4.0 → nomogram-0.5.0}/LICENSE +0 -0
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/__init__.py +0 -0
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/_logo.py +0 -0
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/data/NOTICE.md +0 -0
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/data/catalog/humidity.toml +0 -0
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/data/catalog/sea-water.toml +0 -0
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/data/catalog/verifications.json +0 -0
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/data/catalog/water-vapour.toml +0 -0
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/py.typed +0 -0
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/search.py +0 -0
- {nomogram-0.4.0 → nomogram-0.5.0}/src/nomogram/semantics.py +0 -0
- {nomogram-0.4.0 → nomogram-0.5.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.5.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
|
|
@@ -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.5.0/docs/images/banner-dark.svg">
|
|
38
|
+
<img alt="nomogram" src="https://raw.githubusercontent.com/miki4iaml/nomogram/v0.5.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.5.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.5.0/pyproject.toml)
|
|
48
|
+
[](https://github.com/miki4iaml/nomogram/blob/v0.5.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.5.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.5.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.5.0/docs/search.md)):
|
|
134
155
|
|
|
135
156
|
```python
|
|
136
157
|
catalog.search(variable="rho*") # ρ in every notation
|
|
@@ -138,20 +159,20 @@ 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
|
-
[How nomogram earns trust](docs/approach.md) explains what is checked and
|
|
143
|
-
how, and [Displaying formulas](docs/display.md) shows formulas in notebooks,
|
|
144
|
-
Markdown and plots. [Sea water](docs/sea-water.md) shows the salinity
|
|
162
|
+
references. The vocabulary is mapped in [docs/rosetta.md](https://github.com/miki4iaml/nomogram/blob/v0.5.0/docs/rosetta.md);
|
|
163
|
+
[How nomogram earns trust](https://github.com/miki4iaml/nomogram/blob/v0.5.0/docs/approach.md) explains what is checked and
|
|
164
|
+
how, and [Displaying formulas](https://github.com/miki4iaml/nomogram/blob/v0.5.0/docs/display.md) shows formulas in notebooks,
|
|
165
|
+
Markdown and plots. [Sea water](https://github.com/miki4iaml/nomogram/blob/v0.5.0/docs/sea-water.md) shows the salinity
|
|
145
166
|
scales and the vapour pressure of sea water, and what its 2 % lowering
|
|
146
|
-
changes at the sea surface. [The logo](docs/logo.md) is a working
|
|
167
|
+
changes at the sea surface. [The logo](https://github.com/miki4iaml/nomogram/blob/v0.5.0/docs/logo.md) is a working
|
|
147
168
|
nomogram.
|
|
148
169
|
|
|
149
170
|
## Contributing
|
|
150
171
|
|
|
151
172
|
References to integrate, information or corrections about catalog
|
|
152
173
|
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).
|
|
174
|
+
[ways to contribute](https://github.com/miki4iaml/nomogram/blob/v0.5.0/.github/CONTRIBUTING.md#ways-to-contribute) and
|
|
175
|
+
the [catalog guide](https://github.com/miki4iaml/nomogram/blob/v0.5.0/docs/catalog.md).
|
|
155
176
|
|
|
156
177
|
## Data sources
|
|
157
178
|
|
|
@@ -170,9 +191,9 @@ bundles a subset of each source:
|
|
|
170
191
|
Institute, Inc., [UCUM License](https://ucum.org/license).
|
|
171
192
|
|
|
172
193
|
What nomogram selects, changes and adds is listed in
|
|
173
|
-
[NOTICE.md](src/nomogram/data/NOTICE.md).
|
|
194
|
+
[NOTICE.md](https://github.com/miki4iaml/nomogram/blob/v0.5.0/src/nomogram/data/NOTICE.md).
|
|
174
195
|
|
|
175
196
|
## License
|
|
176
197
|
|
|
177
|
-
Code: [BSD-3-Clause](LICENSE). Bundled data: see
|
|
198
|
+
Code: [BSD-3-Clause](https://github.com/miki4iaml/nomogram/blob/v0.5.0/LICENSE). Bundled data: see
|
|
178
199
|
[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.5.0/docs/images/banner-dark.svg">
|
|
4
|
+
<img alt="nomogram" src="https://raw.githubusercontent.com/miki4iaml/nomogram/v0.5.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.5.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.5.0/pyproject.toml)
|
|
14
|
+
[](https://github.com/miki4iaml/nomogram/blob/v0.5.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.5.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.5.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.5.0/docs/search.md)):
|
|
100
121
|
|
|
101
122
|
```python
|
|
102
123
|
catalog.search(variable="rho*") # ρ in every notation
|
|
@@ -104,20 +125,20 @@ 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
|
-
[How nomogram earns trust](docs/approach.md) explains what is checked and
|
|
109
|
-
how, and [Displaying formulas](docs/display.md) shows formulas in notebooks,
|
|
110
|
-
Markdown and plots. [Sea water](docs/sea-water.md) shows the salinity
|
|
128
|
+
references. The vocabulary is mapped in [docs/rosetta.md](https://github.com/miki4iaml/nomogram/blob/v0.5.0/docs/rosetta.md);
|
|
129
|
+
[How nomogram earns trust](https://github.com/miki4iaml/nomogram/blob/v0.5.0/docs/approach.md) explains what is checked and
|
|
130
|
+
how, and [Displaying formulas](https://github.com/miki4iaml/nomogram/blob/v0.5.0/docs/display.md) shows formulas in notebooks,
|
|
131
|
+
Markdown and plots. [Sea water](https://github.com/miki4iaml/nomogram/blob/v0.5.0/docs/sea-water.md) shows the salinity
|
|
111
132
|
scales and the vapour pressure of sea water, and what its 2 % lowering
|
|
112
|
-
changes at the sea surface. [The logo](docs/logo.md) is a working
|
|
133
|
+
changes at the sea surface. [The logo](https://github.com/miki4iaml/nomogram/blob/v0.5.0/docs/logo.md) is a working
|
|
113
134
|
nomogram.
|
|
114
135
|
|
|
115
136
|
## Contributing
|
|
116
137
|
|
|
117
138
|
References to integrate, information or corrections about catalog
|
|
118
139
|
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).
|
|
140
|
+
[ways to contribute](https://github.com/miki4iaml/nomogram/blob/v0.5.0/.github/CONTRIBUTING.md#ways-to-contribute) and
|
|
141
|
+
the [catalog guide](https://github.com/miki4iaml/nomogram/blob/v0.5.0/docs/catalog.md).
|
|
121
142
|
|
|
122
143
|
## Data sources
|
|
123
144
|
|
|
@@ -136,9 +157,9 @@ bundles a subset of each source:
|
|
|
136
157
|
Institute, Inc., [UCUM License](https://ucum.org/license).
|
|
137
158
|
|
|
138
159
|
What nomogram selects, changes and adds is listed in
|
|
139
|
-
[NOTICE.md](src/nomogram/data/NOTICE.md).
|
|
160
|
+
[NOTICE.md](https://github.com/miki4iaml/nomogram/blob/v0.5.0/src/nomogram/data/NOTICE.md).
|
|
140
161
|
|
|
141
162
|
## License
|
|
142
163
|
|
|
143
|
-
Code: [BSD-3-Clause](LICENSE). Bundled data: see
|
|
164
|
+
Code: [BSD-3-Clause](https://github.com/miki4iaml/nomogram/blob/v0.5.0/LICENSE). Bundled data: see
|
|
144
165
|
[Data sources](#data-sources).
|
|
@@ -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
|
|
|
@@ -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]
|