From 42b5f6ecd5a1233758b7ec8f9a350907781800cc Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Fri, 29 May 2026 15:47:00 +0100 Subject: [PATCH] Add PyScript examples for affine Generated by apply_llm_response.py from prompts/affine/response.toml. Examples included: - transform_basics: Affine transform basics - transforming_a_shape: Transforming a shape - raster_pixel_to_world: Raster pixel-to-world coordinates Generated-By: apply_llm_response.py --- examples/affine/README.md | 18 +++++ examples/affine/order.json | 5 ++ examples/affine/raster_pixel_to_world/code.py | 75 +++++++++++++++++++ .../affine/raster_pixel_to_world/config.toml | 1 + .../affine/raster_pixel_to_world/setup.py | 21 ++++++ examples/affine/transform_basics/code.py | 58 ++++++++++++++ examples/affine/transform_basics/config.toml | 1 + examples/affine/transform_basics/setup.py | 40 ++++++++++ examples/affine/transforming_a_shape/code.py | 59 +++++++++++++++ .../affine/transforming_a_shape/config.toml | 1 + examples/affine/transforming_a_shape/setup.py | 21 ++++++ 11 files changed, 300 insertions(+) create mode 100644 examples/affine/README.md create mode 100644 examples/affine/order.json create mode 100644 examples/affine/raster_pixel_to_world/code.py create mode 100644 examples/affine/raster_pixel_to_world/config.toml create mode 100644 examples/affine/raster_pixel_to_world/setup.py create mode 100644 examples/affine/transform_basics/code.py create mode 100644 examples/affine/transform_basics/config.toml create mode 100644 examples/affine/transform_basics/setup.py create mode 100644 examples/affine/transforming_a_shape/code.py create mode 100644 examples/affine/transforming_a_shape/config.toml create mode 100644 examples/affine/transforming_a_shape/setup.py diff --git a/examples/affine/README.md b/examples/affine/README.md new file mode 100644 index 0000000..3489eea --- /dev/null +++ b/examples/affine/README.md @@ -0,0 +1,18 @@ +# affine Examples + +Each sub-directory contains a self-contained example. The order in +which the examples are to appear is specified in `order.json` (an +array of directory names in the expected order). + +In each example directory you'll find: + +* `config.toml` - must conform to the specification outlined here: + https://docs.pyscript.net/latest/user-guide/configuration/ This is + parsed and ultimately turned into a JSON representation as part of + the package's API object. +* `setup.py` - Python code for contextual and environmental setup, + NOT SEEN BY THE END USER, but is run before the `code.py` code is + evaluated. Allows us to create useful (IPython) shims, avoid + repeating boilerplate and whatnot. +* `code.py` - the actual code added to the editor which forms the + practical example of using the package. diff --git a/examples/affine/order.json b/examples/affine/order.json new file mode 100644 index 0000000..f30e1aa --- /dev/null +++ b/examples/affine/order.json @@ -0,0 +1,5 @@ +[ + "transform_basics", + "transforming_a_shape", + "raster_pixel_to_world" +] diff --git a/examples/affine/raster_pixel_to_world/code.py b/examples/affine/raster_pixel_to_world/code.py new file mode 100644 index 0000000..17b791e --- /dev/null +++ b/examples/affine/raster_pixel_to_world/code.py @@ -0,0 +1,75 @@ +# --------------------------------------------------------------------- +# The classic GIS use case: mapping raster pixels to world coordinates +# using a GDAL-style geotransform, then mapping back with the inverse. +# --------------------------------------------------------------------- + +heading("Pixel ↔ world coordinates with from_gdal") +note( + "Georeferenced rasters carry a 6-number GDAL geotransform: " + "(x_origin, x_pixel_size, x_skew, y_origin, y_skew, " + "y_pixel_size). Affine.from_gdal turns that " + "into an Affine you can use to translate between (column, row) " + "pixel indices and (x, y) world coordinates." +) + +# A made-up geotransform: 425-meter pixels, north-up, with origin +# in the upper-left (note the negative y pixel size). +geotransform = (-237481.5, 425.0, 0.0, 237536.4, 0.0, -425.0) +pixel_to_world = Affine.from_gdal(*geotransform) +world_to_pixel = ~pixel_to_world + +note("Forward transform (pixel → world):") +display(pixel_to_world, append=True) + +# A few points of interest on a 200x200 raster. +points_of_interest = { + "upper-left corner": (0, 0), + "center": (100, 100), + "lower-right corner": (200, 200), +} + +note("Pixel centers mapped to world coordinates:") +for label, (col, row) in points_of_interest.items(): + world_x, world_y = pixel_to_world * (col + 0.5, row + 0.5) + note( + f"{label} pixel ({col}, {row}) → " + f"world ({world_x:.1f}, {world_y:.1f})" + ) + +# Going the other way: which pixel contains a given world coordinate? +target_world = (-150_000.0, 100_000.0) +col_f, row_f = world_to_pixel * target_world +note( + f"World point {target_world} falls in pixel " + f"(col={int(col_f)}, row={int(row_f)})." +) + +# Visualize the raster footprint and the points we computed. +fig, ax = plt.subplots(figsize=(7, 6)) + +# Outline of the full 200x200 raster in world coordinates. +corners_pixel = [(0, 0), (200, 0), (200, 200), (0, 200), (0, 0)] +corners_world = [pixel_to_world * c for c in corners_pixel] +fx, fy = zip(*corners_world) +ax.plot(fx, fy, color="black", linewidth=1.5, label="Raster footprint") +ax.fill(fx, fy, color="lightyellow", alpha=0.5) + +# Plot the points of interest. +for label, (col, row) in points_of_interest.items(): + wx, wy = pixel_to_world * (col + 0.5, row + 0.5) + ax.plot(wx, wy, "o", markersize=8) + ax.annotate(label, (wx, wy), xytext=(8, 8), + textcoords="offset points", fontsize=9) + +# And the target world point. +ax.plot(*target_world, "x", color="red", markersize=12, + markeredgewidth=2, label="Target world point") + +ax.set_aspect("equal") +ax.set_xlabel("World X (m)") +ax.set_ylabel("World Y (m)") +ax.set_title("200×200 raster footprint in world coordinates") +ax.legend(loc="lower left") +ax.grid(True, linestyle=":", alpha=0.6) +fig.tight_layout() +display(fig, append=True) diff --git a/examples/affine/raster_pixel_to_world/config.toml b/examples/affine/raster_pixel_to_world/config.toml new file mode 100644 index 0000000..13c4e27 --- /dev/null +++ b/examples/affine/raster_pixel_to_world/config.toml @@ -0,0 +1 @@ +packages = ["affine", "matplotlib"] diff --git a/examples/affine/raster_pixel_to_world/setup.py b/examples/affine/raster_pixel_to_world/setup.py new file mode 100644 index 0000000..6586ca4 --- /dev/null +++ b/examples/affine/raster_pixel_to_world/setup.py @@ -0,0 +1,21 @@ +"""Lightweight setup for a later notebook cell.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display(*args, **kwargs, target=__pyscript_display_target__) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +from affine import Affine +import matplotlib.pyplot as plt diff --git a/examples/affine/transform_basics/code.py b/examples/affine/transform_basics/code.py new file mode 100644 index 0000000..c0c1ef1 --- /dev/null +++ b/examples/affine/transform_basics/code.py @@ -0,0 +1,58 @@ +""" +A first look at the `affine` package: building 2D affine transforms +and applying them to points. + +An Affine matrix represents a 2D linear transformation followed by a +translation. You compose them with `*`, apply them to points with `*`, +and invert them with `~`. Documentation: https://github.com/rasterio/affine +""" +from IPython.core.display import display, HTML +from affine import Affine + +heading("1. Building transforms with the class methods") +note( + "The most common way to make an Affine is via one of the named " + "constructors: identity, translation, " + "scale, rotation (degrees), and " + "shear. Printing one shows the top two rows of the " + "augmented 3x3 matrix." +) + +identity = Affine.identity() +shift = Affine.translation(10.0, 5.0) +zoom = Affine.scale(2.0) +spin = Affine.rotation(30.0) # 30 degrees, counter-clockwise + +for name, transform in [ + ("identity", identity), + ("translation(10, 5)", shift), + ("scale(2.0)", zoom), + ("rotation(30°)", spin), +]: + note(f"{name}") + display(transform, append=True) + +heading("2. Applying a transform to a point") +note( + "Multiplying an Affine by an (x, y) tuple gives the " + "transformed point. Here we move the point (1, 1) by (10, 5)." +) +moved = shift * (1.0, 1.0) +note(f"shift * (1, 1) = {moved}") + +heading("3. Composing and inverting transforms") +note( + "Transforms compose with *. Read right-to-left: the " + "rightmost transform is applied first. The inverse is written " + "~, and a transform times its inverse is the identity." +) +spin_then_shift = shift * spin +note("shift * spin (rotate first, then translate):") +display(spin_then_shift, append=True) + +inverse = ~spin_then_shift +roundtrip = inverse * spin_then_shift * (3.0, 4.0) +note( + f"Round-trip of (3, 4) through the transform and its inverse: " + f"({roundtrip[0]:.6f}, {roundtrip[1]:.6f})" +) diff --git a/examples/affine/transform_basics/config.toml b/examples/affine/transform_basics/config.toml new file mode 100644 index 0000000..13c4e27 --- /dev/null +++ b/examples/affine/transform_basics/config.toml @@ -0,0 +1 @@ +packages = ["affine", "matplotlib"] diff --git a/examples/affine/transform_basics/setup.py b/examples/affine/transform_basics/setup.py new file mode 100644 index 0000000..a31c46d --- /dev/null +++ b/examples/affine/transform_basics/setup.py @@ -0,0 +1,40 @@ +"""Shim setup for the first example. Includes the full IPython shim.""" +import sys +import types +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +ipython = types.ModuleType("IPython") +core = types.ModuleType("IPython.core") +core_display = types.ModuleType("IPython.core.display") +core_display.display = display +core_display.HTML = HTML +ipython.core = core +core.display = core_display +ipython.get_ipython = lambda: None +ipython.display = core_display +sys.modules["IPython"] = ipython +sys.modules["IPython.core"] = core +sys.modules["IPython.core.display"] = core_display +sys.modules["IPython.display"] = core_display + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +from affine import Affine +import matplotlib.pyplot as plt diff --git a/examples/affine/transforming_a_shape/code.py b/examples/affine/transforming_a_shape/code.py new file mode 100644 index 0000000..d408f98 --- /dev/null +++ b/examples/affine/transforming_a_shape/code.py @@ -0,0 +1,59 @@ +# --------------------------------------------------------------------- +# Visualizing how a composed transform reshapes a polygon. +# --------------------------------------------------------------------- + +heading("Transforming a house-shaped polygon") +note( + "We define a small polygon resembling a house, then build a " + "composed transform that scales it, rotates it, and shifts it. " + "Applying the transform is just a list comprehension over the " + "vertices." +) + +# Vertices of a simple house outline (closed polygon). +house = [ + (0.0, 0.0), + (2.0, 0.0), + (2.0, 1.5), + (1.0, 2.5), + (0.0, 1.5), + (0.0, 0.0), +] + +# Compose: scale up, rotate 25 degrees, then translate to (4, 1). +# Remember: the rightmost transform is applied first. +transform = ( + Affine.translation(4.0, 1.0) + * Affine.rotation(25.0) + * Affine.scale(1.5) +) + +note("The composed transform:") +display(transform, append=True) + +# Apply the transform to every vertex. +transformed = [transform * point for point in house] + +note("First three transformed vertices:") +for original, new in list(zip(house, transformed))[:3]: + nx, ny = new + note(f"({original[0]}, {original[1]}) → ({nx:.3f}, {ny:.3f})") + +# Plot the original and transformed polygons side by side. +fig, ax = plt.subplots(figsize=(7, 5)) + +orig_x, orig_y = zip(*house) +new_x, new_y = zip(*transformed) + +ax.fill(orig_x, orig_y, alpha=0.3, color="steelblue", label="Original") +ax.plot(orig_x, orig_y, color="steelblue", linewidth=2) + +ax.fill(new_x, new_y, alpha=0.3, color="darkorange", label="Transformed") +ax.plot(new_x, new_y, color="darkorange", linewidth=2) + +ax.set_aspect("equal") +ax.grid(True, linestyle=":", alpha=0.6) +ax.set_title("Scale 1.5x, rotate 25°, translate to (4, 1)") +ax.legend(loc="upper left") +fig.tight_layout() +display(fig, append=True) diff --git a/examples/affine/transforming_a_shape/config.toml b/examples/affine/transforming_a_shape/config.toml new file mode 100644 index 0000000..13c4e27 --- /dev/null +++ b/examples/affine/transforming_a_shape/config.toml @@ -0,0 +1 @@ +packages = ["affine", "matplotlib"] diff --git a/examples/affine/transforming_a_shape/setup.py b/examples/affine/transforming_a_shape/setup.py new file mode 100644 index 0000000..6586ca4 --- /dev/null +++ b/examples/affine/transforming_a_shape/setup.py @@ -0,0 +1,21 @@ +"""Lightweight setup for a later notebook cell.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display(*args, **kwargs, target=__pyscript_display_target__) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +from affine import Affine +import matplotlib.pyplot as plt