Skip to content

Commit aa5818f

Browse files
committed
Merge branch 'master' of https://github.com/rustychris/stompy
2 parents 94693d2 + ec176bb commit aa5818f

File tree

7 files changed

+117
-17
lines changed

7 files changed

+117
-17
lines changed

stompy/grid/unstructured_grid.py

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import sys,os,types
99
import logging
10+
import pathlib # python 3 only, but python 2 is not worth supporting anymore.
1011

1112
try:
1213
from osgeo import ogr,osr
@@ -76,6 +77,14 @@ def __init__(self,*a,meshes=[],**k):
7677
super().__init__(*a,**k)
7778
self.meshes=meshes
7879

80+
class DuplicateMeshElement(GridException):
81+
def __init__(self, dest_mesh, *a, dest_cell=-1, src_mesh=None, src_cell=-1, **k):
82+
super().__init__(*a,**k)
83+
self.dest_mesh = dest_mesh
84+
self.src_mesh = src_mesh
85+
self.dest_cell = dest_cell
86+
self.src_cell=src_cell
87+
7988
def request_square(ax,max_bounds=None):
8089
"""
8190
Attempt to set a square aspect ratio on matplotlib axes ax
@@ -521,6 +530,9 @@ def modify_max_sides(self,max_sides):
521530
if not np.all( self.cells['nodes'][:,max_sides:] == self.UNDEFINED ):
522531
raise GridException("Some cells cannot fit in requested max_sides")
523532

533+
# DFM hack, sometimes we have other cell:edge variables.
534+
cell_side_fields=['edges','nodes','NetElemLink']
535+
524536
old_max_sides=self.max_sides
525537
self.max_sides=max_sides
526538

@@ -537,7 +549,7 @@ def modify_max_sides(self,max_sides):
537549
else:
538550
shape=None
539551

540-
if name in ['edges','nodes']:
552+
if name in cell_side_fields: # 'edges','nodes','NetElemLink'
541553
new_cell_dtype.append( (name,vtype,self.max_sides) )
542554
else:
543555
new_cell_dtype.append( typeinfo ) # just copy
@@ -549,7 +561,7 @@ def modify_max_sides(self,max_sides):
549561

550562
for typeinfo in old_dtype:
551563
name=typeinfo[0]
552-
if name in ['edges','nodes']:
564+
if name in cell_side_fields:
553565
if old_max_sides > self.max_sides:
554566
new_cells[name][:,:] = self.cells[name][:,:self.max_sides]
555567
else:
@@ -721,6 +733,9 @@ def from_ugrid(nc,mesh_name=None,skip_edges=False,fields='auto',
721733
face_node_connectivity, edge_node_connectivity, and optionally face_edge
722734
connectivity, edge_face_connectivity, etc.)
723735
"""
736+
if isinstance(nc,pathlib.Path):
737+
nc=str(nc)
738+
724739
if isinstance(nc,six.string_types):
725740
filename=nc
726741
if dialect=='fvcom':
@@ -2906,7 +2921,13 @@ def bad_fields(Adata,Bdata): # field froms B which get dropped
29062921
for i,edge in enumerate(kwargs['edges']):
29072922
kwargs['edges'][i]=edge_map[edge]
29082923

2909-
cell_map[n]=self.add_cell(**kwargs)
2924+
try:
2925+
cell_map[n]=self.add_cell(**kwargs)
2926+
except DuplicateMeshElement as exc:
2927+
exc.src_mesh = ugB
2928+
exc.src_cell = n
2929+
exc.dest_mesh = self
2930+
raise
29102931

29112932
return node_map,edge_map,cell_map
29122933

