pychnosz 1.1.4__cp311-cp311-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.
Files changed (128) hide show
  1. pychnosz/__init__.py +129 -0
  2. pychnosz/biomolecules/__init__.py +29 -0
  3. pychnosz/biomolecules/ionize_aa.py +197 -0
  4. pychnosz/biomolecules/proteins.py +595 -0
  5. pychnosz/core/__init__.py +46 -0
  6. pychnosz/core/affinity.py +1256 -0
  7. pychnosz/core/animation.py +593 -0
  8. pychnosz/core/balance.py +334 -0
  9. pychnosz/core/basis.py +716 -0
  10. pychnosz/core/diagram.py +3336 -0
  11. pychnosz/core/equilibrate.py +813 -0
  12. pychnosz/core/equilibrium.py +554 -0
  13. pychnosz/core/info.py +821 -0
  14. pychnosz/core/retrieve.py +364 -0
  15. pychnosz/core/speciation.py +580 -0
  16. pychnosz/core/species.py +599 -0
  17. pychnosz/core/subcrt.py +1700 -0
  18. pychnosz/core/thermo.py +593 -0
  19. pychnosz/core/unicurve.py +1226 -0
  20. pychnosz/data/__init__.py +11 -0
  21. pychnosz/data/add_obigt.py +327 -0
  22. pychnosz/data/extdata/Berman/BDat17_2017.csv +2 -0
  23. pychnosz/data/extdata/Berman/Ber88_1988.csv +68 -0
  24. pychnosz/data/extdata/Berman/Ber90_1990.csv +5 -0
  25. pychnosz/data/extdata/Berman/DS10_2010.csv +6 -0
  26. pychnosz/data/extdata/Berman/FDM+14_2014.csv +2 -0
  27. pychnosz/data/extdata/Berman/Got04_2004.csv +5 -0
  28. pychnosz/data/extdata/Berman/JUN92_1992.csv +3 -0
  29. pychnosz/data/extdata/Berman/SHD91_1991.csv +12 -0
  30. pychnosz/data/extdata/Berman/VGT92_1992.csv +2 -0
  31. pychnosz/data/extdata/Berman/VPT01_2001.csv +3 -0
  32. pychnosz/data/extdata/Berman/VPV05_2005.csv +2 -0
  33. pychnosz/data/extdata/Berman/ZS92_1992.csv +11 -0
  34. pychnosz/data/extdata/Berman/sympy.R +99 -0
  35. pychnosz/data/extdata/Berman/testing/BA96.bib +12 -0
  36. pychnosz/data/extdata/Berman/testing/BA96_Berman.csv +21 -0
  37. pychnosz/data/extdata/Berman/testing/BA96_OBIGT.csv +21 -0
  38. pychnosz/data/extdata/Berman/testing/BA96_refs.csv +6 -0
  39. pychnosz/data/extdata/OBIGT/AD.csv +25 -0
  40. pychnosz/data/extdata/OBIGT/Berman_cr.csv +93 -0
  41. pychnosz/data/extdata/OBIGT/DEW.csv +211 -0
  42. pychnosz/data/extdata/OBIGT/H2O_aq.csv +4 -0
  43. pychnosz/data/extdata/OBIGT/SLOP98.csv +411 -0
  44. pychnosz/data/extdata/OBIGT/SUPCRT92.csv +178 -0
  45. pychnosz/data/extdata/OBIGT/inorganic_aq.csv +729 -0
  46. pychnosz/data/extdata/OBIGT/inorganic_cr.csv +273 -0
  47. pychnosz/data/extdata/OBIGT/inorganic_gas.csv +20 -0
  48. pychnosz/data/extdata/OBIGT/organic_aq.csv +1104 -0
  49. pychnosz/data/extdata/OBIGT/organic_cr.csv +481 -0
  50. pychnosz/data/extdata/OBIGT/organic_gas.csv +268 -0
  51. pychnosz/data/extdata/OBIGT/organic_liq.csv +533 -0
  52. pychnosz/data/extdata/OBIGT/testing/GEMSFIT.csv +43 -0
  53. pychnosz/data/extdata/OBIGT/testing/IGEM.csv +17 -0
  54. pychnosz/data/extdata/OBIGT/testing/Sandia.csv +8 -0
  55. pychnosz/data/extdata/OBIGT/testing/SiO2.csv +4 -0
  56. pychnosz/data/extdata/misc/AD03_Fig1a.csv +69 -0
  57. pychnosz/data/extdata/misc/AD03_Fig1b.csv +43 -0
  58. pychnosz/data/extdata/misc/AD03_Fig1c.csv +89 -0
  59. pychnosz/data/extdata/misc/AD03_Fig1d.csv +30 -0
  60. pychnosz/data/extdata/misc/BZA10.csv +5 -0
  61. pychnosz/data/extdata/misc/HW97_Cp.csv +90 -0
  62. pychnosz/data/extdata/misc/HWM96_V.csv +229 -0
  63. pychnosz/data/extdata/misc/LA19_test.csv +7 -0
  64. pychnosz/data/extdata/misc/Mer75_Table4.csv +42 -0
  65. pychnosz/data/extdata/misc/OBIGT_check.csv +423 -0
  66. pychnosz/data/extdata/misc/PM90.csv +7 -0
  67. pychnosz/data/extdata/misc/RH95.csv +23 -0
  68. pychnosz/data/extdata/misc/RH98_Table15.csv +17 -0
  69. pychnosz/data/extdata/misc/SC10_Rainbow.csv +19 -0
  70. pychnosz/data/extdata/misc/SK95.csv +55 -0
  71. pychnosz/data/extdata/misc/SOJSH.csv +61 -0
  72. pychnosz/data/extdata/misc/SS98_Fig5a.csv +81 -0
  73. pychnosz/data/extdata/misc/SS98_Fig5b.csv +84 -0
  74. pychnosz/data/extdata/misc/TKSS14_Fig2.csv +25 -0
  75. pychnosz/data/extdata/misc/bluered.txt +1000 -0
  76. pychnosz/data/extdata/protein/Cas/Cas_aa.csv +177 -0
  77. pychnosz/data/extdata/protein/Cas/Cas_uniprot.csv +186 -0
  78. pychnosz/data/extdata/protein/Cas/download.R +34 -0
  79. pychnosz/data/extdata/protein/Cas/mkaa.R +34 -0
  80. pychnosz/data/extdata/protein/POLG.csv +12 -0
  81. pychnosz/data/extdata/protein/TBD+05.csv +393 -0
  82. pychnosz/data/extdata/protein/TBD+05_aa.csv +393 -0
  83. pychnosz/data/extdata/protein/rubisco.csv +28 -0
  84. pychnosz/data/extdata/protein/rubisco.fasta +239 -0
  85. pychnosz/data/extdata/protein/rubisco_aa.csv +28 -0
  86. pychnosz/data/extdata/src/H2O92D.f.orig +3457 -0
  87. pychnosz/data/extdata/src/README.txt +5 -0
  88. pychnosz/data/extdata/taxonomy/names.dmp +215 -0
  89. pychnosz/data/extdata/taxonomy/nodes.dmp +63 -0
  90. pychnosz/data/extdata/thermo/Bdot_acirc.csv +60 -0
  91. pychnosz/data/extdata/thermo/buffer.csv +40 -0
  92. pychnosz/data/extdata/thermo/element.csv +135 -0
  93. pychnosz/data/extdata/thermo/groups.csv +6 -0
  94. pychnosz/data/extdata/thermo/opt.csv +2 -0
  95. pychnosz/data/extdata/thermo/protein.csv +506 -0
  96. pychnosz/data/extdata/thermo/refs.csv +343 -0
  97. pychnosz/data/extdata/thermo/stoich.csv.xz +0 -0
  98. pychnosz/data/loader.py +431 -0
  99. pychnosz/data/mod_obigt.py +322 -0
  100. pychnosz/data/obigt.py +471 -0
  101. pychnosz/data/worm.py +228 -0
  102. pychnosz/fortran/__init__.py +16 -0
  103. pychnosz/fortran/h2o92.dll +0 -0
  104. pychnosz/fortran/h2o92_interface.py +527 -0
  105. pychnosz/geochemistry/__init__.py +21 -0
  106. pychnosz/geochemistry/minerals.py +514 -0
  107. pychnosz/geochemistry/redox.py +500 -0
  108. pychnosz/models/__init__.py +47 -0
  109. pychnosz/models/archer_wang.py +165 -0
  110. pychnosz/models/berman.py +309 -0
  111. pychnosz/models/cgl.py +381 -0
  112. pychnosz/models/dew.py +997 -0
  113. pychnosz/models/hkf.py +523 -0
  114. pychnosz/models/hkf_helpers.py +222 -0
  115. pychnosz/models/iapws95.py +1113 -0
  116. pychnosz/models/supcrt92_fortran.py +238 -0
  117. pychnosz/models/water.py +480 -0
  118. pychnosz/utils/__init__.py +27 -0
  119. pychnosz/utils/expression.py +1074 -0
  120. pychnosz/utils/formula.py +830 -0
  121. pychnosz/utils/formula_ox.py +227 -0
  122. pychnosz/utils/reset.py +33 -0
  123. pychnosz/utils/units.py +259 -0
  124. pychnosz-1.1.4.dist-info/METADATA +197 -0
  125. pychnosz-1.1.4.dist-info/RECORD +128 -0
  126. pychnosz-1.1.4.dist-info/WHEEL +5 -0
  127. pychnosz-1.1.4.dist-info/licenses/LICENSE.txt +19 -0
  128. pychnosz-1.1.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,227 @@
