Skip to content

py3dinterpolations

py3dinterpolations

quick 3D interpolation with python

GridData(data, *, ID='ID', X='X', Y='Y', Z='Z', V='V', preprocessing_params=None)

Container for 3D grid data with spatial coordinates and values.

Standardizes input DataFrames into a canonical format with MultiIndex (ID, X, Y, Z) and a single column V.

Parameters:

Name Type Description Default
data DataFrame

Source DataFrame with spatial data.

required
ID str

Column name for point identifier.

'ID'
X str

Column name for X coordinate.

'X'
Y str

Column name for Y coordinate.

'Y'
Z str

Column name for Z coordinate.

'Z'
V str

Column name for value.

'V'
preprocessing_params PreprocessingParams | None

Parameters from preprocessing applied to this data.

None
Source code in py3dinterpolations/core/griddata.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def __init__(
    self,
    data: pd.DataFrame,
    *,
    ID: str = "ID",
    X: str = "X",
    Y: str = "Y",
    Z: str = "Z",
    V: str = "V",
    preprocessing_params: PreprocessingParams | None = None,
):
    self.preprocessing_params = preprocessing_params
    self.columns = {"ID": ID, "X": X, "Y": Y, "Z": Z, "V": V}
    self.data = self._set_data(data)

specs property

Compute spatial and value extent statistics.

numpy_data property

Return X, Y, Z, V as a numpy array.

hull property

Convex hull of XY coordinates as a shapely geometry.

ModelType

Bases: StrEnum

Supported interpolation model types.

interpolate(griddata, model_type, grid_resolution, model_params=None, model_params_grid=None, preprocessing=None, **predict_kwargs)

Interpolate GridData and return the Modeler with results.

Parameters:

Name Type Description Default
griddata GridData

Source data to interpolate.

required
model_type ModelType | str

Which model to use (e.g. "ordinary_kriging", "idw").

required
grid_resolution float | dict[str, float]

Grid resolution. Float for regular, dict for irregular.

required
model_params dict[str, object] | None

Model constructor parameters.

None
model_params_grid dict[str, list[object]] | None

Parameter grid for cross-validation search.

None
preprocessing PreprocessingKwargs | None

Keyword args for Preprocessor (e.g. downsampling_res, normalize_xyz).

None
**predict_kwargs object

Extra kwargs passed to model.predict().

{}

Returns:

Type Description
Modeler

Modeler instance with .result populated.

Raises:

Type Description
ValueError

If neither or both model_params/model_params_grid are given.

NotImplementedError

If parameter search is used for non-kriging models.

Source code in py3dinterpolations/modelling/interpolate.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def interpolate(
    griddata: GridData,
    model_type: ModelType | str,
    grid_resolution: float | dict[str, float],
    model_params: dict[str, object] | None = None,
    model_params_grid: dict[str, list[object]] | None = None,
    preprocessing: PreprocessingKwargs | None = None,
    **predict_kwargs: object,
) -> Modeler:
    """Interpolate GridData and return the Modeler with results.

    Args:
        griddata: Source data to interpolate.
        model_type: Which model to use (e.g. "ordinary_kriging", "idw").
        grid_resolution: Grid resolution. Float for regular, dict for irregular.
        model_params: Model constructor parameters.
        model_params_grid: Parameter grid for cross-validation search.
        preprocessing: Keyword args for Preprocessor
            (e.g. downsampling_res, normalize_xyz).
        **predict_kwargs: Extra kwargs passed to model.predict().

    Returns:
        Modeler instance with .result populated.

    Raises:
        ValueError: If neither or both model_params/model_params_grid are given.
        NotImplementedError: If parameter search is used for non-kriging models.
    """
    logger.info("Starting interpolation with model=%s", model_type)

    if model_params is None and model_params_grid is None:
        msg = "Either model_params or model_params_grid must be provided"
        raise ValueError(msg)
    if model_params is not None and model_params_grid is not None:
        msg = "Cannot provide both model_params and model_params_grid"
        raise ValueError(msg)

    # Build grid
    grid = create_grid(griddata, grid_resolution)

    # Preprocess if needed
    if preprocessing is not None:
        preprocessor = Preprocessor(griddata, **preprocessing)
        griddata = preprocessor.preprocess()

    # Parameter search via estimator
    if model_params is None:
        model_type_enum = ModelType(model_type)
        if model_type_enum != ModelType.ORDINARY_KRIGING:
            msg = "Parameter search is only supported for ordinary_kriging"
            raise NotImplementedError(msg)

        assert model_params_grid is not None
        est = Estimator(griddata, model_params_grid)
        model_params = dict(est.best_params)
        model_params.pop("method", None)

    # Build and fit model
    model = get_model(model_type, **model_params)
    modeler = Modeler(griddata=griddata, grid=grid, model=model)

    # Predict
    modeler.predict(**predict_kwargs)

    logger.info("Interpolation complete")
    return modeler