@@ -4911,13 +4932,17 @@ def add_cell(self,**kwargs):
49114932
if ( (n1==self.edges['nodes'][j,0]) and
49124933
(n2==self.edges['nodes'][j,1]) ):
49134934
# this cell is on the 'left' side of the edge
4914-
assert self.edges['cells'][j,0]<0
4935+
# assert self.edges['cells'][j,0]<0
4936+
if self.edges['cells'][j,0]>=0:
4937+
raise DuplicateMeshElement(dest_mesh=self, dest_cell=self.edges['cells'][j,0])
49154938
# TODO: probably this ought to be using modify_edge
49164939
self.edges['cells'][j,0]=i
49174940
elif ( (n1==self.edges['nodes'][j,1]) and
49184941
(n2==self.edges['nodes'][j,0]) ):
49194942
# this cell is on the 'right' side of the edge
4920-
assert self.edges['cells'][j,1]<0
4943+
# assert self.edges['cells'][j,1]<0
4944+
if self.edges['cells'][j,1]>=0:
4945+
raise DuplicateMeshElement(dest_mesh=self, dest_cell=self.edges['cells'][j,1])
49214946
# TODO: probably this ought to be using modify_edge
49224947
self.edges['cells'][j,1]=i
49234948
else:
@@ -5925,6 +5950,32 @@ def smooth_matrix(self,f=0.5,K='scaled',dx='grid',V='grid',A='grid',dt=None):
59255950

59265951
return D.tocsr()
59275952

