├── LICENSE
├── README.md
├── cartesian_grid_example.sr3
├── coreflood_geochem.sr3
├── pics
├── cartesian_grid.png
├── min_change.png
└── molalities.png
├── plot_cartesian_grid.py
├── plot_coreflood_geochem.py
└── sr3_reader.py
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Nikolai Andrianov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # SR3 Reader
4 | ## Python routines for reading of Computer Modelling Group Ltd. (CMG) binary output files
5 |
6 |
7 |
8 |
9 |
10 | SR3 Reader is a set of free Python utilities to read and visualize the output of Computer Modelling Group Ltd. (CMG) simulation software. The implemented functionality allows for great flexibility in creating automated workflows for data analysis, sensitivity studies, and history matching.
11 |
12 | ## Installation
13 |
14 | Simply copy the file `sr3_reader.py` where Python can find it.
15 |
16 | ## Examples
17 |
18 | ### Visualization of CMG GEM simulation results of a 1D coreflooding experiment
19 |
20 | The file `coreflood_geochem.sr3` contains CMG GEM results for simulation of a coreflooding experiment, where the supercritical CO2 and a reservoir brine are injected in a core sample in an alternating manner. The resulting geochemical reactions yield variable ions composition at the sample's outlet and a change in mineral composition of the sample.
21 |
22 | The script `plot_coreflood_geochem.py` reads the simulation data and plots a number of graphs using `matplotlib`.
23 |
24 | 
25 | 
26 |
27 | (The pink stripes in the images above denote the CO2 injection periods.)
28 |
29 | ## Visualization of CMG GEM results on a 3D Cartesian grid
30 |
31 | The file `cartesian_grid_example.sr3` contains CMG GEM results for simulation of a CO2 flooding in a Cartesian reservir with wells (inspired by a template from a CMG GEM distribution). The script `plot_cartesian_grid.py` reads the simulation data and plots a snapshot of pressure at the start of the simulation using `pyvista`.
32 |
33 | 
34 |
35 | ## Disclaimer
36 |
37 | Since the internal format for binary SR3 files is not officially documented by CMG, it is sometimes not obvious what this or that data field mean. Although the code has been thoroughly tested on selected datasets, NO WARRANTY OF ANY KIND IS PROVIDED WITH THE SOFTWARE (see the full copyright notice in `sr3_reader.py`).
38 |
39 | ## Contributing
40 |
41 | Anyone who wishes to contribute to this project, whether documentation, features, bug fixes, code cleanup, testing, or code reviews, is very much encouraged to do so.
42 |
--------------------------------------------------------------------------------
/cartesian_grid_example.sr3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nikolai-andrianov/sr3_reader/96ddf866701642d68a37b771344bbea4c110b954/cartesian_grid_example.sr3
--------------------------------------------------------------------------------
/coreflood_geochem.sr3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nikolai-andrianov/sr3_reader/96ddf866701642d68a37b771344bbea4c110b954/coreflood_geochem.sr3
--------------------------------------------------------------------------------
/pics/cartesian_grid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nikolai-andrianov/sr3_reader/96ddf866701642d68a37b771344bbea4c110b954/pics/cartesian_grid.png
--------------------------------------------------------------------------------
/pics/min_change.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nikolai-andrianov/sr3_reader/96ddf866701642d68a37b771344bbea4c110b954/pics/min_change.png
--------------------------------------------------------------------------------
/pics/molalities.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nikolai-andrianov/sr3_reader/96ddf866701642d68a37b771344bbea4c110b954/pics/molalities.png
--------------------------------------------------------------------------------
/plot_cartesian_grid.py:
--------------------------------------------------------------------------------
1 | """
2 | Visualization of CMG GEM results on a 3D Cartesian grid using the SR3 reader and pyvista.
3 |
4 | Copyright 2023 Nikolai Andrianov, nia@geus.dk
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
7 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation
8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
9 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
15 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17 | """
18 |
19 | from sr3_reader import *
20 | import numpy as np
21 | import pandas as pd
22 | import pyvista as pv
23 |
24 | # Read CMG results file
25 | infile = 'cartesian_grid_example.sr3'
26 | sr3 = read_SR3(infile)
27 |
28 | # Get the timeseries data from the wells
29 | wells_ts = get_wells_timeseries(sr3)
30 |
31 | # Get the indices of completed cells
32 | wells_comp = get_wells_completions(sr3)
33 |
34 | # Get the pressures and saturations
35 | (sp_ind, sp) = get_spatial_properties(sr3, ['PRES', 'SG', 'SW'])
36 | t_sp = sr3.times['Days'].iloc[sp_ind]
37 |
38 | pressure = sp['PRES'][0]
39 | sw = sp['SW'][1]
40 |
41 | # The dimensions of grid nodes (+1 to the cells' dimensions)
42 | dim = np.array(sr3.grid.cart_dims) + 1
43 |
44 | # Create the pyvista grid representation
45 | grid = pv.ExplicitStructuredGrid(dim, sr3.grid.cells.corners)
46 | grid = grid.compute_connectivity()
47 | grid = grid.compute_connections()
48 | grid = grid.compute_cell_sizes(length=False, area=False, volume=True)
49 |
50 | # Scale the y- and z-axis for better visibility
51 | grid.scale([1, 3, 10], inplace=True)
52 |
53 | # Hide inactive cells
54 | grid.hide_cells(sr3.grid.cells.inactive, inplace=True)
55 |
56 | # Initialize the pyvista plotter
57 | pl = pv.Plotter()
58 |
59 | # Identify the completed cells
60 | for well, comp in wells_comp.items():
61 | points = []
62 | top_comp = None
63 | for cell in comp:
64 | # Get the global id of the cell
65 | cell_id = grid.cell_id(cell)
66 | # Get the coordinates of the cell center
67 | cv = list(grid.cell)[cell_id].points
68 | center = np.mean(cv, axis=0)
69 | points.append(center)
70 | # Find the coordinates of the top completion
71 | if top_comp is None:
72 | top_comp = center
73 | else:
74 | if top_comp[2] < center[2]:
75 | top_comp = center
76 |
77 | # Set the wellhead 20% away from the top of the reservoir
78 | # Z-coordinate of the reservoir top (z are negative by convention)
79 | zf = max(grid.points[:, 2])
80 | dist = 1.2 * (zf - top_comp[2])
81 | wh = np.array([top_comp[0], top_comp[1], top_comp[2] + dist])
82 |
83 | # Define the vertical section of the well between the top completion and the wellhead
84 | vert_well = np.array([top_comp, wh])
85 | poly = pv.PolyData()
86 | poly.points = vert_well
87 | well_ind = np.arange(0, len(vert_well), dtype=np.int_)
88 | well_ind = np.insert(well_ind, 0, len(vert_well))
89 | poly.lines = well_ind
90 | tube = poly.tube(radius=20)
91 | pl.add_mesh(tube, show_edges=False)
92 | # Add the well labels
93 | pl.add_point_labels([wh], [well])
94 |
95 | # Identify the variables for visualization
96 | grid.cell_data['pressure'] = pressure
97 | grid.cell_data['sw'] = sw
98 |
99 | # Plot pressure
100 | pl.add_mesh(grid, scalars='pressure', opacity=0.5, scalar_bar_args=dict(color='black'))
101 |
102 | # Show the cells
103 | pl.show_grid(color='black')
104 |
105 | # Add the axes
106 | pl.add_axes(color='black')
107 |
108 | # Change the background color
109 | pl.background_color = 'white'
110 |
111 | # Actual plotting
112 | pl.show()
113 |
114 |
--------------------------------------------------------------------------------
/plot_coreflood_geochem.py:
--------------------------------------------------------------------------------
1 | """
2 | Visualization of CMG GEM results for a 1D coreflooding experiment with geochemical reactions using the SR3 reader.
3 |
4 | Copyright 2023 Nikolai Andrianov, nia@geus.dk
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
7 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation
8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
9 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
15 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17 | """
18 |
19 | from sr3_reader import *
20 | from matplotlib import pyplot
21 | import numpy as np
22 |
23 | # Read CMG results file
24 | infile = 'coreflood_geochem.sr3'
25 | sr3 = read_SR3(infile)
26 |
27 | # Get the timeseries data from the wells (which are used to set up the boundary conditions)
28 | wells_ts = get_wells_timeseries(sr3)
29 |
30 | # Get the available molalities and minerals
31 | molalities, minerals = list_molalities_minerals(sr3)
32 | list_mol = ','.join(molalities)
33 | list_min = ','.join(minerals)
34 | print('Available molalities: ' + list_mol)
35 | print('Available minerals: ' + list_min)
36 |
37 | # Get the molalities
38 | (sp_ind, sp) = get_spatial_indexed(sr3, 'MOLALITY', molalities)
39 |
40 | # The time steps, at which the spatially-ditributed data is available
41 | t_sp = sr3.times['Days'].iloc[sp_ind]
42 |
43 | # Plot the ratios of molalities to the molality of Cl- in the cell (inlet is the last cell #30, outlet is the first cell #1)
44 | cell = 1
45 | if 'Cl-' in molalities:
46 | n = 0
47 | figtr, axstr = pyplot.subplots(len(molalities) - 1, 1, num=10)
48 | for name in molalities:
49 | if name != 'Cl-':
50 | axstr[n].plot(t_sp, np.divide(sp[name][:, cell], sp['Cl-'][:, cell]), label=name + '/Cl-')
51 | axstr[n].legend()
52 | axstr[n].set_xlim(min(t_sp), max(t_sp))
53 | axstr[n].xaxis.set_tick_params(labelbottom=False)
54 |
55 | # Plot the injection periods on the right y-axes
56 | ax2 = axstr[n].twinx()
57 | ax2.fill_between(wells_ts['CO2-Injector']['Days'],
58 | 1 - wells_ts['CO2-Injector']['WELLSTATE'], color='red',
59 | alpha=0.1)
60 | ax2.set_yticks([])
61 | ax2.set_xlim(min(t_sp), max(t_sp))
62 | ax2.xaxis.set_tick_params(labelbottom=False)
63 |
64 | n = n + 1
65 |
66 | axstr[-1].xaxis.set_tick_params(labelbottom=True)
67 | axstr[-1].set_xlabel('Time (' + sr3.units['Time'] + ')')
68 | axstr[0].set_title('Ratios of molalities at the outlet')
69 | #figtr.set_figwidth(10)
70 | #figtr.set_figheight(10)
71 | pyplot.show(block=False)
72 |
73 |
74 | # Get the total mineral changes in moles
75 | if minerals:
76 | (sp_ind, sp) = get_spatial_indexed(sr3, 'MINERAL', minerals)
77 |
78 | # Fix the minerals' names for plotting
79 | min_name = {m: m for m in minerals}
80 | for m in min_name:
81 | if min_name[m] == 'K-fe_fel':
82 | min_name[m] = 'Feldspar'
83 | if min_name[m] == 'Glaconit':
84 | min_name[m] = 'Glauconite'
85 |
86 | # Plotting the mean value for minerals vs time
87 | figt, axst = pyplot.subplots(len(minerals), 1, num=33)
88 | for n, name in enumerate(minerals):
89 | # Mean value for the whole core
90 | m = np.mean(sp[name], axis=1)
91 | axst[n].plot(t_sp, m, label=min_name[name])
92 | axst[n].legend()
93 | axst[n].set_xlim(min(t_sp), max(t_sp))
94 | axst[n].xaxis.set_tick_params(labelbottom=False)
95 |
96 | # Plot the injection periods on the right y-axes
97 | ax2 = axst[n].twinx()
98 | ax2.fill_between(wells_ts['CO2-Injector']['Days'],
99 | 1 - wells_ts['CO2-Injector']['WELLSTATE'], color='red',
100 | alpha=0.1)
101 | ax2.set_yticks([])
102 | ax2.set_xlim(min(t_sp), max(t_sp))
103 | ax2.xaxis.set_tick_params(labelbottom=False)
104 |
105 | axst[-1].xaxis.set_tick_params(labelbottom=True)
106 | axst[-1].set_xlabel('Time (' + sr3.units['Time'] + ')')
107 | axst[0].set_title('Mineral change (mol/kgw)')
108 | pyplot.show(block=True)
109 |
--------------------------------------------------------------------------------
/sr3_reader.py:
--------------------------------------------------------------------------------
1 | """
2 | A reader for Computer Modelling Group Ltd. (CMG) SR3 output files.
3 |
4 | Copyright 2023 Nikolai Andrianov, nia@geus.dk
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
7 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation
8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
9 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
15 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17 | """
18 |
19 | import h5py
20 | import os, math
21 | from datetime import datetime
22 | import numpy as np
23 | import pandas as pd
24 | import time
25 | import re
26 |
27 |
28 | class RawHDF:
29 | """
30 | A container for the raw data entries in the SR3 file.
31 | """
32 |
33 | class Grid:
34 | """
35 | A container for the grid structure.
36 | """
37 |
38 | class Cells:
39 | """
40 | A container for the cells structure.
41 | """
42 |
43 | def __init__(self):
44 | # Initialize empty structures for the cells' attributes
45 | self.centroids = np.array([])
46 | self.volumes = np.array([])
47 | self.corners = np.array([])
48 | self.inactive = np.array([])
49 |
50 | def __init__(self):
51 | # Initialize empty structures for cells
52 | self.cells = RawHDF.Grid.Cells()
53 | self.cart_dims = []
54 | self.grid_dim = 0
55 | self.n_cells = None
56 | self.n_active_cells = None
57 |
58 | def __init__(self, file, timeout):
59 | # Initialize empty structures for the datasets and their names
60 | self.data = {}
61 | self.names = []
62 | # Keep the SR3 file name
63 | self.file = file
64 | self.start_time = time.time()
65 | self.timeout = timeout
66 | # Time instants
67 | self.times = pd.DataFrame()
68 | # Indices of time instants, which correspond to valid dates
69 | self.valid_dates_ind = []
70 | # Component names
71 | self.comp_name = []
72 | # Names of spatial properties
73 | self.sp_name = []
74 | # Short and long descriptions of acronyms
75 | self.acronym_desc_short = {}
76 | self.acronym_desc_long = {}
77 | self.grid = RawHDF.Grid()
78 | self.units = {}
79 |
80 | def __call__(self, name, h5obj):
81 | # Only h5py datasets have dtype attribute, so we can search on this
82 | if hasattr(h5obj, 'dtype') and not name in self.names:
83 | self.names += [name]
84 | dataset = np.array(self.file[name][:])
85 | self.data[name] = dataset
86 | # Check if the file read does not take more than ...
87 | curr_time = time.time()
88 | dt_read = round(curr_time - self.start_time)
89 | if dt_read >= self.timeout:
90 | # Make sure that TimeSeries/WELLS/Origins is read (to be used in get_wells_timeseries)
91 | wells_datasets = ['TimeSeries/WELLS/Origins', 'TimeSeries/WELLS/Variables',
92 | 'TimeSeries/WELLS/Data', 'TimeSeries/WELLS/Timesteps']
93 | for name in wells_datasets:
94 | if not name in self.names:
95 | self.names += [name]
96 | dataset = np.array(self.file[name][:])
97 | self.data[name] = dataset
98 |
99 | print('Warning: read %s datasets before the timeout of %s sec is exceeded..' %
100 | (dt_read, len(self.names)))
101 | print('Increase the timeout in read_SR3 to get more datasets..')
102 | return 0
103 | # No return so that the visit function is recursive
104 |
105 |
106 | class Completion:
107 | """
108 | A completion is associated with the (i,j,k) index of the connected grid cell, and with the timeseries data
109 | """
110 |
111 | def __init__(self, ijk):
112 | # Index of the connected grid cell
113 | self.ijk = ijk
114 | # Time series data
115 | self.data = pd.DataFrame()
116 |
117 |
118 | def read_SR3(infile, timeout=60):
119 | """
120 | Reads the specified SR3 file and returns an instance of the RawHDF class with all the datasets from infile,
121 | unless it takes more than timeout seconds. If it takes more then timeout seconds to read the datasets,
122 | reading the SR3 file terminates and the returned instance of RawHDF contains only a part of the datasets.
123 | """
124 | try:
125 | sr3_file = h5py.File(infile, 'r')
126 | sr3 = RawHDF(sr3_file, timeout)
127 | except:
128 | raise IOError('Cannot open ' + infile + ' as a HDF file..')
129 |
130 | # Loop through all objects inside the hdf5 file and store the datasets in sr3
131 | sr3_file.visititems(sr3)
132 |
133 | # Extract report time instants from the master time table, which has entries
134 | assert ('General/MasterTimeTable' in sr3.data)
135 | td = []
136 | date = []
137 | shift = 0
138 | for n, t in enumerate(sr3.data['General/MasterTimeTable']):
139 | day = math.floor(t[2])
140 | frac = t[2] % 1
141 | hour = math.floor(frac * 24)
142 | frac = (frac * 24) % 1
143 | minute = math.floor(frac * 60)
144 | frac = (frac * 60) % 1
145 | second = math.floor(frac * 60)
146 | date_str = str(day) + ' ' + str(hour) + ':' + str(minute) + ':' + str(second)
147 |
148 | # Append the dates only if they can be converted to datetime
149 | try:
150 | dt = datetime.strptime(date_str, "%Y%m%d %H:%M:%S")
151 | td.append(t[1])
152 | date.append(dt)
153 | # Keep the corresponding 1-base index, shifted to account for the invalid dates
154 | sr3.valid_dates_ind.append(n + shift)
155 | except:
156 | # Error can be e.g. when a non-existing date is encountered - such as 20260229
157 | print('Skipping the encountered non-valid date ' + date_str)
158 | # Do not add the corresponding time instant to SR3 and indicate the corresponding index with -1
159 | sr3.valid_dates_ind.append(-1)
160 | # The indices after an encountered invalid dates are shifted downwards
161 | shift -= 1
162 |
163 |
164 | sr3.times['Date'] = date
165 | sr3.times['Days'] = td
166 |
167 | # Augment the raw SR3 data with the component names, which are byte strings of the format (b'NAME',)
168 | for name in sr3.data['General/ComponentTable']:
169 | name_str = str(name)
170 | sr3.comp_name.append(name_str.split('\'')[1])
171 |
172 | # Augment the raw SR3 data with the names of spatial properties
173 | for key in sr3.data:
174 | if 'SpatialProperties/000000/' in key:
175 | sr3.sp_name.append(key.split('SpatialProperties/000000/')[1])
176 |
177 | # Augment the raw SR3 data with the description of acronyms
178 | for name in sr3.data['General/NameRecordTable']:
179 | acronym = str(name[0]).split('\'')[1]
180 | desc_short = str(name[1]).split('\'')[1]
181 | desc_long = str(name[2]).split('\'')[1]
182 |
183 | sr3.acronym_desc_short[acronym] = desc_short
184 | sr3.acronym_desc_long[acronym] = desc_long
185 |
186 | # Populate the grid structure with the properties
187 | get_grid(sr3)
188 |
189 | # Set up the dict with units
190 | get_units(sr3)
191 |
192 | return sr3
193 |
194 |
195 | def search_acronym(sr3, str):
196 | """
197 | Prints the SR3 acronym(s), which either contain str, or whose description contains str.
198 | """
199 | print('\nSearching for the occurences of "' + str + '" among the descriptions of CMG acronyms..')
200 | for key in sr3.acronym_desc_long:
201 | if str.lower() in key.lower() or str.lower() in sr3.acronym_desc_long[key].lower():
202 | print(' ' + key + ': ' + sr3.acronym_desc_long[key])
203 | print('Done.\n')
204 |
205 |
206 | def get_sector_timeseries(sr3):
207 | """
208 | Returns the timeseries for the available sectors.
209 | Currently, only for a single sector.
210 | """
211 |
212 | # Extract variable names from the byte strings of the format (b'NAME',)
213 | var_name = []
214 | for name in sr3.data['TimeSeries/SECTORS/Variables']:
215 | name_str = str(name)
216 | var_name.append(name_str.split('\'')[1])
217 |
218 | # Replace the indices in variable names with the corresponding components' names
219 | comp_fields = ['AQUCOMMOL', 'MINERAMOL', 'MINERACHG']
220 | for n, name in enumerate(var_name):
221 | cf = [c for c in comp_fields if c in name]
222 | if cf:
223 | # Get the index of the component
224 | ind = name[name.find('(') + 1:name.find(')')]
225 | try:
226 | i = int(ind)
227 | except:
228 | raise ValueError('Cannot convert the index ' + ind + ' for the component ' + name)
229 |
230 | # Replace the index with the component name
231 | var_name[n] = re.sub('\(.*?\)', '_' + sr3.comp_name[i - 1], name)
232 |
233 | # Sector can be FIELD, ..
234 | sector = sr3.data['TimeSeries/SECTORS/Origins']
235 | if len(sector) != 1:
236 | raise ValueError('More than one sector in the SR3 file.. Not implemented..')
237 |
238 | # Read the data for a single sector
239 | data = sr3.data['TimeSeries/SECTORS/Data']
240 | data = data.reshape((data.shape[0], data.shape[1]))
241 |
242 | # Augment data with time in days
243 | assert (len(sr3.times['Days']) == data.shape[0])
244 | data = np.c_[sr3.times['Days'], data]
245 |
246 | # Return a dataframe, indexed with date
247 | ts = pd.DataFrame(data=data, index=sr3.times['Date'], columns=['Days'] + var_name)
248 | return ts
249 |
250 |
251 | def get_wells_timeseries(sr3):
252 | """
253 | Returns the timeseries for the available wells.
254 | """
255 |
256 | well_names = []
257 | for name in sr3.data['TimeSeries/WELLS/Origins']:
258 | name_str = str(name)
259 | well_names.append(name_str.split('\'')[1])
260 |
261 | # Extract variable names from the byte strings of the format (b'NAME',)
262 | var_name = []
263 | for name in sr3.data['TimeSeries/WELLS/Variables']:
264 | name_str = str(name)
265 | var_name.append(name_str.split('\'')[1])
266 |
267 | # Don't replace the indices in variable names with the corresponding components' names (for the moment).
268 |
269 | # Read the data for all wells
270 | data = sr3.data['TimeSeries/WELLS/Data']
271 |
272 | # Get the time instants when the well data is available
273 | ts_ind = sr3.data['TimeSeries/WELLS/Timesteps']
274 | t_sp = sr3.times['Days'].iloc[ts_ind]
275 |
276 | # Transform the data as dataframes for separate wells
277 | wells = {}
278 | for n, wn in enumerate(well_names):
279 |
280 | # Augment data with time in days
281 | assert (len(t_sp) == data.shape[0])
282 | well_data = np.c_[t_sp, data[:, :, n]]
283 |
284 | # Return a dataframe, indexed with date
285 | wells[wn] = pd.DataFrame(data=well_data, index=t_sp, columns=['Days'] + var_name)
286 |
287 | return wells
288 |
289 |
290 | def get_layers_timeseries(sr3):
291 | """
292 | Returns the timeseries for the available layers.
293 | """
294 |
295 | # Make sure that LAYERS datasets are available
296 | layers_datasets = ['TimeSeries/LAYERS/Origins', 'TimeSeries/LAYERS/Variables',
297 | 'TimeSeries/LAYERS/Data', 'TimeSeries/LAYERS/Timesteps']
298 | for name in layers_datasets:
299 | if not name in sr3.names:
300 | print(name + ' is not available..')
301 | return None
302 |
303 | layers_names = []
304 | for name in sr3.data['TimeSeries/LAYERS/Origins']:
305 | name_str = str(name)
306 | layers_names.append(name_str.split('\'')[1])
307 |
308 | # Extract variable names from the byte strings of the format (b'NAME',)
309 | var_name = []
310 | for name in sr3.data['TimeSeries/LAYERS/Variables']:
311 | name_str = str(name)
312 | var_name.append(name_str.split('\'')[1])
313 |
314 | # Don't replace the indices in variable names with the corresponding components' names (for the moment).
315 |
316 | # Read the data for all wells
317 | data = sr3.data['TimeSeries/LAYERS/Data']
318 |
319 | # Get the time instants when the well data is available
320 | ts_ind = sr3.data['TimeSeries/LAYERS/Timesteps']
321 | t_sp = sr3.times['Days'].iloc[ts_ind]
322 |
323 | # Transform the data as dataframes for separate wells
324 | layers = {}
325 | for n, wn in enumerate(layers_names):
326 |
327 | # Augment data with time in days
328 | assert (len(t_sp) == data.shape[0])
329 | layers_data = np.c_[t_sp, data[:, :, n]]
330 |
331 | # Return a dataframe, indexed with date
332 | layers[wn] = pd.DataFrame(data=layers_data, index=t_sp, columns=['Days'] + var_name)
333 |
334 | return layers
335 |
336 |
337 | def get_completions_timeseries(sr3):
338 | """
339 | Returns the timeseries for the available completions.
340 |
341 | Apparently completions in SR3 are represented as LAYERS with the names in the format {ic,jc,kc}, where
342 | ic, jc, and kc are the indices of the grid block, connected to the well_name.
343 | """
344 |
345 | # Make sure that LAYERS datasets are available
346 | layers_datasets = ['TimeSeries/LAYERS/Origins', 'TimeSeries/LAYERS/Variables',
347 | 'TimeSeries/LAYERS/Data', 'TimeSeries/LAYERS/Timesteps']
348 | for name in layers_datasets:
349 | if not name in sr3.names:
350 | print(name + ' is not available..')
351 | return None
352 |
353 | layers_names = []
354 | for name in sr3.data['TimeSeries/LAYERS/Origins']:
355 | name_str = str(name)
356 | layers_names.append(name_str.split('\'')[1])
357 |
358 | # Extract variable names from the byte strings of the format (b'NAME',)
359 | # Don't replace the indices in variable names with the corresponding components' names (for the moment).
360 | var_name = []
361 | for name in sr3.data['TimeSeries/LAYERS/Variables']:
362 | name_str = str(name)
363 | var_name.append(name_str.split('\'')[1])
364 |
365 | # Get the time instants when the well data is available
366 | ts_ind = sr3.data['TimeSeries/LAYERS/Timesteps']
367 | t_sp = sr3.times['Days'].iloc[ts_ind]
368 |
369 | # Read the data for all completions
370 | data = sr3.data['TimeSeries/LAYERS/Data']
371 | assert (len(t_sp) == data.shape[0])
372 |
373 | # Extract well names and completion indices from layers_names (which have the format format {ic,jc,kc})
374 | # and create the completions structure
375 | completions = {}
376 | for n, ln in enumerate(layers_names):
377 | try:
378 | # Parse layers_names
379 | wn = ln.split('{')[0]
380 | comp = ln.split('{')[1]
381 | comp = comp.split('}')[0]
382 | comp = comp.split(',')
383 | ind = [int(c) for c in comp]
384 | except:
385 | raise ValueError('Cannot extract wells and completion indices from ' + ln)
386 |
387 | # Write the well completions
388 | if wn not in completions:
389 | completions[wn] = []
390 |
391 | # Add the information on the connected block to the current completion
392 | completions[wn].append(Completion(ind))
393 |
394 | # Add the timeseries data, augmented with time in days
395 | layers_data = np.c_[t_sp, data[:, :, n]]
396 |
397 | # Return a dataframe, indexed with date
398 | completions[wn][-1].data = pd.DataFrame(data=layers_data, index=t_sp, columns=['Days'] + var_name)
399 |
400 | return completions
401 |
402 |
403 | def get_wells_completions(sr3):
404 | """
405 | Returns a dict the indices of completed cells for the available wells.
406 | """
407 |
408 | well_names = []
409 | for name in sr3.data['TimeSeries/WELLS/Origins']:
410 | name_str = str(name)
411 | well_names.append(name_str.split('\'')[1])
412 |
413 | comp = {wn: [] for wn in well_names}
414 | for data in sr3.data['TimeSeries/LAYERS/LayerTable']:
415 | well_name = str(data[3]).split('\'')[1]
416 | comp_cell = str(data[2]).split('\'')[1]
417 | # Get to the 0-based indexing
418 | comp_cell_ind = np.array(comp_cell.split(','), dtype=int) - 1
419 | comp[well_name].append(comp_cell_ind)
420 |
421 | return comp
422 |
423 |
424 | def list_spatial_properties(sr3):
425 | """
426 | Returns a list of spatial properties' names.
427 | """
428 |
429 | # Get a list of spatial properties
430 | prop_name = []
431 | for key in sr3.data:
432 | if 'SpatialProperties/000000/' in key:
433 | prop_name.append(key.split('SpatialProperties/000000/')[1])
434 |
435 | return prop_name
436 |
437 |
438 | def list_data_categories(sr3, level, exclude_numeric):
439 | """
440 | Returns a list of data categories among the data names.
441 |
442 | The data names are strings, separated by slash "/", and category of n-th level is defined as substrings of data
443 | names between the (n-1)-th and n-th slash.
444 | E.g., the 0-th level categories are "General', 'Restart', 'SpaialProperties' etc.
445 |
446 | Exclude the numerical fields by setting exclude_numeric=True.
447 | """
448 |
449 | # Get a list of unique categories
450 | cat_name = []
451 | for key in sr3.data:
452 | token = key.split('/')
453 | if level < len(token):
454 | cat = token[level]
455 | if exclude_numeric:
456 | try:
457 | num = int(cat)
458 | except:
459 | cat_name.append(cat)
460 | else:
461 | cat_name.append(cat)
462 |
463 | # Keep the unique names only
464 | cat_name = set(cat_name)
465 | cat_name = list(cat_name)
466 |
467 | return cat_name
468 |
469 |
470 | def list_wells_properties(sr3):
471 | """
472 | Returns a list of wells properties' names.
473 | """
474 |
475 | # Get a list of spatial properties
476 | prop_name = []
477 | for key in sr3.data:
478 | if 'WELLS' in key:
479 | val = sr3.data[key]
480 | prop_name.append(key)
481 |
482 | return prop_name
483 |
484 |
485 | def list_timeseries_properties(sr3):
486 | """
487 | Returns a list of time series' names.
488 | """
489 |
490 | # Get a list of spatial properties
491 | prop_name = []
492 | for key in sr3.data:
493 | if 'TimeSeries' in key:
494 | val = sr3.data[key]
495 | prop_name.append(key)
496 |
497 | return prop_name
498 |
499 |
500 | def get_spatial_properties(sr3, sel_names, activeonly=True, verbose=True):
501 | """
502 | Returns the tuple (sp_ind, sp), where sp is a dict of the time-dependent spatial properties with names, specified
503 | in sel_names, and sp_ind is a list of indices of corresponding time instants in sr3.times.
504 |
505 | If activeonly==True, the requested spatial parameters are defined on active cells only (e.g. pressure or saturation),
506 | otherwise the requested spatial parameters are defined for all cells (e.g. cell volumes).
507 | """
508 |
509 | if type(sel_names) is not list:
510 | if type(sel_names) is str:
511 | sel_names = [sel_names]
512 | else:
513 | print('Usage: get_spatial_properties(sr3, ')
514 | return {}, []
515 |
516 | # Get a list of time instants when the spatial properties are available
517 | sp = {}
518 | sp_ind = []
519 | for name in sel_names:
520 | prop = []
521 | for key in sr3.data:
522 | if 'SpatialProperties' in key:
523 | # Extract the time index and and the key variable name from the key of the form 'SpatialProperties/000000/VISG'
524 | key_parts = key.split('/')
525 | ind_time = key_parts[1]
526 | try:
527 | key_var = key.split('SpatialProperties/' + ind_time + '/')[-1]
528 | except:
529 | key_var = ''
530 | if name == key_var:
531 | try:
532 | i = int(ind_time)
533 | except:
534 | raise ValueError('Cannot convert the index ' + ind_time + ' for the component ' + name)
535 | # Only get the spatial index if it has not been already included and if it does not correspond to
536 | # an invalid dates
537 | if i not in sp_ind:
538 | #sp_ind.append(i)
539 | vi = sr3.valid_dates_ind[i]
540 | if vi != -1:
541 | sp_ind.append(vi)
542 |
543 | # Accumulate the spatial distribution at the current time step
544 | prop.append(sr3.data[key])
545 |
546 | # Convert the time-dependent spatial distribution to a 3D numpy array
547 | if not prop:
548 | if verbose:
549 | print('Error: ' + name + ' not found among the SR3 spatial parameters! Empty array returned..')
550 | tmp = prop
551 | else:
552 | # If the dimension of the requested property is equal to the number of active cells, reshape the spatial property
553 | # to match active cells; the inactive cells are assigned with zero.
554 | # Otherwise return with the property as is.
555 | tmp = np.array(prop)
556 | ntimes = tmp.shape[0]
557 | n = tmp.shape[1]
558 |
559 | # The number of active cells is defined in get_grid()
560 | if sr3.grid.n_active_cells:
561 | if n == sr3.grid.n_active_cells:
562 | ina = np.tile(sr3.grid.cells.inactive, ntimes)
563 | tmp = np.zeros(sr3.grid.n_cells * ntimes)
564 | tmp[~ina] = np.array(prop).flatten()
565 | tmp = np.reshape(tmp, (ntimes, sr3.grid.n_cells))
566 |
567 | sp[name] = tmp
568 |
569 | return sp_ind, sp
570 |
571 |
572 | def get_spatial_indexed(sr3, prefix, comps):
573 | """
574 | Returns the tuple (sp_ind, sp), where sp is a list of the time-dependent indexed properties (such as MOLALITY or
575 | MINERAL) of the specified components, and sp_ind is a list of indices of corresponding time instants in sr3.times.
576 | """
577 |
578 | # Get a list of internal names of the desired components
579 | sel_names = []
580 | for i, c in enumerate(comps):
581 | try:
582 | i = sr3.comp_name.index(c) + 1
583 | # ic.append(i)
584 | sel_names.append(prefix + '(' + str(i) + ')')
585 | except ValueError:
586 | print(c + ' is not found among available components...')
587 |
588 | (sp_ind, sp) = get_spatial_properties(sr3, sel_names)
589 |
590 | # Change the keys from prefix(i) to component names
591 | assert len(sp) == len(comps), 'The dimensions of the indexed spatial properties do not match the number of components!'
592 | sp_comps = {}
593 | for c, key in zip(comps, sp):
594 | sp_comps[c] = sp[key]
595 |
596 | return sp_ind, sp_comps
597 |
598 |
599 | def list_molalities_minerals(sr3):
600 | """
601 | Returns the lists of spatially distributed molalities and minerals.
602 | """
603 |
604 | # Get the GEM indices between the brackets in MOLALITY(1) etc
605 | ind_mol = [int(name[name.find('(') + 1:name.find(')')]) for name in sr3.sp_name if 'MOLALITY' in name]
606 | molalities = [sr3.comp_name[i - 1] for i in ind_mol]
607 |
608 | # Get the GEM indices between the brackets in MINERAL(1) etc
609 | ind_min = [int(name[name.find('(') + 1:name.find(')')]) for name in sr3.sp_name if 'MINERAL' in name]
610 | minerals = [sr3.comp_name[i - 1] for i in ind_min]
611 |
612 | return molalities, minerals
613 |
614 |
615 | def get_grid(sr3):
616 | """
617 | Assigns the grid structure with cells' volumes and centroids, and sets the grid dimensions.
618 |
619 | Not clear:
620 | * How the depths from SR3 are related to cell tops (the end values do not correspond to DTOP in the input)
621 | * What are the volumes (dimensions 2 times the cells' dimensions - due to dual porosity?)
622 |
623 | """
624 |
625 | # Set up the boolean array of inactive cells
626 | (sp_ind, sp) = get_spatial_properties(sr3, 'GRID/ICSTPS', activeonly=False)
627 | inactind = sp['GRID/ICSTPS'] == 0
628 | sr3.grid.cells.inactive = inactind[0]
629 | sr3.grid.n_active_cells = np.count_nonzero(~sr3.grid.cells.inactive)
630 | sr3.grid.n_cells = len(sr3.grid.cells.inactive)
631 |
632 | # Apparently GRID/NODES are only present in SR3 for corner-point grid geometry
633 | (sp_ind, sp) = get_spatial_properties(sr3, 'GRID/NODES', verbose=False)
634 | if len(sp['GRID/NODES']) > 0:
635 | print('Corner-point grid format detected, the grid geometry might not be visualized properly...')
636 |
637 | (sp_ind, sp) = get_spatial_properties(sr3, ['GRID/BLOCKDEPTH', 'GRID/BLOCKSIZE', # 'GRID/BVOL',
638 | 'GRID/IGNTFR', 'GRID/IGNTGT', 'GRID/IGNTID', 'GRID/IGNTJD',
639 | 'GRID/IGNTKD', 'GRID/IGNTNC', 'GRID/IGNTNS', 'GRID/IGNTZA'],
640 | activeonly=False)
641 |
642 | # Assert that the grid properties are provided at t=0 only (can be different in geomechanics simulations)
643 | if len(sp_ind) != 1:
644 | print('The grid properties are not provided at several time instants, '
645 | 'but the grid is constructed using the data at t=0 only!')
646 |
647 | assert sp_ind[0] == 0, 'The grid properties are not provided at t=0 only!'
648 |
649 |
650 |
651 | depths = np.array(sp['GRID/BLOCKDEPTH'][0])
652 | size = np.array(sp['GRID/BLOCKSIZE'][0])
653 |
654 | # Array dimensons do not match at least in the case of dual porosity (gmgmc080.sr3)
655 | #volumes = np.array(sp['GRID/BVOL'][0])
656 |
657 | # Get the block dimensions
658 | dx = size[0::3]
659 | dy = size[1::3]
660 | dz = size[2::3]
661 |
662 | # Get the number of Cartesian grid blocks
663 | Ni = int(sp['GRID/IGNTID'][0])
664 | Nj = int(sp['GRID/IGNTJD'][0])
665 | Nk = int(sp['GRID/IGNTKD'][0])
666 | sr3.grid.cart_dims = (Ni, Nj, Nk)
667 | # sr3.grid.n_cells = Ni * Nj * Nk
668 | sr3.grid.grid_dim = sum(1 for n in sr3.grid.cart_dims if n > 1)
669 |
670 | # Calculate the centroids and volumes
671 | dx = np.reshape(dx, sr3.grid.cart_dims, order='F')
672 | dy = np.reshape(dy, sr3.grid.cart_dims, order='F')
673 | dz = np.reshape(dz, sr3.grid.cart_dims, order='F')
674 |
675 | # The depths are related to cell centers
676 | d = np.reshape(depths, sr3.grid.cart_dims, order='F')
677 | depthsf = d.flatten(order='F')
678 |
679 | # Cell centroids
680 | xc = np.cumsum(dx, axis=0) - 0.5 * dx
681 | yc = np.cumsum(dy, axis=1) - 0.5 * dy
682 | zc = np.cumsum(dz, axis=2) - 0.5 * dz
683 |
684 | # Flatten so that i changes fastest, k - slowest
685 | xcf = xc.flatten(order='F')
686 | ycf = yc.flatten(order='F')
687 | zcf = zc.flatten(order='F')
688 |
689 | # Assign the sr3.grid.cells fields
690 | sr3.grid.cells.centroids = np.array([xcf, ycf, depthsf])
691 |
692 | volumes = dx * dy * dz
693 | sr3.grid.cells.volumes = volumes.flatten(order='F')
694 |
695 | # Get the cells' corners coordinates excluding the 0-th slices in x-, y, and z-directions
696 | x = np.cumsum(dx, axis=0)
697 | y = np.cumsum(dy, axis=1)
698 | z = np.cumsum(dz, axis=2)
699 |
700 | # Prepare the zeros arrays with proper dimensions
701 | xx = np.zeros((Ni + 1, Nj + 1, Nk + 1))
702 | yy = np.zeros((Ni + 1, Nj + 1, Nk + 1))
703 | zz = np.zeros((Ni + 1, Nj + 1, Nk + 1))
704 |
705 | # Immerse the available coordinates into the arrays of proper dimensions
706 | xx[1:, 1:, 1:] = x
707 | yy[1:, 1:, 1:] = y
708 | zz[1:, 1:, 1:] = z
709 |
710 | # Set up the coordinates for the 0-th slices in x-, y, and z-directions
711 | xx[:, 0, :] = xx[:, 1, :]
712 | xx[:, :, 0] = xx[:, :, 1]
713 |
714 | yy[0, :, :] = yy[1, :, :]
715 | yy[:, :, 0] = yy[:, :, 1]
716 |
717 | zz[0, :, :] = zz[1, :, :]
718 | zz[:, 0, :] = zz[:, 1, :]
719 |
720 | # Shift the depths to be negative so that abs(depths) grows downwards
721 | bottom = d[0, 0, 0] + dz[0, 0, 0] / 2
722 | zz = zz - bottom
723 |
724 | # Duplicate all coordinates ...
725 | xr = xx.copy()
726 | yr = yy.copy()
727 | zr = zz.copy()
728 | for n in range(3):
729 | xr = np.repeat(xr, 2, axis=n)
730 | yr = np.repeat(yr, 2, axis=n)
731 | zr = np.repeat(zr, 2, axis=n)
732 |
733 | # ... except the ones from the edges to get the coordinates of 8 corners per grid block
734 | for n in range(3):
735 | xr = np.delete(xr, [0, -1], axis=n)
736 | yr = np.delete(yr, [0, -1], axis=n)
737 | zr = np.delete(zr, [0, -1], axis=n)
738 |
739 | # Flatten so that i changes fastest, k - slowest
740 | xf = xr.flatten(order='F')
741 | yf = yr.flatten(order='F')
742 | zf = zr.flatten(order='F')
743 |
744 | # Grid the blocks' corners coordinates
745 | corners = np.array([xf, yf, zf])
746 | sr3.grid.cells.corners = np.transpose(corners)
747 |
748 |
749 | def get_units(sr3):
750 | """
751 | Assigns the units structure with the values from SR3.
752 |
753 | The UnitsTable in SR3 has entries of the form (1, b'Time', b'day', b'day'), (2, b'Temperature', b'K', b'F'), etc.
754 | It seems that the units of the data are the 2nd element of the corresponding tuples in the 0-based indexing, and the
755 | 3rd element is the unit in the system, selected in the GEM input file (SI or FIELD).
756 |
757 | The implementation below returns the internal CMG units.
758 | """
759 |
760 | for element in sr3.data['General/UnitsTable']:
761 | # Convert the byte strings of the format b'Time'
762 | name = str(element[1])
763 | unit = str(element[2])
764 | name = name.split('\'')[1]
765 | unit = unit.split('\'')[1]
766 | sr3.units[name] = unit
767 |
--------------------------------------------------------------------------------