CoolPlot 1.0.0__tar.gz → 1.2.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.
- {coolplot-1.0.0/src/CoolPlot.egg-info → coolplot-1.2.0}/PKG-INFO +3 -3
- {coolplot-1.0.0 → coolplot-1.2.0}/pyproject.toml +2 -2
- {coolplot-1.0.0 → coolplot-1.2.0/src/CoolPlot.egg-info}/PKG-INFO +3 -3
- {coolplot-1.0.0 → coolplot-1.2.0}/src/CoolPlot.egg-info/SOURCES.txt +11 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/__init__.py +1 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/__version__.py +1 -1
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/chart.py +173 -27
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/compose/__init__.py +7 -4
- coolplot-1.2.0/src/coolplot/compose/branding.py +63 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/compose/builder.py +143 -20
- coolplot-1.2.0/src/coolplot/compose/corners.py +156 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/compose/layout.py +24 -6
- coolplot-1.2.0/src/coolplot/compose/rails.py +497 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/core/cycle.py +5 -2
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/core/diagram_type.py +31 -7
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/core/floats.py +12 -0
- coolplot-1.2.0/src/coolplot/core/images.py +140 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/core/quantities.py +4 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/core/state.py +25 -3
- coolplot-1.2.0/src/coolplot/core/textwidth.py +51 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/core/units.py +8 -1
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/corelib.py +55 -1
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/engine/background.py +45 -18
- coolplot-1.2.0/src/coolplot/engine/cache.py +126 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/engine/engine.py +73 -5
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/engine/planning.py +62 -6
- coolplot-1.2.0/src/coolplot/engine/store.py +282 -0
- coolplot-1.2.0/src/coolplot/psychrometric.py +177 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/render/base.py +64 -1
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/render/mpl.py +217 -2
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/render/pdf.py +3 -1
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/render/plotly.py +49 -4
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/render/svg.py +100 -34
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/render/web.py +7 -1
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/scene.py +221 -7
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/spec/__init__.py +5 -4
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/spec/diagram.py +168 -17
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/spec/settings.py +143 -14
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/style/theme.py +18 -2
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/style/values.py +2 -2
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/thermo/cycles/paths.py +20 -3
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/thermo/fluid.py +132 -18
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/thermo/humid_air.py +170 -18
- coolplot-1.2.0/src/coolplot/thermo/provider.py +176 -0
- coolplot-1.2.0/src/coolplot/thermo/references.py +318 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_api_vocabulary.py +8 -6
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_background.py +57 -0
- coolplot-1.2.0/tests/test_branding.py +550 -0
- coolplot-1.2.0/tests/test_curve_store.py +339 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_cycles.py +33 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_diagram.py +72 -6
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_engine.py +2 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_events.py +14 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_examples.py +40 -3
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_layout.py +11 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_native_core.py +165 -1
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_pdf.py +1 -0
- coolplot-1.2.0/tests/test_psychro.py +501 -0
- coolplot-1.2.0/tests/test_rails.py +317 -0
- coolplot-1.2.0/tests/test_saturation_anchors.py +213 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_style.py +37 -1
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_web.py +11 -0
- coolplot-1.0.0/src/coolplot/engine/cache.py +0 -73
- coolplot-1.0.0/src/coolplot/psychrometric.py +0 -93
- coolplot-1.0.0/src/coolplot/thermo/provider.py +0 -88
- coolplot-1.0.0/tests/test_psychro.py +0 -244
- {coolplot-1.0.0 → coolplot-1.2.0}/LICENSE +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/README.md +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/setup.cfg +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/CoolPlot.egg-info/dependency_links.txt +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/CoolPlot.egg-info/requires.txt +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/CoolPlot.egg-info/top_level.txt +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/core/__init__.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/core/errors.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/core/events.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/core/implementation.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/core/limits.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/core/numbers.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/diagram.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/engine/__init__.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/export.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/interaction.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/render/__init__.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/render/static/scene_svg.js +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/render/ticks.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/spec/fluid.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/style/__init__.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/testing/__init__.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/testing/kit.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/testing/pdf_reader.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/thermo/__init__.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/thermo/cycles/__init__.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/thermo/cycles/models.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/thermo/isolines.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/thermo/limits.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/thermo/native.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/thermo/process.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/thermo/stepping.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/src/coolplot/thermo/tracer.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_architecture.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_backend_conformance.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_contracts.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_native.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_numerics.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_plotly.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_scene_js.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_scene_render.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_spec.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_thermo.py +0 -0
- {coolplot-1.0.0 → coolplot-1.2.0}/tests/test_web_server.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: CoolPlot
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Thermodynamic property diagrams with CoolProp as data source.
|
|
5
5
|
Author-email: Jorrit Wronski <coolplot@jorrit.org>
|
|
6
6
|
License: MIT License
|
|
@@ -19,14 +19,14 @@ Classifier: Intended Audience :: Science/Research
|
|
|
19
19
|
Classifier: License :: OSI Approved :: MIT License
|
|
20
20
|
Classifier: Programming Language :: Python :: 3
|
|
21
21
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
25
24
|
Classifier: Programming Language :: Python :: 3.13
|
|
26
25
|
Classifier: Programming Language :: Python :: 3.14
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
27
27
|
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
28
28
|
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
29
|
-
Requires-Python: >=3.
|
|
29
|
+
Requires-Python: >=3.11
|
|
30
30
|
Description-Content-Type: text/markdown
|
|
31
31
|
License-File: LICENSE
|
|
32
32
|
Requires-Dist: numpy>=1.22
|
|
@@ -9,7 +9,7 @@ description = "Thermodynamic property diagrams with CoolProp as data source."
|
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {file = "LICENSE"}
|
|
11
11
|
authors = [{name = "Jorrit Wronski", email = "coolplot@jorrit.org"}]
|
|
12
|
-
requires-python = ">=3.
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
13
|
dependencies = [
|
|
14
14
|
"numpy>=1.22",
|
|
15
15
|
"coolprop>=8.0",
|
|
@@ -20,11 +20,11 @@ classifiers = [
|
|
|
20
20
|
"License :: OSI Approved :: MIT License",
|
|
21
21
|
"Programming Language :: Python :: 3",
|
|
22
22
|
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
-
"Programming Language :: Python :: 3.10",
|
|
24
23
|
"Programming Language :: Python :: 3.11",
|
|
25
24
|
"Programming Language :: Python :: 3.12",
|
|
26
25
|
"Programming Language :: Python :: 3.13",
|
|
27
26
|
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Programming Language :: Python :: 3.15",
|
|
28
28
|
"Topic :: Scientific/Engineering :: Physics",
|
|
29
29
|
"Topic :: Scientific/Engineering :: Visualization",
|
|
30
30
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: CoolPlot
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Thermodynamic property diagrams with CoolProp as data source.
|
|
5
5
|
Author-email: Jorrit Wronski <coolplot@jorrit.org>
|
|
6
6
|
License: MIT License
|
|
@@ -19,14 +19,14 @@ Classifier: Intended Audience :: Science/Research
|
|
|
19
19
|
Classifier: License :: OSI Approved :: MIT License
|
|
20
20
|
Classifier: Programming Language :: Python :: 3
|
|
21
21
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
25
24
|
Classifier: Programming Language :: Python :: 3.13
|
|
26
25
|
Classifier: Programming Language :: Python :: 3.14
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
27
27
|
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
28
28
|
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
29
|
-
Requires-Python: >=3.
|
|
29
|
+
Requires-Python: >=3.11
|
|
30
30
|
Description-Content-Type: text/markdown
|
|
31
31
|
License-File: LICENSE
|
|
32
32
|
Requires-Dist: numpy>=1.22
|
|
@@ -16,25 +16,31 @@ src/coolplot/interaction.py
|
|
|
16
16
|
src/coolplot/psychrometric.py
|
|
17
17
|
src/coolplot/scene.py
|
|
18
18
|
src/coolplot/compose/__init__.py
|
|
19
|
+
src/coolplot/compose/branding.py
|
|
19
20
|
src/coolplot/compose/builder.py
|
|
21
|
+
src/coolplot/compose/corners.py
|
|
20
22
|
src/coolplot/compose/layout.py
|
|
23
|
+
src/coolplot/compose/rails.py
|
|
21
24
|
src/coolplot/core/__init__.py
|
|
22
25
|
src/coolplot/core/cycle.py
|
|
23
26
|
src/coolplot/core/diagram_type.py
|
|
24
27
|
src/coolplot/core/errors.py
|
|
25
28
|
src/coolplot/core/events.py
|
|
26
29
|
src/coolplot/core/floats.py
|
|
30
|
+
src/coolplot/core/images.py
|
|
27
31
|
src/coolplot/core/implementation.py
|
|
28
32
|
src/coolplot/core/limits.py
|
|
29
33
|
src/coolplot/core/numbers.py
|
|
30
34
|
src/coolplot/core/quantities.py
|
|
31
35
|
src/coolplot/core/state.py
|
|
36
|
+
src/coolplot/core/textwidth.py
|
|
32
37
|
src/coolplot/core/units.py
|
|
33
38
|
src/coolplot/engine/__init__.py
|
|
34
39
|
src/coolplot/engine/background.py
|
|
35
40
|
src/coolplot/engine/cache.py
|
|
36
41
|
src/coolplot/engine/engine.py
|
|
37
42
|
src/coolplot/engine/planning.py
|
|
43
|
+
src/coolplot/engine/store.py
|
|
38
44
|
src/coolplot/render/__init__.py
|
|
39
45
|
src/coolplot/render/base.py
|
|
40
46
|
src/coolplot/render/mpl.py
|
|
@@ -62,6 +68,7 @@ src/coolplot/thermo/limits.py
|
|
|
62
68
|
src/coolplot/thermo/native.py
|
|
63
69
|
src/coolplot/thermo/process.py
|
|
64
70
|
src/coolplot/thermo/provider.py
|
|
71
|
+
src/coolplot/thermo/references.py
|
|
65
72
|
src/coolplot/thermo/stepping.py
|
|
66
73
|
src/coolplot/thermo/tracer.py
|
|
67
74
|
src/coolplot/thermo/cycles/__init__.py
|
|
@@ -71,7 +78,9 @@ tests/test_api_vocabulary.py
|
|
|
71
78
|
tests/test_architecture.py
|
|
72
79
|
tests/test_backend_conformance.py
|
|
73
80
|
tests/test_background.py
|
|
81
|
+
tests/test_branding.py
|
|
74
82
|
tests/test_contracts.py
|
|
83
|
+
tests/test_curve_store.py
|
|
75
84
|
tests/test_cycles.py
|
|
76
85
|
tests/test_diagram.py
|
|
77
86
|
tests/test_engine.py
|
|
@@ -84,6 +93,8 @@ tests/test_numerics.py
|
|
|
84
93
|
tests/test_pdf.py
|
|
85
94
|
tests/test_plotly.py
|
|
86
95
|
tests/test_psychro.py
|
|
96
|
+
tests/test_rails.py
|
|
97
|
+
tests/test_saturation_anchors.py
|
|
87
98
|
tests/test_scene_js.py
|
|
88
99
|
tests/test_scene_render.py
|
|
89
100
|
tests/test_spec.py
|
|
@@ -46,13 +46,15 @@ from .compose import compose
|
|
|
46
46
|
from .core.cycle import Cycle, StateInput
|
|
47
47
|
from .core.errors import PropertyError
|
|
48
48
|
from .core.quantities import quantity_key
|
|
49
|
-
from .core.state import StatePoint
|
|
49
|
+
from .core.state import StatePoint, with_mollier_temperature
|
|
50
50
|
from .core.units import get_unit_system
|
|
51
51
|
from .engine import BackgroundEngine, CurveCache, Engine, plan
|
|
52
52
|
from .scene import Scene
|
|
53
|
-
from .spec import CycleSpec, DiagramSpec, IsolineSet, get_settings
|
|
53
|
+
from .spec import CycleSpec, DiagramSpec, IsolineSet, SaturationAnchor, get_settings
|
|
54
|
+
from .spec.diagram import ANCHOR_KEYS
|
|
54
55
|
from .style.theme import Theme, get_theme
|
|
55
56
|
from .style.values import LineStyle, MarkerStyle
|
|
57
|
+
from .thermo.cycles.paths import resolve_state
|
|
56
58
|
|
|
57
59
|
|
|
58
60
|
def _known_key(name, inputs):
|
|
@@ -135,6 +137,9 @@ def check_state(state: StatePoint, provider) -> StatePoint:
|
|
|
135
137
|
"""
|
|
136
138
|
if "v" not in state and "rho" in state and provider.kind == "fluid" and state.rho > 0.0:
|
|
137
139
|
state = StatePoint(v=1.0 / state.rho, **state.to_dict())
|
|
140
|
+
if provider.kind == "humid_air":
|
|
141
|
+
# A humid-air state made before 1.2 lacks the temperature scale of the Mollier chart
|
|
142
|
+
state = with_mollier_temperature(state)
|
|
138
143
|
missing = [k for k in provider.quantities if k not in state]
|
|
139
144
|
if missing:
|
|
140
145
|
raise ValueError("The state {0} is not a state of {1}: it lacks {2}.".format(state, provider.name, missing))
|
|
@@ -205,6 +210,7 @@ class Chart:
|
|
|
205
210
|
self.engine = Engine(cache)
|
|
206
211
|
self.scene = Scene()
|
|
207
212
|
self._default_renderer = None
|
|
213
|
+
self._file_renderer = None # the matplotlib renderer of save(), outside pyplot
|
|
208
214
|
self._plot_size_pt = None
|
|
209
215
|
try:
|
|
210
216
|
check_fluid(self.engine, spec.fluid) # a misspelt fluid fails here, not at the first draw
|
|
@@ -306,6 +312,22 @@ class Chart:
|
|
|
306
312
|
def theme(self, value):
|
|
307
313
|
self.settings = self.spec.settings.with_(look=get_theme(value))
|
|
308
314
|
|
|
315
|
+
@property
|
|
316
|
+
def show_saturation(self):
|
|
317
|
+
"""Whether the saturation curve is drawn whatever lines are set.
|
|
318
|
+
|
|
319
|
+
True by default: the quality lines 0 and 1 of a fluid with a
|
|
320
|
+
critical point, or the line of 100 % relative humidity, are drawn
|
|
321
|
+
even when the lines set leave them out, or are not set at all.
|
|
322
|
+
False hides the curve; None leaves it to the values of the quality
|
|
323
|
+
or humidity lines (a chart saved before 1.2).
|
|
324
|
+
"""
|
|
325
|
+
return self.spec.show_saturation
|
|
326
|
+
|
|
327
|
+
@show_saturation.setter
|
|
328
|
+
def show_saturation(self, value):
|
|
329
|
+
self.spec = self.spec.with_(show_saturation=value)
|
|
330
|
+
|
|
309
331
|
@property
|
|
310
332
|
def title(self) -> str:
|
|
311
333
|
"""The title shown above the chart."""
|
|
@@ -358,7 +380,7 @@ class Chart:
|
|
|
358
380
|
# Content
|
|
359
381
|
# ------------------------------------------------------------------
|
|
360
382
|
def set_lines(self, key: str, values=None, num: int = None, range=None, rounding: bool = None,
|
|
361
|
-
labels: bool = None, points: int = None, units=None):
|
|
383
|
+
labels: bool = None, points: int = None, units=None, at_saturation=None):
|
|
362
384
|
"""Add or replace the family of lines of constant ``key``.
|
|
363
385
|
|
|
364
386
|
Parameters
|
|
@@ -366,7 +388,8 @@ class Chart:
|
|
|
366
388
|
key : str
|
|
367
389
|
Quantity held constant: "T", "p", "h", "s", "rho", "u" or "Q"
|
|
368
390
|
in a property diagram; "RH", "Twb", "h" or "v" in a
|
|
369
|
-
psychrometric chart
|
|
391
|
+
psychrometric chart, and also "T" (isotherms) in a Mollier
|
|
392
|
+
h,x chart.
|
|
370
393
|
values : sequence, optional
|
|
371
394
|
Explicit constant values, in the chart's units or in ``units``.
|
|
372
395
|
num : int, optional
|
|
@@ -388,7 +411,27 @@ class Chart:
|
|
|
388
411
|
points : int, optional
|
|
389
412
|
Points per line; default from ``settings.calc``.
|
|
390
413
|
units : str or UnitSystem, optional
|
|
391
|
-
Units of ``values`` and ``
|
|
414
|
+
Units of ``values``, ``range`` and ``at_saturation``; default the
|
|
415
|
+
chart's units.
|
|
416
|
+
at_saturation : dict, optional
|
|
417
|
+
Place the lines through saturation states at even steps instead
|
|
418
|
+
of giving values: ``{"T": 10}`` puts a line through the saturated
|
|
419
|
+
vapour every 10 K of saturation temperature (in deg C charts at
|
|
420
|
+
0, 10, 20 deg C), ``{"p": 2}`` every 2 bar of saturation
|
|
421
|
+
pressure. ``"Q": 0`` uses the saturated liquid (the bubble point
|
|
422
|
+
of a blend) instead of the vapour (the dew point), and
|
|
423
|
+
``"range": (lo, hi)`` limits the saturation temperatures or
|
|
424
|
+
pressures. Steps and range are in ``units``. The labels show the
|
|
425
|
+
lines' own values ("s=2.321 kJ/kg/K"); ``"label": "saturation"``
|
|
426
|
+
shows the saturation state instead ("T_sat=0 deg C"). Charts of
|
|
427
|
+
a fluid only.
|
|
428
|
+
|
|
429
|
+
Examples
|
|
430
|
+
--------
|
|
431
|
+
Isentropes through the saturated vapour every 10 K, as compressor
|
|
432
|
+
lines read off at round evaporating temperatures:
|
|
433
|
+
|
|
434
|
+
>>> diagram.set_lines("s", at_saturation={"T": 10}, labels=True) # doctest: +SKIP
|
|
392
435
|
"""
|
|
393
436
|
key = quantity_key(key)
|
|
394
437
|
family = self._family(key)
|
|
@@ -404,9 +447,43 @@ class Chart:
|
|
|
404
447
|
_checked_range(family, label, range_SI)
|
|
405
448
|
if num is None:
|
|
406
449
|
num = family.default_num
|
|
407
|
-
|
|
450
|
+
anchor = None
|
|
451
|
+
if at_saturation is not None:
|
|
452
|
+
if values is not None or range is not None:
|
|
453
|
+
raise ValueError("Give the lines either values or a range, or at_saturation, not both; a range "
|
|
454
|
+
"of saturation states goes into at_saturation as 'range'.")
|
|
455
|
+
anchor = self._saturation_anchor(at_saturation, units)
|
|
456
|
+
spec = self.spec.with_isolines(IsolineSet(key, values_SI, num, range_SI, rounding, points, labels, anchor))
|
|
457
|
+
if anchor is not None:
|
|
458
|
+
# Place the lines now, so a step that is too small or a fluid
|
|
459
|
+
# without a saturation curve is reported here, not at the next draw
|
|
460
|
+
self.engine.domain(spec)
|
|
461
|
+
self.spec = spec
|
|
408
462
|
return key
|
|
409
463
|
|
|
464
|
+
def _saturation_anchor(self, at_saturation, units) -> SaturationAnchor:
|
|
465
|
+
"""A SaturationAnchor from set_lines(at_saturation=...), converted from ``units`` to SI."""
|
|
466
|
+
if isinstance(at_saturation, SaturationAnchor):
|
|
467
|
+
return at_saturation
|
|
468
|
+
given = dict(at_saturation)
|
|
469
|
+
span = given.pop("range", None)
|
|
470
|
+
label = given.pop("label", "value")
|
|
471
|
+
qualities = [given.pop(name) for name in ("Q", "x", "q") if name in given] # written as the labels write it
|
|
472
|
+
if len(qualities) > 1:
|
|
473
|
+
raise ValueError("Give the vapour quality of the saturation states once, as 'Q' or 'x'.")
|
|
474
|
+
Q = float(qualities[0]) if qualities else 1.0
|
|
475
|
+
if len(given) != 1:
|
|
476
|
+
raise ValueError("at_saturation takes one step, by saturation temperature or pressure, e.g. "
|
|
477
|
+
"{{'T': 10}} or {{'p': 2, 'Q': 0}}, not {0}.".format(at_saturation))
|
|
478
|
+
(by, step), = given.items()
|
|
479
|
+
by = quantity_key(by)
|
|
480
|
+
if by not in ANCHOR_KEYS:
|
|
481
|
+
raise ValueError("Lines are placed by saturation temperature 'T' or pressure 'p', not '{0}'.".format(by))
|
|
482
|
+
unit = self._units(units)[by]
|
|
483
|
+
step_SI = float(step) / abs(unit.scale) # a difference: the scale only, no offset
|
|
484
|
+
span_SI = None if span is None else tuple(float(v) for v in unit.to_SI(span))
|
|
485
|
+
return SaturationAnchor(by, step_SI, Q, span_SI, label)
|
|
486
|
+
|
|
410
487
|
def _family(self, key: str):
|
|
411
488
|
"""The line family of a quantity in this chart, or ValueError."""
|
|
412
489
|
family = self.spec.diagram.family(key)
|
|
@@ -454,7 +531,7 @@ class Chart:
|
|
|
454
531
|
|
|
455
532
|
def add_cycle(self, name, states=None, loops=None, paths=None, numbers: bool = True, units=None,
|
|
456
533
|
line_style: LineStyle = None, marker_style: MarkerStyle = None, steps: int = 20,
|
|
457
|
-
closed: bool = True, **arrays):
|
|
534
|
+
closed: bool = True, labels=None, **arrays):
|
|
458
535
|
"""Add or replace a cycle, drawn on top of the chart.
|
|
459
536
|
|
|
460
537
|
Parameters
|
|
@@ -492,6 +569,11 @@ class Chart:
|
|
|
492
569
|
Segments per drawn path.
|
|
493
570
|
closed : bool
|
|
494
571
|
Whether the default loop returns to the first state.
|
|
572
|
+
labels : dict, optional
|
|
573
|
+
``{number: text}``: a text written next to a state, after its
|
|
574
|
+
number, e.g. ``{2: "ADP 11.2 deg C", 3: "+8.1 kJ/kg"}`` shows
|
|
575
|
+
"2: ADP 11.2 deg C". With ``numbers=False`` the text stands
|
|
576
|
+
alone and states without a label get nothing.
|
|
495
577
|
**arrays
|
|
496
578
|
Instead of ``states``: arrays of two properties, e.g.
|
|
497
579
|
``add_cycle("measured", p=[...], h=[...])``.
|
|
@@ -503,17 +585,18 @@ class Chart:
|
|
|
503
585
|
cycle = name if isinstance(name, Cycle) else (states if isinstance(states, Cycle) else None)
|
|
504
586
|
if cycle is not None:
|
|
505
587
|
spec = self._spec_from_model(cycle, None if isinstance(name, Cycle) else name, loops, paths, numbers,
|
|
506
|
-
line_style, marker_style, steps)
|
|
588
|
+
line_style, marker_style, steps, labels)
|
|
507
589
|
else:
|
|
508
590
|
unit_system = self._units(units)
|
|
509
591
|
inputs = self._state_inputs(_states_from_arrays(states, arrays) if arrays else states, name, unit_system)
|
|
510
592
|
spec = CycleSpec(str(name), inputs, loops or _default_loops(inputs, closed), paths or (), numbers,
|
|
511
|
-
line_style, marker_style, steps)
|
|
593
|
+
line_style, marker_style, steps, () if labels is None else labels)
|
|
512
594
|
self._compute_cycle(spec) # compute now, so that errors show up here
|
|
513
595
|
self.spec = self.spec.with_cycle(spec)
|
|
514
596
|
return spec.name
|
|
515
597
|
|
|
516
|
-
def _spec_from_model(self, cycle: Cycle, name, loops, paths, numbers, line_style, marker_style, steps
|
|
598
|
+
def _spec_from_model(self, cycle: Cycle, name, loops, paths, numbers, line_style, marker_style, steps,
|
|
599
|
+
labels=None):
|
|
517
600
|
"""A CycleSpec for a computed Cycle, after checking that it belongs to this chart's fluid."""
|
|
518
601
|
if cycle.fluid and self._other_fluid(cycle.fluid):
|
|
519
602
|
raise ValueError("Cycle '{0}' was computed for {1}, but this chart shows {2}.".format(
|
|
@@ -521,7 +604,8 @@ class Chart:
|
|
|
521
604
|
states = tuple((n, check_state(state, self.fluid)) for n, state in cycle.states)
|
|
522
605
|
cycle = replace(cycle, states=states) # with v filled in for a cycle computed before 0.7
|
|
523
606
|
return CycleSpec.from_cycle(cycle, name=name, loops=loops, paths=paths, numbers=numbers,
|
|
524
|
-
line_style=line_style, marker_style=marker_style, steps=steps
|
|
607
|
+
line_style=line_style, marker_style=marker_style, steps=steps,
|
|
608
|
+
labels=labels)
|
|
525
609
|
|
|
526
610
|
def _state_inputs(self, states, name, unit_system):
|
|
527
611
|
"""(number, StateInput) pairs from a list (numbered from 1) or a dict of given states."""
|
|
@@ -592,6 +676,24 @@ class Chart:
|
|
|
592
676
|
return {n: states[n].in_units(unit_system) for n in sorted(states)}
|
|
593
677
|
return {n: states[n] for n in sorted(states)}
|
|
594
678
|
|
|
679
|
+
def state(self, entry, units=None) -> StatePoint:
|
|
680
|
+
"""The full state (SI units) from two properties in the chart's units or in ``units``.
|
|
681
|
+
|
|
682
|
+
``entry`` is a dict as for a state of :meth:`add_cycle`, e.g.
|
|
683
|
+
``chart.state({"T": 13, "RH": 95})`` on a psychrometric chart in
|
|
684
|
+
engineering units, or a StatePoint (returned as it is, after a
|
|
685
|
+
check that it belongs to this chart). A "via" needs a state before
|
|
686
|
+
it and is not accepted here.
|
|
687
|
+
"""
|
|
688
|
+
given = state_input(entry, self._units(units), self.fluid)
|
|
689
|
+
if given.via is not None:
|
|
690
|
+
raise ValueError("A state with via '{0}' needs a state before it; give two properties.".format(
|
|
691
|
+
given.via))
|
|
692
|
+
try:
|
|
693
|
+
return resolve_state(self.fluid, given)
|
|
694
|
+
except PropertyError as e:
|
|
695
|
+
raise self._in_units(e) from None
|
|
696
|
+
|
|
595
697
|
# ------------------------------------------------------------------
|
|
596
698
|
# Interaction helpers
|
|
597
699
|
# ------------------------------------------------------------------
|
|
@@ -629,7 +731,8 @@ class Chart:
|
|
|
629
731
|
curves = self.engine.curves(spec, domain, plan(spec, domain))
|
|
630
732
|
previous = {item.id: item for item in self.scene if item.role == "isoline_label"}
|
|
631
733
|
cycles = [self.engine.cycle(spec, c) for c in spec.cycles]
|
|
632
|
-
items, axes = compose(spec, domain, curves, self._plot_size_pt, previous, cycles
|
|
734
|
+
items, axes = compose(spec, domain, curves, self._plot_size_pt, previous, cycles,
|
|
735
|
+
self.engine.source_lines(spec))
|
|
633
736
|
self.scene.replace_all(items, axes=axes)
|
|
634
737
|
return self.scene
|
|
635
738
|
|
|
@@ -662,18 +765,20 @@ class Chart:
|
|
|
662
765
|
"""Let background work finish before a script draws: previews first,
|
|
663
766
|
then the final curves they make room for. Warns, instead of waiting
|
|
664
767
|
for ever, when nothing finishes any more."""
|
|
665
|
-
last = None
|
|
666
768
|
for _ in range(10):
|
|
769
|
+
# Counted from before the scene: a job can end while the scene is
|
|
770
|
+
# built (a crashed process pool fails all of its jobs at once), and
|
|
771
|
+
# the retries it queues next only end in the wait after that scene
|
|
772
|
+
finished = self.engine.finished_jobs
|
|
667
773
|
self.update_scene()
|
|
668
774
|
ready, needed = self.engine.progress
|
|
669
775
|
if not self.engine.busy and ready >= needed: # a failed line may still be due a retry
|
|
670
776
|
return
|
|
671
|
-
|
|
777
|
+
self.wait()
|
|
778
|
+
if self.engine.finished_jobs == finished: # neither the scene nor the wait finished a job
|
|
672
779
|
warnings.warn("Background work makes no progress, the drawing may be incomplete.",
|
|
673
780
|
UserWarning, stacklevel=3)
|
|
674
781
|
return
|
|
675
|
-
last = self.engine.finished_jobs
|
|
676
|
-
self.wait()
|
|
677
782
|
warnings.warn("Background work did not finish, the drawing may be incomplete (is the cache "
|
|
678
783
|
"smaller than the number of lines?).", UserWarning, stacklevel=3)
|
|
679
784
|
|
|
@@ -687,21 +792,62 @@ class Chart:
|
|
|
687
792
|
``.svg`` is written by the SVG writer and ``.html`` by the web
|
|
688
793
|
renderer (a static page), neither of which needs matplotlib;
|
|
689
794
|
every other extension (PNG, PDF, EPS, ...) goes through matplotlib,
|
|
690
|
-
with ``kwargs`` such as ``dpi`` or ``pdf_fonts`` passed on.
|
|
691
|
-
|
|
692
|
-
|
|
795
|
+
with ``kwargs`` such as ``dpi`` or ``pdf_fonts`` passed on.
|
|
796
|
+
|
|
797
|
+
An SVG file has the size of ``settings.page``, stated in points
|
|
798
|
+
(``width``, ``height`` and ``unit`` passed as ``kwargs`` override
|
|
799
|
+
it). Matplotlib files are drawn into a figure of their own outside
|
|
800
|
+
pyplot, of the size of ``settings.page``, so that saving charts in
|
|
801
|
+
a server or a loop does not collect pyplot figures; after
|
|
802
|
+
:meth:`draw` or :meth:`show` made a figure, that figure is saved
|
|
803
|
+
instead, at its own size. An HTML page has the size of
|
|
804
|
+
``settings.page`` in pixels, unless ``width`` and ``height`` are given.
|
|
693
805
|
"""
|
|
694
806
|
extension = str(path).rsplit(".", 1)[-1].lower() if "." in str(path) else ""
|
|
695
|
-
|
|
696
|
-
from .render.svg import PX_PER_PT
|
|
697
|
-
page = self.spec.settings.page
|
|
698
|
-
kwargs.setdefault("width", round(page.width_pt * PX_PER_PT))
|
|
699
|
-
kwargs.setdefault("height", round(page.height_pt * PX_PER_PT))
|
|
807
|
+
page = self.spec.settings.page
|
|
700
808
|
if extension == "svg":
|
|
701
|
-
from .render.svg import SvgRenderer
|
|
702
|
-
|
|
809
|
+
from .render.svg import PX_PER_PT, SvgRenderer
|
|
810
|
+
options = dict(width=page.width_pt * PX_PER_PT, height=page.height_pt * PX_PER_PT, unit="pt")
|
|
811
|
+
options.update(kwargs)
|
|
812
|
+
self.draw(SvgRenderer(**options)).save(path)
|
|
703
813
|
elif extension in ("html", "htm"):
|
|
814
|
+
from .render.svg import PX_PER_PT
|
|
704
815
|
from .render.web import WebRenderer
|
|
816
|
+
kwargs.setdefault("width", round(page.width_pt * PX_PER_PT))
|
|
817
|
+
kwargs.setdefault("height", round(page.height_pt * PX_PER_PT))
|
|
705
818
|
self.draw(WebRenderer(**kwargs)).save(path)
|
|
706
|
-
|
|
819
|
+
elif self._default_renderer is not None:
|
|
707
820
|
self.draw().save(path, **kwargs)
|
|
821
|
+
else:
|
|
822
|
+
self.draw(self._matplotlib_file_renderer()).save(path, **kwargs)
|
|
823
|
+
|
|
824
|
+
def _matplotlib_file_renderer(self):
|
|
825
|
+
"""The renderer of :meth:`save` for matplotlib formats: a figure outside pyplot, sized by settings.page."""
|
|
826
|
+
size = self.spec.settings.page.size_in
|
|
827
|
+
if self._file_renderer is None:
|
|
828
|
+
from .render.mpl import MatplotlibRenderer
|
|
829
|
+
self._file_renderer = MatplotlibRenderer(use_pyplot=False, figsize=size)
|
|
830
|
+
elif tuple(self._file_renderer.figure.get_size_inches()) != tuple(size):
|
|
831
|
+
self._file_renderer.figure.set_size_inches(size) # the page settings changed since
|
|
832
|
+
return self._file_renderer
|
|
833
|
+
|
|
834
|
+
def uncached_curves(self) -> int:
|
|
835
|
+
"""How many isolines of the chart are in neither part of the curve cache.
|
|
836
|
+
|
|
837
|
+
Lines in memory or in the cache's folder on disk (see
|
|
838
|
+
:class:`coolplot.engine.CurveCache`) do not count; looking for one
|
|
839
|
+
on disk loads it into memory, as the next update would. The answer
|
|
840
|
+
costs the calculation region of the chart (the T-p box and, for
|
|
841
|
+
mixtures, the phase envelope), which the next update needs anyway,
|
|
842
|
+
but no line is calculated. An application can use it to tell a
|
|
843
|
+
request that only draws from one that calculates.
|
|
844
|
+
|
|
845
|
+
It counts the final lines of the isoline families only: cycles are
|
|
846
|
+
left out, and with a background engine a line counts until its
|
|
847
|
+
final version is in the cache, also while a job for it is running.
|
|
848
|
+
"""
|
|
849
|
+
spec = self.spec
|
|
850
|
+
domain = self.engine.domain(spec)
|
|
851
|
+
cache = self.engine.cache
|
|
852
|
+
return sum(1 for request in plan(spec, domain)
|
|
853
|
+
if cache.peek(self.engine.cache_key(spec, domain, request)) is None)
|
|
@@ -14,14 +14,17 @@ from .builder import compose as compose_python, label_text, scaled_theme
|
|
|
14
14
|
__all__ = ["compose", "compose_python", "label_text", "scaled_theme"]
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def compose(spec, domain, curves, plot_size_pt=None, previous_labels=None, cycles=None):
|
|
17
|
+
def compose(spec, domain, curves, plot_size_pt=None, previous_labels=None, cycles=None, sources=()):
|
|
18
18
|
"""All scene items and the axes for a diagram, see :func:`coolplot.compose.builder.compose`.
|
|
19
19
|
|
|
20
20
|
Composed by coolplot-core when it is installed and the native mode is
|
|
21
21
|
"on" (see :func:`coolplot.corelib.enabled`), by the Python composer
|
|
22
|
-
otherwise; both give the same scene.
|
|
22
|
+
otherwise; both give the same scene. coolplot-core does not compose
|
|
23
|
+
the branding (badge and watermark), so a branded chart always takes
|
|
24
|
+
the Python composer.
|
|
23
25
|
"""
|
|
24
|
-
|
|
26
|
+
branding = spec.settings.branding
|
|
27
|
+
if corelib.enabled() and not (branding.has_badge or branding.watermark):
|
|
25
28
|
try:
|
|
26
29
|
result = corelib.compose(spec, domain, curves, plot_size_pt, previous_labels, cycles)
|
|
27
30
|
except corelib.CoreRefusal as error:
|
|
@@ -32,4 +35,4 @@ def compose(spec, domain, curves, plot_size_pt=None, previous_labels=None, cycle
|
|
|
32
35
|
result = None
|
|
33
36
|
if result is not None:
|
|
34
37
|
return result
|
|
35
|
-
return compose_python(spec, domain, curves, plot_size_pt, previous_labels, cycles)
|
|
38
|
+
return compose_python(spec, domain, curves, plot_size_pt, previous_labels, cycles, sources)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""The badge and the watermark of a chart, from its branding settings.
|
|
3
|
+
|
|
4
|
+
:func:`branding` turns :class:`coolplot.spec.settings.BrandingSettings`
|
|
5
|
+
into the :class:`coolplot.scene.Badge` and :class:`coolplot.scene.Watermark`
|
|
6
|
+
of the axes, sized in points for the plot area the labels are laid out
|
|
7
|
+
for. The badge is made in the corner of the settings (the lower right
|
|
8
|
+
one for "auto"); the composer then gives it its corner together with the
|
|
9
|
+
legend
|
|
10
|
+
(:func:`coolplot.compose.corners.place_corners`), so that an automatic
|
|
11
|
+
one never takes the corner of the other, and isoline labels keep clear of
|
|
12
|
+
both.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Optional, Sequence, Tuple
|
|
17
|
+
|
|
18
|
+
from ..core.images import load_picture
|
|
19
|
+
from ..scene import Badge, Watermark
|
|
20
|
+
from ..spec.diagram import DiagramSpec
|
|
21
|
+
from ..style.theme import Theme
|
|
22
|
+
from ..style.values import Font, TextStyle
|
|
23
|
+
from .layout import Frame
|
|
24
|
+
|
|
25
|
+
#: The watermark is never taller than this fraction of the plot area.
|
|
26
|
+
WATERMARK_MAX_HEIGHT = 0.8
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def branding(spec: DiagramSpec, theme: Theme, frame: Frame,
|
|
30
|
+
sources: Sequence[str] = ()) -> Tuple[Optional[Badge], Optional[Watermark]]:
|
|
31
|
+
"""(badge, watermark) for the chart; None for what the settings do not ask for.
|
|
32
|
+
|
|
33
|
+
``theme`` is the theme with the page's font scale applied and
|
|
34
|
+
``frame`` the plot area the labels are laid out in. ``sources`` are
|
|
35
|
+
the lines naming the sources of the properties and the versions, from
|
|
36
|
+
:meth:`coolplot.engine.Engine.source_lines` (the composer does not ask
|
|
37
|
+
CoolProp itself); the badge shows them above ``BrandingSettings.lines``.
|
|
38
|
+
The badge sits in the corner of the settings, the lower right one for
|
|
39
|
+
"auto", until the composer places it.
|
|
40
|
+
"""
|
|
41
|
+
settings = spec.settings.branding
|
|
42
|
+
watermark = None
|
|
43
|
+
if settings.watermark:
|
|
44
|
+
pictures = tuple(load_picture(s) for s in settings.watermark)
|
|
45
|
+
width = settings.watermark_width * frame.width_pt
|
|
46
|
+
if width * pictures[0].aspect > WATERMARK_MAX_HEIGHT * frame.height_pt:
|
|
47
|
+
width = WATERMARK_MAX_HEIGHT * frame.height_pt / pictures[0].aspect
|
|
48
|
+
if width > 0.0:
|
|
49
|
+
watermark = Watermark(picture=pictures, width_pt=width, opacity=settings.watermark_opacity)
|
|
50
|
+
if not settings.has_badge:
|
|
51
|
+
return None, watermark
|
|
52
|
+
axes_style = theme.axes
|
|
53
|
+
font_scale = spec.settings.page.font_scale
|
|
54
|
+
text = TextStyle(font=Font(family=axes_style.tick_label.font.family, size_pt=settings.font_size_pt * font_scale),
|
|
55
|
+
color=axes_style.tick_label.color or "#000000", h_align="left", v_align="bottom")
|
|
56
|
+
location = "lower right" if settings.location == "auto" else settings.location
|
|
57
|
+
badge = Badge(location=location, logo=tuple(load_picture(s) for s in settings.logo),
|
|
58
|
+
logo_width_pt=settings.logo_width_pt, lines=tuple(sources) + settings.lines, text=text,
|
|
59
|
+
background=axes_style.background or "#ffffff", background_alpha=settings.background_alpha,
|
|
60
|
+
frame_color=axes_style.frame_color if settings.frame else None)
|
|
61
|
+
if badge.empty:
|
|
62
|
+
return None, watermark
|
|
63
|
+
return badge, watermark
|