pychnosz 1.1.11__cp312-cp312-win_amd64.whl
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.
- pychnosz/__init__.py +129 -0
- pychnosz/biomolecules/__init__.py +29 -0
- pychnosz/biomolecules/ionize_aa.py +197 -0
- pychnosz/biomolecules/proteins.py +595 -0
- pychnosz/core/__init__.py +46 -0
- pychnosz/core/affinity.py +1256 -0
- pychnosz/core/animation.py +593 -0
- pychnosz/core/balance.py +334 -0
- pychnosz/core/basis.py +716 -0
- pychnosz/core/diagram.py +3336 -0
- pychnosz/core/equilibrate.py +813 -0
- pychnosz/core/equilibrium.py +554 -0
- pychnosz/core/info.py +821 -0
- pychnosz/core/retrieve.py +364 -0
- pychnosz/core/speciation.py +580 -0
- pychnosz/core/species.py +599 -0
- pychnosz/core/subcrt.py +1696 -0
- pychnosz/core/thermo.py +593 -0
- pychnosz/core/unicurve.py +1226 -0
- pychnosz/data/__init__.py +11 -0
- pychnosz/data/add_obigt.py +327 -0
- pychnosz/data/extdata/Berman/BDat17_2017.csv +2 -0
- pychnosz/data/extdata/Berman/Ber88_1988.csv +68 -0
- pychnosz/data/extdata/Berman/Ber90_1990.csv +5 -0
- pychnosz/data/extdata/Berman/DS10_2010.csv +6 -0
- pychnosz/data/extdata/Berman/FDM+14_2014.csv +2 -0
- pychnosz/data/extdata/Berman/Got04_2004.csv +5 -0
- pychnosz/data/extdata/Berman/JUN92_1992.csv +3 -0
- pychnosz/data/extdata/Berman/SHD91_1991.csv +12 -0
- pychnosz/data/extdata/Berman/VGT92_1992.csv +2 -0
- pychnosz/data/extdata/Berman/VPT01_2001.csv +3 -0
- pychnosz/data/extdata/Berman/VPV05_2005.csv +2 -0
- pychnosz/data/extdata/Berman/ZS92_1992.csv +11 -0
- pychnosz/data/extdata/Berman/sympy.R +99 -0
- pychnosz/data/extdata/Berman/testing/BA96.bib +12 -0
- pychnosz/data/extdata/Berman/testing/BA96_Berman.csv +21 -0
- pychnosz/data/extdata/Berman/testing/BA96_OBIGT.csv +21 -0
- pychnosz/data/extdata/Berman/testing/BA96_refs.csv +6 -0
- pychnosz/data/extdata/OBIGT/AD.csv +25 -0
- pychnosz/data/extdata/OBIGT/Berman_cr.csv +93 -0
- pychnosz/data/extdata/OBIGT/DEW.csv +211 -0
- pychnosz/data/extdata/OBIGT/H2O_aq.csv +4 -0
- pychnosz/data/extdata/OBIGT/SLOP98.csv +411 -0
- pychnosz/data/extdata/OBIGT/SUPCRT92.csv +178 -0
- pychnosz/data/extdata/OBIGT/inorganic_aq.csv +729 -0
- pychnosz/data/extdata/OBIGT/inorganic_cr.csv +273 -0
- pychnosz/data/extdata/OBIGT/inorganic_gas.csv +20 -0
- pychnosz/data/extdata/OBIGT/organic_aq.csv +1104 -0
- pychnosz/data/extdata/OBIGT/organic_cr.csv +481 -0
- pychnosz/data/extdata/OBIGT/organic_gas.csv +268 -0
- pychnosz/data/extdata/OBIGT/organic_liq.csv +533 -0
- pychnosz/data/extdata/OBIGT/testing/GEMSFIT.csv +43 -0
- pychnosz/data/extdata/OBIGT/testing/IGEM.csv +17 -0
- pychnosz/data/extdata/OBIGT/testing/Sandia.csv +8 -0
- pychnosz/data/extdata/OBIGT/testing/SiO2.csv +4 -0
- pychnosz/data/extdata/misc/AD03_Fig1a.csv +69 -0
- pychnosz/data/extdata/misc/AD03_Fig1b.csv +43 -0
- pychnosz/data/extdata/misc/AD03_Fig1c.csv +89 -0
- pychnosz/data/extdata/misc/AD03_Fig1d.csv +30 -0
- pychnosz/data/extdata/misc/BZA10.csv +5 -0
- pychnosz/data/extdata/misc/HW97_Cp.csv +90 -0
- pychnosz/data/extdata/misc/HWM96_V.csv +229 -0
- pychnosz/data/extdata/misc/LA19_test.csv +7 -0
- pychnosz/data/extdata/misc/Mer75_Table4.csv +42 -0
- pychnosz/data/extdata/misc/OBIGT_check.csv +423 -0
- pychnosz/data/extdata/misc/PM90.csv +7 -0
- pychnosz/data/extdata/misc/RH95.csv +23 -0
- pychnosz/data/extdata/misc/RH98_Table15.csv +17 -0
- pychnosz/data/extdata/misc/SC10_Rainbow.csv +19 -0
- pychnosz/data/extdata/misc/SK95.csv +55 -0
- pychnosz/data/extdata/misc/SOJSH.csv +61 -0
- pychnosz/data/extdata/misc/SS98_Fig5a.csv +81 -0
- pychnosz/data/extdata/misc/SS98_Fig5b.csv +84 -0
- pychnosz/data/extdata/misc/TKSS14_Fig2.csv +25 -0
- pychnosz/data/extdata/misc/bluered.txt +1000 -0
- pychnosz/data/extdata/protein/Cas/Cas_aa.csv +177 -0
- pychnosz/data/extdata/protein/Cas/Cas_uniprot.csv +186 -0
- pychnosz/data/extdata/protein/Cas/download.R +34 -0
- pychnosz/data/extdata/protein/Cas/mkaa.R +34 -0
- pychnosz/data/extdata/protein/POLG.csv +12 -0
- pychnosz/data/extdata/protein/TBD+05.csv +393 -0
- pychnosz/data/extdata/protein/TBD+05_aa.csv +393 -0
- pychnosz/data/extdata/protein/rubisco.csv +28 -0
- pychnosz/data/extdata/protein/rubisco.fasta +239 -0
- pychnosz/data/extdata/protein/rubisco_aa.csv +28 -0
- pychnosz/data/extdata/src/H2O92D.f.orig +3457 -0
- pychnosz/data/extdata/src/README.txt +5 -0
- pychnosz/data/extdata/taxonomy/names.dmp +215 -0
- pychnosz/data/extdata/taxonomy/nodes.dmp +63 -0
- pychnosz/data/extdata/thermo/Bdot_acirc.csv +60 -0
- pychnosz/data/extdata/thermo/buffer.csv +40 -0
- pychnosz/data/extdata/thermo/element.csv +135 -0
- pychnosz/data/extdata/thermo/groups.csv +6 -0
- pychnosz/data/extdata/thermo/opt.csv +2 -0
- pychnosz/data/extdata/thermo/protein.csv +506 -0
- pychnosz/data/extdata/thermo/refs.csv +343 -0
- pychnosz/data/extdata/thermo/stoich.csv.xz +0 -0
- pychnosz/data/loader.py +431 -0
- pychnosz/data/mod_obigt.py +322 -0
- pychnosz/data/obigt.py +471 -0
- pychnosz/data/worm.py +228 -0
- pychnosz/fortran/__init__.py +16 -0
- pychnosz/fortran/h2o92.dll +0 -0
- pychnosz/fortran/h2o92_interface.py +527 -0
- pychnosz/geochemistry/__init__.py +21 -0
- pychnosz/geochemistry/minerals.py +514 -0
- pychnosz/geochemistry/redox.py +500 -0
- pychnosz/models/__init__.py +47 -0
- pychnosz/models/archer_wang.py +165 -0
- pychnosz/models/berman.py +309 -0
- pychnosz/models/cgl.py +381 -0
- pychnosz/models/dew.py +997 -0
- pychnosz/models/hkf.py +523 -0
- pychnosz/models/hkf_helpers.py +231 -0
- pychnosz/models/iapws95.py +1113 -0
- pychnosz/models/supcrt92_fortran.py +238 -0
- pychnosz/models/water.py +480 -0
- pychnosz/utils/__init__.py +27 -0
- pychnosz/utils/expression.py +1074 -0
- pychnosz/utils/formula.py +830 -0
- pychnosz/utils/formula_ox.py +227 -0
- pychnosz/utils/reset.py +33 -0
- pychnosz/utils/units.py +259 -0
- pychnosz-1.1.11.dist-info/METADATA +197 -0
- pychnosz-1.1.11.dist-info/RECORD +128 -0
- pychnosz-1.1.11.dist-info/WHEEL +5 -0
- pychnosz-1.1.11.dist-info/licenses/LICENSE.txt +19 -0
- pychnosz-1.1.11.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1074 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Expression utilities for formatted labels and text.
|
|
3
|
+
|
|
4
|
+
This module provides Python equivalents of the R functions in util.expression.R:
|
|
5
|
+
- ratlab(): Create formatted text for activity ratios
|
|
6
|
+
- expr_species(): Format chemical formula for display
|
|
7
|
+
- syslab(): Create formatted text for thermodynamic systems
|
|
8
|
+
|
|
9
|
+
Author: CHNOSZ Python port
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from typing import Optional
|
|
14
|
+
from .formula import makeup
|
|
15
|
+
|
|
16
|
+
# Optional imports for ratlab_html
|
|
17
|
+
try:
|
|
18
|
+
from wormutils import chemlabel
|
|
19
|
+
from chemparse import parse_formula
|
|
20
|
+
_HTML_DEPS_AVAILABLE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
_HTML_DEPS_AVAILABLE = False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def ratlab(top: str = "K+", bottom: str = "H+", molality: bool = False,
|
|
26
|
+
reverse_charge: bool = False) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Create formatted text label for activity ratio.
|
|
29
|
+
|
|
30
|
+
This function generates a LaTeX-formatted string suitable for use as
|
|
31
|
+
axis labels in matplotlib plots, showing the ratio of activities of
|
|
32
|
+
two ions raised to appropriate powers based on their charges.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
top : str, default "K+"
|
|
37
|
+
Chemical formula for the numerator ion
|
|
38
|
+
bottom : str, default "H+"
|
|
39
|
+
Chemical formula for the denominator ion
|
|
40
|
+
molality : bool, default False
|
|
41
|
+
If True, use 'm' (molality) instead of 'a' (activity)
|
|
42
|
+
reverse_charge : bool, default False
|
|
43
|
+
If True, reverse charge order in formatting (e.g., "Fe+3" becomes "Fe^{3+}")
|
|
44
|
+
If False, keep original order (e.g., "Fe+3" becomes "Fe^{+3}")
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
str
|
|
49
|
+
LaTeX-formatted string for the activity ratio label
|
|
50
|
+
|
|
51
|
+
Examples
|
|
52
|
+
--------
|
|
53
|
+
>>> ratlab("K+", "H+")
|
|
54
|
+
'log($a_{K^{+}}$ / $a_{H^{+}}$)'
|
|
55
|
+
|
|
56
|
+
>>> ratlab("Ca+2", "H+")
|
|
57
|
+
'log($a_{Ca^{+2}}$ / $a_{H^{+}}^{2}$)'
|
|
58
|
+
|
|
59
|
+
>>> ratlab("Ca+2", "H+", reverse_charge=True)
|
|
60
|
+
'log($a_{Ca^{2+}}$ / $a_{H^{+}}^{2}$)'
|
|
61
|
+
|
|
62
|
+
>>> ratlab("Mg+2", "Ca+2")
|
|
63
|
+
'log($a_{Mg^{+2}}$ / $a_{Ca^{+2}}$)'
|
|
64
|
+
|
|
65
|
+
Notes
|
|
66
|
+
-----
|
|
67
|
+
The exponents are determined by the charges of the ions to maintain
|
|
68
|
+
charge balance in the ratio. For example, for Ca+2/H+, the H+ term
|
|
69
|
+
is squared because Ca has a +2 charge.
|
|
70
|
+
|
|
71
|
+
The output format is compatible with matplotlib's LaTeX rendering.
|
|
72
|
+
In R CHNOSZ, this uses plotmath expressions; here we use LaTeX strings
|
|
73
|
+
that matplotlib can render.
|
|
74
|
+
"""
|
|
75
|
+
# Get the charges of the ions
|
|
76
|
+
makeup_top = makeup(top)
|
|
77
|
+
makeup_bottom = makeup(bottom)
|
|
78
|
+
|
|
79
|
+
Z_top = makeup_top.get('Z', 0)
|
|
80
|
+
Z_bottom = makeup_bottom.get('Z', 0)
|
|
81
|
+
|
|
82
|
+
# The exponents for charge balance
|
|
83
|
+
# If top has charge +2 and bottom has +1, bottom gets exponent 2
|
|
84
|
+
exp_bottom = abs(Z_top)
|
|
85
|
+
exp_top = abs(Z_bottom)
|
|
86
|
+
|
|
87
|
+
# Format exponents (don't show if = 1)
|
|
88
|
+
exp_top_str = "" if exp_top == 1 else f"^{{{int(exp_top)}}}"
|
|
89
|
+
exp_bottom_str = "" if exp_bottom == 1 else f"^{{{int(exp_bottom)}}}"
|
|
90
|
+
|
|
91
|
+
# Format the ion formulas for display
|
|
92
|
+
top_formatted = _format_species_latex(top, reverse_charge=reverse_charge)
|
|
93
|
+
bottom_formatted = _format_species_latex(bottom, reverse_charge=reverse_charge)
|
|
94
|
+
|
|
95
|
+
# Choose activity or molality symbol
|
|
96
|
+
a = "m" if molality else "a"
|
|
97
|
+
|
|
98
|
+
# Build the expression
|
|
99
|
+
# Format: log(a_top^exp / a_bottom^exp)
|
|
100
|
+
numerator = f"${a}_{{{top_formatted}}}{exp_top_str}$"
|
|
101
|
+
denominator = f"${a}_{{{bottom_formatted}}}{exp_bottom_str}$"
|
|
102
|
+
|
|
103
|
+
label = f"log({numerator} / {denominator})"
|
|
104
|
+
|
|
105
|
+
return label
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def ratlab_html(top: str = "K+", bottom: str = "H+", molality: bool = False) -> str:
|
|
109
|
+
"""
|
|
110
|
+
Create HTML-formatted text label for activity ratio (for Plotly/HTML rendering).
|
|
111
|
+
|
|
112
|
+
This function generates an HTML-formatted string suitable for use with
|
|
113
|
+
Plotly interactive plots, showing the ratio of activities of two ions
|
|
114
|
+
raised to appropriate powers based on their charges.
|
|
115
|
+
|
|
116
|
+
This is a companion function to ratlab() which produces LaTeX format for
|
|
117
|
+
matplotlib. Use ratlab_html() when creating labels for diagram(..., interactive=True).
|
|
118
|
+
|
|
119
|
+
Parameters
|
|
120
|
+
----------
|
|
121
|
+
top : str, default "K+"
|
|
122
|
+
Chemical formula for the numerator ion
|
|
123
|
+
bottom : str, default "H+"
|
|
124
|
+
Chemical formula for the denominator ion
|
|
125
|
+
molality : bool, default False
|
|
126
|
+
If True, use 'm' (molality) instead of 'a' (activity)
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
str
|
|
131
|
+
HTML-formatted string for the activity ratio label
|
|
132
|
+
|
|
133
|
+
Examples
|
|
134
|
+
--------
|
|
135
|
+
>>> ratlab_html("K+", "H+")
|
|
136
|
+
'log(a<sub>K<sup>+</sup></sub>/a<sub>H<sup>+</sup></sub>)'
|
|
137
|
+
|
|
138
|
+
>>> ratlab_html("Ca+2", "H+")
|
|
139
|
+
'log(a<sub>Ca<sup>2+</sup></sub>/a<sup>2</sup><sub>H<sup>+</sup></sub>)'
|
|
140
|
+
|
|
141
|
+
>>> ratlab_html("Mg+2", "Ca+2")
|
|
142
|
+
'log(a<sub>Mg<sup>2+</sup></sub>/a<sub>Ca<sup>2+</sup></sub>)'
|
|
143
|
+
|
|
144
|
+
Notes
|
|
145
|
+
-----
|
|
146
|
+
The exponents are determined by the charges of the ions to maintain
|
|
147
|
+
charge balance in the ratio. For example, for Ca+2/H+, the H+ term
|
|
148
|
+
is squared because Ca has a +2 charge.
|
|
149
|
+
|
|
150
|
+
The output format uses HTML tags (<sub>, <sup>) compatible with Plotly.
|
|
151
|
+
For matplotlib plots with LaTeX rendering, use ratlab() instead.
|
|
152
|
+
|
|
153
|
+
Requires: WORMutils (for chemlabel) and chemparse (for parse_formula)
|
|
154
|
+
|
|
155
|
+
See Also
|
|
156
|
+
--------
|
|
157
|
+
ratlab : LaTeX version for matplotlib
|
|
158
|
+
"""
|
|
159
|
+
if not _HTML_DEPS_AVAILABLE:
|
|
160
|
+
raise ImportError(
|
|
161
|
+
"ratlab_html() requires 'WORMutils' and 'chemparse' packages.\n"
|
|
162
|
+
"Install with: pip install WORMutils chemparse"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Parse the formulas to get charges
|
|
166
|
+
top_formula = parse_formula(top)
|
|
167
|
+
if "+" in top_formula.keys():
|
|
168
|
+
top_charge = top_formula["+"]
|
|
169
|
+
elif "-" in top_formula.keys():
|
|
170
|
+
top_charge = -top_formula["-"]
|
|
171
|
+
else:
|
|
172
|
+
raise ValueError("Cannot create an ion ratio involving one or more neutral species.")
|
|
173
|
+
|
|
174
|
+
bottom_formula = parse_formula(bottom)
|
|
175
|
+
if "+" in bottom_formula.keys():
|
|
176
|
+
bottom_charge = bottom_formula["+"]
|
|
177
|
+
elif "-" in bottom_formula.keys():
|
|
178
|
+
bottom_charge = -bottom_formula["-"]
|
|
179
|
+
else:
|
|
180
|
+
raise ValueError("Cannot create an ion ratio involving one or more neutral species.")
|
|
181
|
+
|
|
182
|
+
# Convert to integers if whole numbers
|
|
183
|
+
if top_charge.is_integer():
|
|
184
|
+
top_charge = int(top_charge)
|
|
185
|
+
|
|
186
|
+
if bottom_charge.is_integer():
|
|
187
|
+
bottom_charge = int(bottom_charge)
|
|
188
|
+
|
|
189
|
+
# The exponents for charge balance
|
|
190
|
+
# If top has charge +2 and bottom has +1, bottom gets exponent 2
|
|
191
|
+
exp_bottom = abs(top_charge)
|
|
192
|
+
exp_top = abs(bottom_charge)
|
|
193
|
+
|
|
194
|
+
# Format exponents as superscripts (don't show if = 1)
|
|
195
|
+
if exp_top != 1:
|
|
196
|
+
top_exp_str = "<sup>" + str(exp_top) + "</sup>"
|
|
197
|
+
else:
|
|
198
|
+
top_exp_str = ""
|
|
199
|
+
|
|
200
|
+
if exp_bottom != 1:
|
|
201
|
+
bottom_exp_str = "<sup>" + str(exp_bottom) + "</sup>"
|
|
202
|
+
else:
|
|
203
|
+
bottom_exp_str = ""
|
|
204
|
+
|
|
205
|
+
# Choose activity or molality symbol
|
|
206
|
+
if molality:
|
|
207
|
+
sym = "m"
|
|
208
|
+
else:
|
|
209
|
+
sym = "a"
|
|
210
|
+
|
|
211
|
+
# Format the chemical formulas with chemlabel
|
|
212
|
+
top_formatted = chemlabel(top)
|
|
213
|
+
bottom_formatted = chemlabel(bottom)
|
|
214
|
+
|
|
215
|
+
# Build the HTML expression
|
|
216
|
+
# Format: log(a_top^exp / a_bottom^exp)
|
|
217
|
+
return f"log({sym}{top_exp_str}<sub>{top_formatted}</sub>/{sym}{bottom_exp_str}<sub>{bottom_formatted}</sub>)"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _format_species_latex(formula: str, reverse_charge: bool = False) -> str:
|
|
221
|
+
"""
|
|
222
|
+
Format a chemical formula for LaTeX rendering.
|
|
223
|
+
|
|
224
|
+
This converts a chemical formula like "Ca+2" to LaTeX format.
|
|
225
|
+
|
|
226
|
+
Parameters
|
|
227
|
+
----------
|
|
228
|
+
formula : str
|
|
229
|
+
Chemical formula
|
|
230
|
+
reverse_charge : bool, default False
|
|
231
|
+
If True, reverse charge order (e.g., "Fe+3" becomes "Fe^{3+}")
|
|
232
|
+
If False, keep original order (e.g., "Fe+3" becomes "Fe^{+3}")
|
|
233
|
+
|
|
234
|
+
Returns
|
|
235
|
+
-------
|
|
236
|
+
str
|
|
237
|
+
LaTeX-formatted formula
|
|
238
|
+
"""
|
|
239
|
+
# Handle charge at the end
|
|
240
|
+
# Look for patterns like +, -, +2, -2, +3, etc.
|
|
241
|
+
charge_match = re.search(r'([+-])(\d*)$', formula)
|
|
242
|
+
|
|
243
|
+
if charge_match:
|
|
244
|
+
sign = charge_match.group(1)
|
|
245
|
+
magnitude = charge_match.group(2)
|
|
246
|
+
|
|
247
|
+
# Get the base formula (without charge)
|
|
248
|
+
base = formula[:charge_match.start()]
|
|
249
|
+
|
|
250
|
+
# Format the charge
|
|
251
|
+
if magnitude == '' or magnitude == '1':
|
|
252
|
+
# Single charge: Ca+ or Ca-
|
|
253
|
+
charge_str = f"^{{{sign}}}"
|
|
254
|
+
else:
|
|
255
|
+
# Multiple charges: Ca+2 can be Ca^{+3} or Ca^{3+}
|
|
256
|
+
if reverse_charge:
|
|
257
|
+
# Reversed: magnitude first, then sign (e.g., Ca^{2+})
|
|
258
|
+
charge_str = f"^{{{magnitude}{sign}}}"
|
|
259
|
+
else:
|
|
260
|
+
# Original order: sign first, then magnitude (e.g., Ca^{+2})
|
|
261
|
+
charge_str = f"^{{{sign}{magnitude}}}"
|
|
262
|
+
|
|
263
|
+
# Add subscripts for numbers in the base formula
|
|
264
|
+
base_formatted = _add_subscripts(base)
|
|
265
|
+
|
|
266
|
+
return f"{base_formatted}{charge_str}"
|
|
267
|
+
else:
|
|
268
|
+
# No charge, just add subscripts
|
|
269
|
+
return _add_subscripts(formula)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _add_subscripts(formula: str) -> str:
|
|
273
|
+
"""
|
|
274
|
+
Add LaTeX subscripts for numbers in a chemical formula.
|
|
275
|
+
|
|
276
|
+
Parameters
|
|
277
|
+
----------
|
|
278
|
+
formula : str
|
|
279
|
+
Chemical formula without charge
|
|
280
|
+
|
|
281
|
+
Returns
|
|
282
|
+
-------
|
|
283
|
+
str
|
|
284
|
+
Formula with subscripts in LaTeX format
|
|
285
|
+
"""
|
|
286
|
+
# Replace numbers following letters with subscripts
|
|
287
|
+
# H2O becomes H_{2}O, CO2 becomes CO_{2}
|
|
288
|
+
result = re.sub(r'([A-Z][a-z]?)(\d+)', r'\1_{\2}', formula)
|
|
289
|
+
return result
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def syslab(system: list = None, dash: str = "-") -> str:
|
|
293
|
+
"""
|
|
294
|
+
Create formatted text for thermodynamic system.
|
|
295
|
+
|
|
296
|
+
This generates a label showing the components of a thermodynamic system,
|
|
297
|
+
separated by dashes (or other separator).
|
|
298
|
+
|
|
299
|
+
Parameters
|
|
300
|
+
----------
|
|
301
|
+
system : list of str, optional
|
|
302
|
+
List of component formulas. Default: ["K2O", "Al2O3", "SiO2", "H2O"]
|
|
303
|
+
dash : str, default "-"
|
|
304
|
+
Separator between components
|
|
305
|
+
|
|
306
|
+
Returns
|
|
307
|
+
-------
|
|
308
|
+
str
|
|
309
|
+
LaTeX-formatted string for the system label
|
|
310
|
+
|
|
311
|
+
Examples
|
|
312
|
+
--------
|
|
313
|
+
>>> syslab(["K2O", "Al2O3", "SiO2", "H2O"])
|
|
314
|
+
'$K_{2}O-Al_{2}O_{3}-SiO_{2}-H_{2}O$'
|
|
315
|
+
|
|
316
|
+
>>> syslab(["CaO", "MgO", "SiO2"], dash="–")
|
|
317
|
+
'$CaO–MgO–SiO_{2}$'
|
|
318
|
+
"""
|
|
319
|
+
if system is None:
|
|
320
|
+
system = ["K2O", "Al2O3", "SiO2", "H2O"]
|
|
321
|
+
|
|
322
|
+
# Format each component
|
|
323
|
+
formatted_components = []
|
|
324
|
+
for component in system:
|
|
325
|
+
formatted = _add_subscripts(component)
|
|
326
|
+
formatted_components.append(formatted)
|
|
327
|
+
|
|
328
|
+
# Join with separator
|
|
329
|
+
label = dash.join(formatted_components)
|
|
330
|
+
|
|
331
|
+
# Wrap in LaTeX math mode
|
|
332
|
+
return f"${label}$"
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def syslab_html(system: list = None, dash: str = "-") -> str:
|
|
336
|
+
"""
|
|
337
|
+
Create HTML-formatted text for thermodynamic system (for Plotly).
|
|
338
|
+
|
|
339
|
+
This generates a label showing the components of a thermodynamic system,
|
|
340
|
+
separated by dashes (or other separator), using HTML formatting compatible
|
|
341
|
+
with Plotly instead of LaTeX.
|
|
342
|
+
|
|
343
|
+
Parameters
|
|
344
|
+
----------
|
|
345
|
+
system : list of str, optional
|
|
346
|
+
List of component formulas. Default: ["K2O", "Al2O3", "SiO2", "H2O"]
|
|
347
|
+
dash : str, default "-"
|
|
348
|
+
Separator between components
|
|
349
|
+
|
|
350
|
+
Returns
|
|
351
|
+
-------
|
|
352
|
+
str
|
|
353
|
+
HTML-formatted string for the system label
|
|
354
|
+
|
|
355
|
+
Examples
|
|
356
|
+
--------
|
|
357
|
+
>>> syslab_html(["K2O", "Al2O3", "SiO2", "H2O"])
|
|
358
|
+
'K<sub>2</sub>O-Al<sub>2</sub>O<sub>3</sub>-SiO<sub>2</sub>-H<sub>2</sub>O'
|
|
359
|
+
|
|
360
|
+
>>> syslab_html(["CaO", "MgO", "SiO2"], dash="–")
|
|
361
|
+
'CaO–MgO–SiO<sub>2</sub>'
|
|
362
|
+
|
|
363
|
+
Notes
|
|
364
|
+
-----
|
|
365
|
+
Use this function instead of syslab() when creating titles for interactive
|
|
366
|
+
(Plotly) diagrams. The HTML formatting is compatible with Plotly's rendering.
|
|
367
|
+
|
|
368
|
+
Requires: WORMutils (for chemlabel)
|
|
369
|
+
"""
|
|
370
|
+
if not _HTML_DEPS_AVAILABLE:
|
|
371
|
+
raise ImportError(
|
|
372
|
+
"syslab_html() requires 'WORMutils' package.\n"
|
|
373
|
+
"Install with: pip install WORMutils"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
if system is None:
|
|
377
|
+
system = ["K2O", "Al2O3", "SiO2", "H2O"]
|
|
378
|
+
|
|
379
|
+
# Format each component using HTML via chemlabel
|
|
380
|
+
formatted_components = []
|
|
381
|
+
for component in system:
|
|
382
|
+
formatted = chemlabel(component)
|
|
383
|
+
formatted_components.append(formatted)
|
|
384
|
+
|
|
385
|
+
# Join with separator (no HTML wrapper needed)
|
|
386
|
+
label = dash.join(formatted_components)
|
|
387
|
+
|
|
388
|
+
return label
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def expr_species(formula: str, state: Optional[str] = None, use_state: bool = False) -> str:
|
|
392
|
+
"""
|
|
393
|
+
Format a chemical species formula for display.
|
|
394
|
+
|
|
395
|
+
This is a simplified version that returns LaTeX-formatted strings
|
|
396
|
+
suitable for matplotlib. The R version returns plotmath expressions.
|
|
397
|
+
|
|
398
|
+
Parameters
|
|
399
|
+
----------
|
|
400
|
+
formula : str
|
|
401
|
+
Chemical formula
|
|
402
|
+
state : str, optional
|
|
403
|
+
Physical state (aq, cr, gas, liq)
|
|
404
|
+
use_state : bool, default False
|
|
405
|
+
Whether to include state in the formatted output
|
|
406
|
+
|
|
407
|
+
Returns
|
|
408
|
+
-------
|
|
409
|
+
str
|
|
410
|
+
LaTeX-formatted formula string
|
|
411
|
+
|
|
412
|
+
Examples
|
|
413
|
+
--------
|
|
414
|
+
>>> expr_species("H2O")
|
|
415
|
+
'$H_{2}O$'
|
|
416
|
+
|
|
417
|
+
>>> expr_species("Ca+2")
|
|
418
|
+
'$Ca^{2+}$'
|
|
419
|
+
|
|
420
|
+
>>> expr_species("SO4-2")
|
|
421
|
+
'$SO_{4}^{2-}$'
|
|
422
|
+
"""
|
|
423
|
+
formatted = _format_species_latex(formula)
|
|
424
|
+
|
|
425
|
+
if use_state and state:
|
|
426
|
+
# Add state subscript
|
|
427
|
+
return f"${formatted}_{{{state}}}$"
|
|
428
|
+
else:
|
|
429
|
+
return f"${formatted}$"
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def describe_property(property: list = None, value: list = None,
|
|
433
|
+
digits: int = 0, oneline: bool = False,
|
|
434
|
+
ret_val: bool = False) -> list:
|
|
435
|
+
"""
|
|
436
|
+
Create formatted text describing thermodynamic properties and their values.
|
|
437
|
+
|
|
438
|
+
This function generates formatted strings for displaying property-value pairs
|
|
439
|
+
in legends, typically for temperature, pressure, and other conditions.
|
|
440
|
+
|
|
441
|
+
Parameters
|
|
442
|
+
----------
|
|
443
|
+
property : list of str
|
|
444
|
+
Property names (e.g., ["T", "P"])
|
|
445
|
+
value : list
|
|
446
|
+
Property values (e.g., [300, 1000])
|
|
447
|
+
digits : int, default 0
|
|
448
|
+
Number of decimal places to display
|
|
449
|
+
oneline : bool, default False
|
|
450
|
+
If True, combine all properties on one line (not implemented)
|
|
451
|
+
ret_val : bool, default False
|
|
452
|
+
If True, return only values with units (not property names)
|
|
453
|
+
|
|
454
|
+
Returns
|
|
455
|
+
-------
|
|
456
|
+
list of str
|
|
457
|
+
Formatted property descriptions
|
|
458
|
+
|
|
459
|
+
Examples
|
|
460
|
+
--------
|
|
461
|
+
>>> describe_property(["T", "P"], [300, 1000])
|
|
462
|
+
['$T$ = 300 °C', '$P$ = 1000 bar']
|
|
463
|
+
|
|
464
|
+
>>> describe_property(["T"], [25], digits=1)
|
|
465
|
+
['$T$ = 25.0 °C']
|
|
466
|
+
|
|
467
|
+
Notes
|
|
468
|
+
-----
|
|
469
|
+
This is used to create legend entries showing the conditions
|
|
470
|
+
used in thermodynamic calculations.
|
|
471
|
+
"""
|
|
472
|
+
if property is None or value is None:
|
|
473
|
+
raise ValueError("property or value is None")
|
|
474
|
+
|
|
475
|
+
descriptions = []
|
|
476
|
+
|
|
477
|
+
for i in range(len(property)):
|
|
478
|
+
prop = property[i]
|
|
479
|
+
val = value[i]
|
|
480
|
+
|
|
481
|
+
# Get property symbol
|
|
482
|
+
if prop == "T":
|
|
483
|
+
prop_str = "$T$"
|
|
484
|
+
if val == "Psat" or val == "NA":
|
|
485
|
+
val_str = "$P_{sat}$"
|
|
486
|
+
else:
|
|
487
|
+
val_formatted = format(round(float(val), digits), f'.{digits}f')
|
|
488
|
+
val_str = f"{val_formatted} °C"
|
|
489
|
+
elif prop == "P":
|
|
490
|
+
prop_str = "$P$"
|
|
491
|
+
if val == "Psat" or val == "NA":
|
|
492
|
+
val_str = "$P_{sat}$"
|
|
493
|
+
else:
|
|
494
|
+
val_formatted = format(round(float(val), digits), f'.{digits}f')
|
|
495
|
+
val_str = f"{val_formatted} bar"
|
|
496
|
+
elif prop == "pH":
|
|
497
|
+
prop_str = "pH"
|
|
498
|
+
val_formatted = format(round(float(val), digits), f'.{digits}f')
|
|
499
|
+
val_str = val_formatted
|
|
500
|
+
elif prop == "Eh":
|
|
501
|
+
prop_str = "Eh"
|
|
502
|
+
val_formatted = format(round(float(val), digits), f'.{digits}f')
|
|
503
|
+
val_str = f"{val_formatted} V"
|
|
504
|
+
elif prop == "IS":
|
|
505
|
+
prop_str = "$IS$"
|
|
506
|
+
val_formatted = format(round(float(val), digits), f'.{digits}f')
|
|
507
|
+
val_str = val_formatted
|
|
508
|
+
else:
|
|
509
|
+
prop_str = f"${prop}$"
|
|
510
|
+
val_formatted = format(round(float(val), digits), f'.{digits}f')
|
|
511
|
+
val_str = val_formatted
|
|
512
|
+
|
|
513
|
+
if ret_val:
|
|
514
|
+
descriptions.append(val_str)
|
|
515
|
+
else:
|
|
516
|
+
descriptions.append(f"{prop_str} = {val_str}")
|
|
517
|
+
|
|
518
|
+
return descriptions
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def describe_basis(ibasis: list = None, digits: int = 1,
|
|
522
|
+
oneline: bool = False, molality: bool = False,
|
|
523
|
+
use_pH: bool = True) -> list:
|
|
524
|
+
"""
|
|
525
|
+
Create formatted text describing basis species activities/fugacities.
|
|
526
|
+
|
|
527
|
+
This function generates formatted strings for displaying the chemical
|
|
528
|
+
activities or fugacities of basis species, typically for plot legends.
|
|
529
|
+
|
|
530
|
+
Parameters
|
|
531
|
+
----------
|
|
532
|
+
ibasis : list of int, optional
|
|
533
|
+
Indices of basis species to describe (1-based). If None, describes all.
|
|
534
|
+
digits : int, default 1
|
|
535
|
+
Number of decimal places to display
|
|
536
|
+
oneline : bool, default False
|
|
537
|
+
If True, combine all species on one line (not fully implemented)
|
|
538
|
+
molality : bool, default False
|
|
539
|
+
If True, use molality (m) instead of activity (a)
|
|
540
|
+
use_pH : bool, default True
|
|
541
|
+
If True, display H+ as pH instead of log a_H+
|
|
542
|
+
|
|
543
|
+
Returns
|
|
544
|
+
-------
|
|
545
|
+
list of str
|
|
546
|
+
Formatted basis species descriptions
|
|
547
|
+
|
|
548
|
+
Examples
|
|
549
|
+
--------
|
|
550
|
+
>>> from pychnosz.core.basis import basis
|
|
551
|
+
>>> basis(["H2O", "H+", "O2"], [-10, -7, -80])
|
|
552
|
+
>>> describe_basis([2, 3])
|
|
553
|
+
['pH = 7.0', 'log $f_{O_2}$ = -80.0']
|
|
554
|
+
|
|
555
|
+
>>> describe_basis() # All basis species
|
|
556
|
+
['log $a_{H_2O}$ = -10.0', 'pH = 7.0', 'log $f_{O_2}$ = -80.0']
|
|
557
|
+
|
|
558
|
+
Notes
|
|
559
|
+
-----
|
|
560
|
+
This is used to create legend entries showing the basis species
|
|
561
|
+
activities used in thermodynamic calculations.
|
|
562
|
+
"""
|
|
563
|
+
from ..core.basis import get_basis
|
|
564
|
+
|
|
565
|
+
basis_df = get_basis()
|
|
566
|
+
if basis_df is None:
|
|
567
|
+
raise RuntimeError("Basis species are not defined")
|
|
568
|
+
|
|
569
|
+
# Default to all basis species
|
|
570
|
+
if ibasis is None:
|
|
571
|
+
ibasis = list(range(1, len(basis_df) + 1))
|
|
572
|
+
|
|
573
|
+
# Convert to 0-based indexing
|
|
574
|
+
ibasis_0 = [i - 1 for i in ibasis]
|
|
575
|
+
|
|
576
|
+
descriptions = []
|
|
577
|
+
|
|
578
|
+
for i in ibasis_0:
|
|
579
|
+
species_name = basis_df.index[i]
|
|
580
|
+
state = basis_df.iloc[i]['state']
|
|
581
|
+
logact = basis_df.iloc[i]['logact']
|
|
582
|
+
|
|
583
|
+
# Check if logact is numeric
|
|
584
|
+
try:
|
|
585
|
+
logact_val = float(logact)
|
|
586
|
+
is_numeric = True
|
|
587
|
+
except (ValueError, TypeError):
|
|
588
|
+
is_numeric = False
|
|
589
|
+
|
|
590
|
+
if is_numeric:
|
|
591
|
+
# Handle H+ specially with pH
|
|
592
|
+
if species_name == "H+" and use_pH:
|
|
593
|
+
pH_val = -logact_val
|
|
594
|
+
val_formatted = format(round(pH_val, digits), f'.{digits}f')
|
|
595
|
+
descriptions.append(f"pH = {val_formatted}")
|
|
596
|
+
else:
|
|
597
|
+
# Format the activity/fugacity
|
|
598
|
+
val_formatted = format(round(logact_val, digits), f'.{digits}f')
|
|
599
|
+
|
|
600
|
+
# Determine if it's activity or fugacity based on state
|
|
601
|
+
if state in ['aq', 'liq', 'cr']:
|
|
602
|
+
a_or_f = "a" if not molality else "m"
|
|
603
|
+
else:
|
|
604
|
+
a_or_f = "f"
|
|
605
|
+
|
|
606
|
+
# Format the species name
|
|
607
|
+
species_formatted = _format_species_latex(species_name)
|
|
608
|
+
|
|
609
|
+
descriptions.append(f"log ${a_or_f}_{{{species_formatted}}}$ = {val_formatted}")
|
|
610
|
+
else:
|
|
611
|
+
# Non-numeric value (buffer)
|
|
612
|
+
if species_name == "H+" and use_pH:
|
|
613
|
+
descriptions.append(f"pH = {logact}")
|
|
614
|
+
else:
|
|
615
|
+
# For buffers, just show the buffer name
|
|
616
|
+
if state in ['aq', 'liq', 'cr']:
|
|
617
|
+
a_or_f = "a" if not molality else "m"
|
|
618
|
+
else:
|
|
619
|
+
a_or_f = "f"
|
|
620
|
+
|
|
621
|
+
species_formatted = _format_species_latex(species_name)
|
|
622
|
+
descriptions.append(f"${a_or_f}_{{{species_formatted}}}$ = {logact}")
|
|
623
|
+
|
|
624
|
+
return descriptions
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def describe_property_html(property: list = None, value: list = None,
|
|
628
|
+
digits: int = 0, oneline: bool = False,
|
|
629
|
+
ret_val: bool = False) -> list:
|
|
630
|
+
"""
|
|
631
|
+
Create HTML-formatted text describing thermodynamic properties (for Plotly).
|
|
632
|
+
|
|
633
|
+
This function generates HTML-formatted strings for displaying thermodynamic
|
|
634
|
+
properties and their values, typically for plot legends in interactive diagrams.
|
|
635
|
+
|
|
636
|
+
Parameters
|
|
637
|
+
----------
|
|
638
|
+
property : list of str
|
|
639
|
+
Property names (e.g., ["T", "P"])
|
|
640
|
+
value : list
|
|
641
|
+
Property values
|
|
642
|
+
digits : int, default 0
|
|
643
|
+
Number of decimal places to display
|
|
644
|
+
oneline : bool, default False
|
|
645
|
+
If True, format on one line (not implemented)
|
|
646
|
+
ret_val : bool, default False
|
|
647
|
+
If True, return only values without property names
|
|
648
|
+
|
|
649
|
+
Returns
|
|
650
|
+
-------
|
|
651
|
+
list of str
|
|
652
|
+
HTML-formatted property descriptions
|
|
653
|
+
|
|
654
|
+
Examples
|
|
655
|
+
--------
|
|
656
|
+
>>> describe_property_html(["T", "P"], [300, 1000])
|
|
657
|
+
['<i>T</i> = 300 °C', '<i>P</i> = 1000 bar']
|
|
658
|
+
|
|
659
|
+
Notes
|
|
660
|
+
-----
|
|
661
|
+
Use this instead of describe_property() when creating legends for
|
|
662
|
+
interactive (Plotly) diagrams.
|
|
663
|
+
"""
|
|
664
|
+
if property is None or value is None:
|
|
665
|
+
raise ValueError("property or value is None")
|
|
666
|
+
|
|
667
|
+
descriptions = []
|
|
668
|
+
|
|
669
|
+
for i in range(len(property)):
|
|
670
|
+
prop = property[i]
|
|
671
|
+
val = value[i]
|
|
672
|
+
|
|
673
|
+
# Get property symbol (HTML format)
|
|
674
|
+
if prop == "T":
|
|
675
|
+
prop_str = "<i>T</i>"
|
|
676
|
+
if val == "Psat" or val == "NA":
|
|
677
|
+
val_str = "<i>P</i><sub>sat</sub>"
|
|
678
|
+
else:
|
|
679
|
+
val_formatted = format(round(float(val), digits), f'.{digits}f')
|
|
680
|
+
val_str = f"{val_formatted} °C"
|
|
681
|
+
elif prop == "P":
|
|
682
|
+
prop_str = "<i>P</i>"
|
|
683
|
+
if val == "Psat" or val == "NA":
|
|
684
|
+
val_str = "<i>P</i><sub>sat</sub>"
|
|
685
|
+
else:
|
|
686
|
+
val_formatted = format(round(float(val), digits), f'.{digits}f')
|
|
687
|
+
val_str = f"{val_formatted} bar"
|
|
688
|
+
elif prop == "pH":
|
|
689
|
+
prop_str = "pH"
|
|
690
|
+
val_formatted = format(round(float(val), digits), f'.{digits}f')
|
|
691
|
+
val_str = val_formatted
|
|
692
|
+
elif prop == "Eh":
|
|
693
|
+
prop_str = "Eh"
|
|
694
|
+
val_formatted = format(round(float(val), digits), f'.{digits}f')
|
|
695
|
+
val_str = f"{val_formatted} V"
|
|
696
|
+
elif prop == "IS":
|
|
697
|
+
prop_str = "<i>IS</i>"
|
|
698
|
+
val_formatted = format(round(float(val), digits), f'.{digits}f')
|
|
699
|
+
val_str = val_formatted
|
|
700
|
+
else:
|
|
701
|
+
prop_str = f"<i>{prop}</i>"
|
|
702
|
+
val_formatted = format(round(float(val), digits), f'.{digits}f')
|
|
703
|
+
val_str = val_formatted
|
|
704
|
+
|
|
705
|
+
if ret_val:
|
|
706
|
+
descriptions.append(val_str)
|
|
707
|
+
else:
|
|
708
|
+
descriptions.append(f"{prop_str} = {val_str}")
|
|
709
|
+
|
|
710
|
+
return descriptions
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def describe_basis_html(ibasis: list = None, digits: int = 1,
|
|
714
|
+
oneline: bool = False, molality: bool = False,
|
|
715
|
+
use_pH: bool = True) -> list:
|
|
716
|
+
"""
|
|
717
|
+
Create HTML-formatted text describing basis species (for Plotly).
|
|
718
|
+
|
|
719
|
+
This function generates HTML-formatted strings for displaying the chemical
|
|
720
|
+
activities or fugacities of basis species, typically for plot legends in
|
|
721
|
+
interactive diagrams.
|
|
722
|
+
|
|
723
|
+
Parameters
|
|
724
|
+
----------
|
|
725
|
+
ibasis : list of int, optional
|
|
726
|
+
Indices of basis species to describe (1-based). If None, describes all.
|
|
727
|
+
digits : int, default 1
|
|
728
|
+
Number of decimal places to display
|
|
729
|
+
oneline : bool, default False
|
|
730
|
+
If True, combine all species on one line (not fully implemented)
|
|
731
|
+
molality : bool, default False
|
|
732
|
+
If True, use molality (m) instead of activity (a)
|
|
733
|
+
use_pH : bool, default True
|
|
734
|
+
If True, display H+ as pH instead of log a_H+
|
|
735
|
+
|
|
736
|
+
Returns
|
|
737
|
+
-------
|
|
738
|
+
list of str
|
|
739
|
+
HTML-formatted basis species descriptions
|
|
740
|
+
|
|
741
|
+
Examples
|
|
742
|
+
--------
|
|
743
|
+
>>> from pychnosz.core.basis import basis
|
|
744
|
+
>>> basis(["H2O", "H+", "O2"], [-10, -7, -80])
|
|
745
|
+
>>> describe_basis_html([2, 3])
|
|
746
|
+
['pH = 7.0', 'log <i>f</i><sub>O<sub>2</sub></sub> = -80.0']
|
|
747
|
+
|
|
748
|
+
>>> describe_basis_html([4]) # CO2
|
|
749
|
+
['log <i>f</i><sub>CO<sub>2</sub></sub> = -1.0']
|
|
750
|
+
|
|
751
|
+
Notes
|
|
752
|
+
-----
|
|
753
|
+
Use this instead of describe_basis() when creating legends for
|
|
754
|
+
interactive (Plotly) diagrams.
|
|
755
|
+
"""
|
|
756
|
+
if not _HTML_DEPS_AVAILABLE:
|
|
757
|
+
raise ImportError(
|
|
758
|
+
"describe_basis_html() requires 'WORMutils' package.\n"
|
|
759
|
+
"Install with: pip install WORMutils"
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
from ..core.basis import get_basis
|
|
763
|
+
|
|
764
|
+
basis_df = get_basis()
|
|
765
|
+
if basis_df is None:
|
|
766
|
+
raise RuntimeError("Basis species are not defined")
|
|
767
|
+
|
|
768
|
+
# Default to all basis species
|
|
769
|
+
if ibasis is None:
|
|
770
|
+
ibasis = list(range(1, len(basis_df) + 1))
|
|
771
|
+
|
|
772
|
+
# Convert to 0-based indexing
|
|
773
|
+
ibasis_0 = [i - 1 for i in ibasis]
|
|
774
|
+
|
|
775
|
+
descriptions = []
|
|
776
|
+
|
|
777
|
+
for i in ibasis_0:
|
|
778
|
+
species_name = basis_df.index[i]
|
|
779
|
+
state = basis_df.iloc[i]['state']
|
|
780
|
+
logact = basis_df.iloc[i]['logact']
|
|
781
|
+
|
|
782
|
+
# Check if logact is numeric
|
|
783
|
+
try:
|
|
784
|
+
logact_val = float(logact)
|
|
785
|
+
is_numeric = True
|
|
786
|
+
except (ValueError, TypeError):
|
|
787
|
+
is_numeric = False
|
|
788
|
+
|
|
789
|
+
if is_numeric:
|
|
790
|
+
# Handle H+ specially with pH
|
|
791
|
+
if species_name == "H+" and use_pH:
|
|
792
|
+
pH_val = -logact_val
|
|
793
|
+
val_formatted = format(round(pH_val, digits), f'.{digits}f')
|
|
794
|
+
descriptions.append(f"pH = {val_formatted}")
|
|
795
|
+
else:
|
|
796
|
+
# Format the activity/fugacity
|
|
797
|
+
val_formatted = format(round(logact_val, digits), f'.{digits}f')
|
|
798
|
+
|
|
799
|
+
# Determine if it's activity or fugacity based on state
|
|
800
|
+
if state in ['aq', 'liq', 'cr']:
|
|
801
|
+
a_or_f = "a" if not molality else "m"
|
|
802
|
+
else:
|
|
803
|
+
a_or_f = "f"
|
|
804
|
+
|
|
805
|
+
# Format the species name using HTML
|
|
806
|
+
species_formatted = chemlabel(species_name)
|
|
807
|
+
|
|
808
|
+
descriptions.append(f"log <i>{a_or_f}</i><sub>{species_formatted}</sub> = {val_formatted}")
|
|
809
|
+
else:
|
|
810
|
+
# Non-numeric value (buffer)
|
|
811
|
+
if species_name == "H+" and use_pH:
|
|
812
|
+
descriptions.append(f"pH = {logact}")
|
|
813
|
+
else:
|
|
814
|
+
# For buffers, just show the buffer name
|
|
815
|
+
if state in ['aq', 'liq', 'cr']:
|
|
816
|
+
a_or_f = "a" if not molality else "m"
|
|
817
|
+
else:
|
|
818
|
+
a_or_f = "f"
|
|
819
|
+
|
|
820
|
+
species_formatted = chemlabel(species_name)
|
|
821
|
+
descriptions.append(f"<i>{a_or_f}</i><sub>{species_formatted}</sub> = {logact}")
|
|
822
|
+
|
|
823
|
+
return descriptions
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def add_legend(ax, labels: list = None, loc: str = 'best',
|
|
827
|
+
frameon: bool = False, fontsize: float = 9, **kwargs):
|
|
828
|
+
"""
|
|
829
|
+
Add a legend to a diagram with matplotlib or Plotly formatting.
|
|
830
|
+
|
|
831
|
+
This is a convenience function that adds a legend with sensible
|
|
832
|
+
defaults matching R CHNOSZ legend styling. Works with both matplotlib
|
|
833
|
+
and Plotly figures.
|
|
834
|
+
|
|
835
|
+
Parameters
|
|
836
|
+
----------
|
|
837
|
+
ax : matplotlib.axes.Axes or plotly.graph_objs.Figure
|
|
838
|
+
Axes/Figure object to add legend to. For interactive diagrams,
|
|
839
|
+
pass the figure from d['fig'] or d['ax'].
|
|
840
|
+
labels : list of str
|
|
841
|
+
Legend labels (can be from describe_property, describe_basis, etc.)
|
|
842
|
+
loc : str, default 'best'
|
|
843
|
+
Legend location. Options: 'best', 'upper left', 'upper right',
|
|
844
|
+
'lower left', 'lower right', 'right', 'center left', 'center right',
|
|
845
|
+
'lower center', 'upper center', 'center'
|
|
846
|
+
For Plotly: 'best' defaults to 'lower right'
|
|
847
|
+
frameon : bool, default False
|
|
848
|
+
Whether to draw a frame around the legend (R bty="n" equivalent)
|
|
849
|
+
fontsize : float, default 9
|
|
850
|
+
Font size for legend text (R cex=0.9 equivalent)
|
|
851
|
+
**kwargs
|
|
852
|
+
Additional arguments passed to matplotlib legend() or Plotly annotation
|
|
853
|
+
|
|
854
|
+
Returns
|
|
855
|
+
-------
|
|
856
|
+
matplotlib.legend.Legend or plotly.graph_objs.Figure
|
|
857
|
+
The legend object (matplotlib) or the figure (Plotly)
|
|
858
|
+
|
|
859
|
+
Examples
|
|
860
|
+
--------
|
|
861
|
+
>>> from pychnosz.utils.expression import add_legend, describe_property
|
|
862
|
+
>>> # Matplotlib diagram with plot_it=False
|
|
863
|
+
>>> d1 = diagram(a, interactive=False, plot_it=False)
|
|
864
|
+
>>> dprop = describe_property(["T", "P"], [300, 1000])
|
|
865
|
+
>>> add_legend(d1['ax'], dprop, loc='lower right')
|
|
866
|
+
>>> # Display the figure in Jupyter:
|
|
867
|
+
>>> from IPython.display import display
|
|
868
|
+
>>> display(d1['fig'])
|
|
869
|
+
>>> # Or save it:
|
|
870
|
+
>>> d1['fig'].savefig('diagram.png')
|
|
871
|
+
|
|
872
|
+
>>> # Plotly diagram
|
|
873
|
+
>>> d1 = diagram(a, interactive=True, plot_it=False)
|
|
874
|
+
>>> dprop = describe_property(["T", "P"], [300, 1000])
|
|
875
|
+
>>> add_legend(d1['fig'], dprop, loc='lower right')
|
|
876
|
+
>>> d1['fig'].show()
|
|
877
|
+
|
|
878
|
+
Notes
|
|
879
|
+
-----
|
|
880
|
+
Common R legend locations and their matplotlib equivalents:
|
|
881
|
+
- "bottomright" → "lower right"
|
|
882
|
+
- "topleft" → "upper left"
|
|
883
|
+
- "topright" → "upper right"
|
|
884
|
+
- "bottomleft" → "lower left"
|
|
885
|
+
|
|
886
|
+
When using plot_it=False, you need to explicitly display the figure after
|
|
887
|
+
adding legends. In Jupyter notebooks, use display(d['fig']) or d['fig'].show()
|
|
888
|
+
for Plotly diagrams. Outside Jupyter, use plt.show() or save with d['fig'].savefig().
|
|
889
|
+
"""
|
|
890
|
+
if labels is None:
|
|
891
|
+
raise ValueError("labels must be provided")
|
|
892
|
+
|
|
893
|
+
# Detect if this is a Plotly figure
|
|
894
|
+
is_plotly = _is_plotly_figure(ax)
|
|
895
|
+
|
|
896
|
+
if is_plotly:
|
|
897
|
+
return _add_plotly_legend(ax, labels, loc, frameon, fontsize, **kwargs)
|
|
898
|
+
else:
|
|
899
|
+
return _add_matplotlib_legend(ax, labels, loc, frameon, fontsize, **kwargs)
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def _is_plotly_figure(fig):
|
|
903
|
+
"""Check if object is a Plotly figure."""
|
|
904
|
+
return hasattr(fig, 'add_annotation') and hasattr(fig, 'update_layout')
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def _add_matplotlib_legend(ax, labels, loc, frameon, fontsize, **kwargs):
|
|
908
|
+
"""Add legend to matplotlib axes."""
|
|
909
|
+
# Handle R-style location names
|
|
910
|
+
loc_map = {
|
|
911
|
+
'bottomright': 'lower right',
|
|
912
|
+
'bottomleft': 'lower left',
|
|
913
|
+
'topleft': 'upper left',
|
|
914
|
+
'topright': 'upper right',
|
|
915
|
+
'bottom': 'lower center',
|
|
916
|
+
'top': 'upper center',
|
|
917
|
+
'left': 'center left',
|
|
918
|
+
'right': 'center right'
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
# Convert R-style location to matplotlib style
|
|
922
|
+
if loc.lower() in loc_map:
|
|
923
|
+
loc = loc_map[loc.lower()]
|
|
924
|
+
|
|
925
|
+
# Create legend with text-only labels (no symbols)
|
|
926
|
+
# This matches R's legend behavior when just providing character vectors
|
|
927
|
+
# We need to create invisible handles for matplotlib to work properly
|
|
928
|
+
from matplotlib.patches import Rectangle
|
|
929
|
+
|
|
930
|
+
# Create invisible (alpha=0) dummy handles for each label
|
|
931
|
+
handles = [Rectangle((0, 0), 1, 1, fc="white", ec="white", alpha=0)
|
|
932
|
+
for _ in labels]
|
|
933
|
+
|
|
934
|
+
# Create legend with invisible handles and no spacing
|
|
935
|
+
legend = ax.legend(handles, labels, loc=loc, frameon=frameon,
|
|
936
|
+
fontsize=fontsize, handlelength=0, handletextpad=0,
|
|
937
|
+
**kwargs)
|
|
938
|
+
|
|
939
|
+
return legend
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
def _add_plotly_legend(fig, labels, loc, frameon, fontsize, **kwargs):
|
|
943
|
+
"""Add legend-style annotation to Plotly figure."""
|
|
944
|
+
# Handle R-style location names and map to Plotly coordinates
|
|
945
|
+
loc_map = {
|
|
946
|
+
'best': 'lower right',
|
|
947
|
+
'bottomright': 'lower right',
|
|
948
|
+
'bottomleft': 'lower left',
|
|
949
|
+
'topleft': 'upper left',
|
|
950
|
+
'topright': 'upper right',
|
|
951
|
+
'bottom': 'lower center',
|
|
952
|
+
'top': 'upper center',
|
|
953
|
+
'left': 'center left',
|
|
954
|
+
'right': 'center right',
|
|
955
|
+
'lower right': 'lower right',
|
|
956
|
+
'lower left': 'lower left',
|
|
957
|
+
'upper left': 'upper left',
|
|
958
|
+
'upper right': 'upper right',
|
|
959
|
+
'lower center': 'lower center',
|
|
960
|
+
'upper center': 'upper center',
|
|
961
|
+
'center left': 'center left',
|
|
962
|
+
'center right': 'center right',
|
|
963
|
+
'center': 'center'
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
# Normalize location
|
|
967
|
+
loc_normalized = loc_map.get(loc.lower(), 'lower right')
|
|
968
|
+
|
|
969
|
+
# Map to Plotly anchor and position coordinates
|
|
970
|
+
# Format: (x, y, xanchor, yanchor)
|
|
971
|
+
plotly_positions = {
|
|
972
|
+
'upper left': (0.02, 0.98, 'left', 'top'),
|
|
973
|
+
'upper right': (0.98, 0.98, 'right', 'top'),
|
|
974
|
+
'lower left': (0.02, 0.02, 'left', 'bottom'),
|
|
975
|
+
'lower right': (0.98, 0.02, 'right', 'bottom'),
|
|
976
|
+
'upper center': (0.5, 0.98, 'center', 'top'),
|
|
977
|
+
'lower center': (0.5, 0.02, 'center', 'bottom'),
|
|
978
|
+
'center left': (0.02, 0.5, 'left', 'middle'),
|
|
979
|
+
'center right': (0.98, 0.5, 'right', 'middle'),
|
|
980
|
+
'center': (0.5, 0.5, 'center', 'middle')
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
x, y, xanchor, yanchor = plotly_positions[loc_normalized]
|
|
984
|
+
|
|
985
|
+
# Build legend text
|
|
986
|
+
legend_text = '<br>'.join(labels)
|
|
987
|
+
|
|
988
|
+
# Add annotation as legend
|
|
989
|
+
fig.add_annotation(
|
|
990
|
+
x=x,
|
|
991
|
+
y=y,
|
|
992
|
+
xref='paper',
|
|
993
|
+
yref='paper',
|
|
994
|
+
text=legend_text,
|
|
995
|
+
showarrow=False,
|
|
996
|
+
xanchor=xanchor,
|
|
997
|
+
yanchor=yanchor,
|
|
998
|
+
font=dict(size=fontsize),
|
|
999
|
+
bgcolor='rgba(255, 255, 255, 0.8)' if not frameon else 'rgba(255, 255, 255, 1)',
|
|
1000
|
+
bordercolor='black' if frameon else 'rgba(0, 0, 0, 0)',
|
|
1001
|
+
borderwidth=1 if frameon else 0,
|
|
1002
|
+
borderpad=4,
|
|
1003
|
+
align='left'
|
|
1004
|
+
)
|
|
1005
|
+
|
|
1006
|
+
return fig
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def set_title(ax_or_fig, title: str, fontsize: float = 12, **kwargs):
|
|
1010
|
+
"""
|
|
1011
|
+
Set title on a matplotlib axes or Plotly figure.
|
|
1012
|
+
|
|
1013
|
+
This function provides a unified interface for setting titles on both
|
|
1014
|
+
matplotlib and Plotly plots, allowing seamless switching between
|
|
1015
|
+
interactive=True and interactive=False.
|
|
1016
|
+
|
|
1017
|
+
Parameters
|
|
1018
|
+
----------
|
|
1019
|
+
ax_or_fig : matplotlib.axes.Axes or plotly.graph_objs.Figure
|
|
1020
|
+
Axes or Figure object to set title on
|
|
1021
|
+
title : str
|
|
1022
|
+
The title text
|
|
1023
|
+
fontsize : float, default 12
|
|
1024
|
+
Font size for the title
|
|
1025
|
+
**kwargs
|
|
1026
|
+
Additional arguments passed to matplotlib set_title() or Plotly update_layout()
|
|
1027
|
+
|
|
1028
|
+
Returns
|
|
1029
|
+
-------
|
|
1030
|
+
matplotlib.text.Text or plotly.graph_objs.Figure
|
|
1031
|
+
The title object (matplotlib) or the figure (Plotly)
|
|
1032
|
+
|
|
1033
|
+
Examples
|
|
1034
|
+
--------
|
|
1035
|
+
>>> from pychnosz.utils.expression import set_title, syslab
|
|
1036
|
+
>>> # Matplotlib diagram
|
|
1037
|
+
>>> d1 = diagram(a, interactive=False, plot_it=False)
|
|
1038
|
+
>>> title_text = syslab(["H2O", "CO2", "CaO", "MgO", "SiO2"])
|
|
1039
|
+
>>> set_title(d1['ax'], title_text, fontsize=12)
|
|
1040
|
+
>>> # Display the figure in Jupyter:
|
|
1041
|
+
>>> from IPython.display import display
|
|
1042
|
+
>>> display(d1['fig'])
|
|
1043
|
+
|
|
1044
|
+
>>> # Plotly diagram
|
|
1045
|
+
>>> d1 = diagram(a, interactive=True, plot_it=False)
|
|
1046
|
+
>>> title_text = syslab_html(["H2O", "CO2", "CaO", "MgO", "SiO2"])
|
|
1047
|
+
>>> set_title(d1['ax'], title_text, fontsize=12)
|
|
1048
|
+
>>> d1['fig'].show()
|
|
1049
|
+
|
|
1050
|
+
Notes
|
|
1051
|
+
-----
|
|
1052
|
+
When using plot_it=False, you need to explicitly display the figure after
|
|
1053
|
+
setting the title. In Jupyter notebooks, use display(d['fig']) or d['fig'].show()
|
|
1054
|
+
for Plotly diagrams. Outside Jupyter, use plt.show() or save with d['fig'].savefig().
|
|
1055
|
+
"""
|
|
1056
|
+
is_plotly = _is_plotly_figure(ax_or_fig)
|
|
1057
|
+
|
|
1058
|
+
if is_plotly:
|
|
1059
|
+
# Plotly figure
|
|
1060
|
+
title_dict = {'text': title, 'x': 0.5, 'xanchor': 'center'}
|
|
1061
|
+
if fontsize:
|
|
1062
|
+
title_dict['font'] = {'size': fontsize}
|
|
1063
|
+
ax_or_fig.update_layout(title=title_dict, **kwargs)
|
|
1064
|
+
return ax_or_fig
|
|
1065
|
+
else:
|
|
1066
|
+
# Matplotlib axes
|
|
1067
|
+
return ax_or_fig.set_title(title, fontsize=fontsize, **kwargs)
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
# Export main functions
|
|
1071
|
+
__all__ = ['ratlab', 'ratlab_html', 'expr_species', 'syslab', 'syslab_html',
|
|
1072
|
+
'describe_property', 'describe_property_html',
|
|
1073
|
+
'describe_basis', 'describe_basis_html',
|
|
1074
|
+
'add_legend', 'set_title']
|