|
15 | 15 | from devito.finite_differences.elementary import floor |
16 | 16 | from devito.logger import warning |
17 | 17 | from devito.symbolics import INT, retrieve_function_carriers, retrieve_functions |
18 | | -from devito.tools import Pickable, as_tuple, filter_ordered, flatten, memoized_meth |
19 | | -from devito.types import Eq, Evaluable, Inc, SubFunction, Symbol |
| 18 | +from devito.tools import ( |
| 19 | + Pickable, as_fp64_decimal, as_tuple, filter_ordered, flatten, memoized_meth |
| 20 | +) |
| 21 | +from devito.types import CustomDimension, Eq, Evaluable, Inc, SubFunction, Symbol |
20 | 22 | from devito.types.utils import DimensionTuple |
21 | 23 |
|
22 | 24 | __all__ = ['LinearInterpolator', 'PrecomputedInterpolator', 'SincInterpolator'] |
@@ -510,46 +512,220 @@ def _inject(self, field, expr, implicit_dims=None): |
510 | 512 | return filter_ordered(temps) + eqns |
511 | 513 |
|
512 | 514 |
|
| 515 | +def _shift_tag(shifts): |
| 516 | + """Suffix used to distinguish per-staggering table names ("_s10", ...).""" |
| 517 | + if not shifts or not any(shifts): |
| 518 | + return '' |
| 519 | + return '_s' + ''.join('1' if s else '0' for s in shifts) |
| 520 | + |
| 521 | + |
| 522 | +def _shift_values(shifts, grid, spacing): |
| 523 | + """Physical half-cell offsets for each grid dim, as fp64.""" |
| 524 | + if not shifts: |
| 525 | + return np.zeros(grid.dim, dtype=np.float64) |
| 526 | + subs = {d.spacing: float(h) |
| 527 | + for d, h in zip(grid.dimensions, spacing, strict=True)} |
| 528 | + return np.array([float(sympy.sympify(s).xreplace(subs)) for s in shifts]) |
| 529 | + |
| 530 | + |
| 531 | +class _HostTable(SubFunction): |
| 532 | + """SubFunction filled on the host from the parent SparseFunction's |
| 533 | + scattered coordinates by ``LinearInterpolator._arg_defaults`` (invoked |
| 534 | + via ``SparseFunction._arg_defaults``). Gathering is a no-op — these |
| 535 | + tables are input-only.""" |
| 536 | + |
| 537 | + def _arg_apply(self, *args, **kwargs): |
| 538 | + return |
| 539 | + |
| 540 | + def _arg_values(self, estimate_memory=False, **kwargs): |
| 541 | + if self._parent.name in kwargs: |
| 542 | + return {} |
| 543 | + return self._parent._arg_defaults( |
| 544 | + alias=self._parent, estimate_memory=estimate_memory |
| 545 | + ).reduce_all() |
| 546 | + |
| 547 | + def compute(self, coords, grid, spacing, origin): # pragma: no cover |
| 548 | + raise NotImplementedError |
| 549 | + |
| 550 | + def _arg_defaults_data(self, coords, grid, spacing, origin): |
| 551 | + """``(name -> data)`` plus this table's dim entries.""" |
| 552 | + data = self.compute(coords, grid, spacing, origin) |
| 553 | + args = {self.name: data} |
| 554 | + for d, s in zip(self.dimensions, data.shape, strict=True): |
| 555 | + args.update(d._arg_defaults(_min=0, size=s)) |
| 556 | + return args |
| 557 | + |
| 558 | + |
| 559 | +class Gridpoints(_HostTable): |
| 560 | + """int32 cell indices per sparse point, shape ``(npoint, ndim)``.""" |
| 561 | + |
| 562 | + __rkwargs__ = _HostTable.__rkwargs__ + ('shifts',) |
| 563 | + |
| 564 | + def __init_finalize__(self, *args, **kwargs): |
| 565 | + self._shifts = kwargs.pop('shifts', None) |
| 566 | + super().__init_finalize__(*args, **kwargs) |
| 567 | + |
| 568 | + @property |
| 569 | + def shifts(self): |
| 570 | + return self._shifts |
| 571 | + |
| 572 | + def compute(self, coords, grid, spacing, origin): |
| 573 | + return _cell_indices(coords, grid, self._shifts, spacing, origin) |
| 574 | + |
| 575 | + |
| 576 | +class Coeffs(_HostTable): |
| 577 | + """Per-dim `(1 - frac, frac)` interpolation weights, shape ``(npoint, 2)``.""" |
| 578 | + |
| 579 | + __rkwargs__ = _HostTable.__rkwargs__ + ('shifts', 'dim_index') |
| 580 | + |
| 581 | + def __init_finalize__(self, *args, **kwargs): |
| 582 | + self._shifts = kwargs.pop('shifts', None) |
| 583 | + self._dim_index = kwargs.pop('dim_index') |
| 584 | + super().__init_finalize__(*args, **kwargs) |
| 585 | + |
| 586 | + @property |
| 587 | + def shifts(self): |
| 588 | + return self._shifts |
| 589 | + |
| 590 | + @property |
| 591 | + def dim_index(self): |
| 592 | + return self._dim_index |
| 593 | + |
| 594 | + def compute(self, coords, grid, spacing, origin): |
| 595 | + return _linear_weights(coords, grid, self._shifts, self._dim_index, |
| 596 | + self.dtype, spacing, origin) |
| 597 | + |
| 598 | + |
| 599 | +def _resolved_geometry(grid, kwargs): |
| 600 | + """Fp64 (spacing, origin) tuple honoring runtime `h_x`/`o_x`/... overrides.""" |
| 601 | + spacing = np.array([as_fp64_decimal(kwargs.get(s.name, v)) for s, v |
| 602 | + in zip(grid.spacing_symbols, grid.spacing, strict=True)]) |
| 603 | + origin = np.array([as_fp64_decimal(kwargs.get(o.name, v)) for o, v |
| 604 | + in zip(grid.origin_symbols, grid.origin, strict=True)]) |
| 605 | + return spacing, origin |
| 606 | + |
| 607 | + |
| 608 | +def _positions_fp64(coords, grid, shifts, spacing, origin): |
| 609 | + c64 = np.asarray(coords, dtype=np.float64) |
| 610 | + return (c64 - origin - _shift_values(shifts, grid, spacing)) / spacing |
| 611 | + |
| 612 | + |
| 613 | +def _cell_indices(coords, grid, shifts, spacing, origin): |
| 614 | + return np.floor(_positions_fp64(coords, grid, shifts, spacing, |
| 615 | + origin)).astype(np.int32) |
| 616 | + |
| 617 | + |
| 618 | +def _linear_weights(coords, grid, shifts, j, dtype, spacing, origin): |
| 619 | + pos = _positions_fp64(coords, grid, shifts, spacing, origin) |
| 620 | + frac = pos[:, j] - np.floor(pos[:, j]) |
| 621 | + data = np.empty((pos.shape[0], 2), dtype=dtype) |
| 622 | + data[:, 0] = 1.0 - frac |
| 623 | + data[:, 1] = frac |
| 624 | + return data |
| 625 | + |
| 626 | + |
513 | 627 | class LinearInterpolator(WeightedInterpolator): |
514 | 628 | """ |
515 | | - Concrete implementation of WeightedInterpolator implementing a Linear interpolation |
516 | | - scheme, i.e. Bilinear for 2D and Trilinear for 3D problems. |
| 629 | + Linear (bilinear/trilinear) interpolator. |
517 | 630 |
|
518 | | - Parameters |
519 | | - ---------- |
520 | | - sfunction: The SparseFunction that this Interpolator operates on. |
| 631 | + Gridpoints and per-dim `(1-frac, frac)` weights are precomputed on the |
| 632 | + host in fp64 (see `_arg_defaults`) and passed to the kernel as int32/fp |
| 633 | + SubFunctions. The generated C only indexes those tables and never sees |
| 634 | + `(c-o)/h` or `floor` on fp32. |
521 | 635 | """ |
522 | 636 |
|
523 | 637 | _name = 'linear' |
524 | 638 |
|
525 | | - @memoized_meth |
526 | | - def _weights(self, subdomain=None, shifts=None): |
527 | | - rdim = self._rdim(subdomain=subdomain, shifts=shifts) |
528 | | - c = [(1 - p) * (1 - r) + p * r |
529 | | - for (p, d, r) in zip(self._point_symbols(shifts), self._gdims, rdim, |
530 | | - strict=True)] |
531 | | - return Mul(*c) |
| 639 | + def __init__(self, sfunction): |
| 640 | + super().__init__(sfunction) |
| 641 | + # Per-shift `(gridpoints, coeffs_per_dim)` produced by `_positions` / |
| 642 | + # `_weights` during equation construction. Iterated at arg-prep time |
| 643 | + # by `_arg_defaults` to populate the host-side tables. |
| 644 | + self._tables = {} |
532 | 645 |
|
533 | | - @memoized_meth |
534 | | - def _point_symbols(self, shifts=None): |
535 | | - """Symbol for coordinate value in each Dimension of the point.""" |
536 | | - dtype = self.sfunction.coordinates.dtype |
537 | | - symbols = [] |
538 | | - for d in self.grid.dimensions: |
539 | | - if shifts and shifts[self.grid.dimensions.index(d)] != 0: |
540 | | - symbols.append(Symbol(name=f'p{d}_s1', dtype=dtype)) |
541 | | - else: |
542 | | - symbols.append(Symbol(name=f'p{d}', dtype=dtype)) |
543 | | - return DimensionTuple(*symbols, getters=self.grid.dimensions) |
| 646 | + @cached_property |
| 647 | + def _coeff_dtype(self): |
| 648 | + # Weights are real even for complex-valued sparse fields. |
| 649 | + dtype = np.dtype(self.sfunction.dtype) |
| 650 | + if np.issubdtype(dtype, np.complexfloating): |
| 651 | + return np.finfo(dtype).dtype.type |
| 652 | + return dtype.type |
| 653 | + |
| 654 | + def _make_tables(self, shifts): |
| 655 | + # Names must sort alphabetically *after* the parent SparseFunction: |
| 656 | + # the operator's default-arg loop populates the parent (which fills |
| 657 | + # these tables via `interpolator._arg_defaults`) before it would fall |
| 658 | + # back to each SubFunction's own zero-buffer default. See how the |
| 659 | + # sinc `wsinc*` tables rely on the same ordering. |
| 660 | + tag = _shift_tag(shifts) |
| 661 | + sfname = self.sfunction.name |
| 662 | + sfdim = self.sfunction._sparse_dim |
| 663 | + gp_name = f'{sfname}_gp{tag}' |
| 664 | + ddim = CustomDimension(f'{gp_name}d', 0, self.grid.dim - 1, |
| 665 | + self.grid.dim, sfdim) |
| 666 | + gp = Gridpoints(name=gp_name, dtype=np.int32, |
| 667 | + shape=(self.sfunction.npoint, self.grid.dim), |
| 668 | + dimensions=(sfdim, ddim), space_order=0, |
| 669 | + alias=self.sfunction.alias, |
| 670 | + parent=self.sfunction, shifts=shifts) |
| 671 | + coeffs = tuple( |
| 672 | + Coeffs(name=f'{sfname}_w{d.name}{tag}', |
| 673 | + dtype=self._coeff_dtype, |
| 674 | + shape=(self.sfunction.npoint, 2), |
| 675 | + dimensions=(sfdim, r), space_order=0, |
| 676 | + alias=self.sfunction.alias, |
| 677 | + parent=self.sfunction, shifts=shifts, dim_index=i) |
| 678 | + for i, (d, r) in enumerate(zip(self._gdims, self._cdim, strict=True)) |
| 679 | + ) |
| 680 | + return gp, coeffs |
| 681 | + |
| 682 | + def _gridpoints(self, shifts=None): |
| 683 | + key = tuple(shifts) if shifts else None |
| 684 | + if key not in self._tables: |
| 685 | + self._tables[key] = self._make_tables(shifts) |
| 686 | + return self._tables[key][0] |
| 687 | + |
| 688 | + def _coeffs(self, shifts=None): |
| 689 | + key = tuple(shifts) if shifts else None |
| 690 | + if key not in self._tables: |
| 691 | + self._tables[key] = self._make_tables(shifts) |
| 692 | + return self._tables[key][1] |
| 693 | + |
| 694 | + def _positions(self, implicit_dims, shifts=None): |
| 695 | + gp = self._gridpoints(shifts=shifts) |
| 696 | + ddim = gp.dimensions[-1] |
| 697 | + return [Eq(p, gp._subs(ddim, di), implicit_dims=implicit_dims) |
| 698 | + for (di, p) in enumerate( |
| 699 | + self.sfunction._pos_symbols(shifts=shifts))] |
544 | 700 |
|
545 | 701 | def _coeff_temps(self, implicit_dims, shifts=None): |
546 | | - # Positions |
547 | | - pmap = self.sfunction._position_map(shifts=shifts) |
548 | | - psyms = self._point_symbols(shifts) |
549 | | - poseq = [Eq(psyms[d], pos - floor(pos), |
550 | | - implicit_dims=implicit_dims) |
551 | | - for (d, pos) in zip(self._gdims, pmap.keys(), strict=True)] |
552 | | - return poseq |
| 702 | + return [] |
| 703 | + |
| 704 | + @memoized_meth |
| 705 | + def _weights(self, subdomain=None, shifts=None): |
| 706 | + rdims = self._rdim(subdomain=subdomain, shifts=shifts) |
| 707 | + coeffs = self._coeffs(shifts=shifts) |
| 708 | + return Mul(*[ |
| 709 | + w._subs(rd, rd - rd.parent.symbolic_min) |
| 710 | + for (rd, w) in zip(rdims, coeffs, strict=True) |
| 711 | + ]) |
| 712 | + |
| 713 | + def _arg_defaults(self, coords=None, sfunc=None, origin=None): |
| 714 | + """Fill the gridpoints/coeffs tables referenced by the operator from |
| 715 | + the already-scattered ``coords`` handed in by |
| 716 | + ``SparseFunction._arg_defaults``.""" |
| 717 | + if coords is None or sfunc is None: |
| 718 | + raise ValueError("No coordinates or sparse function provided") |
| 719 | + grid = sfunc.grid |
| 720 | + spacing = np.array([as_fp64_decimal(h) for h in grid.spacing]) |
| 721 | + origin = np.array([as_fp64_decimal(o) |
| 722 | + for o in (origin or grid.origin)]) |
| 723 | + args = {} |
| 724 | + for gp, coeffs in self._tables.values(): |
| 725 | + args.update(gp._arg_defaults_data(coords, grid, spacing, origin)) |
| 726 | + for w in coeffs: |
| 727 | + args.update(w._arg_defaults_data(coords, grid, spacing, origin)) |
| 728 | + return args |
553 | 729 |
|
554 | 730 |
|
555 | 731 | class PrecomputedInterpolator(WeightedInterpolator): |
@@ -634,7 +810,7 @@ def _weights(self, subdomain=None, shifts=None): |
634 | 810 | for (rd, w) in zip(rdims, self.interpolation_coeffs, strict=True) |
635 | 811 | ]) |
636 | 812 |
|
637 | | - def _arg_defaults(self, coords=None, sfunc=None): |
| 813 | + def _arg_defaults(self, coords=None, sfunc=None, origin=None): |
638 | 814 | args = {} |
639 | 815 | b = self._b_table[self.r] |
640 | 816 | b0 = i0(b) |
|
0 commit comments