示例#1
0
    def summary_figure(self, num=3):
        fig = plt.figure(num)
        fig.clf()

        ax_map = fig.add_subplot(1, 2, 1)
        ax_A = fig.add_subplot(1, 2, 2)

        #
        pnts = self.section_ls
        L = 5 * self.section_s.max()
        clip = [
            pnts[:, 0].min() - L, pnts[:, 0].max() + L, pnts[:, 1].min() - L,
            pnts[:, 1].max()
        ]

        plot_wkb.plot_wkb(hyp.g_poly,
                          fc='0.8',
                          ec='none',
                          ax=ax_map,
                          zorder=-4)
        # hyp.g.plot_cells(mask=self.region_cells,ax=ax_map,color='orange')
        ax_map.plot(self.section_ls[:, 0], self.section_ls[:, 1], 'b-')

        # Show the location of the reference station
        ax_map.plot([
            self.hyp.his.station_x_coordinate.isel(
                stations=self.section_station)
        ], [
            self.hyp.his.station_y_coordinate.isel(
                stations=self.section_station)
        ], 'go')

        ax_map.text(self.section_ls[0, 0], self.section_ls[0, 1],
                    self.section_name)
        ax_map.axis('equal')
        ax_map.axis(clip)

        order = np.argsort(self.eta)
        ax_A.plot(self.eta[order], self.areas[order], label="model")

        dem_xas = np.array([self.dem_eta_to_xA(eta) for eta in self.eta])
        ax_A.plot(self.eta[order],
                  dem_xas[order],
                  label='DEM',
                  color='k',
                  lw=0.6)

        ax_A.set_ylabel('area')
        ax_A.set_xlabel('eta')

        ax_A.legend()

        return fig
示例#2
0
def make_summary_map(model,mr):
    poly=model.grid.boundary_polygon()
    his_file=model.his_output()
    fig=plt.figure(10)
    fig.set_size_inches((6,9),forward=True)
    fig.clf()
    ax=fig.add_axes([0,0,1,1])
    ax.xaxis.set_visible(0)
    ax.yaxis.set_visible(0)

    plot_wkb.plot_wkb(poly,ax=ax,fc='0.75',ec='0.75',lw=0.8)
    ax.axis('equal')

    for station in stations:
        args = station[1]
        args['ID'] = station[0]
        for key in defaults:
            args.setdefault(key, defaults[key])  # append default parameters if missing

        p = MyPlotter(mr, args)
        if not p.valid: continue

        if args['ID']=='TSL' and args['var']=='flow' and 'grid100_13' in his_file:
            print("Monkey patch sign on TSL flow")
            p.pred *= -1
        metrics=p.calculate_metrics()
        if metrics['ratio']<-1000: continue # probably no data

        xy=p.xy
        if xy.ndim==2:
            xy=xy.mean(axis=0)
        # a little tricky with flow+stage
        if args['var']=='flow':
            kw=dict(va='top')
        else:
            kw=dict(va='bottom')
        kw['clip_on']=True
        kw['fontsize']=8
        ax.text(xy[0],xy[1],"%s %s: %.2f/%.1fmin"%(args['ID'],args['var'],
                                                   metrics['ratio'],
                                                   24*60*metrics['lag']) ,
                **kw)

    ax.axis( (605686., 639321., 4211471, 4267529))
    plot_utils.reduce_text_overlap(ax)
    fig.savefig(os.path.join(plot_dir,'amp_phase_map.png'),dpi=120)
    return fig