plot_2d_model(modeler, axis='Z', plot_points=False, annotate_points=False, figure_width=8)

Plot 2D slices of a 3D interpolation along an axis.

Parameters:

Name Type Description Default
modeler Modeler

Modeler with prediction results.

required
axis str

Axis to slice along ("X", "Y", or "Z").

'Z'
plot_points bool

Whether to overlay training points on slices.

False
annotate_points bool

Whether to annotate point values.

False
figure_width float

Figure width in inches.

8

Returns:

Type Description
Figure

Matplotlib Figure.

Source code in py3dinterpolations/plotting/plot_2d.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
def plot_2d_model(
    modeler: Modeler,
    axis: str = "Z",
    plot_points: bool = False,
    annotate_points: bool = False,
    figure_width: float = 8,
) -> Figure:
    """Plot 2D slices of a 3D interpolation along an axis.

    Args:
        modeler: Modeler with prediction results.
        axis: Axis to slice along ("X", "Y", or "Z").
        plot_points: Whether to overlay training points on slices.
        annotate_points: Whether to annotate point values.
        figure_width: Figure width in inches.

    Returns:
        Matplotlib Figure.
    """
    assert modeler.result is not None
    axis_data = modeler.grid.grid[axis]

    num_rows, num_cols = number_of_plots(len(axis_data), n_cols=2)

    figure_height_ratio = 1.25
    fig = plt.figure(
        dpi=300, figsize=(figure_width, figure_width * figure_height_ratio)
    )
    gs = gridspec.GridSpec(
        num_rows,
        num_cols + 1,
        width_ratios=[1] * num_cols + [0.1],
    )

    axes = []
    for row in range(num_rows):
        for col in range(num_cols):
            axes.append(plt.subplot(gs[row, col]))

    colorbar_ax = plt.subplot(gs[:, -1])
    colorbar_ax.spines["top"].set_visible(False)
    colorbar_ax.spines["bottom"].set_visible(False)
    colorbar_ax.spines["left"].set_visible(False)
    colorbar_ax.spines["right"].set_visible(False)
    colorbar_ax.set_xticks([])
    colorbar_ax.set_yticks([])
    colorbar_ax.set_xticklabels([])
    colorbar_ax.set_yticklabels([])

    colorbar_inset_ax = inset_axes(
        colorbar_ax, width="100%", height="50%", loc="center"
    )

    if modeler.griddata.preprocessing_params is not None:
        gd_reversed = reverse_preprocessing(modeler.griddata)
    else:
        gd_reversed = modeler.griddata
    norm = Normalize(gd_reversed.specs.vmin, gd_reversed.specs.vmax)

    img = None
    for ax, i in zip(axes, range(len(axis_data)), strict=False):
        if axis == "Z":
            matrix = modeler.result.interpolated[i, :, :]
        elif axis == "Y":
            matrix = modeler.result.interpolated[:, i, :]
        elif axis == "X":
            matrix = modeler.result.interpolated[:, :, i]
        else:
            keys = list(SLICING_AXIS.keys())
            msg = f"axis {axis} not implemented. Choose from {keys}"
            raise NotImplementedError(msg)

        img = ax.imshow(
            matrix.squeeze(),
            origin="lower",
            extent=(
                modeler.grid.get_axis(SLICING_AXIS[axis]["X'"]).min,
                modeler.grid.get_axis(SLICING_AXIS[axis]["X'"]).max,
                modeler.grid.get_axis(SLICING_AXIS[axis]["Y'"]).min,
                modeler.grid.get_axis(SLICING_AXIS[axis]["Y'"]).max,
            ),
            cmap="plasma",
            norm=norm,
        )

        from_value = modeler.grid.grid[axis][i]
        axis_res = modeler.grid.get_axis(axis).res
        to_value = from_value + axis_res

        if plot_points:
            points_df = gd_reversed.data.copy().reset_index()
            points = points_df[
                (points_df[axis] >= from_value) & (points_df[axis] < to_value)
            ].copy()
            points = points.sort_values(by=["V"])
            ax.scatter(
                points[SLICING_AXIS[axis]["X'"]],
                points[SLICING_AXIS[axis]["Y'"]],
                c=points["V"],
                cmap="plasma",
                norm=norm,
                s=figure_width / 2,
            )
            if annotate_points:
                for _idx, row in points.iterrows():
                    ax.annotate(
                        f"{row['V']:.0f}",
                        xy=(
                            row[SLICING_AXIS[axis]["X'"]],
                            row[SLICING_AXIS[axis]["Y'"]],
                        ),
                        xytext=(2, 2),
                        textcoords="offset points",
                        fontsize=figure_width / 2,
                    )

        ax.set_title(f"{axis} = {from_value}\u00f7{to_value} m")

    fig.suptitle(f"Along {axis} axis")

    if img is not None:
        plt.colorbar(
            img,
            cax=colorbar_inset_ax,
            format="%.0f",
            fraction=0.1,
        )

    if len(axis_data) < num_rows * num_cols:
        for i in range(len(axis_data), num_rows * num_cols):
            axes[i].set_visible(False)

    return fig

