Field Visualization Project

Problem Description

The goal of this project is to illustrate the reconstruction of a spatial field from scattered sample points. We focus on two example datasets:

This project shows how inverse distance weighting (IDW) can reconstruct a surface from scattered points.

Solution Approach

The solution consists of Python scripts that:

  1. Generate the datasets (CSV files) deterministically using random seeds.
  2. Produce scatter plots to visualize the sampled points.
  3. Reconstruct the field surface using IDW and render a 3D surface plot.
  4. Organize results in a reproducible project structure.

We implemented modular functions to generate clusters, overlay grids, write CSVs, plot scatter points, and reconstruct surfaces.

Visualization Results

Three Clusters Example

Three Clusters Scatter Three Clusters Surface

Zero Grid + Three Clusters Example

Zero Grid Scatter Zero Grid Surface

How It Works

The field reconstruction algorithm uses Inverse Distance Weighting (IDW) to interpolate scattered points onto a grid. Each grid point is estimated as a weighted average of nearby sampled points, with weights inversely proportional to distance raised to a power.

Steps:

  1. Load scattered points from CSV.
  2. Build a KD-tree for fast nearest neighbor search.
  3. For each grid point, find nearest neighbors and compute weighted average.
  4. If a grid point exactly coincides with a sampled point, its value is used directly.
  5. Render the grid as a 3D surface plot and overlay the scatter points.

This approach smoothly reconstructs the field while respecting local variations.

Example Visualizer Code

#!/usr/bin/env python3
from __future__ import annotations

import argparse
from pathlib import Path

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
from scipy.spatial import cKDTree


def load_points(path: str | Path):
    data = np.genfromtxt(path, delimiter=',', names=True)
    x = np.asarray(data['x'], dtype=float)
    y = np.asarray(data['y'], dtype=float)
    values = np.asarray(data['value'], dtype=float)
    return x, y, values


def build_idw_interpolator(x, y, values, neighbors=8, power=2.0, eps=1e-12):
    points = np.column_stack([x, y])
    tree = cKDTree(points)
    k = max(1, min(int(neighbors), len(points)))

    def query(xq, yq):
        q = np.column_stack([np.ravel(xq), np.ravel(yq)])
        distances, indices = tree.query(q, k=k)

        if k == 1:
            result = values[np.asarray(indices)]
            return result.reshape(np.shape(xq))

        distances = np.asarray(distances, dtype=float)
        indices = np.asarray(indices)

        exact = distances[:, 0] < eps
        safe_distances = np.where(distances < eps, eps, distances)
        weights = 1.0 / np.power(safe_distances, power)
        weighted = np.sum(weights * values[indices], axis=1) / np.sum(weights, axis=1)

        if np.any(exact):
            weighted[exact] = values[indices[exact, 0]]

        return weighted.reshape(np.shape(xq))

    return query


def make_plot(x, y, values, output_path, grid_size=180, neighbors=8, power=2.0):
    interpolator = build_idw_interpolator(x, y, values, neighbors=neighbors, power=power)

    limit = max(np.max(np.abs(x)), np.max(np.abs(y)), 1.0)
    grid_x = np.linspace(-limit, limit, grid_size)
    grid_y = np.linspace(-limit, limit, grid_size)
    X, Y = np.meshgrid(grid_x, grid_y)
    Z = interpolator(X, Y)

    fig = plt.figure(figsize=(11, 8.5))
    ax = fig.add_subplot(111, projection='3d')
    ax.plot_surface(X, Y, Z, rstride=1, cstride=1, linewidth=0, antialiased=True, alpha=0.9)
    ax.scatter(x, y, values, s=18, depthshade=True)
    ax.set_title('Reconstructed Field Surface from Sampled Points')
    ax.set_xlabel('X')
    ax.set_ylabel('Y')
    ax.set_zlabel('Field Value')
    ax.set_xlim(-limit, limit)
    ax.set_ylim(-limit, limit)
    fig.tight_layout()
    fig.savefig(output_path, dpi=180)
    plt.close(fig)


def main():
    parser = argparse.ArgumentParser(description='Visualize a scattered scalar field as a 3D surface.')
    parser.add_argument('--input', required=True, help='CSV file with columns x,y,value')
    parser.add_argument('--output', required=True, help='Output image path, e.g. field_surface.png')
    parser.add_argument('--grid-size', type=int, default=180, help='Resolution of the rendered grid')
    parser.add_argument('--neighbors', type=int, default=8, help='Number of nearest points used by IDW')
    parser.add_argument('--power', type=float, default=2.0, help='IDW distance falloff power')
    args = parser.parse_args()

    x, y, values = load_points(args.input)
    make_plot(x, y, values, args.output, grid_size=args.grid_size, neighbors=args.neighbors, power=args.power)


if __name__ == '__main__':
    main()

Generator Script

The generate_examples.py orchestrates data creation, plotting, and calling the visualizer:

#!/usr/bin/env python3
from __future__ import annotations

import argparse
import csv
import subprocess
import sys
from pathlib import Path

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np


APP_DIR = Path(__file__).parent
EXAMPLES_DIR = APP_DIR / 'examples'
VISUALIZER = APP_DIR / 'visualize_field.py'


