├── .gitignore ├── README.md ├── parserasgeo ├── __init__.py ├── features │ ├── __init__.py │ ├── boundary.py │ ├── bridge.py │ ├── cross_section.py │ ├── culvert.py │ ├── description.py │ ├── feature.py │ ├── inline_weir.py │ ├── junction.py │ ├── lateral_weir.py │ ├── river_reach.py │ ├── station.py │ └── tools.py ├── prg.py ├── prplan.py ├── prprj.py └── uflow.py ├── setup.py ├── test └── geo_test.py └── tools ├── bank_sta_balance.py ├── levee.py └── skew_check.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .idea/* 3 | .idea/*.* 4 | .idea/workspace.xml 5 | geos 6 | geos/*.* 7 | geos-old 8 | geos-old/*.* 9 | *.g?? 10 | test/prg_test.g00 11 | *.pyc 12 | *.egg-info 13 | *.egg-info/* 14 | *.swp 15 | test.out 16 | *.out 17 | *uselesschange 18 | build/ 19 | dist/ 20 | test/notebooks 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Parserasgeo 2 | 3 | PARSE hec-RAS GEOmetry - Import/Export [HEC-RAS](https://www.hec.usace.army.mil/software/hec-ras/) geometry files. 4 | 5 | Parserasgeo is a python library for importing, editing, and exporting HEC-RAS geometry files and can be used for automating sensitivity analyses, Monte Carlo analyses, and any other work flow that requires changing RAS geometry programmatically. Parserasgeo is a work in progress, however most cross section functionality exists. Lines that are not understand are stored as text and will be rewritten when the geometry is exported. Parserasgeo is known to work with Python 2 and should also work with Python 3. 6 | 7 | HEC-RAS models can be run automatically using [rascontrol](https://github.com/mikebannis/rascontrol). 8 | 9 | 10 | ## Getting Started 11 | 12 | Parserasgeo is mostly easily installed from GitHub 13 | 14 | ``` 15 | C:\> git clone https://github.com/mikebannis/parserasgeo.git 16 | 17 | C:\> cd parserasgeo 18 | 19 | C:\parserasgeo> pip install . 20 | ``` 21 | 22 | ## Example 23 | 24 | Open a model, increase all Manning's n values by 50%, and save the geometry as a new file. 25 | 26 | ```python 27 | import parserasgeo as prg 28 | 29 | geo = prg.ParseRASGeo('my_model.g01') 30 | 31 | for xs in geo.get_cross_sections(): 32 | n_vals = xs.mannings_n.values 33 | 34 | # n-values are stored as a list of 3-tuples 35 | new_n = [(station, n*1.5, other) for station, n, other in n_vals] 36 | 37 | xs.mannings_n.values = new_n 38 | 39 | geo.write('my_model.g02') 40 | ``` 41 | 42 | ## Contributing 43 | 44 | While currently functional, this is very much a work in progress. Well written and tested pull requests are gladly accepted. 45 | 46 | One of the goals for this library is that exported geometries will match original geometries to the character. This allows easy testing of new functionality by comparing the original geometry file to one exported from parserasgeo (assuming no changes were made). 47 | -------------------------------------------------------------------------------- /parserasgeo/__init__.py: -------------------------------------------------------------------------------- 1 | from .prg import ParseRASGeo, CrossSectionNotFound 2 | from .prplan import ParseRASPlan 3 | from .prprj import ParseRASProject 4 | from .uflow import UnsteadyFlow 5 | -------------------------------------------------------------------------------- /parserasgeo/features/__init__.py: -------------------------------------------------------------------------------- 1 | from .cross_section import CrossSection 2 | from .river_reach import RiverReach 3 | from .culvert import Culvert 4 | from .bridge import Bridge 5 | from .lateral_weir import LateralWeir 6 | from .inline_weir import InlineWeir 7 | from .junction import Junction 8 | -------------------------------------------------------------------------------- /parserasgeo/features/boundary.py: -------------------------------------------------------------------------------- 1 | from .feature import Feature 2 | from .station import Station 3 | from .tools import pad_left, print_list_by_group, split_by_n 4 | 5 | 6 | class Boundary(Feature): 7 | """ 8 | Boundary condition. 9 | """ 10 | 11 | def __init__(self): 12 | # Load all boundary parts 13 | self.header = Header() 14 | self.interval = Interval() 15 | self.hydrograph = Hydrograph() 16 | self.dss = DSS() 17 | self.fixed_start = FixedStart() 18 | self.critical = Critical() 19 | self.parts = [ 20 | self.header, 21 | self.interval, 22 | self.hydrograph, 23 | self.dss, 24 | self.fixed_start, 25 | self.critical, 26 | ] 27 | self.uflow_list = [] # holds all parts and unknown lines (as strings) 28 | 29 | @staticmethod 30 | def test(line): 31 | return Header.test(line) 32 | 33 | def import_geo(self, line, infile): 34 | while True: 35 | part = next((p for p in self.parts if p.test(line)), None) 36 | if part is not None: 37 | line = part.import_geo(line, infile) 38 | self.parts.remove(part) 39 | self.uflow_list.append(part) 40 | else: 41 | break 42 | return line 43 | 44 | def __str__(self): 45 | return "".join((str(l) for l in self.uflow_list)) 46 | 47 | 48 | class Header(Feature): 49 | def __init__(self): 50 | self.river_name = None 51 | self.reach_name = None 52 | self.station = None 53 | 54 | @staticmethod 55 | def test(line): 56 | return line.startswith("Boundary Location=") 57 | 58 | def import_geo(self, line, infile): 59 | self._parts = line.split("=")[1].split(",") 60 | self.river_name = self._parts[0].strip() 61 | self.reach_name = self._parts[1].strip() 62 | self.station = Station(self._parts[2]) 63 | return infile.readline() 64 | 65 | def __str__(self): 66 | lengths = [16, 16, 8, 6, 16, 16, 16, 16] 67 | values = [pad_left(part, length) for part, length in zip(self._parts, lengths)] 68 | s = "Boundary Location=" + ",".join(values) 69 | return s 70 | 71 | 72 | class Interval(Feature): 73 | def __init__(self): 74 | self.interval = None 75 | 76 | @staticmethod 77 | def test(line): 78 | return line.startswith("Interval=") 79 | 80 | def import_geo(self, line, infile): 81 | self.interval = line.split("=")[1].strip() 82 | return infile.readline() 83 | 84 | def __str__(self): 85 | return "Interval={}\n".format(self.interval) 86 | 87 | 88 | class Hydrograph(Feature): 89 | def __init__(self): 90 | self.type = None 91 | self.values = [] 92 | 93 | @staticmethod 94 | def test(line): 95 | return line.split("=")[0].endswith("Hydrograph") 96 | 97 | def import_geo(self, line, infile): 98 | parts = line.split(" Hydrograph=") 99 | self.type = parts[0] 100 | num_pts = int(parts[1]) 101 | line = infile.readline() 102 | while ( 103 | line[:1] == " " or line[:1].isdigit() or line[:1] == "-" or line[:1] == "." 104 | ): 105 | vals = split_by_n(line, 8) 106 | self.values.extend(vals) 107 | line = infile.readline() 108 | assert len(self.values) == num_pts 109 | return line 110 | 111 | def __str__(self): 112 | s = "Hydrograph=" + print_list_by_group(self.values, 8, 10) 113 | return s 114 | 115 | 116 | class DSS(Feature): 117 | def __init__(self): 118 | self.path = None 119 | self.use_dss = None 120 | 121 | @staticmethod 122 | def test(line): 123 | return line.startswith("DSS Path") 124 | 125 | def import_geo(self, line, infile): 126 | self.path = line.split("=")[1].strip() 127 | line = infile.readline() 128 | if line.startswith("Use DSS="): 129 | self.use_dss = line.split("=")[1].strip().lower() == "true" 130 | line = infile.readline() 131 | return line 132 | 133 | def __str__(self): 134 | return "DSS Path={}\nUse DSS={}\n".format(self.path, self.use_dss) 135 | 136 | 137 | class FixedStart(Feature): 138 | def __init__(self): 139 | self.use_fixed_start = None 140 | self.datetime = None 141 | 142 | @staticmethod 143 | def test(line): 144 | return line.startswith("Use Fixed Start Time") 145 | 146 | def import_geo(self, line, infile): 147 | self.use_fixed_start = line.split("=")[1].strip().lower() == "true" 148 | line = infile.readline() 149 | if line.startswith("Fixed Start Date/Time="): 150 | self.datetime = line.split("=")[1].strip() 151 | line = infile.readline() 152 | return line 153 | 154 | def __str__(self): 155 | return "Use Fixed Start Time={}\nFixed Start Date/Time={}\n".format( 156 | self.use_fixed_start, self.datetime 157 | ) 158 | 159 | 160 | class Critical(Feature): 161 | def __init__(self): 162 | self.is_critical = None 163 | self.flow = None 164 | 165 | @staticmethod 166 | def test(line): 167 | return line.startswith("Is Critical Boundary") 168 | 169 | def import_geo(self, line, infile): 170 | self.is_critical = line.split("=")[1].strip().lower() == "true" 171 | line = infile.readline() 172 | if line.startswith("Critical Boundary Flow"): 173 | self.flow = line.split("=")[1].strip() 174 | line = infile.readline() 175 | return line 176 | 177 | def __str__(self): 178 | return "Is Critical Boundary={}\nCritical Boundary Flow={}\n".format( 179 | self.is_critical, self.flow 180 | ) 181 | -------------------------------------------------------------------------------- /parserasgeo/features/bridge.py: -------------------------------------------------------------------------------- 1 | from .tools import fl_int # , split_by_n_str, pad_left, print_list_by_group, split_block_obs, split_by_n 2 | from .description import Description 3 | 4 | 5 | class Feature(object): 6 | """ 7 | This is a template for other features. 8 | """ 9 | def __init__(self): 10 | pass 11 | 12 | @staticmethod 13 | def test(line): 14 | if line.split('=')[0] == 'XS GIS Cut Line': 15 | return True 16 | return False 17 | 18 | def import_geo(self, line, geo_file): 19 | return line 20 | 21 | def __str__(self): 22 | pass 23 | 24 | 25 | # TODO: possibly move header into Bridge 26 | class Header(object): 27 | def __init__(self): 28 | 29 | self.station = None 30 | self.node_type = None 31 | self.value1 = None 32 | self.value2 = None 33 | self.value3 = None 34 | 35 | @staticmethod 36 | def test(line): 37 | if line[:23] == 'Type RM Length L Ch R =': 38 | if line[24:25] == '3': 39 | return True 40 | return False 41 | 42 | def import_geo(self, line, geo_file): 43 | fields = line[23:].split(',') 44 | # print line, fields 45 | assert len(fields) == 5 46 | # vals = [fl_int(x) for x in fields] 47 | # Node type and cross section id 48 | self.node_type = fl_int(fields[0]) 49 | self.station = fl_int(fields[1]) 50 | # TODO: Not sure what these are yet 51 | self.value1 = fields[2] 52 | self.value2 = fields[3] 53 | self.value3 = fields[4] 54 | return next(geo_file) 55 | 56 | def __str__(self): 57 | s = 'Type RM Length L Ch R = ' 58 | s += str(self.node_type) + ' ,' 59 | s += '{:<8}'.format(str(self.station)) + ',' 60 | s += str(self.value1) + ',' + str(self.value2) + ',' + str(self.value3) # + '\n' TODO: Add this back it later once the remainder of the 61 | # header if figured out 62 | return s 63 | 64 | 65 | class Bridge(object): 66 | def __init__(self, river, reach): 67 | self.river = river 68 | self.reach = reach 69 | 70 | # Load all cross sections parts 71 | # self.cutline = CutLine() 72 | self.header = Header() 73 | self.description = Description() 74 | # self.sta_elev = StationElevation() 75 | # self.iefa = IEFA() 76 | # self.mannings_n = Mannings_n() 77 | # self.obstruct = Obstruction() 78 | # self.bank_sta = BankStation() 79 | self.parts = [self.header, self.description] 80 | 81 | self.geo_list = [] # holds all parts and unknown lines (as strings) 82 | 83 | def import_geo(self, line, geo_file): 84 | while line != '\n': 85 | for part in self.parts: 86 | if part.test(line): 87 | # print str(type(part))+' found!' 88 | line = part.import_geo(line, geo_file) 89 | self.parts.remove(part) 90 | self.geo_list.append(part) 91 | break 92 | else: # Unknown line, add as text 93 | self.geo_list.append(line) 94 | line = next(geo_file) 95 | return line 96 | 97 | def __str__(self): 98 | s = '' 99 | for line in self.geo_list: 100 | s += str(line) 101 | return s + '\n' 102 | 103 | @staticmethod 104 | def test(line): 105 | return Header.test(line) 106 | -------------------------------------------------------------------------------- /parserasgeo/features/cross_section.py: -------------------------------------------------------------------------------- 1 | from .tools import fl_int, split_by_n_str, pad_left, print_list_by_group, split_block_obs, split_by_n 2 | from .description import Description 3 | from .station import Station 4 | from math import sqrt, cos, radians 5 | 6 | # Global debug, this is set when initializing CrossSection 7 | DEBUG = False 8 | 9 | class ChannelNError(Exception): 10 | """ 11 | An error to raise if the user attempts to change channel n values without first defining the channel 12 | """ 13 | pass 14 | 15 | class Feature(object): 16 | """ 17 | This is a template for other features. 18 | """ 19 | def __init__(self): 20 | pass 21 | 22 | @staticmethod 23 | def test(line): 24 | if line.split('=')[0] == 'XS GIS Cut Line': 25 | return True 26 | return False 27 | 28 | def import_geo(self, line, geo_file): 29 | return line 30 | 31 | def __str__(self): 32 | pass 33 | 34 | class Levee(object): 35 | """ 36 | Levees. This is poorly implemented and only grabs the line past the "=" as a string 37 | """ 38 | def __init__(self): 39 | self.value = None 40 | 41 | @staticmethod 42 | def test(line): 43 | if line.split('=')[0] == 'Levee': 44 | return True 45 | return False 46 | 47 | def import_geo(self, line, geo_file): 48 | self.value = line.split('=')[1] 49 | return next(geo_file) 50 | 51 | def __str__(self): 52 | return 'Levee=' + self.value # + '\n' - not needed since it's just a dumb string (with the \n) 53 | 54 | class RatingCurve(object): 55 | """ 56 | Rating Curves. This is poorly implemented and only grabs the values on the first line and not the 57 | curve itself. 58 | """ 59 | def __init__(self): 60 | self.value1 = None 61 | self.value2 = None 62 | 63 | @staticmethod 64 | def test(line): 65 | if line.split('=')[0] == 'XS Rating Curve': 66 | return True 67 | return False 68 | 69 | def import_geo(self, line, geo_file): 70 | temp = line.split('=')[1].split(',') 71 | self.value1 = int(temp[0]) 72 | self.value2 = int(temp[1]) 73 | return next(geo_file) 74 | 75 | def __str__(self): 76 | return 'XS Rating Curve= ' + str(self.value1) + ' ,' + str(self.value2) + '\n' 77 | 78 | 79 | class Skew(object): 80 | """ 81 | Cross section skew angle 82 | """ 83 | def __init__(self): 84 | self.angle = None 85 | 86 | @staticmethod 87 | def test(line): 88 | if line.split('=')[0] == 'Skew Angle': 89 | return True 90 | return False 91 | 92 | def import_geo(self, line, geo_file): 93 | self.angle = float(line.split('=')[1]) 94 | return next(geo_file) 95 | 96 | def __str__(self): 97 | return 'Skew Angle= '+str(fl_int(self.angle))+' \n' 98 | 99 | 100 | # TODO: possibly move header into CrossSection 101 | class Header(object): 102 | def __init__(self): 103 | self.station = None 104 | self.node_type = None 105 | self.lob_length = None 106 | self.channel_length = None 107 | self.rob_length = None 108 | 109 | @staticmethod 110 | def test(line): 111 | if line[:23] == 'Type RM Length L Ch R =': 112 | if line[24:25] == '1': 113 | return True 114 | return False 115 | 116 | def import_geo(self, line, geo_file): 117 | fields = line[23:].split(',') 118 | assert len(fields) == 5 119 | 120 | self.node_type = int(fields[0]) 121 | self.station = Station(fields[1]) 122 | self.lob_length = self._to_float(fields[2]) 123 | self.channel_length = self._to_float(fields[3]) 124 | self.rob_length = self._to_float(fields[4]) 125 | 126 | if DEBUG: 127 | print('-'*30) 128 | print('Importing XS:', self.station.id) 129 | 130 | return next(geo_file) 131 | 132 | ### TODO: Handle null reach lengths appropriately 133 | @staticmethod 134 | def _to_float(x): 135 | """ 136 | Reach lengths may be blank at the downstream end. This looks out for 137 | that scenario and returns 0. This will break the ability to reproduce 138 | some geometry file to the character and should be updated at some point! 139 | """ 140 | if x == '' or x == '\n': 141 | return 0.0 142 | return float(x) 143 | 144 | def __str__(self): 145 | s = 'Type RM Length L Ch R = ' 146 | s += str(self.node_type) + ' ,' 147 | s += str(self.station) + ',' 148 | s += str(self.lob_length) + ',' + str(self.channel_length) + ',' + str(self.rob_length) + '\n' 149 | return s 150 | 151 | 152 | class CutLine(object): 153 | def __init__(self): 154 | self.number_pts = None 155 | self.points = [] # [(x1,y1),(x2,y2),(x3,y3),...] Values are currently stored as strings 156 | 157 | @staticmethod 158 | def test(line): 159 | if line.split('=')[0] == 'XS GIS Cut Line': 160 | return True 161 | return False 162 | 163 | def import_geo(self, line, geo_file): 164 | vals = line.split('=') 165 | assert vals[0] == 'XS GIS Cut Line' and len(vals) == 2 166 | self.number_pts = int(vals[1]) 167 | line = next(geo_file) 168 | while line[:1] == ' ' or line[:1].isdigit() or line[:1] == '-' or line[:1] == '.': 169 | vals = split_by_n_str(line, 16) 170 | for i in range(0, len(vals), 2): 171 | self.points.append((vals[i], vals[i + 1])) 172 | line = next(geo_file) 173 | assert self.points != [] 174 | return line 175 | 176 | def __str__(self): 177 | s = 'XS GIS Cut Line=' + str(self.number_pts) + '\n' 178 | pts = [self.points[i:i + 2] for i in range(0, len(self.points), 2)] 179 | for pt in pts: 180 | if len(pt) == 2: 181 | s += pt[0][0] + pt[0][1] + pt[1][0] + pt[1][1] + '\n' 182 | else: 183 | s += pt[0][0] + pt[0][1] + '\n' 184 | return s 185 | 186 | 187 | class LastEdit(object): 188 | pass 189 | 190 | 191 | class StationElevation(object): 192 | def __init__(self): 193 | self.points = [] # [(sta0, elev0), (sta1, elev1), ... ] Values stored as float/int 194 | 195 | @staticmethod 196 | def test(line): 197 | if line[:9] == '#Sta/Elev': 198 | return True 199 | return False 200 | 201 | def elevation(self, sta): 202 | """ 203 | Returns elevation of point at station 'station' 204 | Raises AttributeError if station is not foudn 205 | :param sta: float, station of interest 206 | :return: double, elevation 207 | """ 208 | # TODO - implement more efficient search 209 | for pt in self.points: 210 | if pt[0] == sta: 211 | return pt[1] 212 | raise AttributeError('No station matching ' + str(sta) + ' in current XS.') 213 | 214 | 215 | def import_geo(self, line, geo_file): 216 | """ 217 | Import XS station/elevation points. 218 | :param line: current line of geo_file 219 | :param geo_file: geometry file object 220 | :return: line in geo_file after sta/elev data 221 | """ 222 | num_pts = int(line[10:]) 223 | line = next(geo_file) 224 | while line[:1] == ' ' or line[:1].isdigit() or line[:1] == '-' or line[:1] == '.': 225 | vals = split_by_n(line, 8) 226 | for i in range(0, len(vals), 2): 227 | self.points.append((vals[i], vals[i + 1])) 228 | line = next(geo_file) 229 | if DEBUG: 230 | print('len(self.points)=', len(self.points), 'num_pts=', num_pts) 231 | assert len(self.points) == num_pts 232 | return line 233 | 234 | def __str__(self): 235 | s = '#Sta/Elev= ' + str(len(self.points)) + ' \n' 236 | # unpack tuples 237 | sta_elev_list = [x for tup in self.points for x in tup] 238 | # convert to padded columns of 8 239 | temp_str = print_list_by_group(sta_elev_list, 8, 10) 240 | s += temp_str 241 | return s 242 | 243 | 244 | class IEFA(object): 245 | def __init__(self): 246 | self.num_iefa = None 247 | self.type = None 248 | self.iefa_list = [] 249 | self.iefa_permanence = [] 250 | 251 | @staticmethod 252 | def test(line): 253 | if line[:10] == '#XS Ineff=': 254 | return True 255 | return False 256 | 257 | def import_geo(self, line, geo_file): 258 | values = line.split(',') 259 | self.num_iefa = int(values[0][-3:]) 260 | self.type = int(values[1]) 261 | line = next(geo_file) 262 | # Due to possible blank lines in geometry file, all data must be treated as a string 263 | while line[:1] == ' ' or line[:1].isdigit() or line[:1] == '-' or line[:1] == '.': 264 | values = split_block_obs(line, 8) 265 | assert len(values) % 3 == 0 266 | for i in range(0, len(values), 3): 267 | self.iefa_list.append((values[i], values[i + 1], values[i + 2])) 268 | line = next(geo_file) 269 | assert self.num_iefa == len(self.iefa_list) 270 | 271 | # Process IEFA permanence 272 | if line != 'Permanent Ineff=\n': 273 | raise ValueError('Permanent Ineff= does not follow IEFA in geometry file. Aborting.') 274 | line = next(geo_file) 275 | while line[:1] == ' ': 276 | values = line.split() 277 | for value in values: 278 | if value == 'T': 279 | self.iefa_permanence.append(True) 280 | elif value == 'F': 281 | self.iefa_permanence.append(False) 282 | else: 283 | raise ValueError(value + ' found in IEFA permanence filed. Should be T or F. Aborting') 284 | line = next(geo_file) 285 | assert len(self.iefa_list) == len(self.iefa_permanence) 286 | 287 | return line 288 | 289 | def __str__(self): 290 | temp_iefa = [x for tup in self.iefa_list for x in tup] 291 | s = '#XS Ineff= ' + str(self.num_iefa) + ' ,' + pad_left(self.type, 2) + ' \n' 292 | s += print_list_by_group(temp_iefa, 8, 9) 293 | s += 'Permanent Ineff=\n' 294 | for value in self.iefa_permanence: 295 | if value: 296 | s += ' T' 297 | else: 298 | s += ' F' 299 | s += '\n' 300 | return s 301 | 302 | 303 | class Obstruction(object): 304 | def __init__(self): 305 | self.num_blocked = None 306 | self.blocked_type = None 307 | self.blocked = [] # [(start_sta1, end_sta1, elev1), (start_sta2, end_sta2, elev2), ...] 308 | 309 | @staticmethod 310 | def test(line): 311 | if line[:16] == '#Block Obstruct=': 312 | return True 313 | return False 314 | 315 | def import_geo(self, line, geo_file): 316 | values = line.split(',') 317 | self.num_blocked = int(values[0][-3:]) 318 | self.blocked_type = int(values[1]) 319 | 320 | line = next(geo_file) 321 | # Due to possible missing elevation all values must be treated as strings 322 | while line[:1] == ' ' or line[:1].isdigit() or line[:1] == '-' or line[:1] == '.': 323 | values = split_block_obs(line, 8) 324 | assert len(values) % 3 == 0 325 | for i in range(0, len(values), 3): 326 | self.blocked.append((values[i], values[i + 1], values[i + 2])) 327 | line = next(geo_file) 328 | assert self.num_blocked == len(self.blocked) 329 | return line 330 | 331 | def __str__(self): 332 | # unpack tuples 333 | blocked_list = [x for tup in self.blocked for x in tup] 334 | s = '#Block Obstruct= ' + str(self.num_blocked) + ' ,' + pad_left(self.blocked_type, 2) + ' \n' 335 | s += print_list_by_group(blocked_list, 8, 9) 336 | return s 337 | 338 | 339 | class Mannings_n(object): 340 | def __init__(self): 341 | self.values = [] # [(sta1, n1, 0), (sta2, n2, 0), ...] 342 | self.horizontal = None # 0 or -1 343 | 344 | @staticmethod 345 | def test(line): 346 | if line[:6] == '#Mann=': 347 | return True 348 | return False 349 | 350 | def import_geo(self, line, geo_file): 351 | # Parse manning's n header line 352 | fields = line[6:].split(',') 353 | assert len(fields) == 3 354 | values = [fl_int(x) for x in fields] 355 | test_length = values[0] 356 | self.horizontal = values[1] 357 | 358 | # Parse stations and n-values 359 | line = next(geo_file) 360 | 361 | # Make sure we're still reading n-values 362 | while line[:1] == ' ' or line[:1].isdigit() or line[:1] == '-'or line[:1] == '.': 363 | values = split_by_n(line, 8) 364 | if len(values) % 3 != 0: 365 | raise ValueError ('Error processing n-values: ' + line + '\n' + str(values)) 366 | for i in range(0, len(values), 3): 367 | self.values.append((values[i], values[i + 1], values[i + 2])) 368 | line = next(geo_file) 369 | assert test_length == len(self.values) 370 | return line 371 | 372 | def __str__(self): 373 | s = '#Mann= ' + str(len(self.values)) + ' ,{:>2} , 0 \n'.format(self.horizontal) 374 | # n-values - unpack tuples 375 | n_list = [x for tup in self.values for x in tup] 376 | # convert to padded columns of 8 377 | temp_str = print_list_by_group(n_list, 8, 9) 378 | s += temp_str 379 | return s 380 | 381 | def check_for_duplicate_n_values(self): 382 | """ 383 | Checks cross section for two n-value changes at the same station. This does happen, I'm not sure how, and 384 | HEC-RAS is okay with it. Raises ValueError if self.mannings_n is empty 385 | :return: Returns a list of station with multiple n-value changes if they exist, otherwise returns None 386 | """ 387 | n_values = list(self.values) 388 | if n_values == []: 389 | raise ValueError('Cross section has no Manning\'s n values.') 390 | errors = [] 391 | last_station = n_values.pop(0)[0] 392 | for n_value in n_values: 393 | if n_value[0] == last_station: 394 | errors.append(last_station) 395 | last_station = n_value[0] 396 | if errors != []: 397 | return errors 398 | else: 399 | return None 400 | 401 | def check_for_redundant_n_values(self): 402 | """ 403 | Checks for redundant n-value changes, e.g. 0.035 then 0.035. Raises ValueError if self.mannings_n is empty 404 | :return: Returns a list of station with redundant n-value changes if they exist, otherwise returns None 405 | """ 406 | n_values = list(self.values) 407 | if n_values == []: 408 | raise ValueError('Cross section has no Manning\'s n values.') 409 | errors = [] 410 | last_n_value = n_values.pop(0)[1] 411 | for n_value in n_values: 412 | if n_value[1] == last_n_value: 413 | errors.append(n_value[0]) 414 | last_n_value = n_value[1] 415 | if errors != []: 416 | return errors 417 | else: 418 | return None 419 | 420 | 421 | class BankStation(object): 422 | def __init__(self): 423 | self.left = None 424 | self.right = None 425 | 426 | @staticmethod 427 | def test(line): 428 | if line[:9] == 'Bank Sta=': 429 | return True 430 | return False 431 | 432 | def import_geo(self, line, geo_file): 433 | line = line[9:] 434 | values = line.split(',') 435 | assert len(values) == 2 436 | self.left = fl_int(values[0]) 437 | self.right = fl_int(values[1]) 438 | return next(geo_file) 439 | 440 | def __str__(self): 441 | return 'Bank Sta=' + str(self.left) + ',' + str(self.right) + '\n' 442 | 443 | # TODO: implement contraction/expansion 444 | class ExpansionContraction(object): 445 | def __init__(self): 446 | self.exp_coeff = None 447 | self.contract_coeff = None 448 | 449 | @staticmethod 450 | def test(line): 451 | if line.split('=')[0] == 'XS GIS Cut Line': 452 | return True 453 | return False 454 | 455 | def import_geo(self, line, geo_file): 456 | return line 457 | 458 | def __str__(self): 459 | pass 460 | 461 | 462 | class CrossSection(object): 463 | def __init__(self, river, reach, debug=False): 464 | # Set global debug 465 | global DEBUG 466 | DEBUG = debug 467 | 468 | self.river = river 469 | self.reach = reach 470 | 471 | # Load all cross sections parts 472 | # TODO: Add "Node Name=" tag, see harvard gulch/dry gulch for example 473 | self.cutline = CutLine() 474 | self.header = Header() 475 | self.description = Description() 476 | self.sta_elev = StationElevation() 477 | self.iefa = IEFA() 478 | self.mannings_n = Mannings_n() 479 | self.obstruct = Obstruction() 480 | self.bank_sta = BankStation() 481 | self.skew = Skew() 482 | self.levee = Levee() 483 | self.rating_curve = RatingCurve() 484 | self.parts = [self.header, self.description, self.cutline, self.iefa, self.mannings_n, self.obstruct, self.bank_sta, 485 | self.sta_elev, self.skew, self.levee, self.rating_curve] 486 | 487 | self.geo_list = [] # holds all parts and unknown lines (as strings) 488 | 489 | # used to define n-values in the channel only (i.e. between the bank stations) 490 | # gets defined in define_channel_n 491 | self.channel_n = None 492 | self.is_interpolated = None 493 | 494 | def import_geo(self, line, geo_file): 495 | while line != '\n': 496 | for part in self.parts: 497 | if part.test(line): 498 | # print str(type(part))+' found!' 499 | line = part.import_geo(line, geo_file) 500 | self.parts.remove(part) 501 | self.geo_list.append(part) 502 | break 503 | else: # Unknown line, add as text 504 | self.geo_list.append(line) 505 | line = next(geo_file) 506 | return line 507 | 508 | def cut_line_ratio(self): 509 | """ 510 | Returns ratio of xs geometry length to cutline length. 511 | Raises AttributeError if either are empty 512 | """ 513 | # TODO - correct for skew! 514 | if self.cutline.points == []: 515 | raise AttributeError('Cross section does not have a defined cutline') 516 | 517 | if self.sta_elev.points == []: 518 | raise AttributeError('Cross section does not have a geometry') 519 | 520 | length = self.sta_elev.points[-1][0] - self.sta_elev.points[0][0] 521 | # the line below should work, but needs to be tested 522 | #length = length/cos(radians(self.skew.angle)) 523 | 524 | # Add up length of all segments of the cutline 525 | cl_length = 0.0 526 | last_pt = self.cutline.points[0] 527 | for pt in self.cutline.points[1:]: 528 | dist = sqrt((float(pt[0])-float(last_pt[0]))**2 + (float(pt[1])-float(last_pt[1]))**2) 529 | cl_length += dist 530 | last_pt = pt 531 | 532 | return cl_length/length 533 | 534 | def define_channel_n(self): 535 | """ 536 | Checks if the Manning's n is defined at the bank stations 537 | If they are not defined at the bank stations, add Manning's n values at those points that correspond with the nearest-left n-value 538 | 539 | :returns: None 540 | """ 541 | manning_stations = [x[0] for x in self.mannings_n.values] 542 | 543 | for bank in [self.bank_sta.left, self.bank_sta.right]: 544 | # if the bank station is not in the list, grab the Manning's n to the left 545 | if bank not in manning_stations: 546 | leftward_manning_all = [x[1] for x in self.mannings_n.values if x[0] < bank] 547 | leftward_index = len(leftward_manning_all) 548 | leftward_manning_nearest = leftward_manning_all[-1] 549 | manning_insert = (bank, leftward_manning_nearest, 0) 550 | self.mannings_n.values.insert(leftward_index, manning_insert) 551 | 552 | # do not pull the n-value at the right bank because that defines the n-values to the right of the bank 553 | self.channel_n = [x for x in self.mannings_n.values if (x[0] >= self.bank_sta.left and x[0] num_columns: 89 | last_column = num_columns 90 | # Loop through and add every item in the row 91 | for column in range(0, last_column): 92 | temp = ('{:>'+str(width)+'}').format(values[row + column]) 93 | 94 | # Strip leading 0 from 0.12345 - with or without spaces or '-' 95 | if temp[:2] == '0.' and len(temp) > width: 96 | temp = temp[1:width+1] 97 | elif temp[:2] == '0.': 98 | temp = ' ' + temp[1:] 99 | elif len(temp) > width: 100 | temp = temp[:width] 101 | temp = temp.replace(' 0.', ' .') 102 | temp = temp.replace('-0.', ' -.') 103 | 104 | s += temp 105 | # End of row, add newline 106 | s += '\n' 107 | return s 108 | 109 | 110 | def pad_left(guts, pad_number): 111 | """ 112 | pads guts (left) with spaces up to pad_number 113 | :param guts: anything 114 | :param pad_number: int 115 | :return: string 116 | """ 117 | return ('{:>'+str(pad_number)+'}').format(guts) 118 | -------------------------------------------------------------------------------- /parserasgeo/prg.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | """ 3 | rasgeotool - tools for importing, modifying, and exporting HEC-RAS geometry files (myproject.g01 etc) 4 | 5 | This has NOT been extensively tested. 6 | 7 | Mike Bannister 2/24/2016 8 | mike.bannister@respec.com 9 | 10 | """ 11 | import os.path 12 | import warnings 13 | 14 | from .features import ( 15 | Bridge, CrossSection, Culvert, Junction, InlineWeir, LateralWeir, RiverReach 16 | ) 17 | 18 | 19 | # TODO - create geolist object 20 | 21 | 22 | class CrossSectionNotFound(Exception): 23 | pass 24 | 25 | class CulvertNotFound(Exception): 26 | pass 27 | 28 | class ParseRASGeo(object): 29 | def __init__(self, geo_filename, chatty=False, debug=False): 30 | # add test for file existence 31 | self.geo_list = [] 32 | num_xs = 0 33 | num_river = 0 34 | num_bridge = 0 35 | num_culvert = 0 36 | num_lat_weir = 0 37 | num_inline_weir = 0 38 | num_junc = 0 39 | num_unknown = 0 40 | river = None 41 | reach = None 42 | 43 | if debug: 44 | print('Debugging is turned on') 45 | 46 | if geo_filename == '' or geo_filename is None: 47 | raise AttributeError('Filename passed to ParseRASGeo is blank.') 48 | 49 | if not os.path.isfile(geo_filename): 50 | raise AttributeError('File ' + str(geo_filename) + ' does not appear to exist.') 51 | 52 | # TODO - add 'debug' to all objects 53 | with open(geo_filename, 'rt') as geo_file: 54 | for line in geo_file: 55 | if RiverReach.test(line): 56 | rr = RiverReach(debug) 57 | rr.import_geo(line, geo_file) 58 | river, reach = rr.header.river_name, rr.header.reach_name 59 | num_river += 1 60 | self.geo_list.append(rr) 61 | elif CrossSection.test(line): 62 | xs = CrossSection(river, reach, debug) 63 | xs.import_geo(line, geo_file) 64 | num_xs += 1 65 | self.geo_list.append(xs) 66 | elif Culvert.test(line): 67 | culvert = Culvert(river, reach, debug) 68 | culvert.import_geo(line, geo_file) 69 | num_culvert += 1 70 | self.geo_list.append(culvert) 71 | elif Bridge.test(line): 72 | bridge = Bridge(river, reach) 73 | bridge.import_geo(line, geo_file) 74 | num_bridge += 1 75 | self.geo_list.append(bridge) 76 | elif LateralWeir.test(line): 77 | lat_weir = LateralWeir(river, reach) 78 | lat_weir.import_geo(line, geo_file) 79 | num_lat_weir += 1 80 | self.geo_list.append(lat_weir) 81 | elif InlineWeir.test(line): 82 | lat_weir = InlineWeir(river, reach) 83 | lat_weir.import_geo(line, geo_file) 84 | num_inline_weir += 1 85 | self.geo_list.append(lat_weir) 86 | elif Junction.test(line): 87 | junc = Junction() 88 | junc.import_geo(line, geo_file) 89 | num_junc += 1 90 | self.geo_list.append(junc) 91 | else: 92 | # Unknown line encountered. Store it as text. 93 | self.geo_list.append(line) 94 | num_unknown += 1 95 | if chatty: 96 | print(str(num_river)+' rivers/reaches imported') 97 | print(str(num_junc)+' junctions imported') 98 | print(str(num_xs)+' cross sections imported') 99 | print(str(num_bridge)+' bridge imported') 100 | print(str(num_culvert)+' culverts imported') 101 | print(str(num_lat_weir)+' lateral structures imported') 102 | print(str(num_inline_weir)+' lateral structures imported') 103 | print(str(num_unknown) + ' unknown lines imported') 104 | 105 | def write(self, out_geo_filename): 106 | with open(out_geo_filename, 'wt', newline='\r\n') as outfile: 107 | for line in self.geo_list: 108 | outfile.write(str(line)) 109 | 110 | def get_cross_sections( 111 | self, 112 | station_value=None, 113 | station_id=None, 114 | river=None, 115 | reach=None, 116 | interpolated=None, 117 | ): 118 | """Get a list of matching CrossSection 119 | :param station_value: Optional float representing the location of the station or 2-tuple representing a range 120 | :param station_id: Optional string representing the CrossSection station as text 121 | :param river: Optional string of the name of river 122 | :param reach: Optional string of the name of reach 123 | :param interpolated: Optional bool to select based on if the CrossSection was interpolated 124 | :return: List of matching CrossSection instances 125 | """ 126 | # Linear Search is the most efficient strategy at the moment 127 | cross_sections = (item for item in self.geo_list if isinstance(item, CrossSection)) 128 | if station_id is not None: 129 | cross_sections = ( 130 | xs for xs in cross_sections if xs.header.station.id == station_id 131 | ) 132 | if station_value is not None: 133 | if isinstance(station_value, tuple): 134 | assert len(station_value) == 2 135 | if station_value[0] is not None: 136 | cross_sections = ( 137 | xs for xs in cross_sections 138 | if xs.header.station.value >= station_value[0] 139 | ) 140 | if station_value[1] is not None: 141 | cross_sections = ( 142 | xs for xs in cross_sections 143 | if xs.header.station.value <= station_value[1] 144 | ) 145 | else: 146 | cross_sections = ( 147 | xs for xs in cross_sections 148 | if xs.header.station.value == station_value 149 | ) 150 | if river is not None: 151 | cross_sections = (xs for xs in cross_sections if xs.river == river) 152 | if reach is not None: 153 | cross_sections = (xs for xs in cross_sections if xs.reach == reach) 154 | if interpolated is not None: 155 | cross_sections = ( 156 | xs for xs in cross_sections 157 | if xs.header.station.is_interpolated == bool(interpolated) 158 | ) 159 | 160 | return list(cross_sections) 161 | 162 | def return_xs_by_id(self, xs_id, rnd=False, digits=0): 163 | """ 164 | Returns XS with ID xs_id. Rounds XS ids to digits decimal places if (rnd==True) 165 | :param xs_id: id of cross section, assumed to be in ..... format 166 | :param rnd: rounds xs_id to 'digits' if True 167 | :param digits: number of digits to round xs_id to 168 | :return: CrossSection object 169 | """ 170 | warnings.warn( 171 | "return_xs_by_id is deprecated, use get_cross_sections instead", 172 | FutureWarning, 173 | ) 174 | for item in self.geo_list: 175 | if isinstance(item, CrossSection): 176 | if rnd: 177 | if round(item.header.station.value, digits) == round(xs_id, digits): 178 | return item 179 | else: 180 | if item.header.station.value == xs_id: 181 | return item 182 | raise CrossSectionNotFound 183 | 184 | def return_xs(self, xs_id, river, reach, strip=False, rnd=False, digits=0): 185 | """ 186 | returns matching CrossSection if it is in self.geo_list. raises CrossSectionNotFound otherwise 187 | 188 | :param xs_id: cross section id number 189 | :param river: name of river 190 | :param reach: name of reach 191 | :param strip: strips whitespace off river and reach if true 192 | :return: CrossSection object 193 | "raises CrossSectionNotFound: raises error if xs is not in the geometry file 194 | """ 195 | warnings.warn( 196 | "return_xs is deprecated, use get_cross_sections instead", 197 | FutureWarning, 198 | ) 199 | return self._return_node(CrossSection, xs_id, river, reach, strip, rnd, digits) 200 | 201 | def return_culvert(self, culvert_id, river, reach, strip=False, rnd=False, digits=0): 202 | """ 203 | returns matching Culvert if it is in self.geo_list. raises CulvertNotFound otherwise 204 | 205 | :param culvert_id: culvert id number 206 | :param river: name of river 207 | :param reach: name of reach 208 | :param strip: strips whitespace off river and reach if true 209 | :return: Culvert object 210 | :raises CulvertNotFound: raises error if culvert is not in the geometry file 211 | """ 212 | warnings.warn( 213 | "return_culvert is deprecated, use get_culverts instead", 214 | FutureWarning, 215 | ) 216 | return self._return_node(Culvert, culvert_id, river, reach, strip, rnd, digits) 217 | 218 | def get_culverts(self, station=None, river=None, reach=None): 219 | """Returns list of all culverts in geometry 220 | :param station: Optional number specifying the culvert station 221 | :param river: Optional string of the name of river 222 | :param reach: Optional string of the name of reach 223 | :return: List of matching Culvert instances 224 | """ 225 | culverts = (item for item in self.geo_list if isinstance(item, Culvert)) 226 | if station is not None: 227 | culverts = (c for c in culverts if c.header.station == station) 228 | if river is not None: 229 | culverts = (c for c in culverts if c.river == river) 230 | if reach is not None: 231 | culverts = (c for c in culverts if c.reach == reach) 232 | 233 | return list(culverts) 234 | 235 | def extract_all_xs(self): 236 | """ 237 | Returns list of all cross sections in geometry 238 | """ 239 | warnings.warn( 240 | "extract_all_xs is deprecated, use get_cross_sections instead", 241 | FutureWarning, 242 | ) 243 | return self.get_cross_sections() 244 | 245 | def extract_all_culverts(self): 246 | """ 247 | Returns list of all culverts in geometry 248 | """ 249 | warnings.warn( 250 | "extract_all_culverts is deprecated, use get_culverts instead", 251 | FutureWarning, 252 | ) 253 | return self.get_culverts() 254 | 255 | def get_junctions(self): 256 | """ 257 | Returns list of all Junction in geometry 258 | """ 259 | return [item for item in self.geo_list if isinstance(item, Junction)] 260 | 261 | 262 | def get_bridges(self): 263 | """ 264 | Returns list of all Bridge in geometry 265 | """ 266 | return [item for item in self.geo_list if isinstance(item, Bridge)] 267 | 268 | def get_lateral_weirs(self): 269 | """ 270 | Returns list of all LateralWeir in geometry 271 | """ 272 | return [item for item in self.geo_list if isinstance(item, LateralWeir)] 273 | 274 | def get_inline_weirs(self, river=None, reach=None): 275 | """ 276 | Returns list of all InlineWeir instances in geometry 277 | :param river: Optional string of the name of river 278 | :param reach: Optional string of the name of reach 279 | """ 280 | inline_weirs = (item for item in self.geo_list if isinstance(item, InlineWeir)) 281 | if river is not None: 282 | inline_weirs = (iw for iw in inline_weirs if iw.river == river) 283 | if reach is not None: 284 | inline_weirs = (iw for iw in inline_weirs if iw.reach == reach) 285 | 286 | return list(inline_weirs) 287 | 288 | def get_reaches(self, river=None, reach=None): 289 | """ 290 | Returns list of all RiverReach in geometry 291 | :param river: Optional string of the name of river 292 | :param reach: Optional string of the name of reach 293 | """ 294 | reaches = (item for item in self.geo_list if isinstance(item, RiverReach)) 295 | if river is not None: 296 | reaches = (r for r in reaches if r.header.river_name == river) 297 | if reach is not None: 298 | reaches = (r for r in reaches if r.header.reach_name == reach) 299 | 300 | return list(reaches) 301 | 302 | def _return_node(self, node_type, node_id, river, reach, strip=False, rnd=False, digits=0): 303 | """ 304 | This semi-private method is written in a general format. 305 | It is meant to be called by more user friendly methods, such as 306 | return_xs or return_culvert 307 | 308 | returns matching node if it is in self.geo_list. raises NotFound otherwise where 309 | is a type of node 310 | 311 | :param node_type: the type of node to be returned 312 | :param culvert_id: culvert id number 313 | :param river: name of river 314 | :param reach: name of reach 315 | :param strip: strips whitespace off river and reach if true 316 | :return: Culvert object 317 | :raises NodeNotFound: raises error if the node is not found in the geometry file 318 | """ 319 | wanted_river = river 320 | wanted_reach = reach 321 | wanted_node_id = node_id 322 | 323 | if node_type.__name__ is 'CrossSection': 324 | node_name = 'XS' 325 | NodeNotFound = CrossSectionNotFound 326 | if node_type.__name__ is 'Culvert': 327 | node_name = 'culvert' 328 | NodeNotFound = CulvertNotFound 329 | 330 | if strip: 331 | if type(river) is not str and type(river) is not unicode: 332 | raise AttributeError('For {} "river" is not a string, got: {} instead.'.format(node_name, river)) 333 | if type(reach) is not str and type(reach) is not unicode: 334 | raise AttributeError('For {} "reach" is not a string, got: {} instead.'.format(node_name, reach)) 335 | wanted_river = river.strip() 336 | wanted_reach = reach.strip() 337 | if rnd: 338 | wanted_node_id = round(node_id, digits) 339 | 340 | for item in self.geo_list: 341 | if isinstance(item, node_type): 342 | test_river = item.river 343 | test_reach = item.reach 344 | 345 | # TODO: get rid of if-else statements after changing xs_id to station in cross_section.py 346 | if node_type.__name__ is 'CrossSection': 347 | test_node_id = item.header.station.value 348 | else: 349 | test_node_id = item.header.station 350 | 351 | if strip: 352 | test_river = test_river.strip() 353 | test_reach = test_reach.strip() 354 | if rnd: 355 | test_node_id = round(test_node_id, digits) 356 | 357 | # Rounding and strip is done, see if this is the right XS 358 | if test_node_id == wanted_node_id and test_river == wanted_river and test_reach == wanted_reach: 359 | return item 360 | raise NodeNotFound 361 | -------------------------------------------------------------------------------- /parserasgeo/prplan.py: -------------------------------------------------------------------------------- 1 | """ 2 | prplan.py - parse RAS plan file 3 | 4 | Version 0.001 5 | 6 | Parses very basic information from a RAS plan file. 7 | 8 | """ 9 | 10 | class ParseRASPlan(object): 11 | def __init__(self, plan_filename): 12 | self.plan_title = None # Full plan name 13 | self.plan_id = None # Short id 14 | self.geo_file = None # geometry file extension: g01, g02, .. 15 | self.plan_file = None # plan file extension: f01, f02, .. 16 | 17 | with open(plan_filename, 'rt') as plan_file: 18 | for line in plan_file: 19 | fields = line[:-1].split('=') # Strip the newline and split by = 20 | 21 | # lookout for lines missing = 22 | if len(fields) == 1: 23 | continue 24 | var = fields[0] 25 | value = fields[1] 26 | 27 | if var == 'Geom File': 28 | self.geo_file = value 29 | elif var == 'Flow File': 30 | self.plan_file = value 31 | elif var == 'Plan Title': 32 | self.plan_title = value 33 | elif var == 'Short Identifier': 34 | self.plan_id = value 35 | 36 | def __str__(self): 37 | s = 'Plan Title='+self.plan_title+'\n' 38 | s += 'Short Identifier='+self.plan_id+'\n' 39 | s += 'Geom File='+self.geo_file+'\n' 40 | s += 'Flow File='+self.plan_file+'\n' 41 | return s 42 | 43 | def main(): 44 | import sys 45 | 46 | prp = ParseRASPlan(sys.argv[1]) 47 | print(dir(prp)) 48 | print(str(prp)) 49 | 50 | 51 | if __name__ == '__main__': 52 | main() 53 | -------------------------------------------------------------------------------- /parserasgeo/prprj.py: -------------------------------------------------------------------------------- 1 | """ 2 | prprj.py - parse RAS project file 3 | 4 | Version 0.001 5 | 6 | Parses very basic information from a RAS project file. 7 | 8 | """ 9 | 10 | class ParseRASProject(object): 11 | def __init__(self, project_filename): 12 | self.project_title = None # Full project name 13 | self.plan_files = [] # list of plan file extension: p01, p02, .. 14 | self.geom_files = [] # list of geom file extension: g01, g02, .. 15 | self.unsteady_files = [] # list of unsteady file extension: u01, u02, ... 16 | 17 | with open(project_filename, 'rt') as project_file: 18 | for line in project_file: 19 | fields = line[:-1].split('=')# Strip the newline 20 | 21 | # lookout for lines missing = 22 | if len(fields) == 1: 23 | continue 24 | var = fields[0] 25 | value = fields[1] 26 | 27 | if var == 'Proj Title': 28 | self.project_title = value 29 | elif var == 'Plan File': 30 | self.plan_files.append(value) 31 | elif var == 'Geom File': 32 | self.geom_files.append(value) 33 | elif var == 'Unsteady File': 34 | self.unsteady_files.append(value) 35 | 36 | def __str__(self): 37 | s = 'Proj Title='+self.project_title+'\n' 38 | for plan in self.plan_files: 39 | s += 'Plan File='+plan+'\n' 40 | for geom in self.geom_files: 41 | s += 'Geom file='+geom+'\n' 42 | for unsteady in self.unsteady_files: 43 | s += 'Unsteady File='+unsteady+'\n' 44 | return s 45 | 46 | def main(): 47 | import sys 48 | 49 | prp = ParseRASProject(sys.argv[1]) 50 | print(dir(prp)) 51 | print(str(prp)) 52 | 53 | 54 | if __name__ == '__main__': 55 | main() 56 | -------------------------------------------------------------------------------- /parserasgeo/uflow.py: -------------------------------------------------------------------------------- 1 | from .features.boundary import Boundary 2 | 3 | 4 | class UnsteadyFlow(object): 5 | """ 6 | Imports RAS unsteady flow data in filename, i.e. project_name.u?? 7 | """ 8 | 9 | def __init__(self, filename): 10 | self.filename = filename 11 | self.uflow_list = [] 12 | 13 | with open(filename, "rt") as infile: 14 | line = infile.readline() 15 | while line: 16 | if Boundary.test(line): 17 | boundary = Boundary() 18 | line = boundary.import_geo(line, infile) 19 | self.uflow_list.append(boundary) 20 | else: # Unknown line, add as text 21 | self.uflow_list.append(line) 22 | line = infile.readline() 23 | 24 | def export(self, outfilename): 25 | """ 26 | Writes unsteady flow data to outfilename. 27 | """ 28 | with open(outfilename, "wt", newline="\r\n") as outfile: 29 | for line in self.uflow_list: 30 | outfile.write(str(line)) 31 | 32 | def get_boundaries( 33 | self, river=None, reach=None, station_value=None, hydrograph_type=None 34 | ): 35 | """Get Boundary instances from unsteady flow 36 | :param river: Optional string of the name of river 37 | :param reach: Optional string of the name of reach 38 | :param station_value: Optional float representing the location of the station or 2-tuple representing a range 39 | :param hydrograph_type: Optional string matching the boundary hydrograph type 40 | :return: List of matching Boundary instances 41 | """ 42 | boundaries = (item for item in self.uflow_list if isinstance(item, Boundary)) 43 | if river is not None: 44 | boundaries = (bnd for bnd in boundaries if bnd.header.river_name == river) 45 | if reach is not None: 46 | boundaries = (bnd for bnd in boundaries if bnd.header.reach_name == reach) 47 | if station_value is not None: 48 | if isinstance(station_value, tuple): 49 | assert len(station_value) == 2 50 | if station_value[0] is not None: 51 | boundaries = ( 52 | bnd 53 | for bnd in boundaries 54 | if bnd.header.station.value is not None 55 | and bnd.header.station.value >= station_value[0] 56 | ) 57 | if station_value[1] is not None: 58 | boundaries = ( 59 | bnd 60 | for bnd in boundaries 61 | if bnd.header.station.value is not None 62 | and bnd.header.station.value <= station_value[1] 63 | ) 64 | else: 65 | boundaries = ( 66 | bnd 67 | for bnd in boundaries 68 | if bnd.header.station.value == station_value 69 | ) 70 | if hydrograph_type is not None: 71 | boundaries = ( 72 | bnd for bnd in boundaries if bnd.hydrograph.type == hydrograph_type 73 | ) 74 | 75 | return list(boundaries) 76 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='parserasgeo', version='0.12e', 4 | description='Read and write HEC-RAS geometry files', 5 | author='Mike Bannister', author_email='mikebannis@gmail.com', 6 | packages=['parserasgeo', 'parserasgeo.features']) 7 | -------------------------------------------------------------------------------- /test/geo_test.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | import glob 3 | import sys 4 | import os 5 | import filecmp 6 | import subprocess 7 | 8 | sys.path.insert(0, os.path.abspath('..')) 9 | import parserasgeo as prg 10 | 11 | def main(): 12 | print '-' * 50 13 | print 'Using', prg.__path__, 'for parserasgeo' 14 | print '-' * 50, '\n' 15 | 16 | outfile = 'test.out' 17 | test_files = glob.glob('../geos/*.g??') 18 | #print len(test_files) 19 | #for x in test_files: 20 | #print type(x), x 21 | # print x 22 | 23 | for test_file in test_files: 24 | print '*' * 30, 25 | print 'Processing ', test_file 26 | geo = prg.ParseRASGeo(test_file) 27 | xss = geo.extract_xs() 28 | #xss[0].alter_channel_n() 29 | geo.write(outfile) 30 | 31 | if filecmp.cmp(test_file, outfile, shallow=False): 32 | print 'Geometry file', test_file, 'exported correctly.' 33 | else: 34 | print 'WARNING: file', test_file, 'did not export properly' 35 | subprocess.Popen(["diff", test_file, outfile]) 36 | sys.exit('WARNING: file' + test_file + 'did not export properly') 37 | 38 | if __name__ == '__main__': 39 | main() 40 | -------------------------------------------------------------------------------- /tools/bank_sta_balance.py: -------------------------------------------------------------------------------- 1 | import parserasgeo as prg 2 | 3 | def main(): 4 | infile = r"Z:\UDFCD PLANNING\FHAD Review\Niver Round 3\Orig\20161012 FHAD Submittal\HEC-RAS\NiverCreek_FHAD_Sept.g01" 5 | infile = r"Z:\UDFCD PLANNING\Grange Hall Creek\02 HYDRAULICS\HEC-RAS\GHC_FHAD.g01" 6 | #infile = r"Z:\UDFCD PLANNING\FHAD Review\Sloans Lake Round 1\Orig\20161013 - Sloan'sLake_FHAD_HEC-RAS-20161007\HEC-RAS\SloansFHAD2017.g01" 7 | infile = r"Z:\UDFCD PLANNING\FHAD Review\Niver Round 3\Orig\20161012 FHAD Submittal\HEC-RAS\NiverCreek_FHAD_Sept.g03" 8 | #infile = r"Z:\UDFCD PLANNING\FHAD Review\SPR - 6th to 58th\Orig\20160907 Merrick_SPR 6th-58th_08-26-2016\SPR 6th-58th_Existing_HECRAS\SPR_Downstream.g04" 9 | infile = r"Z:\UDFCD PLANNING\Second Creek\12 FHAD\01_RAS\HEC-RAS\Submittal 2 - working\SCFHAD.g02" 10 | 11 | tolerance = 1 12 | 13 | geo = prg.ParseRASGeo(infile) 14 | cross_sections = geo.extract_xs() 15 | 16 | for xs in cross_sections: 17 | left_elv = xs.sta_elev.elevation(xs.bank_sta.left) 18 | right_elv = xs.sta_elev.elevation(xs.bank_sta.right) 19 | diff = left_elv - right_elv 20 | if abs(diff) >= tolerance: 21 | print xs.river+','+xs.reach+','+str(xs.header.xs_id)+','+str(diff) 22 | 23 | if __name__ == '__main__': 24 | main() 25 | 26 | -------------------------------------------------------------------------------- /tools/levee.py: -------------------------------------------------------------------------------- 1 | import parserasgeo as prg 2 | 3 | def main(): 4 | infile = r"Z:\UDFCD PLANNING\FHAD Review\Niver Round 3\Orig\20161012 FHAD Submittal\HEC-RAS\NiverCreek_FHAD_Sept.g01" 5 | infile = r"Z:\UDFCD PLANNING\Grange Hall Creek\02 HYDRAULICS\HEC-RAS\GHC_FHAD.g01" 6 | #infile = r"Z:\UDFCD PLANNING\FHAD Review\Sloans Lake Round 1\Orig\20161013 - Sloan'sLake_FHAD_HEC-RAS-20161007\HEC-RAS\SloansFHAD2017.g01" 7 | infile = r"Z:\UDFCD PLANNING\FHAD Review\Niver Round 3\Orig\20161012 FHAD Submittal\HEC-RAS\NiverCreek_FHAD_Sept.g01" 8 | #infile = r"Z:\UDFCD PLANNING\FHAD Review\SPR - 6th to 58th\Orig\20160907 Merrick_SPR 6th-58th_08-26-2016\SPR 6th-58th_Existing_HECRAS\SPR_Downstream.g04" 9 | infile = r"Z:\UDFCD PLANNING\FHAD Review\South Platte\Orig\20160629 5-27-16 SPR FHAD submittal\5-27-16 submittal\HEC-RAS\SouthPlatteFHAD.g08" 10 | infile = r"Z:\UDFCD PLANNING\Second Creek\12 FHAD\01_RAS\HEC-RAS\Submittal 2 - working\SCFHAD.g02" 11 | 12 | 13 | 14 | geo = prg.ParseRASGeo(infile) 15 | cross_sections = geo.extract_xs() 16 | 17 | print 'Levees are at the following XS:' 18 | for xs in cross_sections: 19 | if xs.levee.value is not None: 20 | print xs.river+','+xs.reach + ',' + str(xs.header.xs_id) 21 | 22 | if __name__ == '__main__': 23 | main() 24 | -------------------------------------------------------------------------------- /tools/skew_check.py: -------------------------------------------------------------------------------- 1 | import parserasgeo as prg 2 | 3 | def main(): 4 | infile = r"Z:\UDFCD PLANNING\FHAD Review\Niver Round 3\Orig\20161012 FHAD Submittal\HEC-RAS\NiverCreek_FHAD_Sept.g01" 5 | #infile = r"Z:\UDFCD PLANNING\Grange Hall Creek\02 HYDRAULICS\HEC-RAS\GHC_FHAD.g01" 6 | infile = r"Z:\UDFCD PLANNING\FHAD Review\Sloans Lake Round 1\Orig\20161013 - Sloan'sLake_FHAD_HEC-RAS-20161007\HEC-RAS\SloansFHAD2017.g01" 7 | infile = r"Z:\UDFCD PLANNING\FHAD Review\Clear Creek Round 1\Orig\20161013 Review Submittal\ClearCreek_ModelReviewSubmittal\Hydraulics\ClearCreek_FHAD2016.g07" 8 | infile = r"Z:\UDFCD PLANNING\Second Creek\12 FHAD\01_RAS\HEC-RAS\Submittal 2 - working\SCFHAD.g02" 9 | 10 | geo = prg.ParseRASGeo(infile) 11 | cross_sections = geo.extract_xs() 12 | 13 | print 'There are skews at the flowing cross sections - river, reach, xs_id, skew_angle' 14 | 15 | skew = False 16 | for xs in cross_sections: 17 | if xs.skew.angle is not None: 18 | print xs.river+','+xs.reach+','+str(xs.header.xs_id)+','+str(xs.skew.angle) 19 | skew = True 20 | 21 | if not skew: 22 | print 'No cross sections are skewed' 23 | 24 | if __name__ == '__main__': 25 | main() 26 | --------------------------------------------------------------------------------