示例#3
0
def make_phase_figure(model,mr):
    poly=model.grid.boundary_polygon()
    
    period_s=12.4206*3600
    results=calc_phases(model,period_s=period_s)
    
    fig=plt.figure(11)
    fig.clf()
    fig.set_size_inches((10,10),forward=True)
    fig,axs=plt.subplots(1,2,sharex=True,sharey=True,num=11)
    axs[0].set_position([0,0,0.5,1.0])
    axs[1].set_position([0.5,0,0.5,1.0])

    caxs=[ fig.add_axes([0.05,0.5,0.03,0.45]),
           fig.add_axes([0.55,0.5,0.03,0.45]) ]

    ccolls=[]
    for proc in range(model.num_procs):
        proc_data=results[proc]
        for ax,cax,ph,name in zip(axs,caxs,
                                  [proc_data['eta_phases']-results['ref_eta_phase'],
                                   proc_data['uf_phases']-results['ref_uf_phase']],
                                  ['eta','u']):
            gsub=proc_data['grid']

            phase_relative=(ph + 180)%360 - 180
            phase_minutes=-phase_relative/360. * (period_s/60.)

            period_minutes=period_s/60.
            ccoll=gsub.plot_cells(values=phase_minutes%period_minutes,
                                  clim=[0,period_minutes],ax=ax)
            if 0: # contours are messy
                ecoll=gsub.scalar_contour(phase_minutes,V=np.arange(-30,500,15))
                ax.add_collection(ecoll)
            ccoll.set_cmap('hsv')

            if proc==0:
                ax.xaxis.set_visible(0)
                ax.yaxis.set_visible(0)
                plot_wkb.plot_wkb(poly,ax=ax,fc='none',ec='0.5',lw=0.8,zorder=-10)
                plt.colorbar(ccoll,orientation='vertical',cax=cax,
                             label='%s phase (minutes)'%name)

    ax.axis('equal')
    fig.savefig(os.path.join(plot_dir,"phases.png"))
示例#4
0
def setup_figure(num=1):
    plt.figure(num).clf()
    fig, ax = plt.subplots(num=num)
    fig.set_size_inches([6.4, 4.8], forward=True)
    ax.set_position([0, 0, 1, 1])
    ax.xaxis.set_visible(0)
    ax.yaxis.set_visible(0)
    ax.set_facecolor('0.7')

    lcoll = collections.LineCollection(lin_segs, lw=0.8, color='k')
    ax.add_collection(lcoll)
    plot_wkb.plot_wkb(domain_poly, facecolor='none', lw=0.8, ax=ax)
    plot_wkb.plot_wkb(domain_poly,
                      edgecolor='none',
                      facecolor='w',
                      lw=0.8,
                      ax=ax,
                      zorder=-.5)

    cax = fig.add_axes([0.08, 0.2, 0.3, 0.03])

    return fig, ax, cax
示例#5
0
noise[:winsize] = 0  # so the ends still match up
noise[-winsize:] = 0
noise_lp = filters.lowpass_fir(noise, winsize)
noise_lp *= noise_w / np.sqrt(np.mean(noise_lp**2))

# domain boundary including the random noise
ring_noise = ring + noise_lp[:, None] * ring_norm

# Create the curvilinear section
thalweg = centerline[50:110]

plt.figure(1).clf()
plt.plot(centerline[:, 0], centerline[:, 1], 'k-', zorder=2)
plt.axis('equal')

plot_wkb.plot_wkb(channel, zorder=-2)
plt.plot(ring_noise[:, 0], ring_noise[:, 1], 'm-')

plt.plot(thalweg[:, 0], thalweg[:, 1], 'r--', lw=3)

##

# First, just the curvilinear section:
g = unstructured_grid.UnstructuredGrid(max_sides=4)

thalweg_resamp = linestring_utils.resample_linearring(thalweg,
                                                      50,
                                                      closed_ring=0)

