nomogram 0.2.0__tar.gz → 0.4.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.2.0 → nomogram-0.4.0}/PKG-INFO +70 -20
- nomogram-0.4.0/README.md +144 -0
- {nomogram-0.2.0 → nomogram-0.4.0}/pyproject.toml +5 -1
- {nomogram-0.2.0 → nomogram-0.4.0}/pyproject.toml.orig +10 -1
- nomogram-0.4.0/src/nomogram/__init__.py +72 -0
- nomogram-0.4.0/src/nomogram/_logo.py +228 -0
- nomogram-0.4.0/src/nomogram/catalog.py +3509 -0
- {nomogram-0.2.0 → nomogram-0.4.0}/src/nomogram/comparison.py +29 -5
- nomogram-0.4.0/src/nomogram/data/NOTICE.md +40 -0
- nomogram-0.4.0/src/nomogram/data/catalog/core.toml +298 -0
- nomogram-0.4.0/src/nomogram/data/catalog/humidity.toml +12 -0
- nomogram-0.4.0/src/nomogram/data/catalog/moist-air.toml +2056 -0
- nomogram-0.4.0/src/nomogram/data/catalog/radio.toml +326 -0
- nomogram-0.4.0/src/nomogram/data/catalog/sea-water.toml +658 -0
- nomogram-0.4.0/src/nomogram/data/catalog/verifications.json +205 -0
- nomogram-0.4.0/src/nomogram/data/catalog/water-vapour.toml +1080 -0
- nomogram-0.4.0/src/nomogram/data/semantics.json +1426 -0
- nomogram-0.4.0/src/nomogram/dimensions.py +622 -0
- nomogram-0.4.0/src/nomogram/display.py +807 -0
- nomogram-0.4.0/src/nomogram/expression.py +709 -0
- {nomogram-0.2.0 → nomogram-0.4.0}/src/nomogram/inversion.py +323 -54
- nomogram-0.4.0/src/nomogram/model.py +1727 -0
- nomogram-0.4.0/src/nomogram/plot.py +695 -0
- nomogram-0.4.0/src/nomogram/relations.py +1568 -0
- nomogram-0.4.0/src/nomogram/search.py +913 -0
- {nomogram-0.2.0 → nomogram-0.4.0}/src/nomogram/semantics.py +29 -9
- nomogram-0.2.0/README.md +0 -94
- nomogram-0.2.0/src/nomogram/__init__.py +0 -17
- nomogram-0.2.0/src/nomogram/catalog.py +0 -535
- nomogram-0.2.0/src/nomogram/data/NOTICE.md +0 -25
- nomogram-0.2.0/src/nomogram/data/catalog/humidity.toml +0 -807
- nomogram-0.2.0/src/nomogram/data/semantics.json +0 -513
- nomogram-0.2.0/src/nomogram/expression.py +0 -232
- nomogram-0.2.0/src/nomogram/model.py +0 -646
- nomogram-0.2.0/src/nomogram/plot.py +0 -176
- {nomogram-0.2.0 → nomogram-0.4.0}/LICENSE +0 -0
- {nomogram-0.2.0 → nomogram-0.4.0}/src/nomogram/py.typed +0 -0
- {nomogram-0.2.0 → nomogram-0.4.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.4.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
|
|
@@ -32,7 +32,12 @@ Project-URL: Changelog, https://github.com/miki4iaml/nomogram/blob/main/CHANGELO
|
|
|
32
32
|
Provides-Extra: viz
|
|
33
33
|
Description-Content-Type: text/markdown
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
<p align="center">
|
|
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">
|
|
39
|
+
</picture>
|
|
40
|
+
</p>
|
|
36
41
|
|
|
37
42
|
**English** | [Français](README.fr.md)
|
|
38
43
|
|
|
@@ -47,9 +52,19 @@ Description-Content-Type: text/markdown
|
|
|
47
52
|
[](https://mypy-lang.org/)
|
|
48
53
|
[](https://www.conventionalcommits.org/en/v1.0.0/)
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
**Formulas as data, not as code.**
|
|
56
|
+
|
|
57
|
+
nomogram does not hard-code functions: it catalogs formulas, each one
|
|
58
|
+
|
|
59
|
+
- **meaningful**: QUDT quantity kinds and units, links to CF and
|
|
60
|
+
Wikidata, its source, its validity domain when stated;
|
|
61
|
+
- **checked**: dimensions and units on loading, equivalences proved
|
|
62
|
+
symbolically, values cross-checked against references;
|
|
63
|
+
- **ready to use**: compute, compose, invert, plot, and compare with its
|
|
64
|
+
alternatives.
|
|
65
|
+
|
|
66
|
+
Built for trust, not for speed: to show that a calculation is right, and
|
|
67
|
+
where it comes from.
|
|
53
68
|
|
|
54
69
|
Pure Python, built on SymPy, numpy and pint; it works on an offline
|
|
55
70
|
workstation (no online engine).
|
|
@@ -77,25 +92,28 @@ Python 3.12 or later. Plots need the `viz` extra (matplotlib):
|
|
|
77
92
|
## Example
|
|
78
93
|
|
|
79
94
|
Every saturation vapour pressure formulation of the catalog against the
|
|
80
|
-
IAPWS reference
|
|
81
|
-
validity domain
|
|
95
|
+
IAPWS reference, from 0 to 60 °C and within 1 %; curves stop where a formulation leaves its
|
|
96
|
+
stated validity domain, and the figure takes the option set of nomogram's
|
|
97
|
+
documentation ([script](docs/examples/saturation_vapour_pressure.py)):
|
|
82
98
|
|
|
83
99
|
```python
|
|
84
100
|
import numpy as np
|
|
85
|
-
|
|
86
|
-
from nomogram.comparison import compare
|
|
87
|
-
from nomogram.plot import plot_deviations # extra viz
|
|
101
|
+
import nomogram as nomo
|
|
88
102
|
from nomogram.units import Q_
|
|
89
103
|
|
|
90
|
-
|
|
91
|
-
|
|
104
|
+
catalog = nomo.catalog.catalog()
|
|
105
|
+
formulations = catalog.formulations_of("SaturationVapourPressureOverWater")
|
|
106
|
+
reference = catalog.formulations["iapws_sr1_86"]
|
|
92
107
|
temperatures = Q_(np.linspace(-60, 110, 341), "degC")
|
|
93
108
|
|
|
94
|
-
table = compare(
|
|
95
|
-
|
|
109
|
+
table = nomo.comparison.compare(
|
|
110
|
+
formulations, reference, {"ThermodynamicTemperature": temperatures}
|
|
111
|
+
)
|
|
112
|
+
with nomo.display.display_options(nomo.display.DisplayOptions.DOCUMENTATION):
|
|
113
|
+
nomo.plot.plot_deviations(table, xlim=(0, 60), ylim=(-1.0, 1.0)) # extra viz
|
|
96
114
|
```
|
|
97
115
|
|
|
98
|
-

|
|
99
117
|
|
|
100
118
|
## Usage
|
|
101
119
|
|
|
@@ -105,16 +123,28 @@ otherwise (`outside="ignore"`). Formulations also chain, for instance
|
|
|
105
123
|
the absolute humidity from any saturation vapour pressure formulation:
|
|
106
124
|
|
|
107
125
|
```python
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
126
|
+
ha = nomo.comparison.compose(
|
|
127
|
+
catalog.formulations["mass_concentration_ideal_gas"], {"e_s": reference}
|
|
128
|
+
)
|
|
111
129
|
ha.evaluate(RH=Q_(100, "percent"), T=Q_(20, "degC")).to("g/m^3") # 17.29
|
|
112
130
|
```
|
|
113
131
|
|
|
132
|
+
Formulations are found by words, wildcards and fields, whatever their
|
|
133
|
+
notation ([Finding formulations](docs/search.md)):
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
catalog.search(variable="rho*") # ρ in every notation
|
|
137
|
+
catalog.search(kind="VapourPressure", source="buck*")
|
|
138
|
+
```
|
|
139
|
+
|
|
114
140
|
Each formulation keeps its units, validity domain, temperature scale and
|
|
115
141
|
references. The vocabulary is mapped in [docs/rosetta.md](docs/rosetta.md);
|
|
116
142
|
[How nomogram earns trust](docs/approach.md) explains what is checked and
|
|
117
|
-
how.
|
|
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
|
|
145
|
+
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
|
|
147
|
+
nomogram.
|
|
118
148
|
|
|
119
149
|
## Contributing
|
|
120
150
|
|
|
@@ -123,6 +153,26 @@ entries, new formulas and code are all welcome: see the
|
|
|
123
153
|
[ways to contribute](.github/CONTRIBUTING.md#ways-to-contribute) and
|
|
124
154
|
the [catalog guide](docs/catalog.md).
|
|
125
155
|
|
|
156
|
+
## Data sources
|
|
157
|
+
|
|
158
|
+
nomogram's units come from QUDT, and so do its quantity kinds, except
|
|
159
|
+
those QUDT lacks, which nomogram defines as specializations of QUDT
|
|
160
|
+
kinds. nomogram links some kinds to CF standard names, which come, with
|
|
161
|
+
their canonical units, from the CF standard name table. The package
|
|
162
|
+
bundles a subset of each source:
|
|
163
|
+
|
|
164
|
+
- QUDT — Quantities, Units, Dimensions and Types, by QUDT.org
|
|
165
|
+
(<https://qudt.org>), release 3.5.2, licensed under
|
|
166
|
+
[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/);
|
|
167
|
+
- the [CF standard name table](https://cfconventions.org/Data/cf-standard-names/95/build/cf-standard-name-table.html),
|
|
168
|
+
version 95, [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/);
|
|
169
|
+
- the UCUM codes of the units, as QUDT gives them: © Regenstrief
|
|
170
|
+
Institute, Inc., [UCUM License](https://ucum.org/license).
|
|
171
|
+
|
|
172
|
+
What nomogram selects, changes and adds is listed in
|
|
173
|
+
[NOTICE.md](src/nomogram/data/NOTICE.md).
|
|
174
|
+
|
|
126
175
|
## License
|
|
127
176
|
|
|
128
|
-
[BSD-3-Clause](LICENSE).
|
|
177
|
+
Code: [BSD-3-Clause](LICENSE). Bundled data: see
|
|
178
|
+
[Data sources](#data-sources).
|
nomogram-0.4.0/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<p align="center">
|
|
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">
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
**English** | [Français](README.fr.md)
|
|
9
|
+
|
|
10
|
+
[](https://github.com/miki4iaml/nomogram/actions/workflows/ci.yml)
|
|
11
|
+
[](https://github.com/miki4iaml/nomogram/actions/workflows/github-code-scanning/codeql)
|
|
12
|
+
[](https://codecov.io/gh/miki4iaml/nomogram)
|
|
13
|
+
[](pyproject.toml)
|
|
14
|
+
[](LICENSE)
|
|
15
|
+
[](https://doi.org/10.5281/zenodo.23373134)
|
|
16
|
+
[](https://github.com/astral-sh/uv)
|
|
17
|
+
[](https://github.com/astral-sh/ruff)
|
|
18
|
+
[](https://mypy-lang.org/)
|
|
19
|
+
[](https://www.conventionalcommits.org/en/v1.0.0/)
|
|
20
|
+
|
|
21
|
+
**Formulas as data, not as code.**
|
|
22
|
+
|
|
23
|
+
nomogram does not hard-code functions: it catalogs formulas, each one
|
|
24
|
+
|
|
25
|
+
- **meaningful**: QUDT quantity kinds and units, links to CF and
|
|
26
|
+
Wikidata, its source, its validity domain when stated;
|
|
27
|
+
- **checked**: dimensions and units on loading, equivalences proved
|
|
28
|
+
symbolically, values cross-checked against references;
|
|
29
|
+
- **ready to use**: compute, compose, invert, plot, and compare with its
|
|
30
|
+
alternatives.
|
|
31
|
+
|
|
32
|
+
Built for trust, not for speed: to show that a calculation is right, and
|
|
33
|
+
where it comes from.
|
|
34
|
+
|
|
35
|
+
Pure Python, built on SymPy, numpy and pint; it works on an offline
|
|
36
|
+
workstation (no online engine).
|
|
37
|
+
|
|
38
|
+
> Early stage project (alpha).
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
pip install nomogram # core
|
|
44
|
+
pip install "nomogram[viz]" # with plots (matplotlib)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Installation from source
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
git clone https://github.com/miki4iaml/nomogram.git
|
|
51
|
+
cd nomogram
|
|
52
|
+
uv sync
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Python 3.12 or later. Plots need the `viz` extra (matplotlib):
|
|
56
|
+
`uv sync --extra viz`.
|
|
57
|
+
|
|
58
|
+
## Example
|
|
59
|
+
|
|
60
|
+
Every saturation vapour pressure formulation of the catalog against the
|
|
61
|
+
IAPWS reference, from 0 to 60 °C and within 1 %; curves stop where a formulation leaves its
|
|
62
|
+
stated validity domain, and the figure takes the option set of nomogram's
|
|
63
|
+
documentation ([script](docs/examples/saturation_vapour_pressure.py)):
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import numpy as np
|
|
67
|
+
import nomogram as nomo
|
|
68
|
+
from nomogram.units import Q_
|
|
69
|
+
|
|
70
|
+
catalog = nomo.catalog.catalog()
|
|
71
|
+
formulations = catalog.formulations_of("SaturationVapourPressureOverWater")
|
|
72
|
+
reference = catalog.formulations["iapws_sr1_86"]
|
|
73
|
+
temperatures = Q_(np.linspace(-60, 110, 341), "degC")
|
|
74
|
+
|
|
75
|
+
table = nomo.comparison.compare(
|
|
76
|
+
formulations, reference, {"ThermodynamicTemperature": temperatures}
|
|
77
|
+
)
|
|
78
|
+
with nomo.display.display_options(nomo.display.DisplayOptions.DOCUMENTATION):
|
|
79
|
+
nomo.plot.plot_deviations(table, xlim=(0, 60), ylim=(-1.0, 1.0)) # extra viz
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+

|
|
83
|
+
|
|
84
|
+
## Usage
|
|
85
|
+
|
|
86
|
+
The table holds the values and the relative deviations, by formulation;
|
|
87
|
+
values outside a formulation's validity domain are NaN unless asked
|
|
88
|
+
otherwise (`outside="ignore"`). Formulations also chain, for instance
|
|
89
|
+
the absolute humidity from any saturation vapour pressure formulation:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
ha = nomo.comparison.compose(
|
|
93
|
+
catalog.formulations["mass_concentration_ideal_gas"], {"e_s": reference}
|
|
94
|
+
)
|
|
95
|
+
ha.evaluate(RH=Q_(100, "percent"), T=Q_(20, "degC")).to("g/m^3") # 17.29
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Formulations are found by words, wildcards and fields, whatever their
|
|
99
|
+
notation ([Finding formulations](docs/search.md)):
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
catalog.search(variable="rho*") # ρ in every notation
|
|
103
|
+
catalog.search(kind="VapourPressure", source="buck*")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
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
|
|
111
|
+
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
|
|
113
|
+
nomogram.
|
|
114
|
+
|
|
115
|
+
## Contributing
|
|
116
|
+
|
|
117
|
+
References to integrate, information or corrections about catalog
|
|
118
|
+
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).
|
|
121
|
+
|
|
122
|
+
## Data sources
|
|
123
|
+
|
|
124
|
+
nomogram's units come from QUDT, and so do its quantity kinds, except
|
|
125
|
+
those QUDT lacks, which nomogram defines as specializations of QUDT
|
|
126
|
+
kinds. nomogram links some kinds to CF standard names, which come, with
|
|
127
|
+
their canonical units, from the CF standard name table. The package
|
|
128
|
+
bundles a subset of each source:
|
|
129
|
+
|
|
130
|
+
- QUDT — Quantities, Units, Dimensions and Types, by QUDT.org
|
|
131
|
+
(<https://qudt.org>), release 3.5.2, licensed under
|
|
132
|
+
[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/);
|
|
133
|
+
- the [CF standard name table](https://cfconventions.org/Data/cf-standard-names/95/build/cf-standard-name-table.html),
|
|
134
|
+
version 95, [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/);
|
|
135
|
+
- the UCUM codes of the units, as QUDT gives them: © Regenstrief
|
|
136
|
+
Institute, Inc., [UCUM License](https://ucum.org/license).
|
|
137
|
+
|
|
138
|
+
What nomogram selects, changes and adds is listed in
|
|
139
|
+
[NOTICE.md](src/nomogram/data/NOTICE.md).
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
Code: [BSD-3-Clause](LICENSE). Bundled data: see
|
|
144
|
+
[Data sources](#data-sources).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nomogram"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "Repository of scientific formulas: notations, units, validity domains"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -120,6 +120,9 @@ fail_under = 80
|
|
|
120
120
|
show_missing = true
|
|
121
121
|
exclude_also = ['if __name__ == "__main__":']
|
|
122
122
|
|
|
123
|
+
[tool.ruff.format]
|
|
124
|
+
exclude = ["*.md"]
|
|
125
|
+
|
|
123
126
|
[tool.ruff.lint]
|
|
124
127
|
extend-select = [
|
|
125
128
|
"ARG",
|
|
@@ -138,6 +141,7 @@ extend-select = [
|
|
|
138
141
|
|
|
139
142
|
[tool.codespell]
|
|
140
143
|
skip = "*.fr.md,uv.lock"
|
|
144
|
+
ignore-words-list = "flate,alph,mesures"
|
|
141
145
|
|
|
142
146
|
[tool.repo-review.ignore]
|
|
143
147
|
PC110 = "ruff format runs as a local hook through uv run --frozen (single version source: uv.lock)"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nomogram"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "Repository of scientific formulas: notations, units, validity domains"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -102,6 +102,12 @@ fail_under = 80
|
|
|
102
102
|
show_missing = true
|
|
103
103
|
exclude_also = ['if __name__ == "__main__":']
|
|
104
104
|
|
|
105
|
+
[tool.ruff.format]
|
|
106
|
+
# ruff also formats the code blocks of Markdown files when run without
|
|
107
|
+
# arguments; the documents keep their own layout (aligned comments), and
|
|
108
|
+
# tests/test_examples.py reads the values their code comments state.
|
|
109
|
+
exclude = ["*.md"]
|
|
110
|
+
|
|
105
111
|
[tool.ruff.lint]
|
|
106
112
|
# On top of ruff's default selection, which Dependabot bumps surface in CI.
|
|
107
113
|
extend-select = ["ARG", "B", "C4", "PIE", "PT", "PTH", "RET", "T20"]
|
|
@@ -114,6 +120,9 @@ extend-select = ["ARG", "B", "C4", "PIE", "PT", "PTH", "RET", "T20"]
|
|
|
114
120
|
|
|
115
121
|
[tool.codespell]
|
|
116
122
|
skip = "*.fr.md,uv.lock"
|
|
123
|
+
# Format terms: the Flate filter of PDF, the ALPH chunk of WebP; the
|
|
124
|
+
# French name of the BIPM (Bureau International des Poids et Mesures).
|
|
125
|
+
ignore-words-list = "flate,alph,mesures"
|
|
117
126
|
|
|
118
127
|
[tool.repo-review.ignore]
|
|
119
128
|
PC110 = "ruff format runs as a local hook through uv run --frozen (single version source: uv.lock)"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Miki
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
|
|
4
|
+
"""nomogram: a repository of scientific formulas.
|
|
5
|
+
|
|
6
|
+
Submodules are imported on demand, so that ``import nomogram`` stays
|
|
7
|
+
light: :mod:`nomogram.units` (pint), :mod:`nomogram.expression`
|
|
8
|
+
(SymPy), :mod:`nomogram.semantics` (bundled QUDT and CF subsets),
|
|
9
|
+
:mod:`nomogram.model` and :mod:`nomogram.catalog` (formulations),
|
|
10
|
+
:mod:`nomogram.dimensions`, :mod:`nomogram.comparison`,
|
|
11
|
+
:mod:`nomogram.inversion`, :mod:`nomogram.relations`,
|
|
12
|
+
:mod:`nomogram.search`, :mod:`nomogram.display` and :mod:`nomogram.plot`.
|
|
13
|
+
|
|
14
|
+
Each is also an attribute of the package, imported the first time it is
|
|
15
|
+
used (PEP 562)::
|
|
16
|
+
|
|
17
|
+
import nomogram as nomo
|
|
18
|
+
|
|
19
|
+
formulations = nomo.catalog.catalog().formulations_of("VapourPressure")
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from importlib import import_module
|
|
23
|
+
from importlib.metadata import version
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from nomogram import catalog as catalog
|
|
28
|
+
from nomogram import comparison as comparison
|
|
29
|
+
from nomogram import dimensions as dimensions
|
|
30
|
+
from nomogram import display as display
|
|
31
|
+
from nomogram import expression as expression
|
|
32
|
+
from nomogram import inversion as inversion
|
|
33
|
+
from nomogram import model as model
|
|
34
|
+
from nomogram import plot as plot
|
|
35
|
+
from nomogram import relations as relations
|
|
36
|
+
from nomogram import search as search
|
|
37
|
+
from nomogram import semantics as semantics
|
|
38
|
+
from nomogram import units as units
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"__version__",
|
|
42
|
+
"catalog",
|
|
43
|
+
"comparison",
|
|
44
|
+
"dimensions",
|
|
45
|
+
"display",
|
|
46
|
+
"expression",
|
|
47
|
+
"inversion",
|
|
48
|
+
"model",
|
|
49
|
+
"plot",
|
|
50
|
+
"relations",
|
|
51
|
+
"search",
|
|
52
|
+
"semantics",
|
|
53
|
+
"units",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
#: Submodules reachable as attributes of the package.
|
|
57
|
+
_SUBMODULES = frozenset(__all__) - {"__version__"}
|
|
58
|
+
|
|
59
|
+
__version__ = version("nomogram")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def __getattr__(name: str) -> object:
|
|
63
|
+
"""Import a submodule the first time it is used as an attribute."""
|
|
64
|
+
if name in _SUBMODULES:
|
|
65
|
+
return import_module(f"{__name__}.{name}")
|
|
66
|
+
msg = f"module {__name__!r} has no attribute {name!r}"
|
|
67
|
+
raise AttributeError(msg)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def __dir__() -> list[str]:
|
|
71
|
+
"""The names of the package, submodules included before their import."""
|
|
72
|
+
return sorted({*globals(), *_SUBMODULES})
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Miki
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
|
|
4
|
+
"""Geometry of the logo, NOMO, shared by its SVG files and the plots.
|
|
5
|
+
|
|
6
|
+
- N is a working alignment chart (an N or Z chart): two parallel scales,
|
|
7
|
+
u read downward on the left stroke and v upward on the right one, and a
|
|
8
|
+
diagonal scale for w = u / v, graduated at s = w / (1 + w) of its
|
|
9
|
+
length. A straight reading line through u and v crosses the diagonal at
|
|
10
|
+
u / v; :func:`n_chart` checks it.
|
|
11
|
+
- The first O is a sphere of linked nodes (shared vocabularies), the
|
|
12
|
+
second a globe in orthographic projection (the Earth sciences).
|
|
13
|
+
- M is plain, and the reading line runs on through O, M and O.
|
|
14
|
+
|
|
15
|
+
Shapes are in SVG units, y downward, with a role that says how each is
|
|
16
|
+
painted (:data:`THEMES`): ``ink`` and ``fine`` strokes (``fine`` at 55 %
|
|
17
|
+
opacity), ``read`` for the reading line, filled ``sea``, ``dot`` and
|
|
18
|
+
``node``. ``scripts/make_logo.py`` writes them as SVG,
|
|
19
|
+
:func:`nomogram.plot.sign` as matplotlib artists. Standard library only.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import math
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
|
|
27
|
+
#: Letter height and stroke widths, in SVG units.
|
|
28
|
+
H = 100.0
|
|
29
|
+
STROKE = 8.0
|
|
30
|
+
THIN = 1.6
|
|
31
|
+
TICK = 5.0
|
|
32
|
+
|
|
33
|
+
#: Reading line: u on the left scale, v on the right one (0 to 5).
|
|
34
|
+
U, V = 3.0, 2.2
|
|
35
|
+
|
|
36
|
+
#: Colours: ink, sea of the globe, accent of the reading line.
|
|
37
|
+
THEMES = {
|
|
38
|
+
"light": {"ink": "#1f2937", "sea": "#dbeafe", "accent": "#c2410c"},
|
|
39
|
+
"dark": {"ink": "#e5e7eb", "sea": "#1e3a5f", "accent": "#fb923c"},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#: Opacity of the ``fine`` strokes (graduations, graticule, links).
|
|
43
|
+
FINE_OPACITY = 0.55
|
|
44
|
+
|
|
45
|
+
Point = tuple[float, float]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class Line:
|
|
50
|
+
start: Point
|
|
51
|
+
end: Point
|
|
52
|
+
role: str
|
|
53
|
+
width: float
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class Circle:
|
|
58
|
+
centre: Point
|
|
59
|
+
radius: float
|
|
60
|
+
role: str
|
|
61
|
+
#: Stroke width; 0 for a filled circle.
|
|
62
|
+
width: float = 0.0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class Ellipse:
|
|
67
|
+
centre: Point
|
|
68
|
+
rx: float
|
|
69
|
+
ry: float
|
|
70
|
+
role: str
|
|
71
|
+
width: float
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class Polyline:
|
|
76
|
+
points: tuple[Point, ...]
|
|
77
|
+
role: str
|
|
78
|
+
width: float
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
Shape = Line | Circle | Ellipse | Polyline
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def n_chart(
|
|
85
|
+
x: float, y: float, w: float, *, stroke: float = STROKE, ticks: bool = True
|
|
86
|
+
) -> tuple[list[Shape], Point, Point]:
|
|
87
|
+
"""The N as an alignment chart; returns its shapes and the reading points."""
|
|
88
|
+
scale = H / 5.0
|
|
89
|
+
out: list[Shape] = [
|
|
90
|
+
Line((x, y), (x, y + H), "ink", stroke),
|
|
91
|
+
Line((x + w, y), (x + w, y + H), "ink", stroke),
|
|
92
|
+
Line((x, y), (x + w, y + H), "ink", stroke),
|
|
93
|
+
]
|
|
94
|
+
if ticks:
|
|
95
|
+
# u downward on the left stroke, v upward on the right one.
|
|
96
|
+
for k in range(11):
|
|
97
|
+
value = k / 2.0
|
|
98
|
+
length = TICK * (1.6 if k % 2 == 0 else 1.0)
|
|
99
|
+
left_y, right_y = y + scale * value, y + H - scale * value
|
|
100
|
+
out.append(
|
|
101
|
+
Line(
|
|
102
|
+
(x - length - stroke / 2, left_y),
|
|
103
|
+
(x - stroke / 2, left_y),
|
|
104
|
+
"fine",
|
|
105
|
+
THIN,
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
out.append(
|
|
109
|
+
Line(
|
|
110
|
+
(x + w + stroke / 2, right_y),
|
|
111
|
+
(x + w + stroke / 2 + length, right_y),
|
|
112
|
+
"fine",
|
|
113
|
+
THIN,
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
# w = u / v on the diagonal, at s = w / (1 + w) from its top end.
|
|
117
|
+
norm = math.hypot(w, H)
|
|
118
|
+
nx, ny = H / norm, -w / norm # unit normal, up and right
|
|
119
|
+
for value in (0.25, 0.5, 1.0, 2.0, 4.0):
|
|
120
|
+
s = value / (1.0 + value)
|
|
121
|
+
cx, cy = x + s * w, y + s * H
|
|
122
|
+
near, far = stroke / 2, stroke / 2 + 1.6 * TICK
|
|
123
|
+
out.append(
|
|
124
|
+
Line(
|
|
125
|
+
(cx + nx * near, cy + ny * near),
|
|
126
|
+
(cx + nx * far, cy + ny * far),
|
|
127
|
+
"fine",
|
|
128
|
+
THIN,
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
left = (x, y + scale * U)
|
|
132
|
+
right = (x + w, y + H - scale * V)
|
|
133
|
+
# The reading line crosses the diagonal at w = U / V: check it.
|
|
134
|
+
s = (U / V) / (1.0 + U / V)
|
|
135
|
+
crossing = (left[1] - y) / ((left[1] - y) + (y + H - right[1]))
|
|
136
|
+
if abs(crossing - s) > 1e-12: # pragma: no cover - a broken chart
|
|
137
|
+
msg = f"the reading line crosses the diagonal at {crossing}, not {s}"
|
|
138
|
+
raise AssertionError(msg)
|
|
139
|
+
out.append(Circle((x + s * w, y + s * H), stroke * 0.55, "dot"))
|
|
140
|
+
return out, left, right
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def reading_line(
|
|
144
|
+
left: Point, right: Point, start: float, end: float, width: float
|
|
145
|
+
) -> Line:
|
|
146
|
+
"""The straight line through both reading points, from x = start to end."""
|
|
147
|
+
slope = (right[1] - left[1]) / (right[0] - left[0])
|
|
148
|
+
return Line(
|
|
149
|
+
(start, left[1] + slope * (start - left[0])),
|
|
150
|
+
(end, left[1] + slope * (end - left[0])),
|
|
151
|
+
"read",
|
|
152
|
+
width,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def globe(centre: Point, radius: float) -> list[Shape]:
|
|
157
|
+
"""A sphere in orthographic projection, seen from the equator."""
|
|
158
|
+
cx, cy = centre
|
|
159
|
+
out: list[Shape] = [Circle(centre, radius, "sea")]
|
|
160
|
+
for longitude in (30.0, 60.0):
|
|
161
|
+
rx = radius * math.sin(math.radians(longitude))
|
|
162
|
+
out.append(Ellipse(centre, rx, radius, "fine", THIN))
|
|
163
|
+
out.append(Line((cx, cy - radius), (cx, cy + radius), "fine", THIN))
|
|
164
|
+
for latitude in (-60.0, -30.0, 0.0, 30.0, 60.0):
|
|
165
|
+
yy = cy - radius * math.sin(math.radians(latitude))
|
|
166
|
+
half = radius * math.cos(math.radians(latitude))
|
|
167
|
+
out.append(Line((cx - half, yy), (cx + half, yy), "fine", THIN))
|
|
168
|
+
out.append(Circle(centre, radius, "ink", STROKE))
|
|
169
|
+
return out
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def nodes(centre: Point, radius: float) -> list[Shape]:
|
|
173
|
+
"""A sphere of linked nodes: a Fibonacci lattice, its visible side."""
|
|
174
|
+
cx, cy = centre
|
|
175
|
+
count, points = 24, []
|
|
176
|
+
golden = math.pi * (3.0 - math.sqrt(5.0))
|
|
177
|
+
for i in range(count):
|
|
178
|
+
z = 1.0 - 2.0 * (i + 0.5) / count
|
|
179
|
+
ring = math.sqrt(1.0 - z * z)
|
|
180
|
+
x3, y3 = ring * math.cos(golden * i), ring * math.sin(golden * i)
|
|
181
|
+
if x3 > -0.1:
|
|
182
|
+
points.append((cx + 0.82 * radius * y3, cy - 0.82 * radius * z))
|
|
183
|
+
out: list[Shape] = []
|
|
184
|
+
for i, point in enumerate(points):
|
|
185
|
+
nearest = sorted(points[i + 1 :], key=lambda p: math.dist(p, point))[:2]
|
|
186
|
+
out += [
|
|
187
|
+
Line(point, other, "fine", THIN)
|
|
188
|
+
for other in nearest
|
|
189
|
+
if math.dist(point, other) < 0.75 * radius
|
|
190
|
+
]
|
|
191
|
+
out += [Circle(point, 2.2, "node") for point in points]
|
|
192
|
+
out.append(Circle(centre, radius, "ink", STROKE))
|
|
193
|
+
return out
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def m_letter(x: float, y: float, w: float) -> list[Shape]:
|
|
197
|
+
points = ((x, y + H), (x, y), (x + w / 2, y + 0.62 * H), (x + w, y), (x + w, y + H))
|
|
198
|
+
return [Polyline(points, "ink", STROKE)]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def logo() -> tuple[list[Shape], float, float]:
|
|
202
|
+
"""The shapes of NOMO, with the width and height they take."""
|
|
203
|
+
margin, width, gap = 22.0, 64.0, 26.0
|
|
204
|
+
x_n = margin
|
|
205
|
+
x_nodes = x_n + width + gap
|
|
206
|
+
x_m = x_nodes + H + gap
|
|
207
|
+
x_globe = x_m + width + gap
|
|
208
|
+
y = margin
|
|
209
|
+
shapes, left, right = n_chart(x_n, y, width)
|
|
210
|
+
shapes += nodes((x_nodes + H / 2, y + H / 2), H / 2)
|
|
211
|
+
shapes += m_letter(x_m, y, width)
|
|
212
|
+
shapes += globe((x_globe + H / 2, y + H / 2), H / 2)
|
|
213
|
+
shapes.append(reading_line(left, right, x_n - 12.0, x_globe + H + 12.0, 2.6))
|
|
214
|
+
shapes += [Circle(point, STROKE * 0.55, "dot") for point in (left, right)]
|
|
215
|
+
return shapes, x_globe + H + margin, H + 2 * margin
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def icon(*, small: bool = False) -> tuple[list[Shape], float, float]:
|
|
219
|
+
"""The N chart alone; ``small`` drops the graduations (favicon)."""
|
|
220
|
+
margin, width = 26.0, 64.0
|
|
221
|
+
stroke = 14.0 if small else STROKE
|
|
222
|
+
shapes, left, right = n_chart(margin, margin, width, stroke=stroke, ticks=not small)
|
|
223
|
+
line_width = 7.0 if small else 3.2
|
|
224
|
+
shapes.append(
|
|
225
|
+
reading_line(left, right, margin - 14.0, margin + width + 14.0, line_width)
|
|
226
|
+
)
|
|
227
|
+
shapes += [Circle(point, stroke * 0.55, "dot") for point in (left, right)]
|
|
228
|
+
return shapes, width + 2 * margin, H + 2 * margin
|