forked from acroucher/PyTOUGH
-
Notifications
You must be signed in to change notification settings - Fork 0
/
t2listing.py
executable file
·1865 lines (1737 loc) · 84.1 KB
/
t2listing.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""For reading TOUGH2 listing files.
Copyright 2012 University of Auckland.
This file is part of PyTOUGH.
PyTOUGH is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
PyTOUGH is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along with PyTOUGH. If not, see <http://www.gnu.org/licenses/>."""
try:
import numpy as np
from numpy import float64
except ImportError: # try importing Numeric on old installs
import Numeric as np
from Numeric import Float64 as float64
from mulgrids import fix_blockname, valid_blockname
from fixed_format_file import fortran_float, fortran_int
import io
import sys
class listingtable(object):
"""Class for table in listing file, with values addressable by index
(0-based) or row name, and column name: e.g. table[i] returns the
ith row (as a dictionary), table[rowname] returns the row with the
specified name, and table[colname] returns the column with the
specified name."""
def __init__(self, cols, rows, row_format = None, row_line = None, num_keys = 1,
allow_reverse_keys = False, header_skiplines = 0, skiplines = []):
"""The row_format parameter is a dictionary with three keys,
'key','index' and 'values'. These contain the positions, in
each row of the table, of the start of the keys, index and
data fields. The row_line parameter is a list containing, for
each row of the table, the number of lines before it in the
listing file, from the start of the table. This is needed for
TOUGH2_MP listing files, in which the rows are not in index
order and can also be duplicated.
"""
self.column_name = cols
self.row_name = rows
self.row_format = row_format
self.row_line = row_line
self.num_keys = num_keys
self.allow_reverse_keys = allow_reverse_keys
self.header_skiplines = header_skiplines
self.skiplines = skiplines
self._col = dict([(c,i) for i,c in enumerate(cols)])
self._row = dict([(r,i) for i,r in enumerate(rows)])
self._data = np.zeros((len(rows), len(cols)), float64)
def __repr__(self):
return repr(self.column_name) + '\n' + repr(self._data)
def __getitem__(self,key):
if isinstance(key,int):
return dict(zip(['key'] + self.column_name, [self.row_name[key]] +
list(self._data[key,:])))
else:
if key in self.column_name:
return self._data[:,self._col[key]]
elif key in self.row_name:
rowindex = self._row[key]
return dict(zip(['key'] + self.column_name,
[self.row_name[rowindex]] +
list(self._data[rowindex,:])))
elif len(key) > 1 and self.allow_reverse_keys:
revkey = key[::-1] # try reversed key for multi-key tables
if revkey in self.row_name:
rowindex = self._row[revkey]
return dict(zip(['key'] + self.column_name,
[self.row_name[rowindex][::-1]] +
list(-self._data[rowindex,:])))
else: return None
def __setitem__(self, key, value):
if isinstance(key,int): self._data[key,:] = value
else: self._data[self._row[key],:] = value
def get_num_columns(self):
return len(self.column_name)
num_columns=property(get_num_columns)
def get_num_rows(self):
return len(self.row_name)
num_rows=property(get_num_rows)
def key_from_line(self, line):
key = [fix_blockname(line[pos: pos + 5]) for pos in self.row_format['key']]
if len(key) == 1: return key[0]
else: return tuple(key)
def rows_matching(self, pattern, index = 0, match_any = False):
"""Returns rows in the table with keys matching the specified regular expression pattern
string.
For tables with multiple keys, pattern can be a list or tuple of regular expressions. If
a single string pattern is given for a multiple-key table, the pattern is matched on the
index'th key (and any value of the other key- unless match_any is used; see below).
If match_any is set to True, rows are returned with keys matching any of the specified
patterns (instead of all of them). If this option is used in conjunction with a single
string pattern, the specified pattern is applied to all keys."""
from re import search
if self.num_keys == 1:
return [self[key] for key in self.row_name if search(pattern,key)]
else:
if isinstance(pattern, str): pattern = [pattern]
else: pattern = list(pattern)
if len(pattern) < self.num_keys:
if match_any: default = [pattern[0]]
else: default = ['.*']
if 0 <= index <= self.num_keys:
pattern = default * index + pattern + default * (self.num_keys - 1 - index)
else: return []
combine = [all, any][match_any]
return [self[key] for key in self.row_name if
combine([search(p, n) for p, n in zip(pattern,key)])]
def __add__(self, other):
"""Adds two listing tables together."""
if self.column_name == other.column_name and self.row_name == other.row_name:
from copy import copy
result = listingtable(copy(self.column_name), copy(self.row_name), num_keys = self.num_keys,
allow_reverse_keys = self.allow_reverse_keys)
result._data = self._data + other._data
return result
else: raise Exception("Incompatible tables: can't be added together.")
def __sub__(self, other):
"""Subtracts one listing table from another."""
if self.column_name == other.column_name and self.row_name == other.row_name:
from copy import copy
result = listingtable(copy(self.column_name), copy(self.row_name), num_keys = self.num_keys,
allow_reverse_keys = self.allow_reverse_keys)
result._data = self._data - other._data
return result
else: raise Exception("Incompatible tables: can't be subtracted.")
def is_header(self, line):
"""Returns True if all column headers in the table are present in the given line. Used for
detecting internal table headers in TOUGH2 listings. Some internal header lines have different
spacings from the top header, so a simple string comparison doesn't always work."""
return all([col in line for col in self.column_name])
def get_DataFrame(self):
"""Returns data in the table as a pandas DataFrame object."""
import pandas as pd
row_header = 'row'
datadict = {row_header: self.row_name}
for colname in self.column_name: datadict[colname] = self[colname]
return pd.DataFrame(datadict, columns = [row_header] + self.column_name)
DataFrame = property(get_DataFrame)
class t2listing(object):
"""Class for TOUGH2 listing file. The element, connection and
generation tables can be accessed via the element, connection
and generation fields. (For example, the pressure in block
'aa100' is given by element['aa100']['Pressure'].) It is
possible to navigate through time in the listing by using the
next() and prev() functions to step through, or using the
first() and last() functions to go to the start or end, or to
set the index, step (model time step number) or time properties
directly."""
def __init__(self, filename = None, skip_tables = None, encoding = 'latin-1'):
if skip_tables is None: skip_tables = []
self._table = {}
self._tablenames = []
self.filename = filename
self.skip_tables = skip_tables
self.encoding = encoding
self._file = io.open(filename, 'rb', newline = None)
self.detect_simulator()
if self.simulator is None:
raise Exception('Could not detect simulator type.')
else:
self.setup_short_types()
self.setup_pos()
if self.num_fulltimes > 0:
self._index = 0
self.setup_tables()
self.set_table_attributes()
self.setup_short_indices()
self.first()
else:
raise Exception('No full results found in listing file.')
def __repr__(self): return self.title
def close(self):
self._file.close()
if sys.version_info < (3,):
def readline(self):
"""Reads next line and returns it as a string. In Python 2.x,
readline() already returns a string and doesn't need decoding.
Skipping the decoding step speeds it up."""
return self._file.readline()
else:
def readline(self):
"""Reads next line, decodes it and returns it as a string."""
return self._file.readline().decode(self.encoding)
def get_index(self): return self._index
def set_index(self, i):
self._file.seek(self._fullpos[i])
self._index = i
if self._index < 0: self._index += self.num_fulltimes
self.read_tables()
index = property(get_index,set_index)
def get_time(self): return self._time
def set_time(self, t):
if t < self.fulltimes[0]: self.index = 0
elif t > self.fulltimes[-1]: self.index = -1
else:
dt = np.abs(self.fulltimes - t)
self.index = np.argmin(dt)
time = property(get_time, set_time)
def get_num_times(self): return len(self.times)
num_times = property(get_num_times)
def get_num_fulltimes(self): return len(self.fulltimes)
num_fulltimes = property(get_num_fulltimes)
def get_step(self): return self._step
def set_step(self, step):
if step < self.fullsteps[0]: self.index = 0
elif step > self.fullsteps[-1]: self.index = -1
else:
dstep = np.abs(self.fullsteps - step)
self.index = np.argmin(dstep)
step = property(get_step,set_step)
def get_table_names(self):
return sorted(self._table.keys())
table_names = property(get_table_names)
def rewind(self):
"""Rewinds to start of listing (without reading any results)"""
self._file.seek(0)
self._index = -1
def first(self): self.index = 0
def last(self): self.index = -1
def next(self):
"""Find and read next set of results; returns false if at end of listing"""
more = self.index < self.num_fulltimes - 1
if more: self.index += 1
return more
def prev(self):
"""Find and read previous set of results; returns false if at start of listing"""
more = self.index > 0
if more: self.index -= 1
return more
def skiplines(self, number = 1):
"""Skips specified number of lines in listing file"""
for i in range(number): self._file.readline()
def skipto(self, keyword = '', start = 1):
"""Skips to line starting with keyword. keyword can be either a
string or a list of strings, in which case it skips to a line
starting with any of the specified strings. Returns the
keyword found, or false if it can't find any of them. The
start parameter specifies which character in the line is to be
considered the first to search."""
line = ''
if isinstance(keyword, list): keywords = keyword
else: keywords = [keyword]
while not any([line[start:].startswith(kw) for kw in keywords]):
line = self.readline()
if line == '': return False
return [kw for kw in keywords if line[start:].startswith(kw)][0]
def skip_to_nonblank(self):
"""Skips to start of next non-blank line."""
pos = self._file.tell()
while not self.readline().strip(): pos = self._file.tell()
self._file.seek(pos)
def skip_to_blank(self):
"""Skips to start of next blank line."""
pos = self._file.tell()
while self.readline().strip(): pos = self._file.tell()
self._file.seek(pos)
def skip_over_next_blank(self):
"""Skips past next blank line."""
while self.readline().strip(): pass
def detect_simulator(self):
"""Detects whether the listing has been produced by AUTOUGH2,
TOUGH2, TOUGH2_MP, TOUGH+ or TOUGH3, and sets some internal methods
according to the simulator type."""
self.simulator = None
self._file.seek(0)
simulator = {'EEEEE':'AUTOUGH2','ESHORT':'AUTOUGH2','BBBBB':'AUTOUGH2',
'@@@@@':'TOUGH2','=====':'TOUGH+'}
MP = self.filename.endswith('OUTPUT_DATA') and self.readline().startswith('\f') \
and not ('@@@@@' in self.readline())
line = ' '
while not ('output data after' in line or 'output after' in line or line == ''):
line = self.readline().lower()
ip = line.find('is a program for')
if ip >= 0 and self.simulator is None:
self.simulator = line[:ip].strip().upper()
if line != '' and self.simulator is None:
self.skip_to_nonblank()
line = self.readline()
if line[1:].startswith('THE TIME IS'): line = self.readline() # AUTOUGH2
linechars = line[1:6]
if linechars in simulator.keys():
self.simulator = simulator[linechars]
if self.simulator == 'TOUGH2' and MP: self.simulator += '_MP'
if self.simulator:
# Set internal methods according to simulator type:
simname = self.simulator.replace('+','plus')
internal_fns = ['setup_pos','table_type','setup_table',
'setup_tables','read_header','read_table','next_table',
'read_tables','skip_to_table','read_table_line','read_title',
'skip_table']
for fname in internal_fns:
fname_sim = fname + '_' + simname
# use TOUGH2 methods unless there are customized methods for these simulators:
if simname in ['TOUGH2_MP', 'TOUGHplus', 'TOUGHREACT', 'TOUGH3'] and \
not hasattr(self, fname_sim):
fname_sim = fname_sim.replace(simname, 'TOUGH2')
setattr(self, fname, getattr(self, fname_sim))
def table_type_AUTOUGH2(self, keyword):
"""Returns AUTOUGH2 table name based on the 5-character keyword read at the top of the table."""
keytable = {'EEEEE': 'element', 'CCCCC': 'connection', 'GGGGG': 'generation'}
if keyword in keytable: return keytable[keyword]
else: return None
def table_type_TOUGH2(self, headers):
"""Returns TOUGH2 table name based on a tuple of the first three column headings."""
if headers[0:2] in [('ELEM.','INDEX'),('ELEM.','IND.')]:
if headers[2] == 'P': return 'element'
elif headers[2] == 'X1': return 'primary'
elif headers[0:3] == ('ELEM1','ELEM2','INDEX'): return 'connection'
elif headers[0:3] in [('ELEMENT','SOURCE','INDEX'), ('ELEM.','SOURCE','INDEX')]:
return 'generation'
else: return None
def table_type_TOUGHplus(self, headers):
"""Returns TOUGH+ table name based on a tuple of the first three column headings."""
if headers[0:2] == ('ELEM','INDEX'):
if headers[2] == 'X1': return 'primary'
else: return 'element'
else:
keytable = {('ELEM1','ELEM2','INDEX'): 'connection',
('ELEMENT','SOURCE','INDEX'): 'generation'}
if headers in keytable: return keytable[headers]
return None
def setup_short_types(self):
"""Sets up short_types, for handling short output."""
self.short_types = []
if self.simulator == 'AUTOUGH2':
startpos = self._file.tell()
self._file.seek(0)
done = False
while not done:
shortkw = self.skipto(['ESHORT','CSHORT','GSHORT'])
if (shortkw in self.short_types) or not shortkw: done = True
else:
self.short_types.append(shortkw)
self.skipto(shortkw)
self.skipto(shortkw) # to end of short table
self._file.seek(startpos)
# (no SHORT output for TOUGH2 or TOUGH+)
def setup_short_indices(self):
"""Sets up short_indices (indices of main table items in short tables)."""
self.short_indices = {}
if self.simulator == 'AUTOUGH2':
startpos = self._file.tell()
shortpos = [pos for pos,short in zip(self._pos, self._short) if short]
if len(shortpos) > 0:
self._file.seek(shortpos[0])
for itable,table in enumerate(self.short_types):
fulltable_keyword = table[0].upper()*5
fulltable = self.table_type(fulltable_keyword)
if itable > 0: self.skipto(table)
self.short_indices[table] = {}
self.skipto(table)
self.skip_to_blank()
self.skip_to_nonblank()
if fulltable in self._table:
def rowindex(line): # can get row indices from full table
key = self._table[fulltable].key_from_line(line)
return self._table[fulltable]._row[key]
self.skip_to_blank()
else:
# have to get row index from index in table (possibly not necessarily reliable)
indexpos = self.readline().index('INDEX')
def rowindex(line): return fortran_int(line[indexpos: indexpos + 5]) - 1
self.skip_to_nonblank()
endtable = False
lineindex = 0
while not endtable:
line = self.readline()
if line[1:].startswith(table): endtable = True
else:
index = rowindex(line)
self.short_indices[table][index] = lineindex
lineindex += 1
self._file.seek(startpos)
def setup_pos_AUTOUGH2(self):
"""Sets up _pos list for AUTOUGH2 listings, containing file position
at the start of each set of results. Also sets up the times
and steps arrays.
"""
self._file.seek(0)
# set up pos,times, steps and short arrays:
self._fullpos, self._pos, self._short = [], [], []
fullt, fulls, t, s = [], [], [], []
keywords = ['EEEEE']
if len(self.short_types) > 0: keywords.append(self.short_types[0])
endfile = False
while not endfile:
kwfound = self.skipto(keywords)
if kwfound:
self._pos.append(self._file.tell())
self.read_header_AUTOUGH2()
if kwfound == 'EEEEE': # full results
self._fullpos.append(self._pos[-1])
fullt.append(self.time)
fulls.append(self.step)
self._short.append(False)
else: self._short.append(True)
t.append(self.time)
s.append(self.step)
self._file.readline()
self.skipto(kwfound) # to end of table
else: endfile = True
self.times = np.array(t)
self.steps = np.array(s)
self.fulltimes = np.array(fullt)
self.fullsteps = np.array(fulls)
def setup_pos_TOUGH2(self):
"""Sets up _pos list for TOUGH2 listings, containing file position at
the start of each set of results. Also sets up the times and steps arrays."""
self._file.seek(0)
# set up pos,times, steps and short arrays:
self._fullpos,self._pos = [],[]
t,s = [],[]
endfile = False
while not endfile:
line = ' '
while not (line.lstrip().lower().startswith('output data after') or line == ''):
line = self.readline()
if line != '':
while not ('total time' in self.readline().lower()): pass
pos = self._file.tell()
self._pos.append(pos)
self._fullpos.append(pos)
self.read_header()
t.append(self.time)
s.append(self.step)
self.skipto('@@@@@')
else: endfile = True
self.times = np.array(t)
self.steps = np.array(s)
self.fulltimes = np.array(t)
self.fullsteps = np.array(s)
self._short = [False for p in self._pos]
def set_table_attributes(self):
"""Makes tables in self._table accessible as attributes."""
for key,table in self._table.items(): setattr(self, key, table)
def setup_tables_AUTOUGH2(self):
"""Sets up configuration of element, connection and generation tables."""
tablename = 'element'
self._file.seek(self._fullpos[0])
while tablename:
self.read_header()
if tablename in self.skip_tables: self.skip_table(tablename)
else: self.setup_table(tablename)
tablename = self.next_table()
def setup_tables_TOUGH2(self):
self.read_title()
tablename = 'element'
self._file.seek(self._fullpos[0])
self.read_header() # only one header at each time
while tablename:
if tablename in self.skip_tables: self.skip_table(tablename)
else: self.setup_table(tablename)
tablename = self.next_table()
def setup_tables_TOUGHplus(self):
self.read_title()
tablename = 'element'
self._file.seek(self._fullpos[0])
self.read_header() # only one header at each time
nelt_tables = 0 # can have multiple element tables
while tablename:
if tablename in self.skip_tables: self.skip_table(tablename)
else: self.setup_table(tablename)
tablename = self.next_table()
if tablename == 'element':
nelt_tables += 1
tablename += str(nelt_tables)
def next_table_AUTOUGH2(self):
"""Goes to start of next table at current time and returns its type,
or None if there are no more."""
keyword = self.readline()[1:6]
return self.table_type(keyword)
def next_table_TOUGH2(self):
found = False
while not found:
line = '\n'
while not ((line.strip().startswith('KCYC') and 'ITER' in line) or line == ''):
line = self.readline()
if line == '': return None
else:
pos = self._file.tell()
if (self.num_fulltimes > 1) and (self.index < self.num_fulltimes-1):
if pos >= self._fullpos[self.index+1]: return None
self.skip_to_nonblank()
headpos = self._file.tell()
line = self.readline().strip()
if line == 'MASS FLOW RATES (KG/S) FROM DIFFUSION':
# skip over extra mass flow rate table in EOS7c listings:
self.skipto('@@@@@')
else:
headers = tuple(line.strip().split()[0:3])
self._file.seek(headpos)
found = True
return self.table_type(headers)
def next_table_TOUGHplus(self):
if self.skipto('_____',0):
self._file.readline()
pos = self._file.tell()
if (self.num_fulltimes>1) and (self.index<self.num_fulltimes-1):
if pos >= self._fullpos[self.index+1]: return None
headpos = self._file.tell()
headers = tuple(self.readline().strip().split()[0:3])
self._file.seek(headpos)
return self.table_type(headers)
else: return None
def skip_to_table_AUTOUGH2(self, tablename, last_tablename, nelt_tables):
"""Skips forwards to headers of table with specified name at the current time."""
tablechar = tablename[0].upper()
if self._short[self._index]:
keyword, first_tablechar = tablechar + 'SHORT', self.short_types[0][0]
else: keyword, first_tablechar = tablechar*5, 'E'
if tablechar != first_tablechar: self.skipto(keyword)
self.skipto('OUTPUT')
self.skipto(keyword)
self.skip_to_blank()
self.skip_to_nonblank()
def skip_to_table_TOUGH2(self, tablename, last_tablename, nelt_tables):
if last_tablename is None:
self.skipto('@@@@@')
self.skip_to_nonblank()
tname = 'element'
else: tname = last_tablename
while tname != tablename:
self.skipto('@@@@@')
tname = self.next_table_TOUGH2()
def skip_to_table_TOUGHplus(self, tablename, last_tablename, nelt_tables):
if last_tablename is None:
self.skipto('=====',0)
self.skip_to_nonblank()
tname = 'element'
nelt_tables = 0
else: tname = last_tablename
while tname != tablename:
if tname == 'primary': keyword='_____'
else: keyword = '@@@@@'
self.skipto(keyword,0)
tname = self.next_table_TOUGHplus()
if tname == 'element':
nelt_tables += 1
tname += str(nelt_tables)
def start_of_values(self, line, columns):
"""Returns start index of values in a table line. Characters before
this start index are taken to contain the key(s) and row index."""
start = None
pt = line.find('.')
if pt >= 2:
nextpt = line.find('.', pt + 1)
if nextpt < 0 : nextpt = len(line)
s = line[pt + 1: nextpt - 1].lower()
exponential = s.find('e') >= 0 or s.find('+') >= 0 or s.find('-') >= 0
if exponential:
c = line[pt - 2]
if c in ['-',' ']: start = pt - 2
elif c.isdigit(): start = pt - 1
else:
pos = pt - 1
while line[pos] != ' ' and pos > 0 : pos -= 1
while line[pos] == ' ' and pos > 0 : pos -= 1
if pos > 0 : start = pos + 1
if columns[0] == 'I': start -= 2 # e.g. ECO2M element table
return start
def key_positions(self, line, nkeys):
"""Returns detected positions of keys in the start of a table line.
This works on the assumption that key values must have a digit
present in their last character. It searches backwards from
the end of the position of INDEX (or IND.) in the header line-
sometimes keys can overlap into this area. """
keylength = 5
keypos = []
pos = len(line)-1
while line[pos] == ' ': pos -= 1
while line[pos] != ' ': pos -= 1
for k in range(nkeys):
while not line[pos].isdigit() and pos >= keylength:
pos -= 1
pos -= (keylength - 1)
if valid_blockname(line[pos: pos + keylength]):
keypos.append(pos)
pos -= 1
else: return None
keypos.reverse()
if len(keypos) != nkeys: return None
else: return keypos
def parse_table_header_AUTOUGH2(self):
"""Parses table header line for AUTOUGH2, returning the number of keys
and the column names."""
cols = []
headline = self.readline()
headstrs = headline.strip().split()
indexstr = 'INDEX'
nkeys = headstrs.index(indexstr)
for s in headstrs[nkeys+1:]:
if s[0] == s[0].upper(): cols.append(s)
else: cols[-1] += ' ' + s
return nkeys,cols
def setup_table_AUTOUGH2(self, tablename):
"""Sets up table from AUTOUGH2 listing file."""
keyword = tablename[0].upper()*5
self.skiplines(3)
nkeys, cols = self.parse_table_header_AUTOUGH2()
self._file.readline()
line = self.readline()
start = self.start_of_values(line, cols)
rows = []
# Double-check number of columns:
nvalues = len([s for s in line[start:].strip().split()])
if (len(cols) == nvalues):
keypos = self.key_positions(line[:start], nkeys)
if keypos:
# determine row names:
while line[1:6] != keyword:
keyval = [fix_blockname(line[kp: kp + 5]) for kp in keypos]
if len(keyval) > 1: keyval = tuple(keyval)
else: keyval = keyval[0]
rows.append(keyval)
line = self.readline()
row_format = {'key': keypos, 'values': [start]}
allow_rev = tablename == 'connection'
self._table[tablename] = listingtable(cols, rows, row_format, num_keys = nkeys,
allow_reverse_keys = allow_rev)
self._tablenames.append(tablename)
self._file.readline()
else: raise Exception('Error parsing '+tablename+' table keys: table not created.')
else:
raise Exception('Error parsing '+tablename+' table columns: table not created.')
def parse_table_line(self, line, start, columns):
"""Parses line of a table and returns starting indices of each column"""
numpos = [start]
if columns[0] == 'I': # e.g. ECO2M element table
spos = line.find(' ', start + 1)
if spos >= 0: numpos.append(spos)
from re import finditer,escape
# find all decimal points:
pts = [match.start() for match in finditer(escape('.'), line)]
for i, pt in enumerate(pts[: -1]):
nextpt = pts[i+1]
pstart, pend = pt+1, nextpt-1
spacepos = line.find(' ', pstart, pend)
if spacepos > 0: next_start = spacepos
else: # no space at end
exppos = line.find('E', pstart, pend)
if exppos > 0:
endpos = exppos + 3
next_start = endpos + 1
else: raise Exception("Unable to parse table line:\n" + line)
numpos.append(next_start)
numpos.append(len(line))
return numpos
def parse_table_header_TOUGH2(self):
"""Parses table header line for TOUGH2, returning the number of keys and the column names."""
cols = []
if self.simulator == 'TOUGH+': flow_headers = ['Flow', 'Veloc']
else: flow_headers = ['RATE']
headline = self.readline().strip()
headstrs = headline.split()
indexstrs = ['INDEX','IND.'] # for EWASG
for s in indexstrs:
if s in headstrs:
nkeys = headstrs.index(s)
break
for s in headstrs[nkeys+1:]:
if s in flow_headers: cols[-1] += ' ' + s
else: cols.append(s)
return nkeys,cols
def is_results_line(self, line, expected_floats):
"""Detects whether the given string represents a line of results values, by seeing if
it contains at least expected_floats floating point numbers."""
from re import findall
return len(findall('\.[0-9]+', line)) >= expected_floats
def skip_to_results_line(self, expected_floats):
"""Skips to the start of the next line of results values, returning the number
of lines skipped."""
found, num_lines = False, 1
while not found:
pos = self._file.tell()
line = self.readline().strip()
found = self.is_results_line(line, expected_floats)
if found: self._file.seek(pos)
else: num_lines += 1
return num_lines
def table_expected_floats(self, tablename, columns):
"""Returns number of floating point numbers expected in each line of the table.
For most tables this is the number of table columns, but generation tables can
have incomplete lines in them and sometimes have as few as one value per line."""
num_floats = 1 if tablename == 'generation' else len(columns)
if columns[0] == 'I': num_floats -= 1 # e.g. ECO2M element table
return num_floats
def setup_table_TOUGH2(self, tablename):
"""Sets up table from TOUGH2 (or TOUGH+) listing file."""
nkeys,cols = self.parse_table_header_TOUGH2()
expected_floats = self.table_expected_floats(tablename, cols)
header_skiplines = self.skip_to_results_line(expected_floats)
line = self.readline()
start = self.start_of_values(line, cols)
keypos = self.key_positions(line[:start],nkeys)
if keypos:
index_pos = [keypos[-1]+5, start]
longest_line = line
rowdict = {}
count,index = 0,-1
skiplines = []
lsep = 60
more = True
internal_header_skiplines = None
def count_read(count): return self.readline(), count + 1
def is_header(line): return all([col in line for col in cols])
def is_separator(line): return len(line)>lsep and line[1:lsep+1] == line[1]*lsep
while more:
keyval = [fix_blockname(line[kp:kp+5]) for kp in keypos]
if len(keyval) > 1: keyval = tuple(keyval)
else: keyval = keyval[0]
indexstr = line[index_pos[0]:index_pos[1]]
try: index = int(indexstr) - 1
# To handle overflow (****) in index field: assume indices continue:
except ValueError: index += 1
# Use a dictionary to deal with duplicate row indices (TOUGH2_MP):
rowdict[index] = (count,keyval)
if len(line.strip()) > len(longest_line): longest_line = line
pos = self._file.tell()
last_count = count
line,count = count_read(count)
internal_header = False
if is_header(line): internal_header = True
elif is_separator(line): # end of table
more = False
self._file.seek(pos)
elif not line.strip(): # blank- check next line:
pos = self._file.tell()
line,count = count_read(count)
stripline = line.strip()
if is_header(line): internal_header = True
elif is_separator(line) or stripline == self.title or not stripline:
more = False # end of table
self._file.seek(pos)
if more and internal_header:
if internal_header_skiplines is None:
internal_header_skiplines = self.skip_to_results_line(expected_floats)
count += internal_header_skiplines
line = self.readline()
else:
for i in range(internal_header_skiplines): line,count = count_read(count)
skiplines.append(count - last_count - 1)
indices = sorted(rowdict.keys())
row_line = [rowdict[index][0] for index in indices]
rows = [rowdict[index][1] for index in indices]
numpos = self.parse_table_line(longest_line, start, cols)
row_format = {'key': keypos, 'index': keypos[-1] + 5, 'values': numpos}
allow_rev = tablename == 'connection'
self._table[tablename] = listingtable(cols, rows, row_format, row_line, num_keys = nkeys,
allow_reverse_keys = allow_rev,
header_skiplines = header_skiplines,
skiplines = skiplines)
self._tablenames.append(tablename)
else: raise Exception('Error parsing '+tablename+' table keys: table not created.')
def read_header_AUTOUGH2(self):
"""Reads header info (title and time data) for one set of AUTOUGH2 listing results."""
self.read_title()
line = self.readline()
istart, iend = line.find('AFTER') + 5, line.find('TIME STEPS')
try: self._step = fortran_int(line[istart:iend])
except ValueError: self._step = -1 # to handle overflow
istart = iend + 10
iend = line.find('SECONDS')
self._time=fortran_float(line[istart:iend])
self._file.readline()
def read_header_TOUGH2(self):
"""Reads header info (time data) for one set of TOUGH2 listing results."""
strs = self.readline().split()
self._time, self._step = fortran_float(strs[0]), fortran_int(strs[1])
marker = ['@@@@@','====='][self.simulator=='TOUGH+']
self.skipto(marker)
self.skip_to_nonblank()
pos = self._file.tell()
strs = self.readline().split()
if len(strs) < 4: self.skip_to_nonblank() # to skip over extra lines in EOS7c listings
else: self._file.seek(pos)
def read_title_AUTOUGH2(self):
"""Read simulation title for AUTOUGH2 listings, from current position- in all headers."""
self.title = self.readline().strip()
def read_title_TOUGH2(self):
"""Reads simulation title for TOUGH2 listings, at top of file."""
self._file.seek(0)
line = ' '
while not ('problem title' in line.lower() and ':' in line) or (line == ''):
line = self.readline()
if line == '': self.title = ''
else:
colonpos = line.find(':')
if colonpos >= 0: self.title = line[colonpos + 1:].strip()
else: self.title = ''
def read_title_TOUGH2_MP(self):
"""Reads simulation title for TOUGH2_MP listings, at top of file."""
self._file.seek(0)
self._file.readline()
self.title = self.readline().strip()
def next_tablename(self, tablename):
"""Returns name of table after the specified one, or None if it is the last."""
if tablename is None: return self._tablenames[0]
i = self._tablenames.index(tablename)
if i < len(self._tablenames)-1: return self._tablenames[i+1]
else: return None
def read_tables_AUTOUGH2(self):
tablename = 'element'
while tablename:
self.read_header()
if tablename in self.skip_tables: self.skip_table(tablename)
else: self.read_table(tablename)
tablename = self.next_table()
def read_tables_TOUGH2(self):
tablename = 'element'
self.read_header() # only one header at each time
last_tablename = None
while tablename:
if tablename in self.skip_tables: self.skip_table(tablename)
elif tablename in self._table: self.read_table(tablename)
else: # tables not present at first time step
next_tablename = self.next_tablename(last_tablename)
if next_tablename:
self.skip_to_table(next_tablename, last_tablename, 1)
last_tablename = tablename
tablename = self.next_table()
def read_tables_TOUGHplus(self):
tablename='element'
self.read_header() # only one header at each time
nelt_tables = 0
while tablename:
if tablename in self.skip_tables: self.skip_table(tablename)
else: self.read_table(tablename)
tablename = self.next_table()
if tablename == 'element':
nelt_tables += 1
tablename += str(nelt_tables)
def read_table_AUTOUGH2(self, tablename):
fmt = self._table[tablename].row_format
keyword = tablename[0].upper()*5
self.skip_to_blank()
self._file.readline()
self.skip_to_blank()
self.skip_to_nonblank()
line = self.readline()
row = 0
while line[1:6] != keyword:
self._table[tablename][row] = self.read_table_line_AUTOUGH2(line, fmt = fmt)
row += 1
line = self.readline()
self._file.readline()
def skip_table_AUTOUGH2(self, tablename):
keyword = tablename[0].upper()*5
self.skip_to_blank()
line = self.readline()
while line[1:6] != keyword: line = self.readline()
self._file.readline()
def read_table_line_AUTOUGH2(self, line, num_columns = None, fmt = None):
start = fmt['values'][0]
vals = [fortran_float(s) for s in line[start:].strip().split()]
return vals
def read_table_line_TOUGH2(self, line, num_columns, fmt):
"""Reads values from a line in a TOUGH2 listing, given the number of columns, and format."""
nvals = len(fmt['values']) - 1
return [fortran_float(line[fmt['values'][i]: fmt['values'][i+1]])
for i in range(nvals)] + [0.0] * (num_columns - nvals)
def read_table_TOUGH2(self, tablename):
table = self._table[tablename]
ncols = table.num_columns
fmt = table.row_format
self.skiplines(table.header_skiplines)
for skip in table.skiplines:
line = self.readline()
key = table.key_from_line(line)
table[key] = self.read_table_line_TOUGH2(line, ncols, fmt)
self.skiplines(skip)
def skip_table_TOUGH2(self, tablename):
if tablename in self._table:
table = self._table[tablename]
self.skiplines(table.header_skiplines + table.num_rows + sum(table.skiplines))
else:
if self.simulator == 'TOUGH+' and tablename == 'primary': chars = '_____'
else: chars = '@@@@@'
self.skipto(chars)
def history(self, selection, short = True, start_datetime = None):
"""Returns time histories for specified selection of table type, names
(or indices) and column names. Table type is specified as
'e','c','g' or 'p' (upper or lower case) for element table,
connection table, generation table or primary table
respectively. For TOUGH+ results, additional element
tables may be specified as 'e1' or 'e2'. If the short
parameter is True, results from 'short output' (AUTOUGH2
only) are included in the results. If a start_datetime is
specified (a Python datetime object) then times will be
returned as datetimes."""
# This can obviously be done much more simply using next(), and accessing self._table,
# but that is too slow for large listing files. This method reads only the required data lines
# in each table.
def tablename_from_specification(tabletype): # expand table specification to table name:
from string import digits
namemap = {'e': 'element', 'c': 'connection', 'g': 'generation', 'p': 'primary'}
type0 = tabletype[0].lower()
if type0 in namemap:
name = namemap[type0]
if tabletype[-1] in digits:
name += tabletype[-1] # additional TOUGH+ element tables
return name
else: return None
def ordered_selection(selection, tables, short_types, short_indices):
"""Given the initial history selection, returns a list of tuples of
table name and table selections. The tables are in the
same order as they appear in the listing file. Each table
selection is a list of tuples of (table row index, column
name, reversed, selection index) for each table, ordered
by table row index. This ordering means all data can be
read sequentially to make it more efficient. There is a
table selection each for full and short output, to account
for possible differences in ordering between them."""
converted_selection = []
for sel_index,(tspec, key, h) in enumerate(selection):
# convert keys to indices as necessary, and expand table names:
tablename = tablename_from_specification(tspec)
if tablename in tables:
if isinstance(key, int): index, reverse = key, False
else:
index, reverse = None, False
if key in tables[tablename].row_name: