├── 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 | ![Ratios of molalities at the outlet](./pics/molalities.png) 25 | ![Mineral change](./pics/min_change.png) 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 | ![Ratios of molalities at the outlet](pics/cartesian_grid.png) 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 | --------------------------------------------------------------------------------