plot_3d_model(modeler, plot_points=False, scale_points=1.0, volume_kwargs=None)

Plot 3D interpolation result as a plotly Volume.

Parameters:

Name Type Description Default
modeler Modeler

Modeler with prediction results.

required
plot_points bool

Whether to overlay training points.

False
scale_points float

Scale factor for point marker sizes.

1.0
volume_kwargs dict[str, object] | None

Extra kwargs for go.Volume.

None

Returns:

Type Description
Figure

Plotly Figure.

Source code in py3dinterpolations/plotting/plot_3d.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def plot_3d_model(
    modeler: Modeler,
    plot_points: bool = False,
    scale_points: float = 1.0,
    volume_kwargs: dict[str, object] | None = None,
) -> go.Figure:
    """Plot 3D interpolation result as a plotly Volume.

    Args:
        modeler: Modeler with prediction results.
        plot_points: Whether to overlay training points.
        scale_points: Scale factor for point marker sizes.
        volume_kwargs: Extra kwargs for go.Volume.

    Returns:
        Plotly Figure.
    """
    if volume_kwargs is None:
        volume_kwargs = {}

    if modeler.griddata.preprocessing_params is not None:
        gd_reversed = reverse_preprocessing(modeler.griddata)
    else:
        gd_reversed = modeler.griddata

    assert modeler.result is not None
    # ZYX -> XYZ
    values = np.einsum("ZXY->XYZ", modeler.result.interpolated)

    data: list[go.Volume | go.Scatter3d] = [
        go.Volume(
            x=modeler.grid.mesh["X"].flatten(),
            y=modeler.grid.mesh["Y"].flatten(),
            z=modeler.grid.mesh["Z"].flatten(),
            value=values.flatten(),
            opacityscale=[(0, 0), (1, 1)],
            cmin=gd_reversed.specs.vmin,
            cmax=gd_reversed.specs.vmax,
            **volume_kwargs,
        ),
    ]

    if plot_points:
        params = modeler.griddata.preprocessing_params
        if params is not None:
            points = gd_reversed.data.copy().reset_index()
        else:
            points = modeler.griddata.data.copy().reset_index()

        data.append(
            go.Scatter3d(
                x=points["X"],
                y=points["Y"],
                z=points["Z"],
                mode="markers",
                marker=dict(
                    size=points["V"].to_list(),
                    sizemode="area",
                    sizeref=2.0 * max(points["V"]) / (scale_points**2),
                    color=points["V"],
                    sizemin=1,
                ),
            )
        )

    fig = go.Figure(data=data)
    fig.update_scenes(aspectmode="data")
    return fig