pyEQL 0.5.2__py3-none-any.whl → 1.0.3__py3-none-any.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 (62) hide show
  1. pyEQL/__init__.py +50 -43
  2. pyEQL/activity_correction.py +481 -707
  3. pyEQL/database/geothermal.dat +5693 -0
  4. pyEQL/database/llnl.dat +19305 -0
  5. pyEQL/database/phreeqc_license.txt +54 -0
  6. pyEQL/database/pyeql_db.json +35902 -0
  7. pyEQL/engines.py +793 -0
  8. pyEQL/equilibrium.py +148 -228
  9. pyEQL/functions.py +121 -416
  10. pyEQL/pint_custom_units.txt +2 -2
  11. pyEQL/presets/Ringers lactate.yaml +20 -0
  12. pyEQL/presets/normal saline.yaml +17 -0
  13. pyEQL/presets/rainwater.yaml +17 -0
  14. pyEQL/presets/seawater.yaml +29 -0
  15. pyEQL/presets/urine.yaml +26 -0
  16. pyEQL/presets/wastewater.yaml +21 -0
  17. pyEQL/salt_ion_match.py +53 -284
  18. pyEQL/solute.py +126 -191
  19. pyEQL/solution.py +2163 -2090
  20. pyEQL/utils.py +211 -0
  21. pyEQL-1.0.3.dist-info/AUTHORS.md +13 -0
  22. {pyEQL-0.5.2.dist-info → pyEQL-1.0.3.dist-info}/COPYING +1 -1
  23. pyEQL-0.5.2.dist-info/LICENSE → pyEQL-1.0.3.dist-info/LICENSE.txt +3 -7
  24. pyEQL-1.0.3.dist-info/METADATA +131 -0
  25. pyEQL-1.0.3.dist-info/RECORD +27 -0
  26. {pyEQL-0.5.2.dist-info → pyEQL-1.0.3.dist-info}/WHEEL +1 -1
  27. pyEQL/chemical_formula.py +0 -1006
  28. pyEQL/database/Erying_viscosity.tsv +0 -18
  29. pyEQL/database/Jones_Dole_B.tsv +0 -32
  30. pyEQL/database/Jones_Dole_B_inorganic_Jenkins.tsv +0 -75
  31. pyEQL/database/LICENSE +0 -4
  32. pyEQL/database/dielectric_parameter.tsv +0 -30
  33. pyEQL/database/diffusion_coefficient.tsv +0 -116
  34. pyEQL/database/hydrated_radius.tsv +0 -35
  35. pyEQL/database/ionic_radius.tsv +0 -35
  36. pyEQL/database/partial_molar_volume.tsv +0 -22
  37. pyEQL/database/pitzer_activity.tsv +0 -169
  38. pyEQL/database/pitzer_volume.tsv +0 -132
  39. pyEQL/database/template.tsv +0 -14
  40. pyEQL/database.py +0 -300
  41. pyEQL/elements.py +0 -4552
  42. pyEQL/logging_system.py +0 -53
  43. pyEQL/parameter.py +0 -435
  44. pyEQL/tests/__init__.py +0 -32
  45. pyEQL/tests/test_activity.py +0 -578
  46. pyEQL/tests/test_bulk_properties.py +0 -86
  47. pyEQL/tests/test_chemical_formula.py +0 -279
  48. pyEQL/tests/test_debye_length.py +0 -79
  49. pyEQL/tests/test_density.py +0 -106
  50. pyEQL/tests/test_dielectric.py +0 -153
  51. pyEQL/tests/test_effective_pitzer.py +0 -276
  52. pyEQL/tests/test_mixed_electrolyte_activity.py +0 -154
  53. pyEQL/tests/test_osmotic_coeff.py +0 -99
  54. pyEQL/tests/test_pyeql_volume_concentration.py +0 -428
  55. pyEQL/tests/test_salt_matching.py +0 -337
  56. pyEQL/tests/test_solute_properties.py +0 -251
  57. pyEQL/water_properties.py +0 -352
  58. pyEQL-0.5.2.dist-info/AUTHORS +0 -7
  59. pyEQL-0.5.2.dist-info/METADATA +0 -72
  60. pyEQL-0.5.2.dist-info/RECORD +0 -47
  61. pyEQL-0.5.2.dist-info/entry_points.txt +0 -3
  62. {pyEQL-0.5.2.dist-info → pyEQL-1.0.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,20 @@
1
+ '@module': pyEQL.solution
2
+ '@class': Solution
3
+ '@version': 0.0.post1.dev699+g0764b1c
4
+ solutes:
5
+ H2O(aq): 55.2313771443148 mol
6
+ Na[+1]: 0.13 mol
7
+ Cl[-1]: 0.109 mol
8
+ H5(CO)3[-1]: 0.028 mol
9
+ K[+1]: 0.004 mol
10
+ Ca[+2]: 0.0015 mol
11
+ H[+1]: 3.162277660168379e-07 mol
12
+ OH[-1]: 3.162277660168379e-08 mol
13
+ volume: 1 l
14
+ temperature: 298.15 K
15
+ pressure: 1 atm
16
+ pH: 6.5
17
+ pE: 8.5
18
+ balance_charge:
19
+ solvent: H2O(aq)
20
+ engine: native
@@ -0,0 +1,17 @@
1
+ '@module': pyEQL.solution
2
+ '@class': Solution
3
+ '@version': 0.0.post1.dev699+g0764b1c
4
+ solutes:
5
+ H2O(aq): 55.19703719794332 mol
6
+ Na[+1]: 0.154 mol
7
+ Cl[-1]: 0.154 mol
8
+ H[+1]: 1e-07 mol
9
+ OH[-1]: 1e-07 mol
10
+ volume: 1 l
11
+ temperature: 298.15 K
12
+ pressure: 1 atm
13
+ pH: 7.0
14
+ pE: 8.5
15
+ balance_charge:
16
+ solvent: H2O(aq)
17
+ engine: native
@@ -0,0 +1,17 @@
1
+ '@module': pyEQL.solution
2
+ '@class': Solution
3
+ '@version': 0.0.post1.dev699+g0764b1c
4
+ solutes:
5
+ H2O(aq): 55.34454944845822 mol
6
+ HCO3[-1]: 3.162277660168379e-06 mol
7
+ H[+1]: 1e-06 mol
8
+ OH[-1]: 1e-08 mol
9
+ CO3[-2]: 1e-09 mol
10
+ volume: 1 l
11
+ temperature: 298.15 K
12
+ pressure: 1 atm
13
+ pH: 6.0
14
+ pE: 8.5
15
+ balance_charge:
16
+ solvent: H2O(aq)
17
+ engine: native
@@ -0,0 +1,29 @@
1
+ '@module': pyEQL.solution
2
+ '@class': Solution
3
+ '@version': 0.0.post1.dev699+g0764b1c
4
+ solutes:
5
+ H2O(aq): 55.34455401423017 mol
6
+ Cl[-1]: 0.54425785619973 mol
7
+ Na[+1]: 0.4675827371496296 mol
8
+ Mg[+2]: 0.05266118052346798 mol
9
+ SO4[-2]: 0.02815187344845402 mol
10
+ Ca[+2]: 0.010251594148212317 mol
11
+ K[+1]: 0.010177468379526855 mol
12
+ HCO3[-1]: 0.0017126511769261989 mol
13
+ Br[-1]: 0.0008395244921424561 mol
14
+ B(OH)3(aq): 0.0003134669156396757 mol
15
+ CO3[-2]: 0.00023825904349479544 mol
16
+ B(OH)4(aq): 0.0001005389715937341 mol
17
+ Sr[+2]: 9.046483353663284e-05 mol
18
+ F[-1]: 6.822478260456777e-05 mol
19
+ CO2(aq): 9.515218476861173e-06 mol
20
+ OH[-1]: 8.207436858780224e-06 mol
21
+ H[+1]: 7.943282347242822e-09 mol
22
+ volume: 1.0080615264452506 l
23
+ temperature: 298.15 K
24
+ pressure: 1 atm
25
+ pH: 8.103487039827968
26
+ pE: 8.5
27
+ balance_charge:
28
+ solvent: H2O(aq)
29
+ engine: native
@@ -0,0 +1,26 @@
1
+ '@module': pyEQL.solution
2
+ '@class': Solution
3
+ '@version': 0.0.post1.dev699+g0764b1c
4
+ solutes:
5
+ H2O(aq): 55.135679438263864 mol
6
+ H4CN2O(aq): 0.333026615820163 mol
7
+ Na[+1]: 0.26098565526795925 mol
8
+ Cl[-1]: 0.05359207965475418 mol
9
+ K[+1]: 0.038364839391994025 mol
10
+ H4N[+1]: 0.027718552470665455 mol
11
+ SO4[-2]: 0.018737781405042127 mol
12
+ PO4[-3]: 0.012635387918307416 mol
13
+ H7C4N3O(aq): 0.008840335409397701 mol
14
+ HCO3[-1]: 0.004916675462052772 mol
15
+ Mg[+2]: 0.004114379757251594 mol
16
+ H4C5N4O3(aq): 0.0017845430730997621 mol
17
+ H[+1]: 1e-07 mol
18
+ OH[-1]: 1e-07 mol
19
+ volume: 1 l
20
+ temperature: 298.15 K
21
+ pressure: 1 atm
22
+ pH: 7.0
23
+ pE: 8.5
24
+ balance_charge:
25
+ solvent: H2O(aq)
26
+ engine: native
@@ -0,0 +1,21 @@
1
+ '@module': pyEQL.solution
2
+ '@class': Solution
3
+ '@version': 0.0.post1.dev699+g0764b1c
4
+ solutes:
5
+ H2O(aq): 55.342123269143364 mol
6
+ CH3COOH(aq): 0.006827420786931851 mol
7
+ Cl[-1]: 0.0016641751050686824 mol
8
+ H3N(aq): 0.0014268501490265714 mol
9
+ K[+1]: 0.0004092249535146029 mol
10
+ SO4[-2]: 0.00027065684251727516 mol
11
+ PO4[-3]: 8.002412348261364e-05 mol
12
+ H[+1]: 1e-07 mol
13
+ OH[-1]: 1e-07 mol
14
+ volume: 1 l
15
+ temperature: 298.15 K
16
+ pressure: 1 atm
17
+ pH: 7.0
18
+ pE: 8.5
19
+ balance_charge:
20
+ solvent: H2O(aq)
21
+ engine: native
pyEQL/salt_ion_match.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """
2
- pyEQL salt matching library
2
+ pyEQL salt matching library.
3
3
 
4
4
  This file contains functions that allow a pyEQL Solution object composed of
5
5
  individual species (usually ions) to be mapped to a solution of one or more
@@ -7,72 +7,51 @@ salts. This mapping is necessary because some parameters (such as activity
7
7
  coefficient data) can only be determined for salts (e.g. NaCl) and not individual
8
8
  species (e.g. Na+)
9
9
 
10
- :copyright: 2013-2020 by Ryan S. Kingsbury
10
+ :copyright: 2013-2024 by Ryan S. Kingsbury
11
11
  :license: LGPL, see LICENSE for more details.
12
12
 
13
13
  """
14
- # logging system
15
- import logging
16
14
 
17
- # add a filter to emit only unique log messages to the handler
18
- from pyEQL.logging_system import Unique
15
+ from monty.json import MSONable
16
+ from pymatgen.core.ion import Ion
19
17
 
20
- import pyEQL.chemical_formula as chem
18
+ from pyEQL.utils import standardize_formula
21
19
 
22
- logger = logging.getLogger(__name__)
23
20
 
24
- unique = Unique()
25
- logger.addFilter(unique)
26
-
27
- # add a handler for console output, since pyEQL is meant to be used interactively
28
- ch = logging.StreamHandler()
29
-
30
- # create formatter for the log
31
- formatter = logging.Formatter("(%(name)s) - %(levelname)s - %(message)s")
32
-
33
- # add formatter to the handler
34
- ch.setFormatter(formatter)
35
- logger.addHandler(ch)
36
-
37
-
38
- class Salt:
39
- """
40
- Class to represent a salt.
41
- """
42
-
43
- def __init__(self, cation, anion):
44
- self.cation = cation
45
- self.anion = anion
21
+ class Salt(MSONable):
22
+ """Class to represent a salt."""
46
23
 
24
+ def __init__(self, cation, anion) -> None:
47
25
  """
48
- Create a salt object based on its component ions
49
-
26
+ Create a Salt object based on its component ions.
27
+
50
28
  Parameters:
51
- ----------
52
- cation, anion : str
53
- Chemical formula of the cation and anion, respectively
54
-
29
+ cation, anion: (str) Chemical formula of the cation and anion, respectively.
30
+
55
31
  Returns:
56
- -------
57
- Salt : An object representing the properties of the salt
58
-
32
+ Salt : An object representing the properties of the salt
33
+
59
34
  Examples:
60
- --------
61
- >>> Salt('Na+','Cl-').formula
62
- 'NaCl'
63
-
64
- >>> Salt('Mg++','Cl-').formula
65
- 'MgCl2'
66
-
35
+ >>> Salt('Na+','Cl-').formula
36
+ 'NaCl'
37
+
38
+ >>> Salt('Mg++','Cl-').formula
39
+ 'MgCl2'
67
40
  """
41
+ # create pymatgen Ion objects
42
+ pmg_cat = Ion.from_formula(cation)
43
+ pmg_an = Ion.from_formula(anion)
44
+ # standardize the cation and anion formulas
45
+ self.cation = standardize_formula(cation)
46
+ self.anion = standardize_formula(anion)
68
47
 
69
48
  # get the charges on cation and anion
70
- self.z_cation = chem.get_formal_charge(cation)
71
- self.z_anion = chem.get_formal_charge(anion)
49
+ self.z_cation = pmg_cat.charge
50
+ self.z_anion = pmg_an.charge
72
51
 
73
52
  # assign stoichiometric coefficients by finding a common multiple
74
- self.nu_cation = abs(self.z_anion)
75
- self.nu_anion = abs(self.z_cation)
53
+ self.nu_cation = int(abs(self.z_anion))
54
+ self.nu_anion = int(abs(self.z_cation))
76
55
 
77
56
  # if both coefficients are the same, set each to one
78
57
  if self.nu_cation == self.nu_anion:
@@ -83,261 +62,51 @@ class Salt:
83
62
  salt_formula = ""
84
63
  if self.nu_cation > 1:
85
64
  # add parentheses if the cation is a polyatomic ion
86
- if len(chem.get_elements(cation)) > 1:
65
+ if len(pmg_cat.elements) > 1:
87
66
  salt_formula += "("
88
- salt_formula += _trim_formal_charge(cation)
67
+ salt_formula += self.cation.split("[")[0]
89
68
  salt_formula += ")"
90
69
  else:
91
- salt_formula += _trim_formal_charge(cation)
70
+ salt_formula += self.cation.split("[")[0]
92
71
  salt_formula += str(self.nu_cation)
93
72
  else:
94
- salt_formula += _trim_formal_charge(cation)
73
+ salt_formula += self.cation.split("[")[0]
95
74
 
96
75
  if self.nu_anion > 1:
97
76
  # add parentheses if the anion is a polyatomic ion
98
- if len(chem.get_elements(anion)) > 1:
77
+ if len(pmg_an.elements) > 1:
99
78
  salt_formula += "("
100
- salt_formula += _trim_formal_charge(anion)
79
+ salt_formula += self.anion.split("[")[0]
101
80
  salt_formula += ")"
102
81
  else:
103
- salt_formula += _trim_formal_charge(anion)
82
+ salt_formula += self.anion.split("[")[0]
104
83
  salt_formula += str(self.nu_anion)
105
84
  else:
106
- salt_formula += _trim_formal_charge(anion)
85
+ salt_formula += self.anion.split("[")[0]
107
86
 
108
87
  self.formula = salt_formula
109
88
 
89
+ # TODO - consider whether this should be adjusted to be based on total concentrations or not
90
+ # NOTE: speciating the solution results in a decrease in the overall ionic strength, because some of the
91
+ # Mg+2 is converted to monovalent complexes like MgOH+. Hence, the activity coefficients deviate a bit from
92
+ # the published values.
110
93
  def get_effective_molality(self, ionic_strength):
111
- """ Calculate the effective molality according to [#]_
94
+ r"""Calculate the effective molality according to [mistry]_.
112
95
 
113
- .. math:: 2 I \\over (\\nu_+ z_+^2 + \\nu_- z_- ^2)
96
+ .. math:: 2 I \over (\nu_+ z_+^2 + \nu_- z_- ^2)
114
97
 
115
- Parameters
116
- ----------
117
- ionic_strength: Quantity
118
- The ionic strength of the parent solution, mol/kg
98
+ Args:
99
+ ionic_strength: Quantity
100
+ The ionic strength of the parent solution, mol/kg
119
101
 
120
- Returns
121
- -------
122
- Quantity: the effective molality of the salt in the parent solution
102
+ Returns:
103
+ Quantity: the effective molality of the salt in the parent solution
123
104
 
124
- References
125
- ----------
126
- .. [#] Mistry, K. H.; Hunter, H. a.; Lienhard V, J. H. Effect of \
127
- composition and nonideal solution behavior on desalination calculations \
128
- for mixed electrolyte solutions with comparison to seawater. \
129
- Desalination 2013, 318, 34–47.
105
+ References:
106
+ .. [mistry] Mistry, K. H.; Hunter, H. a.; Lienhard V, J. H. Effect of composition and nonideal solution behavior
107
+ on desalination calculations for mixed electrolyte solutions with comparison to seawater. Desalination
108
+ 2013, 318, 34-47.
130
109
  """
131
- m_effective = (
132
- 2
133
- * ionic_strength
134
- / (self.nu_cation * self.z_cation ** 2 + self.nu_anion * self.z_anion ** 2)
135
- )
110
+ m_effective = 2 * ionic_strength / (self.nu_cation * self.z_cation**2 + self.nu_anion * self.z_anion**2)
136
111
 
137
112
  return m_effective.to("mol/kg")
138
-
139
-
140
- def _sort_components(Solution, type="all"):
141
- """
142
- Sort the components of a solution in descending order (by mol).
143
-
144
- Parameters:
145
- ----------
146
- Solution : Solution object
147
- type : The type of component to be sorted. Defaults to 'all' for all
148
- solutes. Other valid arguments are 'cations' and 'anions' which
149
- return sorted lists of cations and anions, respectively.
150
-
151
- Returns:
152
- -------
153
- A list whose keys are the component names (formulas) and whose
154
- values are the component objects themselves
155
-
156
-
157
- """
158
- formula_list = []
159
-
160
- # populate a list with component names
161
- for item in Solution.components:
162
- if type == "all":
163
- formula_list.append(item)
164
- elif type == "cations":
165
- if Solution.get_solute(item).get_formal_charge() > 0:
166
- formula_list.append(item)
167
- elif type == "anions":
168
- if Solution.get_solute(item).get_formal_charge() < 0:
169
- formula_list.append(item)
170
-
171
- # populate a dictionary with formula:concentration pairs
172
- mol_list = {}
173
- for item in formula_list:
174
- mol_list.update({item: Solution.get_amount(item, "mol")})
175
-
176
- return sorted(formula_list, key=mol_list.__getitem__, reverse=True)
177
-
178
-
179
- def identify_salt(Solution):
180
- """
181
- Analyze the components of a solution and identify the salt that most closely
182
- approximates it.
183
- (e.g., if a solution contains 0.5 mol/kg of Na+ and Cl-, plus traces of H+
184
- and OH-, the matched salt is 0.5 mol/kg NaCl)
185
-
186
- Create a Salt object for this salt.
187
-
188
- Returns:
189
- -------
190
- A Salt object.
191
- """
192
- # sort the components by moles
193
- sort_list = _sort_components(Solution)
194
-
195
- # default to returning water as the salt
196
- cation = "H+"
197
- anion = "OH-"
198
-
199
- # return water if there are no solutes
200
- if len(sort_list) < 3 and sort_list[0] == "H2O":
201
- logger.info("Salt matching aborted because there are not enough solutes.")
202
- return Salt(cation, anion)
203
-
204
- # warn if something other than water is the predominant component
205
- if sort_list[0] != "H2O":
206
- logger.warning("H2O is not the most prominent component")
207
-
208
- # take the dominant cation and anion and assemble a salt from them
209
- for item in sort_list:
210
- if chem.get_formal_charge(item) > 0 and cation == "H+":
211
- cation = item
212
- elif chem.get_formal_charge(item) < 0 and anion == "OH-":
213
- anion = item
214
- else:
215
- pass
216
-
217
- # assemble the salt
218
- return Salt(cation, anion)
219
-
220
-
221
- def generate_salt_list(Solution, unit="mol/kg"):
222
- """
223
- Generate a list of salts that represents the ionic composition of a
224
- solution.
225
-
226
- Returns:
227
- -------
228
- dict
229
- A dictionary of Salt objects, keyed to the formula of the salt.
230
-
231
- """
232
- salt_list = {}
233
-
234
- # sort the cations and anions by moles
235
- cation_list = _sort_components(Solution, type="cations")
236
- anion_list = _sort_components(Solution, type="anions")
237
-
238
- # iterate through the lists of ions
239
- # create salts by matching the equivalent concentrations of cations
240
- # and anions along the way
241
- len_cat = len(cation_list)
242
- len_an = len(anion_list)
243
-
244
- # start with the first cation and anion
245
- index_cat = 0
246
- index_an = 0
247
-
248
- # calculate the equivalent concentrations of each ion
249
- c1 = Solution.get_amount(cation_list[index_cat], unit) * chem.get_formal_charge(
250
- cation_list[index_cat]
251
- )
252
- a1 = Solution.get_amount(anion_list[index_an], unit) * abs(
253
- chem.get_formal_charge(anion_list[index_an])
254
- )
255
-
256
- while index_cat < len_cat and index_an < len_an:
257
- # if the cation concentration is greater, there will be leftover cations
258
- if c1 > a1:
259
- # create the salt
260
- x = Salt(cation_list[index_cat], anion_list[index_an])
261
- # there will be leftover cation, so use the anion amount
262
- amount = a1 / abs(x.z_anion)
263
- # add it to the list
264
- salt_list.update({x: amount})
265
- # adjust the amounts of the respective ions
266
- c1 = c1 - a1
267
- # move to the next anion
268
- index_an += 1
269
- try:
270
- a1 = Solution.get_amount(anion_list[index_an], unit) * abs(
271
- chem.get_formal_charge(anion_list[index_an])
272
- )
273
- except IndexError:
274
- continue
275
- # if the anion concentration is greater, there will be leftover anions
276
- if c1 < a1:
277
- # create the salt
278
- x = Salt(cation_list[index_cat], anion_list[index_an])
279
- # there will be leftover anion, so use the cation amount
280
- amount = c1 / x.z_cation
281
- # add it to the list
282
- salt_list.update({x: amount})
283
- # calculate the leftover cation amount
284
- a1 = a1 - c1
285
- # move to the next cation
286
- index_cat += 1
287
- try:
288
- c1 = Solution.get_amount(
289
- cation_list[index_cat], unit
290
- ) * chem.get_formal_charge(cation_list[index_cat])
291
- except IndexError:
292
- continue
293
- if c1 == a1:
294
- # create the salt
295
- x = Salt(cation_list[index_cat], anion_list[index_an])
296
- # there will be nothing leftover, so it doesn't matter which ion you use
297
- amount = c1 / x.z_cation
298
- # add it to the list
299
- salt_list.update({x: amount})
300
- # move to the next cation and anion
301
- index_an += 1
302
- index_cat += 1
303
- try:
304
- c1 = Solution.get_amount(
305
- cation_list[index_cat], unit
306
- ) * chem.get_formal_charge(cation_list[index_cat])
307
- a1 = Solution.get_amount(anion_list[index_an], unit) * abs(
308
- chem.get_formal_charge(anion_list[index_an])
309
- )
310
- except IndexError:
311
- continue
312
-
313
- return salt_list
314
-
315
-
316
- def _trim_formal_charge(formula):
317
- """
318
- remove the formal charge from a chemical formula
319
-
320
- Examples:
321
- --------
322
- >>> _trim_formal_charge('Fe+++')
323
- 'Fe'
324
- >>> _trim_formal_charge('SO4-2')
325
- 'SO4'
326
- >>> _trim_formal_charge('Na+')
327
- 'Na'
328
-
329
- """
330
- charge = chem.get_formal_charge(formula)
331
- output = ""
332
- if charge > 0:
333
- output = formula.split("+")[0]
334
- elif charge < 0:
335
- output = formula.split("-")[0]
336
-
337
- return output
338
-
339
-
340
- # TODO - turn doctest back on when the nosigint error is gone
341
- # if __name__ == "__main__":
342
- # import doctest
343
- # doctest.testmod()