nomogram 0.3.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.
Files changed (35) hide show
  1. {nomogram-0.3.0 → nomogram-0.4.0}/PKG-INFO +61 -22
  2. {nomogram-0.3.0 → nomogram-0.4.0}/README.md +60 -21
  3. {nomogram-0.3.0 → nomogram-0.4.0}/pyproject.toml +5 -1
  4. {nomogram-0.3.0 → nomogram-0.4.0}/pyproject.toml.orig +10 -1
  5. nomogram-0.4.0/src/nomogram/__init__.py +72 -0
  6. nomogram-0.4.0/src/nomogram/_logo.py +228 -0
  7. {nomogram-0.3.0 → nomogram-0.4.0}/src/nomogram/catalog.py +1376 -129
  8. {nomogram-0.3.0 → nomogram-0.4.0}/src/nomogram/comparison.py +17 -3
  9. nomogram-0.4.0/src/nomogram/data/NOTICE.md +40 -0
  10. {nomogram-0.3.0 → nomogram-0.4.0}/src/nomogram/data/catalog/core.toml +124 -10
  11. nomogram-0.4.0/src/nomogram/data/catalog/moist-air.toml +2056 -0
  12. {nomogram-0.3.0 → nomogram-0.4.0}/src/nomogram/data/catalog/radio.toml +2 -1
  13. {nomogram-0.3.0 → nomogram-0.4.0}/src/nomogram/data/catalog/sea-water.toml +16 -14
  14. nomogram-0.4.0/src/nomogram/data/catalog/verifications.json +205 -0
  15. {nomogram-0.3.0 → nomogram-0.4.0}/src/nomogram/data/catalog/water-vapour.toml +232 -6
  16. {nomogram-0.3.0 → nomogram-0.4.0}/src/nomogram/data/semantics.json +684 -74
  17. {nomogram-0.3.0 → nomogram-0.4.0}/src/nomogram/dimensions.py +102 -24
  18. {nomogram-0.3.0 → nomogram-0.4.0}/src/nomogram/display.py +85 -6
  19. nomogram-0.4.0/src/nomogram/expression.py +709 -0
  20. {nomogram-0.3.0 → nomogram-0.4.0}/src/nomogram/inversion.py +219 -55
  21. {nomogram-0.3.0 → nomogram-0.4.0}/src/nomogram/model.py +547 -38
  22. nomogram-0.4.0/src/nomogram/plot.py +695 -0
  23. {nomogram-0.3.0 → nomogram-0.4.0}/src/nomogram/relations.py +617 -35
  24. {nomogram-0.3.0 → nomogram-0.4.0}/src/nomogram/search.py +158 -37
  25. {nomogram-0.3.0 → nomogram-0.4.0}/src/nomogram/semantics.py +29 -9
  26. nomogram-0.3.0/src/nomogram/__init__.py +0 -17
  27. nomogram-0.3.0/src/nomogram/data/NOTICE.md +0 -28
  28. nomogram-0.3.0/src/nomogram/data/catalog/moist-air.toml +0 -323
  29. nomogram-0.3.0/src/nomogram/data/catalog/verifications.json +0 -133
  30. nomogram-0.3.0/src/nomogram/expression.py +0 -384
  31. nomogram-0.3.0/src/nomogram/plot.py +0 -237
  32. {nomogram-0.3.0 → nomogram-0.4.0}/LICENSE +0 -0
  33. {nomogram-0.3.0 → nomogram-0.4.0}/src/nomogram/data/catalog/humidity.toml +0 -0
  34. {nomogram-0.3.0 → nomogram-0.4.0}/src/nomogram/py.typed +0 -0
  35. {nomogram-0.3.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.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
- # nomogram
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
  [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
48
53
  [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-fe5196)](https://www.conventionalcommits.org/en/v1.0.0/)
49
54
 
50
- A repository of scientific formulas: alternative notations across
51
- authors and conventions, parameter units (including empirical ones),
52
- validity domains, LaTeX rendering and symbolic computation.
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; curves stop where a formulation leaves its stated
81
- validity domain ([script](docs/examples/saturation_vapour_pressure.py)):
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
- from nomogram.catalog import catalog
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
- formulations = catalog().formulations_of("SaturationVapourPressureOverWater")
91
- reference = catalog().formulations["iapws_sr1_86"]
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(formulations, reference, {"ThermodynamicTemperature": temperatures})
95
- plot_deviations(table)
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
- ![Deviation of saturation vapour pressure formulations from IAPWS](https://raw.githubusercontent.com/miki4iaml/nomogram/v0.3.0/docs/images/saturation-vapour-pressure.png)
116
+ ![Deviation of saturation vapour pressure formulations from IAPWS](https://raw.githubusercontent.com/miki4iaml/nomogram/v0.4.0/docs/images/saturation-vapour-pressure.png)
99
117
 
100
118
  ## Usage
101
119
 
@@ -105,9 +123,9 @@ 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
- from nomogram.comparison import compose
109
-
110
- ha = compose(catalog().formulations["mass_concentration_ideal_gas"], {"e_s": reference})
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
 
@@ -115,8 +133,8 @@ Formulations are found by words, wildcards and fields, whatever their
115
133
  notation ([Finding formulations](docs/search.md)):
116
134
 
117
135
  ```python
118
- catalog().search(variable="rho*") # ρ in every notation
119
- catalog().search(kind="VapourPressure", source="buck*")
136
+ catalog.search(variable="rho*") # ρ in every notation
137
+ catalog.search(kind="VapourPressure", source="buck*")
120
138
  ```
121
139
 
122
140
  Each formulation keeps its units, validity domain, temperature scale and
@@ -125,7 +143,8 @@ references. The vocabulary is mapped in [docs/rosetta.md](docs/rosetta.md);
125
143
  how, and [Displaying formulas](docs/display.md) shows formulas in notebooks,
126
144
  Markdown and plots. [Sea water](docs/sea-water.md) shows the salinity
127
145
  scales and the vapour pressure of sea water, and what its 2 % lowering
128
- changes at the sea surface.
146
+ changes at the sea surface. [The logo](docs/logo.md) is a working
147
+ nomogram.
129
148
 
130
149
  ## Contributing
131
150
 
@@ -134,6 +153,26 @@ entries, new formulas and code are all welcome: see the
134
153
  [ways to contribute](.github/CONTRIBUTING.md#ways-to-contribute) and
135
154
  the [catalog guide](docs/catalog.md).
136
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
+
137
175
  ## License
138
176
 
139
- [BSD-3-Clause](LICENSE).
177
+ Code: [BSD-3-Clause](LICENSE). Bundled data: see
178
+ [Data sources](#data-sources).
@@ -1,4 +1,9 @@
1
- # nomogram
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>
2
7
 
3
8
  **English** | [Français](README.fr.md)
4
9
 
@@ -13,9 +18,19 @@
13
18
  [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
14
19
  [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-fe5196)](https://www.conventionalcommits.org/en/v1.0.0/)
15
20
 
16
- A repository of scientific formulas: alternative notations across
17
- authors and conventions, parameter units (including empirical ones),
18
- validity domains, LaTeX rendering and symbolic computation.
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.
19
34
 
20
35
  Pure Python, built on SymPy, numpy and pint; it works on an offline
21
36
  workstation (no online engine).
@@ -43,25 +58,28 @@ Python 3.12 or later. Plots need the `viz` extra (matplotlib):
43
58
  ## Example
44
59
 
45
60
  Every saturation vapour pressure formulation of the catalog against the
46
- IAPWS reference; curves stop where a formulation leaves its stated
47
- validity domain ([script](docs/examples/saturation_vapour_pressure.py)):
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)):
48
64
 
49
65
  ```python
50
66
  import numpy as np
51
- from nomogram.catalog import catalog
52
- from nomogram.comparison import compare
53
- from nomogram.plot import plot_deviations # extra viz
67
+ import nomogram as nomo
54
68
  from nomogram.units import Q_
55
69
 
56
- formulations = catalog().formulations_of("SaturationVapourPressureOverWater")
57
- reference = catalog().formulations["iapws_sr1_86"]
70
+ catalog = nomo.catalog.catalog()
71
+ formulations = catalog.formulations_of("SaturationVapourPressureOverWater")
72
+ reference = catalog.formulations["iapws_sr1_86"]
58
73
  temperatures = Q_(np.linspace(-60, 110, 341), "degC")
59
74
 
60
- table = compare(formulations, reference, {"ThermodynamicTemperature": temperatures})
61
- plot_deviations(table)
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
62
80
  ```
63
81
 
64
- ![Deviation of saturation vapour pressure formulations from IAPWS](https://raw.githubusercontent.com/miki4iaml/nomogram/v0.3.0/docs/images/saturation-vapour-pressure.png)
82
+ ![Deviation of saturation vapour pressure formulations from IAPWS](https://raw.githubusercontent.com/miki4iaml/nomogram/v0.4.0/docs/images/saturation-vapour-pressure.png)
65
83
 
66
84
  ## Usage
67
85
 
@@ -71,9 +89,9 @@ otherwise (`outside="ignore"`). Formulations also chain, for instance
71
89
  the absolute humidity from any saturation vapour pressure formulation:
72
90
 
73
91
  ```python
74
- from nomogram.comparison import compose
75
-
76
- ha = compose(catalog().formulations["mass_concentration_ideal_gas"], {"e_s": reference})
92
+ ha = nomo.comparison.compose(
93
+ catalog.formulations["mass_concentration_ideal_gas"], {"e_s": reference}
94
+ )
77
95
  ha.evaluate(RH=Q_(100, "percent"), T=Q_(20, "degC")).to("g/m^3") # 17.29
78
96
  ```
79
97
 
@@ -81,8 +99,8 @@ Formulations are found by words, wildcards and fields, whatever their
81
99
  notation ([Finding formulations](docs/search.md)):
82
100
 
83
101
  ```python
84
- catalog().search(variable="rho*") # ρ in every notation
85
- catalog().search(kind="VapourPressure", source="buck*")
102
+ catalog.search(variable="rho*") # ρ in every notation
103
+ catalog.search(kind="VapourPressure", source="buck*")
86
104
  ```
87
105
 
88
106
  Each formulation keeps its units, validity domain, temperature scale and
@@ -91,7 +109,8 @@ references. The vocabulary is mapped in [docs/rosetta.md](docs/rosetta.md);
91
109
  how, and [Displaying formulas](docs/display.md) shows formulas in notebooks,
92
110
  Markdown and plots. [Sea water](docs/sea-water.md) shows the salinity
93
111
  scales and the vapour pressure of sea water, and what its 2 % lowering
94
- changes at the sea surface.
112
+ changes at the sea surface. [The logo](docs/logo.md) is a working
113
+ nomogram.
95
114
 
96
115
  ## Contributing
97
116
 
@@ -100,6 +119,26 @@ entries, new formulas and code are all welcome: see the
100
119
  [ways to contribute](.github/CONTRIBUTING.md#ways-to-contribute) and
101
120
  the [catalog guide](docs/catalog.md).
102
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
+
103
141
  ## License
104
142
 
105
- [BSD-3-Clause](LICENSE).
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.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.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