crunchflow 2.0.2__tar.gz → 2.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: crunchflow
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: A Python toolbox for working with the CrunchFlow reactive transport code
5
5
  License: GPL-3.0-or-later
6
6
  Author: Zach Perzan
@@ -62,7 +62,7 @@ class Runtime(KeywordBlock):
62
62
  self.coordinate = ''
63
63
  self.coordinates = ''
64
64
  self.correction_max = ''
65
- self.Courant_number = ''
65
+ self.courant_number = ''
66
66
  self.database = ''
67
67
  self.database_sweep = ''
68
68
  self.debye_huckel = ''
@@ -476,7 +476,7 @@ class Gases(SpeciesBlock):
476
476
  pass
477
477
 
478
478
 
479
- class KineticsBlock(KeywordBlock):
479
+ class SurfaceComplexation(KeywordBlock):
480
480
  def __init__(self):
481
481
  super().__init__()
482
482
  self.species = []
@@ -494,8 +494,43 @@ class KineticsBlock(KeywordBlock):
494
494
  return "\n".join(result)
495
495
 
496
496
 
497
- class SurfaceComplexation(KineticsBlock):
498
- pass
497
+ class KineticsBlock(KeywordBlock):
498
+ def __init__(self):
499
+ super().__init__()
500
+ self.species_dict = {}
501
+ self.species = []
502
+
503
+ def set_parameters(self, parameters):
504
+ for species, details in parameters.items():
505
+ label = details.pop('label', None)
506
+ if species not in self.species_dict:
507
+ self.species_dict[species] = {}
508
+ self.species.append(species)
509
+ if label:
510
+ self.species_dict[species][label] = details
511
+ else:
512
+ self.species_dict[species]['default'] = details
513
+
514
+ def update_parameters(self, species, label, new_details):
515
+ if species in self.species_dict and label in self.species_dict[species]:
516
+ self.species_dict[species][label].update(new_details)
517
+ else:
518
+ print(f"Species '{species}' with label '{label}' not found.")
519
+
520
+ def __str__(self):
521
+ result = []
522
+ for species, labels in self.species_dict.items():
523
+ if 'default' in labels and not labels['default']:
524
+ result.append(f"{species:<20}")
525
+ else:
526
+ for label, details in labels.items():
527
+ if label == 'default' and not details:
528
+ result.append(f"{species:<20}")
529
+ else:
530
+ details_str = ' '.join(f'-{k} {v}' for k, v in details.items())
531
+ label_str = f"-label {label} " if label != 'default' else ""
532
+ result.append(f"{species:<20} {label_str}{details_str}")
533
+ return "\n".join(result)
499
534
 
500
535
 
501
536
  class Minerals(KineticsBlock):
@@ -63,7 +63,7 @@ class InputFile:
63
63
  # Initialize other blocks as needed
64
64
 
65
65
  def set_block(self, block_name, parameters):
66
- """Set the parameters for a block in the Run instance.
66
+ """Set the parameters of a block in an InputFile instance.
67
67
 
68
68
  Parameters
69
69
  ----------
@@ -93,11 +93,9 @@ class InputFile:
93
93
  # Note that the "CONDITION <name>" is included in
94
94
  # Condition.__str__ method, so it is omitted here
95
95
  result.append(f"{str(condition)}\nEND")
96
- # elif isinstance(value, list):
97
- # for item in value:
98
- # if any(val for val in item.__dict__.values() if val):
99
- # block_name = format_class_name(item.__class__.__name__)
100
- # result.append(f"{block_name}\n{str(item)}\nEND")
96
+
97
+ # Otherwise, check if the block has any attributes set
98
+ # and if so, format the block name then print it
101
99
  elif any(val for val in value.__dict__.values() if val):
102
100
  block_name = format_class_name(value.__class__.__name__)
103
101
  result.append(f"{block_name}\n{str(value)}\nEND")
@@ -114,7 +112,12 @@ class InputFile:
114
112
  The path to the output file. Default is the current directory.
115
113
  update_pestcontrol : bool, optional
116
114
  Whether to update PestControl.ant with the name of the
117
- CrunchFlow input file. Default is False."""
115
+ CrunchFlow input file. Default is False.
116
+
117
+ Returns
118
+ -------
119
+ None
120
+ The CrunchFlow input file is saved to disk."""
118
121
 