g.add_rectilinear_on_line(
    thalweg_resamp,
for j in cdt.valid_edge_iter():
    c1, c2 = cdt.edges['cells'][j]
    if c1 < 0 or c2 < 0:
        continue
    if not (cell_sub[c1] and cell_sub[c2]):
        continue
    for c in [c1, c2]:
        if c not in cell_to_node:
            cell_to_node[c] = ma.add_node(x=cc[c])
    ma.add_edge(nodes=[cell_to_node[c1], cell_to_node[c2]])

##

# That gets to a reasonable MA - but still a lot of work to do...

##

plt.figure(21).clf()
fig, ax = plt.subplots(num=21)

x, y = dem2.xy()
X, Y = np.meshgrid(x, y)

plot_wkb.plot_wkb(poly, ax=ax, fc='none')
ma.plot_edges(ax=ax, color='m', lw=2)

ax.axis('equal')
plt.pause(0.01)

##
示例#7
0
            if l1 > l2:
                l1, l2 = l2, l1
            zs = xyz[l1:l2 + 1, 2]

            if closed:
                # allow for possibility that the levee is a closed ring
                # and this grid edge actually straddles the end,
                dist_fwd = dists[l2] - dists[l1]
                dist_rev = dists[-1] - dist_fwd
                if dist_rev < dist_fwd:
                    print("wraparound")
                    zs = np.concatenate([xyz[l2:, 2], xyz[:l1, 2]])

            z = zs.min()
            levee_de[j] = z
    return levee_de


##
if 0:
    zoom = (585813.9075730171, 586152.9743682485, 4146296.412810114,
            4146567.6036336995)

    plt.figure(1).clf()
    ecoll = g.plot_edges(values=levee_de, mask=np.isfinite(levee_de), lw=2.5)

    plot_wkb.plot_wkb(poly, fc="0.8", zorder=-3)

    plt.axis('equal')
    plt.axis(zoom)
示例#8
0
from stompy.plot import plot_wkb

buffers = [geometry.Point(x).buffer(L, 8) for x in cutoff_xyz[:, :2]]
total_poly = cascaded_union(buffers)

assert total_poly.type == 'Polygon'

total_poly = total_poly.simplify(Lres / 2.)

##

plt.figure(3).clf()
fig, ax = plt.subplots(1, 1, num=3)

scat = ax.scatter(cutoff_xyz[:, 0], cutoff_xyz[:, 1], 20, cutoff_xyz[:, 2])
plot_wkb.plot_wkb(total_poly, zorder=-2)

ax.axis('equal')

##
from stompy.spatial import field
from stompy.grid import paver
six.moves.reload_module(paver)

p = paver.Paving(geom=total_poly, density=field.ConstantField(Lres))
p.verbose = 1
p.pave_all()

##

# But better to just use a subset of the existing grid.
示例#9
0
seed_point = no_mans_land.representative_point()  # [500870., 4170787.]
seed_point = np.array(seed_point)
# 5000 ought to be plenty of nodes to get around this loop
nodes = g_merge.enclosing_nodestring(seed_point, 5000)

##
xy_shore = g_merge.nodes['x'][nodes]

if 1:
    plt.figure(2).clf()
    fig, ax = plt.subplots(num=2)

    g_merge.plot_edges(ax=ax, color='k', lw=0.8)

    plot_wkb.plot_wkb(no_mans_land, facecolor='r', alpha=0.3)
    ax.plot(xy_shore[:, 0], xy_shore[:, 1], 'r-')

##

# Construct a scale based on existing spacing
# But only do this for edges that are part of one of the original grids
g_merge.edge_to_cells()  # update edges['cells']
sample_xy = []
sample_scale = []
ec = g_merge.edges_center()

for na, nb in utils.circular_pairs(nodes):
    j = g_merge.nodes_to_edge([na, nb])
    if np.any(g_merge.edges['cells'][j] >= 0):
        sample_xy.append(ec[j])
示例#10
0
    grp_poly = ops.cascaded_union([model.grid.cell_polygon(i) for i in grp])
    assert grp_poly.type == 'Polygon', "Hmm - add code to deal with multipolygons"
    polys.append(grp_poly)

agg_shp_fn = "dwaq_aggregation.shp"
wkb2shp.wkb2shp(agg_shp_fn, polys, overwrite=True)
##

import matplotlib.pyplot as plt
plt.figure(1).clf()
ax = plt.gca()
model.grid.plot_cells(values=permute[labels], ax=ax)
ax.axis('equal')

for poly in polys:
    plot_wkb.plot_wkb(poly, ax=ax, fc='none', lw=3)

##

hyd_path = os.path.join(model.run_dir, "DFM_DELWAQ_%s" % model.mdu.name,
                        "%s.hyd" % model.mdu.name)
assert os.path.exists(hyd_path)

##

from stompy.model.delft import waq_hydro_editor, waq_scenario

six.moves.reload_module(waq_scenario)
six.moves.reload_module(waq_hydro_editor)

##
tekno_clip=tekno[ (tekno.Epoch_Sec>=t_min) & (tekno.Epoch_Sec<=t_max) ]

##

yaps_in=yaps3_clip.in_poly.sum()
yaps_tot=len(yaps3_clip)
print(f"{yaps_in}/{yaps_tot} ({100.0*yaps_in/yaps_tot:.2f}% 3+ rx yaps solutions are within the domain")
print(f"{yaps_tot-yaps_in}/{yaps_tot} ({100.0*(1.0-yaps_in/yaps_tot):.2f}% 3+ rx yaps solutions are outside the domain")
tek_in=tekno_clip.in_poly.sum()
tek_tot=len(tekno_clip)
print(f"{tek_in}/{tek_tot} ({100.0*tek_in/tek_tot:.2f}% Teknologic solutions are within the domain")
print(f"{tek_tot-tek_in}/{tek_tot} ({100.0*(1.0-tek_in/tek_tot):.2f}% Teknologic solutions are outside the domain")

##

plt.figure(1).clf()
fig,ax=plt.subplots(num=1)

sel=tekno_clip.in_poly
ax.plot(tekno_clip.loc[sel,'X_UTM'],tekno_clip.loc[sel,'Y_UTM'],'k.')
sel=~sel
ax.plot(tekno_clip.loc[sel,'X_UTM'],tekno_clip.loc[sel,'Y_UTM'],'k+')

sel=yaps3_clip.in_poly
ax.plot(yaps3_clip.loc[sel,'x'],yaps3_clip.loc[sel,'y'],'r.')
sel=~sel
ax.plot(yaps3_clip.loc[sel,'x'],yaps3_clip.loc[sel,'y'],'r+')

plot_wkb.plot_wkb(g_poly,ax=ax,zorder=-2,alpha=0.3)
ax.axis('equal')
示例#12
0
       (levee_bounds[:, 3] >= dem_xxyy[2]))