5953+
def fill_by_smoothing_func(self,cells,count=7):
5954+
"""
5955+
Return a function that takes a subset of cell values and returns a dense, interpolated
5956+
field across all the cells of a grid.
5957+
cells: cell indexes where data is known
5958+
count: iterations of filling
5959+
"""
5960+
M = self.smooth_matrix()
5961+
def fill_func(subset,cells=cells,M=M,count=count):
5962+
v=np.full(self.Ncells(), 0.0) # values
5963+
w=np.full(self.Ncells(), 0.0) # weights
5964+
valid=np.isfinite(subset)
5965+
v[cells[valid]] = subset[valid]
5966+
w[cells[valid]] = 1.0
5967+
5968+
for _ in range(count):
5969+
v=M.dot(v)
5970+
w=M.dot(w)
5971+
5972+
thresh=0.01
5973+
result = v/w.clip(thresh)
5974+
result[ w<thresh ]=np.nan
5975+
return result
5976+
return fill_func
5977+
5978+
59285979
def edge_clip_mask(self,xxyy,ends=False):
59295980
"""
59305981
return a bitmask over edges falling in the boundiny box.
@@ -6014,7 +6065,7 @@ def plot_cells(self,ax=None,mask=None,values=None,clip=None,centers=False,labele
60146065
values = np.asanyarray(values)
60156066

60166067
if ragged_edges is None:
6017-
ragged_edges='edgecolor' in kwargs
6068+
ragged_edges='edgecolor' in kwargs or 'ec' in kwargs
60186069

60196070
if mask is None:
60206071
mask=~self.cells['deleted']

stompy/model/delft/dflow_model.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ def copy_files_for_restart(self):
185185

186186
for fn in os.listdir(self.restart_from.run_dir):
187187
_,suffix = os.path.splitext(fn)
188-
do_copy = ( (suffix in ['.tim','.pli','.pliz','.ext','.xyz','.ini','.xyn'])
188+
do_copy = ( (suffix in ['.tim','.pli','.pliz','.ext','.xyz','.ini','.xyn','.pol'])
189189
or (fn in flowfm_ext)
190190
# sources.sub is explicitly handled in write_config
191191
or (fn in flowfm_mdu and fn !='sources.sub')
@@ -687,6 +687,11 @@ def partition(self,partition_grid=None):
687687
# slashes in the path to the mdu (ver 1.4.4)
688688
# since run_dflowfm uses run_dir as the working directory
689689
# here we strip to the basename
690+
# Note that it may be tempting to add genpolygon=1 here, but
691+
# in tests with 2023.01 that led to corrupt partitioning
692+
# with ghost cells modified on some partitions but not
693+
# other partitions, including creating egregious concave
694+
# cells.
690695
cmd=["--partition:ndomains=%d:icgsolver=6"%self.num_procs,
691696
os.path.basename(self.mdu.filename)]
692697
print(f"About to call {cmd}")
@@ -707,7 +712,13 @@ def partition(self,partition_grid=None):
707712

708713
# not a cross platform solution!
709714
gen_parallel=os.path.join(self.dfm_bin_dir,"generate_parallel_mdu.sh")
710-
cmd=[gen_parallel,os.path.basename(self.mdu.filename),"%d"%self.num_procs,'6']
715+
716+
cmd=[gen_parallel,os.path.basename(self.mdu.filename),"%d"%self.num_procs]
717+
part_file =self.mdu['geometry','PartitioneFile']
718+
if part_file not in [None,""]:
719+
self.log.info("Passing partition file to generate_parallel_mdu.sh")
720+
cmd.append(part_file)
721+
cmd.append('6') # icgsolver
711722
print(f"About to call {cmd}")
712723
return utils.call_with_path(cmd,self.run_dir)
713724

@@ -1508,7 +1519,7 @@ def map_outputs(self):
15081519

15091520
_mu=None
15101521
def map_dataset(self,force_multi=False,grid=None,chain=False,xr_kwargs={},
1511-
clone_from=None):
1522+
clone_from=None,refresh=False):
15121523
"""
15131524
Return map dataset. For MPI runs, this will emulate a single, merged
15141525
global dataset via multi_ugrid. For serial runs it directly opens
@@ -1527,10 +1538,17 @@ def map_dataset(self,force_multi=False,grid=None,chain=False,xr_kwargs={},
15271538
which should speedup the loading process. Currently only handled when
15281539
chain is False.
15291540
"""
1541+
default_xr_kwargs = dict(decode_timedelta=True)
1542+
xr_kwargs = default_xr_kwargs | xr_kwargs
1543+
15301544
if not chain:
15311545
if self.num_procs<=1 and not force_multi:
15321546
# xarray caches this.
15331547
ds=xr.open_dataset(self.map_outputs()[0],**xr_kwargs)
1548+
if refresh:
1549+
ds.close()
1550+
ds=xr.open_dataset(self.map_outputs()[0],**xr_kwargs)
1551+
15341552
# put the grid in as an attribute so that the non-multiugrid
15351553
# and multiugrid return values can be used the same way.
15361554
ds.attrs['grid'] = ugrid.UnstructuredGrid.read_ugrid(ds)
@@ -1541,6 +1559,8 @@ def map_dataset(self,force_multi=False,grid=None,chain=False,xr_kwargs={},
15411559
if self._mu is None:
15421560
self._mu=multi_ugrid.MultiUgrid(self.map_outputs(),grid=grid,xr_kwargs=xr_kwargs,
15431561
clone_from=clone_from)
1562+
if refresh:
1563+
self._mu.reload() # not sufficient if the grid changes, though.
15441564
return self._mu
15451565
else:
15461566
# as with his_dataset(), caching is not aware of options like chain.
@@ -1796,6 +1816,13 @@ def set_restart_from(self,model,deep=True,mdu_suffix=""):
17961816
else:
17971817
assert self.run_dir == model.run_dir
17981818

1819+
part_files = glob.glob(os.path.join(model.run_dir,"*_part.pol"))
1820+
if len(part_files)>0:
1821+
fn=part_files[0]
1822+
# Should get copied in copy_for_restart
1823+
self.log.info("Automatically adding {os.path.basename(fn)} as a partition file")
1824+
self.mdu['geometry','PartitionFile']=os.path.basename(fn)
1825+
17991826
self.set_restart_file()
18001827
def set_restart_file(self):
18011828
"""

stompy/model/delft/io.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ def read_pli(fn,one_per_line=True):
403403
"""
404404
features=[]
405405

406-
with open(fn,'rt') as fp:
406+
with open(fn,'rt',encoding='latin-1') as fp:
407407
if not one_per_line:
408408
toker=inp_tok(fp)
409409
token=lambda: six.next(toker)
@@ -1372,7 +1372,7 @@ def format_section(self,s):
13721372
def get_value(self,sec_key):
13731373
"""
13741374
return the string-valued settings for a given key.
1375-
if they key is not found, returns None.
1375+
if the key is not found, returns None.
13761376
If the key is present but with no value, returns the empty string
13771377
"""
13781378
section=self.format_section(sec_key[0])

stompy/model/delft/waq_scenario.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9274,9 +9274,19 @@ def bloom_path(self):
92749274
def original_bloominp_path(self):
92759275
# this gets copied into the model run directory
92769276
return os.path.join(self.share_path,'bloominp.d09')
9277+
9278+
_proc_path=None
92779279
@property
92789280
def proc_path(self):
9281+
if self._proc_path is not None:
9282+
return self._proc_path
92799283
return os.path.join(self.share_path,'proc_def')
9284+
9285+
@proc_path.setter
9286+
def proc_path(self,value):
9287+
if value.endswith('.def') or value.endswith('.dat'):
9288+
raise ValueError("proc_path should not include the extension")
9289+
self._proc_path=value
92809290

92819291
# plot process diagrams
92829292
def cmd_plot_process(self,run_name='dwaq'):

stompy/plot/plot_utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1503,6 +1503,14 @@ def plot_with_isolated(x,y,*a,**k):
15031503
k.pop('label','')
15041504
ax.plot(x[isolated],y[isolated],marker=marker, *a,**k)
15051505

1506+
def zoom(fac, ax=None):
1507+
# fac:0 no zoom
1508+
# fac:1 double width, zooming out
1509+
# fac:-1 collapse to empty, zoom in infinitely.
1510+
ax = ax or plt.gca()
1511+
xxyy = utils.expand_xxyy(ax.axis(),fac)
1512+
ax.axis(xxyy)
1513+
15061514
def add_scroller(ax):
15071515
def on_scroll(event):
15081516
if event.inaxes != ax:
@@ -1520,3 +1528,4 @@ def on_scroll(event):
15201528
for cb in list(fig.canvas.callbacks.callbacks['scroll_event'])[2:]:
15211529
fig.canvas.mpl_disconnect(cb)
15221530
fig.canvas.mpl_connect('scroll_event', on_scroll)
1531+

stompy/spatial/join_features.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -510,8 +510,10 @@ def lines_to_polygons(new_features,close_arc=False,single_feature=True,force_ori
510510

511511
log.info("Building index")
512512
# Because the index only hands back the poly, not an index.
513+
# Shapely no longer likes user attributes, use a dict instead.
514+
poly_to_join_id = {}
513515
for i,p in enumerate(simple_polys):
514-
p.join_id=i
516+
poly_to_join_id[p]=i # p.join_id=i
515517

516518
index=STRtree(simple_polys)
517519
log.info("done building index")
@@ -535,7 +537,8 @@ def lines_to_polygons(new_features,close_arc=False,single_feature=True,force_ori
535537
prep_ext_poly = prepare_geometry(ext_poly)
536538

537539
hits=index.query(ext_poly)
538-
hit_indexes=[p.join_id for p in hits]
540+
hit_indexes=[poly_to_join_id(p) # p.join_id
541+
for p in hits]
539542
# this keeps us comparing large->small, needed to avoid
540543
# confusing islands in lake with lakes
541544
hit_indexes.sort()

stompy/utils.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -307,16 +307,16 @@ def within_2d(vecs,xxyy):
307307

308308

309309
def expand_xyxy(xyxy,factor):
310-
dx=xyxy[2] - xyxy[0]
311-
dy=xyxy[3] - xyxy[1]
310+
dx=(xyxy[2] - xyxy[0])/2.0
311+
dy=(xyxy[3] - xyxy[1])/2.0
312312
return [ xyxy[0] - dx*factor,
313313
xyxy[1] - dy*factor,
314314
xyxy[2] + dx*factor,
315315
xyxy[3] + dy*factor]
316316

317317
def expand_xxyy(xxyy,factor):
318-
dx=xxyy[1] - xxyy[0]
319-
dy=xxyy[3] - xxyy[2]
318+
dx=(xxyy[1] - xxyy[0])/2.0
319+
dy=(xxyy[3] - xxyy[2])/2.0
320320
return [ xxyy[0] - dx*factor,
321321
xxyy[1] + dx*factor,
322322
xxyy[2] - dy*factor,

0 commit comments

Comments
 (0)