def clear_examples_directory(path: Path) -> None:
    path.mkdir(parents=True, exist_ok=True)
    for item in path.iterdir():
        if item.is_file() or item.is_symlink():
            item.unlink()


def cluster_specs():
    return [
        {"center": (-0.55, -0.35), "value": 1.35, "n": 40, "spread": 0.10},
        {"center": (0.55, -0.15),  "value": -1.30, "n": 40, "spread": 0.10},
        {"center": (0.00, 0.60),   "value": 0.75, "n": 40, "spread": 0.09},
    ]


def generate_three_clusters(seed: int = 42):
    rng = np.random.default_rng(seed)

    xs = []
    ys = []
    zs = []

    for spec in cluster_specs():
        cx, cy = spec['center']
        x = rng.normal(cx, spec['spread'], spec['n'])
        y = rng.normal(cy, spec['spread'], spec['n'])
        z = rng.normal(spec['value'], 0.05, spec['n'])
        xs.append(np.clip(x, -1.0, 1.0))
        ys.append(np.clip(y, -1.0, 1.0))
        zs.append(z)

    return np.concatenate(xs), np.concatenate(ys), np.concatenate(zs)


def generate_zero_grid_three_clusters(seed: int = 42):
    x_clusters, y_clusters, z_clusters = generate_three_clusters(seed=seed)

    base = np.linspace(-1.0, 1.0, 10)
    X, Y = np.meshgrid(base, base)
    x0 = X.ravel()
    y0 = Y.ravel()
    z0 = np.zeros_like(x0, dtype=float)

    x = np.concatenate([x0, x_clusters])
    y = np.concatenate([y0, y_clusters])
    z = np.concatenate([z0, z_clusters])

    return x, y, z


def write_csv(x, y, z, path: Path) -> None:
    with path.open('w', newline='') as f:
        writer = csv.writer(f)
        writer.writerow(['x', 'y', 'value'])
        writer.writerows(zip(x, y, z))


def make_scatter_plot(x, y, z, output_path: Path, title: str) -> None:
    fig = plt.figure(figsize=(7, 6))
    ax = fig.add_subplot(111)
    scatter = ax.scatter(x, y, c=z, s=28)
    ax.set_title(title)
    ax.set_xlabel('X')
    ax.set_ylabel('Y')
    ax.set_xlim(-1.0, 1.0)
    ax.set_ylim(-1.0, 1.0)
    fig.colorbar(scatter, ax=ax, label='Field Value')
    fig.tight_layout()
    fig.savefig(output_path, dpi=180)
    plt.close(fig)


def run_visualizer(csv_path: Path, output_path: Path, grid_size: int, neighbors: int, power: float) -> None:
    subprocess.run(
        [
            sys.executable,
            str(VISUALIZER),
            '--input', str(csv_path),
            '--output', str(output_path),
            '--grid-size', str(grid_size),
            '--neighbors', str(neighbors),
            '--power', str(power),
        ],
        check=True,
    )


def render_example(name: str, x, y, z, grid_size: int, neighbors: int, power: float) -> None:
    csv_path = EXAMPLES_DIR / f'{name}.csv'
    scatter_path = EXAMPLES_DIR / f'{name}_scatter.png'
    surface_path = EXAMPLES_DIR / f'{name}_surface.png'

    write_csv(x, y, z, csv_path)
    make_scatter_plot(x, y, z, scatter_path, title=f'Sampled Points: {name}')
    run_visualizer(csv_path, surface_path, grid_size=grid_size, neighbors=neighbors, power=power)
    print(f'Created {csv_path.name}, {scatter_path.name}, and {surface_path.name}')


def main() -> None:
    parser = argparse.ArgumentParser(
        description='Generate example datasets, scatter plots, and reconstructed surface images.'
    )
    parser.add_argument(
        '--example',
        choices=['all', 'three_clusters', 'zero_grid_three_clusters'],
        default='all',
        help='Which example set to generate',
    )
    parser.add_argument('--cluster-seed', type=int, default=42, help='Seed for the cluster-based examples')
    parser.add_argument('--grid-size', type=int, default=180, help='Resolution of the rendered grid')
    parser.add_argument('--neighbors', type=int, default=8, help='Number of nearest points used by IDW')
    parser.add_argument('--power', type=float, default=2.0, help='IDW distance falloff power')
    args = parser.parse_args()

    clear_examples_directory(EXAMPLES_DIR)

    if args.example in ('all', 'three_clusters'):
        x, y, z = generate_three_clusters(seed=args.cluster_seed)
        render_example(
            name='three_clusters',
            x=x,
            y=y,
            z=z,
            grid_size=args.grid_size,
            neighbors=args.neighbors,
            power=args.power,
        )

    if args.example in ('all', 'zero_grid_three_clusters'):
        x, y, z = generate_zero_grid_three_clusters(seed=args.cluster_seed)
        render_example(
            name='zero_grid_three_clusters',
            x=x,
            y=y,
            z=z,
            grid_size=args.grid_size,
            neighbors=args.neighbors,
            power=args.power,
        )

    print(f'Finished. Outputs are in: {EXAMPLES_DIR}')


if __name__ == '__main__':
    main()

Download Project

Download the complete project, including Python scripts, CSV files, visualizations, and this web page:

Download field_visualization_two_examples.zip