levee_clip = levees[sel]  # 23k

##

plt.figure(1).clf()
fig, ax = plt.subplots(num=1)
dem.downsample(4).plot(cmap='gray', vmin=-1, vmax=5, ax=ax, zorder=-2)

ax.axis('off')

from stompy.plot import plot_wkb

for l in levee_clip['geom'][:1000]:
    plot_wkb.plot_wkb(l, ax=ax, color='m', lw=2, zorder=2)
ax.axis('equal')

##

# Would like to burn in the Z_Min from the levees to the DEM, but only
# where higher than existing values.

levee_burn_fn = "levee_burn.tif"

if not os.path.exists(levee_burn_fn):
    levee_burn = dem.copy()
    levee_burn.F[:, :] = np.nan
    levee_burn.write_gdal(levee_burn_fn)

    cmd = (
img = dem_clip.plot(ax=ax, cmap=cm_topobathy)
img.set_clim([-8, 4])

ac = aerial.crop(clip).plot(ax=ax, zorder=-2)

grid_coll = grid.plot_edges(lw=0.5, color='k', alpha=0.3, ax=ax)

cbar = plt.colorbar(img,
                    cax=cax,
                    label='Bathymetry (m NAVD88)',
                    orientation='horizontal')
cbar.set_ticks([-8, -5, -2, 1, 4])

plot_wkb.plot_wkb(grid_poly,
                  edgecolor='k',
                  lw=1.,
                  zorder=3,
                  ax=ax,
                  facecolor='none')

ax.plot(rx_locs['yap_x'],
        rx_locs['yap_y'],
        'yo',
        label='Hydrophone',
        mew=1,
        mec='k')
ax.axis(zoom)

leg_ax.legend(loc='upper left',
              frameon=False,
              bbox_to_anchor=[0, 0.95],
              handles=ax.lines)
示例#14
0
    def add_mouth_as_bathy(self, crest=0.5, width=50.0, plot=True):
        """
        Update bed elevation in the grid to reflect the QCM geometry
        at a specific time (run_start). Uses the 'mouth_centerline' feature
        in the shapefile inputs to define the centerline and which nodes will
        be updated. Bed is static, though.
        """
        center = self.match_gazetteer(name='mouth_centerline')[0]['geom']
        node_sel, node_long, node_lat = self.centerline_to_node_coordinates(
            center)
        # Make a copy of original depth data and update
        self.grid.add_node_field('node_z_bed_orig',
                                 self.grid.nodes['node_z_bed'],
                                 on_exists='pass')

        def channel(n, c_long, c_lat):
            # from mouth_as_structure:
            # Trapezoid, but the flat part is the full width from the QCM
            lat_slope = 1.0 / 10  # outside the width rise with 1:10 slope
            lat_shape = (c_lat - width / 2).clip(0) * lat_slope

            # And a trapezoidal longitudinal shape, flat in the middle.
            # sloping down up/downstream.
            rise = 1.0
            run = (c_long.max() - c_long.min()) / 2

            lon_mid = np.median(c_long)  # or the lon coord nearest the middle
            lon_slope = rise / run
            lon_flat = 10.0  # half-width of flat length along channel

            print("Longitudinal slope is %.3f (%.2f:%.2f)" %
                  (lon_slope, rise, run))

            # additional ad hoc adjustment to qcm_z_thalweg of -0.10, and
            # slope down away from the middle-ish point.
            lon_shape = -lon_slope * (abs(c_long - lon_mid) - lon_flat).clip(0)

            z_node = crest + lon_shape + lat_shape
            z_orig = self.grid.nodes['node_z_bed_orig'][n]
            return np.maximum(z_orig, z_node)

        self.grid.nodes['node_z_bed'][node_sel] = channel(
            node_sel, node_long, node_lat)

        if plot:
            fig = plt.figure(1)
            fig.clf()
            self.grid.plot_edges(color='k', lw=0.4)
            self.grid.plot_nodes(mask=node_sel)

            plot_wkb.plot_wkb(center, color='orange')

            #plt.scatter(pnts[:,0],pnts[:,1],30,node_lat,cmap=turbo)
            self.grid.contourf_node_values(self.grid.nodes['node_z_bed'],
                                           np.linspace(0, 2.5, 30),
                                           cmap=turbo)

            plt.axis('tight')
            plt.axis('equal')
            plt.axis('off')
            plt.axis((552070., 552178., 4124574., 4124708.))
示例#15
0
cf=CompositeField(shp_fn='sources_v03.shp',
                  factory=factory,
                  priority_field='priority',
                  data_mode='data_mode',
                  alpha_mode='alpha_mode')

bounds=[590000,591000,4142000,4143000]
dem=cf.to_grid(dx=2,dy=2,bounds=bounds)


## 

plt.figure(1).clf()
fig,ax=plt.subplots(num=1)

img=dem.plot(ax=ax,interpolation='nearest',vmin=-5,vmax=4)
plot_utils.cbar(img,ax=ax)

for src_i in range(len(cf.sources)):
    if cf.sources['priority'][src_i]<0:
        continue
    plot_wkb.plot_wkb( cf.sources['geom'][src_i], ax=ax, fc='none',ec='k',lw=0.5)

## 

# looking pretty good, about ready to scale it up...
# The test tile took 10.8s, with all the time in mask_outside.

# So far so good - last thing is to bring in the holey USGS 2m, so it can sit
# on top of the pond bathy.
示例#16
0
plt.figure(1).clf()
fig,ax=plt.subplots(num=1)
blend.plot(interpolation='nearest',ax=ax,vmin=-10,vmax=5)

blend.plot_hillshade(ax=ax)

## 


# this does require that someone drew the bounding polygons to basically
# follow the areas with actual data.

## 

from stompy.plot import plot_wkb
plt.figure(2).clf()
fig,ax=plt.subplots(num=2)

# it's not taking...

for g in mbf.bfs[0].ic.shp_data['geom']:
    plot_wkb.plot_wkb(g,ax=ax,alpha=0.3)

ax.axis('equal')


## 


merged_old=field.GdalGrid('tiles_5m_20170501/merged_5m.tif')
示例#17
0
ac = aerial.crop(clip).plot(ax=ax, zorder=-2)

grid_coll = grid.plot_edges(lw=0.5, color='k', alpha=0.3, ax=ax)

cbar = plt.colorbar(img,
                    cax=cax,
                    label='Bathymetry (m NAVD88)',
                    orientation='horizontal')

cbar.set_ticks([-8, -5, -2, 1, 4])
plt.setp(cax.get_xticklabels(), fontsize=9)
plt.setp(cax.xaxis.label, fontsize=9)

plot_wkb.plot_wkb(grid_poly,
                  edgecolor='k',
                  lw=1.,
                  zorder=3,
                  ax=ax,
                  facecolor='none')

ax.plot(rx_locs['yap_x'],
        rx_locs['yap_y'],
        'yo',
        label='Hydrophone',
        mew=1,
        mec='k')
ax.axis(zoom)

leg_ax.legend(loc='upper left',
              frameon=False,
              bbox_to_anchor=[0, 0.90],
              fontsize=9,
示例#18
0
    def summary_figure(self, num=3):
        fig = plt.figure(num)
        fig.clf()

        ax_map = fig.add_subplot(1, 2, 1)
        ax_V = fig.add_subplot(1, 2, 2)

        xyxy = self.region_poly.bounds
        clip = [xyxy[0], xyxy[2], xyxy[1], xyxy[3]]
        hyp = self.hyp

        # hyp.g.plot_edges(clip=clip,ax=ax_map,color='k',lw=0.4,alpha=0.4)
        plot_wkb.plot_wkb(hyp.g_poly,
                          fc='0.8',
                          ec='none',
                          ax=ax_map,
                          zorder=-4)
        hyp.g.plot_cells(mask=self.region_cells, ax=ax_map, color='orange')

        # Show the location of the reference station
        ax_map.plot([
            self.hyp.his.station_x_coordinate.isel(
                stations=self.region_station)
        ], [
            self.hyp.his.station_y_coordinate.isel(
                stations=self.region_station)
        ], 'go')
        # bound_xs=self.bounding_cross_sections()
        xys = []
        uvs = []
        for k in self.bound_xs.keys():
            sgn = self.bound_xs[k]
            sec_x = self.hyp.his.cross_section_x_coordinate.isel(
                cross_section=k).values
            sec_y = self.hyp.his.cross_section_y_coordinate.isel(
                cross_section=k).values
            sec_xy = np.c_[sec_x, sec_y]
            sec_xy = sec_xy[sec_xy[:, 0] < 1e10]
            ax_map.plot(sec_xy[:, 0], sec_xy[:, 1], 'r-')
            uvs.append(sgn * (sec_xy[-1] - sec_xy[0]))
            xys.append(sec_xy.mean(axis=0))
        xys = np.array(xys)
        uvs = np.array(uvs)
        uvs = utils.rot(-np.pi / 2, utils.to_unit(uvs))
        ax_map.quiver(xys[:, 0], xys[:, 1], uvs[:, 0], uvs[:, 1])

        ax_map.axis('equal')

        order = np.argsort(self.eta)
        ax_V.plot(self.eta[order], self.vol_and_Q[order], label="model")
        ax_V.plot(self.dem_etas,
                  self.dem_volumes,
                  label='DEM',
                  color='k',
                  lw=0.6)

        # and what we expect from the model, assuming nonlin2d=0
        cell_etas, cell_vols = self.calculate_cell_volumes()
        ax_V.plot(cell_etas, cell_vols, label='Cell depth', color='r', lw=0.6)

        ax_V.set_ylabel('Vol')
        ax_V.set_xlabel('eta')

        ax_V.legend()
        return fig
示例#19
0
    ]
]

