Regridding & Sampling#
Moving a field from one mesh to another, to points, or onto a raster.
Regridder builds a sparse weight matrix once and applies it to any number of fields. The two
area-based methods differ only in their normalisation, and they agree whenever the target is fully
covered by the source — so the distinction only shows on a partially covered target:
| method | weight | answers |
|---|---|---|
overlap |
area(t, s) / sum_s area(t, s) |
the mean of the source it covers; rows sum to 1 |
conservative |
area(t, s) / target_area[t] |
preserves sum(value * area) when the target covers the source |
centroid copies from the nearest source centroid and barycentric interpolates in the Delaunay
triangulation, reproducing a plane exactly.
These two differ from the area-based pair in what they do with a target the source does not cover.
centroid has no distance cutoff, so it fills every target from its nearest source face however far
away that is, and never answers NaN. barycentric triangulates the source centroids, whose hull
is strictly inside the source footprint — a 3x3 unit source over [0, 3]² has centroids spanning
only [0.5, 2.5]² — so a target lying inside the source mesh but near its boundary is outside that
hull and does come back NaN.
conservative conserves only when the target's footprint contains the source's. Source area the
target does not reach has its mass dropped — a 3x3 unit source over [0, 3]² onto a 2x2 unit target
over [0, 2]² goes from an integral of 45.0 to 12.0 — and because an uncovered target element is
NaN, the target integral is nan whenever the target reaches past the source.
sample reads a field at points; a point lying exactly on a face boundary — an interior shared edge,
or one of the mesh's own nodes — belongs to a face that meets there, not to nothing.
rasterize_like interpolates onto another raster's grid, taking the cell centres from that grid's
full affine so a rotated reference is sampled where it actually lies. An uncovered target element
comes back as the declared no-data value.
pyramids.netcdf.ugrid.regrid
#
Standalone mesh regridding, resampling, and point/time interpolation.
This module is the compute kernel behind the UGRID regridding surface. It holds
pure numpy / scipy / shapely implementations with no heavy
geometry dependency (no numba, no cell-tree), so every function here runs on a
core install. The :class:~pyramids.netcdf.ugrid.dataset.UgridDataset facades
(sample / rasterize_like / regrid / interp_time) are thin
wrappers that marshal a :class:MeshVariable's array and location into these
functions and rewrap the result.
The four capabilities, matching the B-epic regridding/interpolation tasks:
- :func:
sample(B2) — value of a variable at arbitrary points, with barycentric-linearrecovery for triangular node data andcontains/nearestlookups reusing :class:~pyramids.netcdf.ugrid.spatial.MeshSpatialIndex. - :func:
rasterize_like(B1) — snap the mesh-to-raster interpolation onto an existing :class:~pyramids.dataset.Dataset's grid (geotransform + shape), rather than onto a freshcell_size+boundsgrid. - :func:
interp_time(B4) — interpolate a temporal variable onto new time coordinates via :func:scipy.interpolate.interp1d. - :class:
Regridder(B3) — overlap / conservative / barycentric / centroid regridding between two meshes, exposing reusable.weightsand a :meth:Regridder.from_weightsconstructor.
Depends on
- mesh.py: Mesh2d (geometry, centroids, areas, triangulation)
- spatial.py: MeshSpatialIndex (STRtree containment, cKDTree nearest)
- interpolation.py: _get_source_data / _interpolate_nearest / _interpolate_linear
- scipy.spatial: cKDTree, Delaunay
- scipy.interpolate: interp1d
- scipy.sparse: csr_matrix (weight storage / reuse)
- shapely: vectorized polygon intersection + STRtree
Regridder
#
Weight-based regridder between two meshes (or a mesh and a raster-like target).
Builds a sparse weight matrix W of shape (n_target, n_source) once, then applies it
to any number of data slabs via :meth:regrid (target = W @ source per slab, broadcast
over leading time/layer axes). The weights are reusable — compute once with __init__,
reuse via :attr:weights / :meth:from_weights.
Methods (method=):
- "overlap" — area-weighted mean: W[t, s] = area(t, s) / sum_s area(t, s). Rows
sum to 1, so the overlap-mean of a constant field is that constant.
- "conservative" — first-order conservative:
W[t, s] = area(t, s) / target_area[t]. Conserves the integral
sum(value * area) when the target covers the whole source -- any source
area the target does not reach has its mass dropped (see :meth:regrid).
- "centroid" (alias "centroid_locator") — nearest-source copy: each target takes
the value of the source face whose centroid is nearest its own (weight 1.0). There is
no distance cutoff, so every target gets a value however far it lies from the
source: a unit target 1000 units away still answers its nearest source face's value.
This method therefore never produces an empty weight row, and never answers NaN.
- "barycentric" — barycentric interpolation of target centroids in the Delaunay
triangulation of the source face centroids. That hull is strictly smaller than the
source footprint -- a 3x3 unit source over [0, 3]^2 has centroids spanning only
[0.5, 2.5]^2 -- so a target inside the source mesh but near its boundary is
outside the hull and comes back NaN.
Overlap weights are true polygon-intersection areas computed with :mod:shapely and indexed
with :class:shapely.STRtree to avoid the O(n*m) all-pairs scan. This is the pure
numpy/scipy/shapely route (no numba cell-tree): slower but correct.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Mesh2d
|
The mesh the values come from. |
required |
target
|
Mesh2d
|
The mesh the values are moved onto. |
required |
method
|
str
|
The weighting. |
'overlap'
|
source_location
|
str
|
The facet the source values sit on. Only |
'face'
|
Attributes:
| Name | Type | Description |
|---|---|---|
weights |
csr_matrix
|
The sparse |
Raises:
| Type | Description |
|---|---|
ValueError
|
An unknown |
Source code in src/pyramids/netcdf/ugrid/regrid.py
533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 | |
weights
property
#
The sparse (n_target, n_source) weight matrix (reusable / serializable).
method
property
#
The regridding method this instance was built with.
n_source
property
#
Number of source elements the weights expect.
n_target
property
#
Number of target elements the weights produce.
from_weights(weights, target, *, source_location='face')
classmethod
#
Build a regridder from a precomputed weight matrix, skipping the geometry pass.
Lets weights computed once (and optionally persisted) be reused on a fresh process or a different data set without rebuilding the intersection/triangulation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
Any
|
Any |
required |
target
|
Mesh2d
|
The target mesh the weights map onto. |
required |
source_location
|
str
|
Mesh location the source data live on. |
'face'
|
Returns:
| Name | Type | Description |
|---|---|---|
Regridder |
Regridder
|
An instance whose :meth: |
Source code in src/pyramids/netcdf/ugrid/regrid.py
regrid(values)
#
Apply the weights to source values, returning the data on the target mesh.
Broadcasts over leading time/layer axes: the element axis must be last and of length
n_source; every leading slab is regridded by the same sparse matmul. Target elements
with no source contribution (empty weight rows) are NaN.
Whether an uncovered target has an empty row is the method's business, and only the
two area-based ones answer the way the sentence above suggests: "overlap" and
"conservative" leave a target with no overlap empty, "barycentric" leaves one
outside the source-centroid hull empty, and "centroid" leaves none empty at all --
it copies from the nearest source face at any distance, so it can never answer NaN.
Conservation (method="conservative"): because W[t, s] = area(t, s) / target_area[t],
sum(target * target_area) == sum(source * source_area) when the target covers the
whole source -- the target's footprint has to contain the source's, not the other way
round. Source area outside the target contributes to no row, so its mass is dropped: a
3x3 unit source over [0, 3]^2 regridded onto a 2x2 unit target over [0, 2]^2
loses the uncovered ring (measured integrals 45.0 vs 12.0). And because an uncovered
target element is NaN, the target integral is nan -- hence unevaluable -- whenever
the target reaches beyond the source.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values
|
ndarray
|
|
required |
Returns:
| Type | Description |
|---|---|
NDArray
|
numpy.ndarray: |
Raises:
| Type | Description |
|---|---|
ValueError
|
|
Source code in src/pyramids/netcdf/ugrid/regrid.py
sample(mesh, values, x, y, *, method='nearest', location='face', nodata=np.nan, index=None)
#
Sample mesh values at arbitrary points.
The compute kernel the UgridDataset.sample facade wires into. It resolves each
query point to a mesh element and gathers (or interpolates) the value there. It is
temporal-aware: when values is 2-D (n_time, n_elements) the leading time axis
is preserved and the result is (n_time, n_points).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mesh
|
Mesh2d
|
Source mesh topology. |
required |
values
|
ndarray
|
Data on the mesh, |
required |
x
|
float | ndarray
|
Query x-coordinate(s), in the mesh CRS. |
required |
y
|
float | ndarray
|
Query y-coordinate(s), in the mesh CRS. |
required |
method
|
str
|
One of |
'nearest'
|
location
|
str
|
Mesh location the values live on — |
'face'
|
nodata
|
float
|
Value written where a point falls outside the mesh. Defaults to NaN. |
nan
|
index
|
MeshSpatialIndex | None
|
An existing spatial index to reuse; a new one is built when |
None
|
Returns:
| Type | Description |
|---|---|
NDArray
|
numpy.ndarray: One value per query point (float64), shaped |
NDArray
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
An unknown |
Source code in src/pyramids/netcdf/ugrid/regrid.py
rasterize_like(mesh, values, other_grid, *, location='face', method='nearest', nodata=-9999.0, source_epsg=None, max_distance=None)
#
Rasterize mesh values onto an existing grid, co-registered with other_grid.
Unlike the cell_size + bounds path of
:func:~pyramids.netcdf.ugrid.interpolation.mesh_to_grid, this snaps the interpolation
onto the geotransform / shape / CRS of an existing :class:~pyramids.dataset.Dataset, so
the output is byte-for-byte aligned with it. The underlying cKDTree / LinearNDInterpolator
machinery is reused via :func:~pyramids.netcdf.ugrid.interpolation._get_source_data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mesh
|
Mesh2d
|
Source mesh topology. |
required |
values
|
ndarray
|
Data on the mesh. A temporal |
required |
other_grid
|
Dataset
|
Reference raster whose grid the output matches. |
required |
location
|
str
|
Mesh location the values live on — |
'face'
|
method
|
str
|
|
'nearest'
|
nodata
|
float
|
Fill value for cells outside the mesh / convex hull. |
-9999.0
|
source_epsg
|
int | None
|
EPSG of the mesh coordinates. When it differs from |
None
|
max_distance
|
float | None
|
Nearest/IDW cutoff distance, in CRS units. Defaults to twice the longer of the reference's two cell edges. Sizing it from one axis alone starved an anisotropic grid: a reference stepping 1 along a column and 10 down a row got a 2.0 cutoff, under a quarter of one row step, and every cell came back no-data. One isotropic radius cannot be "two steps along either axis" on an anisotropic
grid: covering two steps of the long axis necessarily reaches
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
Dataset |
Dataset
|
A raster co-registered with |
Raises:
| Type | Description |
|---|---|
ValueError
|
An unknown |
Source code in src/pyramids/netcdf/ugrid/regrid.py
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 | |
interp_time(times, values, new_times, *, method='linear', axis=0, bounds='nan')
#
Interpolate a temporal variable onto new time coordinates.
Distinct from gap-filling: this blends between existing time steps at requested labels.
A field that is linear in time is reproduced exactly at any interior label. This is the
kernel behind the UgridDataset.interp_time facade, which supplies the variable's own
time axis as axis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
times
|
ndarray
|
Source time coordinate, 1-D and with no repeated labels (any order). |
required |
values
|
ndarray
|
Data with time along |
required |
new_times
|
ndarray
|
Requested time labels. |
required |
method
|
str
|
|
'linear'
|
axis
|
int
|
Axis of |
0
|
bounds
|
str
|
Out-of-range policy — |
'nan'
|
Returns:
| Type | Description |
|---|---|
NDArray
|
numpy.ndarray: |
Raises:
| Type | Description |
|---|---|
ValueError
|
|
Source code in src/pyramids/netcdf/ugrid/regrid.py
424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 | |