Resolving a polygon boundary

A concave polygon with slanted edges makes rasterization error visible. All lengths are in nm. delta limits the raster-cell size, not the size of the final cuboids: merging adjacent occupied cells into larger cuboids preserves the rasterized geometry exactly. Refining delta improves the staircase approximation along oblique edges. The previous axis-aligned L-shape could be represented exactly by two large cuboids; a small cuboid count alone does not imply a coarse approximation.

import numpy as np

from microcubed import cuboidize

polygon = np.array([(0, 0), (120, 15), (95, 65), (55, 45), (35, 115), (-15, 80)])
thickness = 20
magnetization = [0, 0, 8e5]
delta = 0.5
shape = cuboidize(polygon, t=thickness, delta=delta, mag=magnetization)
field = shape.Bfield([50, 50, -60])
print(f"Raster spacing: {delta} nm; merged cuboids: {len(shape)}")
print("Field at (50, 50, -60) nm (T):", field.ravel())
Raster spacing: 0.5 nm; merged cuboids: 248
Field at (50, 50, -60) nm (T): [-0.00920275 -0.00701831  0.04365604]

Compare boundary resolution

The orange dashed curve is the input polygon. Blue rectangles are projected cuboids, including their internal boundaries. Compare the staircase error at 10 nm, 2 nm, and 0.5 nm; large rectangles in the interior are an exact compression of occupied cells, not a loss of resolution.

import matplotlib.pyplot as plt
from matplotlib.patches import Polygon

closed_polygon = np.vstack([polygon, polygon[0]])
fig, axes = plt.subplots(1, 3, figsize=(12, 4.5), layout="constrained")
for ax, spacing in zip(axes, (10, 2, delta)):
    model = shape if spacing == delta else cuboidize(polygon, t=thickness, delta=spacing, mag=magnetization)
    for cuboid in model:
        ax.add_patch(
            Polygon(cuboid.union_boundary("xy")[0].T, facecolor="#dbeafe", edgecolor="#2563eb", linewidth=0.35)
        )
    ax.plot(*closed_polygon.T, color="#c2410c", linestyle="--", linewidth=1.5, label="Input polygon")
    ax.set(
        xlim=(-25, 130),
        ylim=(-10, 125),
        aspect="equal",
        xlabel="x (nm)",
        ylabel="y (nm)",
        title=f"delta = {spacing:g} nm; {len(model)} cuboids",
    )
    ax.legend(fontsize=8, loc="lower right")
plt.show()
../_images/95c3c5df3eec279c8e760a0a9bbfb0dd44e4359d27a818acf69951fd07e39161.png

Check field convergence

Evaluate identical exterior points for every resolution. The reference uses delta = 0.25 nm; it is a finer raster approximation, not an exact polygon solution. The relative error below is the RMS vector-field difference divided by the reference RMS vector-field magnitude. Raster errors need not decrease monotonically at every step. Area error and field error measure different properties; repeat this check at the observation distances needed for your application.

probe_x = np.linspace(-25, 130, 81)
probes = np.vstack([probe_x, np.full_like(probe_x, 45), np.full_like(probe_x, -60)])
reference = cuboidize(polygon, t=thickness, delta=0.25, mag=magnetization).Bfield(probes)
reference_norm = np.linalg.norm(reference)
polygon_area = (
    abs(np.dot(polygon[:, 0], np.roll(polygon[:, 1], -1)) - np.dot(polygon[:, 1], np.roll(polygon[:, 0], -1))) / 2
)
errors = []
print("delta (nm) | cuboids | area error (%) | relative field error (%)")
for spacing in (10, 5, 2, 1, delta):
    model = shape if spacing == delta else cuboidize(polygon, t=thickness, delta=spacing, mag=magnetization)
    values = model.Bfield(probes)
    area = sum(np.prod(m.size.ravel()[:2]) for m in model)
    error = np.linalg.norm(values - reference) / reference_norm
    errors.append(error)
    print(
        f"{spacing:10g} | {len(model):7d} | {100 * abs(area - polygon_area) / polygon_area:14.5f} | {100 * error:24.5f}"
    )
assert np.isfinite(field).all() and np.isfinite(reference).all()
assert np.isfinite(shape.dBfield(probes)).all()
assert errors[-1] < errors[0], "The refined geometry should improve this exterior-field comparison."
fig, ax = plt.subplots(figsize=(6, 3.5), layout="constrained")
ax.loglog((10, 5, 2, 1, delta), np.array(errors) * 100, "o-")
ax.set(xlabel="Raster spacing (nm)", ylabel="Relative field error (%)", title="Compared with the 0.25 nm raster")
ax.grid(which="both", alpha=0.2)
plt.show()
delta (nm) | cuboids | area error (%) | relative field error (%)
        10 |      11 |        2.86697 |                  1.99932
         5 |      23 |        0.00000 |                  0.17036
         2 |      56 |        0.29891 |                  0.30652
         1 |     121 |        0.00000 |                  0.00513
       0.5 |     248 |        0.00000 |                  0.00102
../_images/2e6b39d1637ef49bc9af89cf770302d89299de7c6bc6a74f4f3f24f039bec241.png

Refined field map and material boundary

The field is sampled on a 201 × 201 grid at z = -60 nm, below the 20 nm-thick structure. This sampling resolution is independent of the source raster spacing. Thin black lines show the cuboid decomposition; the orange outline marks the input polygon. The cyan union_boundary("xy") follows the actual rasterized material, retaining the notch. It removes internal cuboid edges and returns separate closed loops for disconnected pieces and holes. All outlines are XY projections onto the map.

import matplotlib.pyplot as plt

fig, ax = shape.plot_2d(x=(-40, 145, 201), y=(-25, 140, 201), z=-60, component="z")
for index, cuboid in enumerate(shape):
    ax.plot(
        *cuboid.union_boundary("xy")[0],
        color="black",
        linewidth=0.3,
        alpha=0.35,
        label="Projected cuboids" if index == 0 else None,
    )
ax.plot(*np.vstack([polygon, polygon[0]]).T, color="#ff9500", linewidth=1.5, label="Input polygon")
for index, boundary in enumerate(shape.union_boundary("xy")):
    ax.plot(*boundary, color="#00ffff", linewidth=1.5, label="Union boundary" if index == 0 else None)
ax.set(xlabel="x (nm)", ylabel="y (nm)", title="Bz (T) at z = -60 nm; delta = 0.5 nm", aspect="equal")
ax.legend(loc="upper right", fontsize=7, facecolor="#555555", labelcolor="white", framealpha=0.95)
fig.tight_layout()
plt.show()
../_images/c8291a8f229ecdac99710f9a1a7e8a1bb79eed0ef499a755d46bd4ea42c24fc5.png