g = grids[0].copy()
for other in grids[1:]:
    g.add_grid(other)

#

fig = plt.figure(1)
fig.clf()
fig, axs = plt.subplots(2, 1, sharex=True, sharey=True, num=1)

ax = axs[0]
for centerline in centerlines['geom']:
    plot_wkb.plot_wkb(centerline, color='r', ax=ax)
for bound in bounds['geom']:
    plot_wkb.plot_wkb(bound, color='0.8', zorder=-2, ax=ax)
g.plot_edges(color='k', ax=ax)
ax.axis('equal')

##

from stompy.grid import orthogonalize

tweaker = orthogonalize.Tweaker(g)

n_iters = 20

angle_thresh_deg = 1.0
angles = g.angle_errors()
示例#20
0
edge_mask = self.g.edges['sign'] != 0

plt.figure(1).clf()
fig, ax = plt.subplots(num=1)

self.g.plot_cells(color='0.7', lw=0.5, mask=self.visited < 0, ax=ax)
ccoll = self.g.plot_cells(values=self.visited,
                          lw=0.5,
                          mask=self.visited >= 0,
                          ax=ax,
                          alpha=0.3)
ccoll.set_edgecolors('k')
ccoll.set_cmap('jet')

[plot_wkb.plot_wkb(geo, color='k', ax=ax) for geo in self.flow_lines['geom']]

fluxes = self.fluxes
areas = self.fluxareas(t=0).clip(1, np.inf)
self.g.plot_edges(labeler=lambda j, r: "%.1f (%.2fm/s)" % (self.g.edges[
    'sign'][j] * fluxes[j], self.g.edges['sign'][j] * fluxes[j] / areas[j]),
                  mask=edge_mask,
                  ax=ax)

p_xy = np.zeros((len(self.particles), 2), np.float64)

for p_i, loc in enumerate(self.particles):
    p_xy[p_i, :] = router.bary_to_xy(loc)

ax.plot(p_xy[:, 0], p_xy[:, 1], 'bo')