119
122
  full_path = os.path.join(path, filename)
120
123
  with open(full_path, 'w') as file:
@@ -127,7 +130,7 @@ class InputFile:
127
130
  if update_pestcontrol:
128
131
  folder = os.path.dirname(full_path)
129
132
  pestfile = os.path.join(folder, 'PestControl.ant')
130
- with open('PestControl.ant', 'w') as file:
133
+ with open(pestfile, 'w') as file:
131
134
  file.write('%s \n' % os.path.basename(full_path))
132
135
 
133
136
  @classmethod
@@ -160,7 +163,7 @@ class InputFile:
160
163
  # Define attributes and blocks to be handled as special cases
161
164
  # SpeciesBlock and KineticsBlock instances are handled differently below
162
165
  species_blocks = ['primary_species', 'secondary_species', 'gases']
163
- kinetics_blocks = ['minerals', 'aqueous_kinetics', 'surface_complexation']
166
+ kinetics_blocks = ['minerals', 'aqueous_kinetics']
164
167
 
165
168
  # Some attributes can be set multiple times within a single block
166
169
  multiply_defined = ['time_series', 'pressure', 'mineral',
@@ -229,13 +232,35 @@ class InputFile:
229
232
  parts = line.split()
230
233
  current_block.species.append(parts[0])
231
234
 
232
- elif current_block_name in kinetics_blocks:
235
+ elif current_block_name == 'surface_complexation':
233
236
  parts = line.split()
234
237
  species = parts[0]
235
238
  details = " ".join(parts[1:]) if len(parts) > 1 else ""
236
239
  current_block.species.append(species)
237
240
  current_block.species_dict[species] = details
238
241
 
242
+ elif current_block_name in kinetics_blocks:
243
+ parts = line.split()
244
+ species = parts[0]
245
+ details = {}
246
+
247
+ # Assume the default label
248
+ # This will be updated below if it is included in details
249
+ label = 'default'
250
+
251
+ # If there are details associated with the species_dict, then parse them
252
+ if len(parts) > 1:
253
+ # Loop through kinetic options (e.g., -activation, -rate, etc.)
254
+ # and store this information in a dictionary
255
+ for i in range(1, len(parts), 2):
256
+ key = parts[i].lstrip('-')
257
+ value = parts[i+1] if (i+1) < len(parts) else None
258
+ if key == 'label':
259
+ label = value
260
+ else:
261
+ details[key] = value
262
+ current_block.set_parameters({species: {**details, 'label': label}})
263
+
239
264
  # All other blocks, split on whitespace
240
265
  else:
241
266
  parts = line.split()
@@ -42,21 +42,24 @@ def get_tec_metadata(file, folder='.'):
42
42
  # Open file and read line by line
43
43
  inpath = os.path.join(folder, file)
44
44
  with open(inpath) as f:
45
+ lines = f.readlines()
46
+
45
47
  # Read the first line to determine the format of the file
46
- line = f.readline()
47
- if "TITLE" in line:
48
- title = line.split('"')[1]
48
+ if "TITLE" in lines[0]:
49
+ title = lines[0].split('"')[1]
49
50
  fmt = '.tec'
50
- elif 'Time' in line:
51
+ elif 'Time' in lines[0]:
51
52
  title = ''
52
53
  fmt = '.out'
54
+ elif 'Units' in lines[0]:
55
+ title = ''
56
+ fmt = '.dat'
53
57
  else:
54
58
  raise ValueError("Could not determine the format of this file")
55
59
 
56
60
  # Read the rest of the header
57
61
  if fmt == '.tec':
58
- line = f.readline()
59
- columns = line.split('"')
62
+ columns = lines[1].split('"')
60
63
 
61
64
  # Remove x, y, z
62
65
  columns = [col.strip() for col in columns]
@@ -70,11 +73,14 @@ def get_tec_metadata(file, folder='.'):
70
73
  for col in remove_fields:
71
74
  columns.remove(col)
72
75
  elif fmt == '.out':
73
- line = f.readline() # skip units line
74
- line = f.readline()
76
+ # pH files do not have a units line
77
+ if 'pH' in file:
78
+ columns_line = lines[1] # skip units line
79
+ else:
80
+ columns_line = lines[2]
75
81
 
76
82
  # Split on whitespace
77
- raw_cols = line.split()
83
+ raw_cols = columns_line.split()
78
84
 
79
85
  # Some versions of CrunchFlow don't include column names
80
86
  # in this case, the column entries should be numeric and we'll
@@ -84,6 +90,35 @@ def get_tec_metadata(file, folder='.'):
84
90
  else:
85
91
  columns = raw_cols
86
92
 
93
+ elif fmt == '.dat':
94
+ title = lines[1].split('"')[1]
95
+ columns = lines[2].split('"')
96
+
97
+ # Remove x, y, z
98
+ columns = [col.strip() for col in columns]
99
+ for col in ['VARIABLES =']:
100
+ if col in columns:
101
+ columns.remove(col)
102
+
103
+ # Remove commas, which can be repeated so will not be removed with loop above
104
+ columns = [col.replace(',', '') for col in columns]
105
+
106
+ # Remove whitespace, which was reduced to len 0 via col.strip()
107
+ remove_fields = [col for col in columns if len(col) == 0]
108
+ for col in remove_fields:
109
+ columns.remove(col)
110
+
111
+ # With .dat files, the naming scheme for x, y and z varies
112
+ # depending on the spatial units
113
+ if len(lines[3].split(',')) == 3:
114
+ # then this is 2d, so remove first two columns
115
+ columns = columns[2:]
116
+ elif len(lines[3].split(',')) == 4:
117
+ # then this is 3d, so remove first three columns
118
+ columns = columns[3:]
119
+ else:
120
+ raise ValueError("Could not determine the format of this file")
121
+
87
122
  return columns, title, fmt
88
123
 
89
124
 
@@ -197,6 +232,29 @@ class SpatialProfile:
197
232
  Get line segments that outline all the regions equal to
198
233
  the provided value.
199
234
 
235
+ __init__(fileprefix, folder, output_times, suffix)
236
+ Read in and get basic info about all .tec files matching `fileprefix`.
237
+ For example, `tec('volume')` will read in all files matching
238
+ 'volume[0-9]+.tec'
239
+
240
+ Parameters
241
+ ----------
242
+ fileprefix : str
243
+ file prefix of the tec file to read in, without the timestep
244
+ number or ".tec" file ending. For example, if your files are
245
+ "volume1.tec", "volume2.tec", etc., then fileprefix should be
246
+ "volume".
247
+ folder : str, optional
248
+ folder in which the .tec files are located. The default is the
249
+ current directory
250
+ output_times : list of float, optional
251
+ list of the actual output times at which the .tec files were
252
+ output, in CrunchFlow time units
253
+ suffix : str, optional
254
+ file ending of the tec files to read in. This can vary depending on the
255
+ version of CrunchTope used. The default is '.tec', but '.out' is also
256
+ tried if '.tec' files are not found.
257
+
200
258
  Examples
201
259
  --------
202
260
  >>> vol = SpatialProfile('volume')
@@ -207,7 +265,7 @@ class SpatialProfile:
207
265
 
208
266
  def __init__(self, fileprefix, folder='.', output_times=None, suffix='.tec'):
209
267
  """Read in and get basic info about all .tec files matching `fileprefix`.
210
- For example, `tec('volume')` will read in all files matching
268
+ For example, `SpatialProfile('volume')` will read in all files matching
211
269
  'volume[0-9]+.tec'
212
270
 
213
271
  Parameters
@@ -217,27 +275,30 @@ class SpatialProfile:
217
275
  number or ".tec" file ending. For example, if your files are
218
276
  "volume1.tec", "volume2.tec", etc., then fileprefix should be
219
277
  "volume".
220
- folder : str
278
+ folder : str, optional
221
279
  folder in which the .tec files are located. The default is the
222
280
  current directory
223
- output_times : list of float
281
+ output_times : list of float, optional
224
282
  list of the actual output times at which the .tec files were
225
283
  output, in CrunchFlow time units
226
- suffix : str
284
+ suffix : str, optional
227
285
  file ending of the tec files to read in. This can vary depending on the
228
- version of CrunchTope used. The default is '.tec', but '.out' is also
229
- tried if '.tec' files are not found.
286
+ version of CrunchTope used. The default is '.tec', but '.out' and '.dat'
287
+ are also tried if '.tec' files are not found.
230
288
  """
231
289
 
232
290
  # Glob doesn't support regex, so match manually using re and os
233
291
  search_str = '^' + fileprefix + '[0-9]+%s' % suffix
234
292
  unsort_files = [f for f in os.listdir(folder) if re.search(search_str, f)]
235
293
 
294
+ # Try again with .out or .dat suffix
236
295
  if len(unsort_files) == 0:
237
- # Try again with .out suffix
238
- search_str = '^' + fileprefix + '[0-9]+.out'
239
- unsort_files = [f for f in os.listdir(folder) if re.search(search_str, f)]
240
- suffix = '.out'
296
+ for try_suffix in ['.out', '.dat']:
297
+ search_str = '^' + fileprefix + '[0-9]+' + try_suffix
298
+ unsort_files = [f for f in os.listdir(folder) if re.search(search_str, f)]
299
+ if len(unsort_files) > 0:
300
+ suffix = try_suffix
301
+ break
241
302
 
242
303
  if len(unsort_files) == 0:
243
304
  raise ValueError("No files found matching %s. Double check the suffix and\n "
@@ -263,7 +324,7 @@ class SpatialProfile:
263
324
  # Otherwise, try setting it by reading from each .out file
264
325
  if self.fmt == '.out':
265
326
  self.output_times = [get_out_output_time(f) for f in self.files]
266
- elif self.fmt == '.tec':
327
+ elif self.fmt in ['.tec', '.dat']:
267
328
  self.output_times = None
268
329
 
269
330
  # Get the grid size
@@ -298,6 +359,24 @@ class SpatialProfile:
298
359
  else:
299
360
  skiprows = 3
300
361
  self.coords = np.loadtxt(self.files[0], skiprows=skiprows, usecols=[0])
362
+ elif self.fmt == '.dat':
363
+ with open(self.files[0]) as f:
364
+ lines = f.readlines()
365
+
366
+ fields = lines[3].split(',')[1:]
367
+ if len(fields) == 2:
368
+ self.nx = int(re.sub('\\D', '', fields[0]))
369
+ self.ny = int(re.sub('\\D', '', fields[1]))
370
+ self.nz = np.nan
371
+ elif len(fields) == 3:
372
+ self.nx = int(re.sub('\\D', '', fields[0]))
373
+ self.ny = int(re.sub('\\D', '', fields[1]))
374
+ self.nz = int(re.sub('\\D', '', fields[2]))
375
+ else:
376
+ raise ValueError("Could not determine the coordinates in this file")
377
+ self.coords = np.loadtxt(self.files[0], skiprows=4, usecols=[0, 1])
378
+ self.griddedX = self.coords[:, 0].reshape(self.ny, self.nx)
379
+ self.griddedY = self.coords[:, 1].reshape(self.ny, self.nx)
301
380
 
302
381
  def plot(self, var, time=None, plot_type='image', figsize=(12,3),
303
382
  **kwargs):
@@ -561,6 +640,16 @@ class SpatialProfile:
561
640
  skiprows = 3
562
641
  col_no = self.columns.index(var)
563
642
  data = np.loadtxt(self.files[time - 1], skiprows=skiprows, usecols=col_no)
643
+ elif self.fmt == '.dat':
644
+ if np.isnan(self.nz):
645
+ col_no = self.columns.index(var) + 2
646
+ data = np.loadtxt(self.files[time - 1], skiprows=4, usecols=col_no)
647
+ data = data.reshape(self.ny, self.nx)
648
+ else:
649
+ col_no = self.columns.index(var) + 3
650
+ data = np.loadtxt(self.files[time - 1], skiprows=4, usecols=col_no)
651
+ data = data.reshape(self.nz, self.ny, self.nx)
652
+
564
653
  else:
565
654
  raise ValueError("Could not determine the format of this file")
566
655
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "crunchflow"
3
- version = "2.0.2"
3
+ version = "2.0.4"
4
4
  description = "A Python toolbox for working with the CrunchFlow reactive transport code"
5
5
  authors = ["Zach Perzan <zach.perzan@unlv.edu>"]
6
6
  license = "GPL-3.0-or-later"
File without changes
File without changes