1
+ """
2
+ Formula oxidation state utilities for CHNOSZ.
3
+
4
+ This module provides functions for working with chemical formulas that include
5
+ element oxidation states, specifically for use with the WORM thermodynamic database.
6
+ """
7
+
8
+ from typing import Union, Dict, List
9
+ import pandas as pd
10
+
11
+ from ..core.thermo import thermo
12
+ from ..core.info import info
13
+
14
+
15
+ def get_formula_ox(name: Union[str, int]) -> Dict[str, float]:
16
+ """
17
+ Get quantities of elements and their oxidation states in a chemical compound.
18
+
19
+ This function only works when a thermodynamic database with the 'formula_ox'
20
+ column is loaded (e.g., the WORM database). For example, an input of "magnetite"
21
+ would return the following: {'Fe+3': 2.0, 'Fe+2': 1.0, 'O-2': 4.0}.
22
+
23
+ Parameters
24
+ ----------
25
+ name : str or int
26
+ The name or database index of the chemical species of interest. Example:
27
+ "magnetite" or 738.
28
+
29
+ Returns
30
+ -------
31
+ dict
32
+ A dictionary where each key represents an element in a specific
33
+ oxidation state, and its value is the number of that element in the
34
+ chemical species' formula.
35
+
36
+ Raises
37
+ ------
38
+ TypeError
39
+ If input is not a string or integer.
40
+ AttributeError
41
+ If the WORM thermodynamic database is not loaded (no formula_ox attribute).
42
+ ValueError
43
+ If the species is not found in the database or does not have oxidation
44
+ state information.
45
+
46
+ Examples
47
+ --------
48
+ >>> import pychnosz
49
+ >>> # Load the WORM database
50
+ >>> pychnosz.thermo("WORM")
51
+ >>> # Get formula with oxidation states for magnetite
52
+ >>> pychnosz.get_formula_ox("magnetite")
53
+ {'Fe+3': 2.0, 'Fe+2': 1.0, 'O-2': 4.0}
54
+ >>> # Can also use species index
55
+ >>> pychnosz.get_formula_ox(738)
56
+ {'Fe+3': 2.0, 'Fe+2': 1.0, 'O-2': 4.0}
57
+
58
+ Notes
59
+ -----
60
+ This function requires the wormutils package to be installed for parsing
61
+ the formula_ox strings. Install it with: pip install wormutils
62
+ """
63
+
64
+ # Import parse_formula_ox from wormutils
65
+ try:
66
+ from wormutils import parse_formula_ox
67
+ except ImportError:
68
+ raise ImportError(
69
+ "The wormutils package is required to use get_formula_ox(). "
70
+ "Install it with: pip install wormutils"
71
+ )
72
+
73
+ # Validate input type
74
+ if not isinstance(name, str) and not isinstance(name, int):
75
+ raise TypeError(
76
+ "Must provide input as a string (chemical species name) or "
77
+ "an integer (chemical species index)."
78
+ )
79
+
80
+ # Get the thermo system
81
+ thermo_sys = thermo()
82
+
83
+ # Convert index to name if necessary
84
+ if isinstance(name, int):
85
+ species_info = info(name, messages=False)
86
+ if species_info is None or len(species_info) == 0:
87
+ raise ValueError(f"Species index {name} not found in the database.")
88
+ name = species_info.name.iloc[0]
89
+
90
+ # Check if formula_ox exists in thermo()
91
+ if not hasattr(thermo_sys, 'formula_ox') or thermo_sys.formula_ox is None:
92
+ raise AttributeError(
93
+ "The 'formula_ox' attribute is not available. "
94
+ "This function only works when the WORM thermodynamic database "
95
+ "is loaded. Load it with: pychnosz.thermo('WORM')"
96
+ )
97
+
98
+ df = thermo_sys.formula_ox
99
+
100
+ # Check if the species name exists in the database
101
+ if name not in list(df["name"]):
102
+ raise ValueError(
103
+ f"The species '{name}' was not found in the loaded thermodynamic database."
104
+ )
105
+
106
+ # Get the formula_ox string for this species
107
+ try:
108
+ formula_ox_str = df[df["name"] == name]["formula_ox"].iloc[0]
109
+ except (KeyError, IndexError):
110
+ raise ValueError(
111
+ f"The species '{name}' does not have elemental oxidation states "
112
+ "given in the 'formula_ox' column of the loaded thermodynamic database."
113
+ )
114
+
115
+ # Check if formula_ox is valid (not NaN or empty)
116
+ if formula_ox_str is None or (isinstance(formula_ox_str, float) and pd.isna(formula_ox_str)) or formula_ox_str == "":
117
+ raise ValueError(
118
+ f"The species '{name}' does not have elemental oxidation states "
119
+ "given in the 'formula_ox' column of the loaded thermodynamic database."
120
+ )
121
+
122
+ # Parse the formula_ox string and return
123
+ return parse_formula_ox(formula_ox_str)
124
+
125
+
126
+ def get_n_element_ox(names: Union[str, int, List[Union[str, int]], pd.Series],
127
+ element_ox: str,
128
+ binary: bool = False) -> List[Union[float, bool]]:
129
+ """
130
+ Get the number of an element of a chosen oxidation state in chemical species formulas.
131
+
132
+ This function only works when a thermodynamic database with the 'formula_ox'
133
+ column is loaded (e.g., the WORM database).
134
+
135
+ If binary is False, returns a list containing the number of the chosen
136
+ element and oxidation state in the chemical species. For example, how many
137
+ ferrous irons are in the formulae of hematite, fayalite, and magnetite,
138
+ respectively?
139
+
140
+ >>> get_n_element_ox(names=["hematite", "fayalite", "magnetite"],
141
+ ... element_ox="Fe+2",
142
+ ... binary=False)
143
+ [0, 2.0, 1.0]
144
+
145
+ If binary is True, returns a list of whether or not ferrous iron is in their
146
+ formulas:
147
+
148
+ >>> get_n_element_ox(names=["hematite", "fayalite", "magnetite"],
149
+ ... element_ox="Fe+2",
150
+ ... binary=True)
151
+ [False, True, True]
152
+
153
+ Parameters
154
+ ----------
155
+ names : str, int, list of str/int, or pd.Series
156
+ The name or database index of a chemical species, or a list of
157
+ names or indices. Can also be a pandas Series (e.g., from retrieve()).
158
+ Example: ["hematite", "fayalite", "magnetite"] or [788, 782, 798].
159
+ element_ox : str
160
+ An element with a specific oxidation state. For example: "Fe+2" for
161
+ ferrous iron.
162
+ binary : bool, default False
163
+ Should the output list show True/False for presence or absence of the
164
+ element defined by `element_ox`? By default, this parameter is set to
165
+ False so the output list shows quantities of the element instead.
166
+
167
+ Returns
168
+ -------
169
+ list of float or list of bool
170
+ A list containing quantities of the chosen element oxidation state in
171
+ the formulas of the chemical species (if `binary=False`) or whether the
172
+ chosen element oxidation state is present in the formulae (if `binary=True`).
173
+
174
+ Raises
175
+ ------
176
+ AttributeError
177
+ If the WORM thermodynamic database is not loaded (no formula_ox attribute).
178
+ ValueError
179
+ If a species is not found in the database or does not have oxidation
180
+ state information.
181
+
182
+ Examples
183
+ --------
184
+ >>> import pychnosz
185
+ >>> # Load the WORM database
186
+ >>> pychnosz.thermo("WORM")
187
+ >>> # Get counts of Fe+2 in several minerals
188
+ >>> pychnosz.get_n_element_ox(["hematite", "fayalite", "magnetite"], "Fe+2")
189
+ [0, 2.0, 1.0]
190
+ >>> # Get binary presence/absence
191
+ >>> pychnosz.get_n_element_ox(["hematite", "fayalite", "magnetite"], "Fe+2", binary=True)
192
+ [False, True, True]
193
+ >>> # Can also use with retrieve()
194
+ >>> r = pychnosz.retrieve("Fe", ["Si", "O", "H"], state=["cr"])
195
+ >>> pychnosz.get_n_element_ox(r, "Fe+2")
196
+ [1, 0, 0, 2.0, 1, 0, 1, 3.0, 1, 3.0, 0, 7.0]
197
+
198
+ Notes
199
+ -----
200
+ This function requires the wormutils package to be installed for parsing
201
+ the formula_ox strings. Install it with: pip install wormutils
202
+ """
203
+
204
+ # Handle pandas Series (e.g., from retrieve())
205
+ if isinstance(names, pd.Series):
206
+ # Convert Series to list of indices
207
+ names = names.values.tolist()
208
+ # Handle single name/index
209
+ elif not isinstance(names, list):
210
+ names = [names]
211
+
212
+ # Get the count of element_ox for each species
213
+ n_list = []
214
+ for name in names:
215
+ # Get the formula_ox dictionary for this species
216
+ formula_ox_dict = get_formula_ox(name)
217
+ # Get the count of element_ox (default to 0 if not present)
218
+ count = formula_ox_dict.get(element_ox, 0)
219
+ n_list.append(count)
220
+
221
+ # Convert to binary if requested
222
+ if binary:
223
+ out_list = [True if n != 0 else False for n in n_list]
224
+ else:
225
+ out_list = n_list
226
+
227
+ return out_list
@@ -0,0 +1,33 @@
1
+ """
2
+ Reset function for initializing the CHNOSZ thermodynamic system.
3
+
4
+ This provides the reset() function that initializes/resets the global
5
+ thermodynamic system, equivalent to reset() in the R version.
6
+ """
7
+
8
+ from ..core.thermo import get_thermo_system
9
+
10
+
11
+ def reset(messages: bool = True):
12
+ """
13
+ Initialize or reset the CHNOSZ thermodynamic system.
14
+
15
+ This function initializes the global thermodynamic system by loading
16
+ all thermodynamic data files, setting up the OBIGT database, and
17
+ preparing the system for calculations.
18
+
19
+ This is equivalent to the reset() function in the R version of CHNOSZ.
20
+
21
+ Parameters
22
+ ----------
23
+ messages : bool, default True
24
+ Whether to print informational messages
25
+
26
+ Examples
27
+ --------
28
+ >>> import pychnosz
29
+ >>> pychnosz.reset() # Initialize the system
30
+ reset: thermodynamic system initialized
31
+ """
32
+ thermo_system = get_thermo_system()
33
+ thermo_system.reset(messages=messages)
@@ -0,0 +1,259 @@
1
+ """
2
+ Unit conversion utilities for CHNOSZ.
3
+
4
+ This module provides functions for converting between different units commonly
5
+ used in geochemical thermodynamics, such as temperature (C/K), pressure (bar/MPa),
6
+ energy (cal/J), and electrochemical potentials (Eh/pe).
7
+
8
+ Based on R CHNOSZ util.units.R
9
+ """
10
+
11
+ import numpy as np
12
+ from typing import Union, List, Optional
13
+ import warnings
14
+
15
+ from ..core.thermo import thermo
16
+ from ..core.subcrt import subcrt
17
+
18
+
19
+ def convert(value: Union[float, np.ndarray, List[float]],
20
+ units: str,
21
+ T: Union[float, np.ndarray] = 298.15,
22
+ P: Union[float, np.ndarray] = 1,
23
+ pH: Union[float, np.ndarray] = 7,
24
+ logaH2O: Union[float, np.ndarray] = 0,
25
+ messages: bool = True) -> Union[float, np.ndarray]:
26
+ """
27
+ Convert values to the specified units.
28
+
29
+ This function converts thermodynamic values between different units commonly
30
+ used in geochemistry.
31
+
32
+ Parameters
33
+ ----------
34
+ value : float, ndarray, or list
35
+ Value(s) to convert
36
+ units : str
37
+ Target units. Options include:
38
+ - Temperature: 'C', 'K'
39
+ - Energy: 'J', 'cal'
40
+ - Pressure: 'bar', 'MPa'
41
+ - Thermodynamic: 'G', 'logK'
42
+ - Electrochemical: 'Eh', 'pe', 'E0', 'logfO2'
43
+ - Volume: 'cm3bar', 'joules'
44
+ T : float or ndarray, default 298.15
45
+ Temperature in K (for Eh/pe/logK conversions)
46
+ P : float or ndarray, default 1
47
+ Pressure in bar (for E0/logfO2 conversions)
48
+ pH : float or ndarray, default 7
49
+ pH value (for E0/logfO2 conversions)
50
+ logaH2O : float or ndarray, default 0
51
+ Log activity of water (for E0/logfO2 conversions)
52
+ messages : bool, default True
53
+ Whether to print informational messages
54
+
55
+ Returns
56
+ -------
57
+ float or ndarray
58
+ Converted value(s)
59
+
60
+ Examples
61
+ --------
62
+ >>> convert(25, 'K') # Convert 25°C to K
63
+ 298.15
64
+ >>> convert(1.0, 'pe', T=298.15) # Convert 1V Eh to pe
65
+ 16.9
66
+ """
67
+
68
+ if value is None:
69
+ return None
70
+
71
+ # Convert to numpy array for uniform handling
72
+ value = np.asarray(value)
73
+ T = np.asarray(T)
74
+ P = np.asarray(P)
75
+ pH = np.asarray(pH)
76
+ logaH2O = np.asarray(logaH2O)
77
+
78
+ units = units.lower()
79
+
80
+ # Temperature conversions (C <-> K)
81
+ if units in ['c', 'k']:
82
+ CK = 273.15
83
+ if units == 'k':
84
+ return value + CK
85
+ if units == 'c':
86
+ return value - CK
87
+
88
+ # Energy conversions (J <-> cal)
89
+ elif units in ['j', 'cal']:
90
+ Jcal = 4.184
91
+ if units == 'j':
92
+ return value * Jcal
93
+ if units == 'cal':
94
+ return value / Jcal
95
+
96
+ # Gibbs energy <-> logK conversions
97
+ elif units in ['g', 'logk']:
98
+ # Gas constant (J K^-1 mol^-1)
99
+ R = 8.314463 # NIST value
100
+ if units == 'logk':
101
+ return value / (-np.log(10) * R * T)
102
+ if units == 'g':
103
+ return value * (-np.log(10) * R * T)
104
+
105
+ # Volume conversions (cm3bar <-> joules)
106
+ elif units in ['cm3bar', 'joules']:
107
+ if units == 'cm3bar':
108
+ return value * 10
109
+ if units == 'joules':
110
+ return value / 10
111
+
112
+ # Electrochemical potential conversions (Eh <-> pe)
113
+ elif units in ['eh', 'pe']:
114
+ R = 0.00831470 # Gas constant in kJ K^-1 mol^-1
115
+ F = 96.4935 # Faraday constant in kJ V^-1 mol^-1
116
+ if units == 'pe':
117
+ return value * F / (np.log(10) * R * T)
118
+ if units == 'eh':
119
+ return value * (np.log(10) * R * T) / F
120
+
121
+ # Pressure conversions (bar <-> MPa)
122
+ elif units in ['bar', 'mpa']:
123
+ barmpa = 10
124
+ if units == 'mpa':
125
+ return value / barmpa
126
+ if units == 'bar':
127
+ return value * barmpa
128
+
129
+ # Eh <-> logfO2 conversions
130
+ elif units in ['e0', 'logfo2']:
131
+ # Calculate equilibrium constant for: H2O = 1/2 O2 + 2 H+ + 2 e-
132
+ # Handle P="Psat" case (pass it directly to subcrt)
133
+ # Check if P is a string (including numpy string types)
134
+ P_is_psat = False
135
+ if isinstance(P, (str, np.str_)):
136
+ P_is_psat = str(P).lower() == 'psat'
137
+ elif isinstance(P, (list, tuple)):
138
+ # P is a list/tuple - check if it's a single-element string
139
+ if len(P) == 1 and isinstance(P[0], (str, np.str_)):
140
+ P_is_psat = str(P[0]).lower() == 'psat'
141
+ elif isinstance(P, np.ndarray):
142
+ # P is a numpy array
143
+ if P.ndim == 0:
144
+ # Scalar array - check if it's a string
145
+ try:
146
+ if isinstance(P.item(), (str, np.str_)):
147
+ P_is_psat = str(P.item()).lower() == 'psat'
148
+ except (ValueError, AttributeError):
149
+ pass
150
+ elif P.size == 1:
151
+ # Single-element array - check if it's a string
152
+ try:
153
+ if isinstance(P.flat[0], (str, np.str_)):
154
+ P_is_psat = str(P.flat[0]).lower() == 'psat'
155
+ except (ValueError, AttributeError, IndexError):
156
+ pass
157
+
158
+ if P_is_psat:
159
+ P_arg = 'Psat'
160
+ T_arg = np.atleast_1d(T)
161
+ if len(T_arg) == 1:
162
+ T_arg = float(T_arg[0])
163
+ else:
164
+ T_arg = T_arg.tolist()
165
+ else:
166
+ # Convert T and P to proper format for subcrt
167
+ T_vals = np.atleast_1d(T)
168
+ P_vals = np.atleast_1d(P)
169
+
170
+ # subcrt needs lists for multiple T/P values
171
+ if len(T_vals) > 1 or len(P_vals) > 1:
172
+ T_arg = T_vals.tolist() if len(T_vals) > 1 else float(T_vals[0])
173
+ P_arg = P_vals.tolist() if len(P_vals) > 1 else float(P_vals[0])
174
+ else:
175
+ T_arg = float(T_vals[0])
176
+ P_arg = float(P_vals[0])
177
+
178
+ supcrt_out = subcrt(['H2O', 'oxygen', 'H+', 'e-'],
179
+ [-1, 0.5, 2, 2],
180
+ T=T_arg, P=P_arg, convert=False, messages=messages, show=False)
181
+
182
+ # Extract logK values
183
+ if hasattr(supcrt_out.out, 'logK'):
184
+ logK = supcrt_out.out.logK
185
+ else:
186
+ logK = supcrt_out.out['logK']
187
+
188
+ # Convert to numpy array
189
+ logK = np.asarray(logK)
190
+
191
+ if units == 'logfo2':
192
+ # Convert Eh to logfO2
193
+ pe_value = convert(value, 'pe', T=T, messages=messages)
194
+ return 2 * (logK + logaH2O + 2*pH + 2*pe_value)
195
+ if units == 'e0':
196
+ # Convert logfO2 to Eh
197
+ pe_value = (-logK - 2*pH + value/2 - logaH2O) / 2
198
+ return convert(pe_value, 'Eh', T=T, messages=messages)
199
+
200
+ else:
201
+ warnings.warn(f"convert: no conversion to {units} found")
202
+ return value
203
+
204
+
205
+ def envert(value: Union[float, np.ndarray, List[float]],
206
+ units: str) -> Union[float, np.ndarray]:
207
+ """
208
+ Convert values to the specified units from those given in thermo()$opt.
209
+
210
+ This function is used internally to convert from the user's preferred units
211
+ (stored in thermo().opt) to standard internal units.
212
+
213
+ Parameters
214
+ ----------
215
+ value : float, ndarray, or list
216
+ Value(s) to convert
217
+ units : str
218
+ Target units ('C', 'K', 'bar', 'MPa', 'J', 'cal')
219
+
220
+ Returns
221
+ -------
222
+ float or ndarray
223
+ Converted value(s)
224
+ """
225
+
226
+ if not isinstance(value, (int, float, np.ndarray, list)):
227
+ return value
228
+
229
+ value = np.asarray(value)
230
+
231
+ # Check if first element is numeric
232
+ if value.size > 0 and not np.issubdtype(value.dtype, np.number):
233
+ return value
234
+
235
+ units = units.lower()
236
+ opt = thermo().opt
237
+
238
+ # Temperature conversions
239
+ if units in ['c', 'k', 't.units']:
240
+ if units == 'c' and opt['T.units'] == 'K':
241
+ return convert(value, 'c')
242
+ if units == 'k' and opt['T.units'] == 'C':
243
+ return convert(value, 'k')
244
+
245
+ # Energy conversions
246
+ if units in ['j', 'cal', 'e.units']:
247
+ if units == 'j' and opt['E.units'] == 'cal':
248
+ return convert(value, 'j')
249
+ if units == 'cal' and opt['E.units'] == 'J':
250
+ return convert(value, 'cal')
251
+
252
+ # Pressure conversions
253
+ if units in ['bar', 'mpa', 'p.units']:
254
+ if units == 'mpa' and opt['P.units'] == 'bar':
255
+ return convert(value, 'mpa')
256
+ if units == 'bar' and opt['P.units'] == 'MPa':
257
+ return convert(value, 'bar')
258
+
259
+ return value