From 89e4ff604bb82cb1b7e78ee85dc902e03e4a76da Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Fri, 29 May 2026 15:47:11 +0100 Subject: [PATCH 1/2] Add PyScript examples for netcdf4 Generated by apply_llm_response.py from prompts/netcdf4/response.toml. Examples included: - create_and_read_dataset: Create and read a netCDF dataset - groups_and_multidim_grids: Groups, multi-dimensional grids, and time decoding - in_memory_roundtrip: In-memory round-trip: bytes in, Dataset out Generated-By: apply_llm_response.py --- examples/netcdf4/README.md | 18 +++++ .../netcdf4/create_and_read_dataset/code.py | 61 +++++++++++++++ .../create_and_read_dataset/config.toml | 1 + .../netcdf4/create_and_read_dataset/setup.py | 42 ++++++++++ .../netcdf4/groups_and_multidim_grids/code.py | 78 +++++++++++++++++++ .../groups_and_multidim_grids/config.toml | 1 + .../groups_and_multidim_grids/setup.py | 24 ++++++ examples/netcdf4/in_memory_roundtrip/code.py | 54 +++++++++++++ .../netcdf4/in_memory_roundtrip/config.toml | 1 + examples/netcdf4/in_memory_roundtrip/setup.py | 23 ++++++ examples/netcdf4/order.json | 5 ++ 11 files changed, 308 insertions(+) create mode 100644 examples/netcdf4/README.md create mode 100644 examples/netcdf4/create_and_read_dataset/code.py create mode 100644 examples/netcdf4/create_and_read_dataset/config.toml create mode 100644 examples/netcdf4/create_and_read_dataset/setup.py create mode 100644 examples/netcdf4/groups_and_multidim_grids/code.py create mode 100644 examples/netcdf4/groups_and_multidim_grids/config.toml create mode 100644 examples/netcdf4/groups_and_multidim_grids/setup.py create mode 100644 examples/netcdf4/in_memory_roundtrip/code.py create mode 100644 examples/netcdf4/in_memory_roundtrip/config.toml create mode 100644 examples/netcdf4/in_memory_roundtrip/setup.py create mode 100644 examples/netcdf4/order.json diff --git a/examples/netcdf4/README.md b/examples/netcdf4/README.md new file mode 100644 index 0000000..6d2afe9 --- /dev/null +++ b/examples/netcdf4/README.md @@ -0,0 +1,18 @@ +# netcdf4 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/netcdf4/create_and_read_dataset/code.py b/examples/netcdf4/create_and_read_dataset/code.py new file mode 100644 index 0000000..69766fa --- /dev/null +++ b/examples/netcdf4/create_and_read_dataset/code.py @@ -0,0 +1,61 @@ +""" +A first look at netCDF4: write a small dataset to memory, then read it back. + +netCDF4 is the Python interface to the netCDF C library, the de-facto file +format for atmospheric, oceanographic, and climate data. A netCDF file is a +self-describing container of named dimensions, variables, and attributes. + +Docs: https://unidata.github.io/netcdf4-python/ +""" +from IPython.core.display import display, HTML + +# We use diskless=True so the file lives in browser memory rather than +# touching a real filesystem. With persist=False the bytes vanish on close. +ocean = Dataset("ocean_buoy.nc", mode="w", diskless=True, persist=False, + format="NETCDF4") + +# Global attributes describe the dataset as a whole. +ocean.title = "Synthetic ocean buoy readings" +ocean.institution = "PyScript Demo" +ocean.summary = "Hourly sea surface temperature for a single buoy." + +# Dimensions are named axes. "time" is unlimited so we can append to it. +ocean.createDimension("time", None) + +# Each dimension typically has a coordinate variable of the same name. +times = ocean.createVariable("time", "f8", ("time",)) +times.units = "hours since 2026-01-01 00:00:00" +times.calendar = "gregorian" +times.long_name = "Time of observation" + +# A data variable with a units attribute and an explicit fill value. +sst = ocean.createVariable("sst", "f4", ("time",), fill_value=-999.0) +sst.units = "degrees_C" +sst.long_name = "Sea surface temperature" + +# Write 48 hours of data. Assigning to a slice grows an unlimited dimension. +n_hours = 48 +hours = np.arange(n_hours, dtype="f8") +temperature = 14.5 + 0.8 * np.sin(hours * 2 * np.pi / 24) + rng.normal(0, 0.2, n_hours) + +times[:] = hours +sst[:] = temperature.astype("f4") + +heading("A netCDF dataset, summarized") +note("Printing the Dataset object gives a structured summary of its contents.") +display(HTML(f"
{ocean}
"), append=True) + +heading("Reading variables back") +note( + "netCDF variables behave like NumPy arrays. Slicing returns a NumPy " + "(masked) array, and attributes like units are accessible " + "as Python attributes." +) +display(HTML( + f"

sst.units: {sst.units}
" + f"sst.shape: {sst.shape}
" + f"First 6 hours: {np.round(sst[:6], 3).tolist()}
" + f"Mean SST: {sst[:].mean():.3f} °C

" +), append=True) + +ocean.close() diff --git a/examples/netcdf4/create_and_read_dataset/config.toml b/examples/netcdf4/create_and_read_dataset/config.toml new file mode 100644 index 0000000..ed8e8a7 --- /dev/null +++ b/examples/netcdf4/create_and_read_dataset/config.toml @@ -0,0 +1 @@ +packages = ["netcdf4", "numpy"] diff --git a/examples/netcdf4/create_and_read_dataset/setup.py b/examples/netcdf4/create_and_read_dataset/setup.py new file mode 100644 index 0000000..51b9911 --- /dev/null +++ b/examples/netcdf4/create_and_read_dataset/setup.py @@ -0,0 +1,42 @@ +"""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) + + +import numpy as np +from netCDF4 import Dataset + +rng = np.random.default_rng(0) diff --git a/examples/netcdf4/groups_and_multidim_grids/code.py b/examples/netcdf4/groups_and_multidim_grids/code.py new file mode 100644 index 0000000..769c35f --- /dev/null +++ b/examples/netcdf4/groups_and_multidim_grids/code.py @@ -0,0 +1,78 @@ +# --------------------------------------------------------------------- +# Multi-dimensional data, hierarchical groups, and CF-style time axes. +# --------------------------------------------------------------------- +# +# Real-world climate files often contain a (time, lat, lon) grid plus +# coordinate variables, organized into groups (like folders inside the +# file). Here we build a tiny synthetic temperature grid and use +# `num2date` to turn the numeric time axis into real datetimes. + +heading("A gridded temperature field with groups") + +climate = Dataset("climate.nc", mode="w", diskless=True, persist=False, + format="NETCDF4") +climate.Conventions = "CF-1.8" + +# Define the spatial and temporal axes at the root. +n_time, n_lat, n_lon = 12, 9, 18 +climate.createDimension("time", n_time) +climate.createDimension("lat", n_lat) +climate.createDimension("lon", n_lon) + +t = climate.createVariable("time", "f8", ("time",)) +t.units = "days since 2026-01-01 00:00:00" +t.calendar = "standard" +t[:] = np.arange(n_time) * 30 # roughly monthly + +lat = climate.createVariable("lat", "f4", ("lat",)) +lat.units = "degrees_north" +lat[:] = np.linspace(-80, 80, n_lat) + +lon = climate.createVariable("lon", "f4", ("lon",)) +lon.units = "degrees_east" +lon[:] = np.linspace(-170, 170, n_lon) + +# Groups behave like sub-datasets; great for keeping observations and +# model output side by side in one file. +observations = climate.createGroup("observations") +temp = observations.createVariable( + "air_temperature", "f4", ("time", "lat", "lon"), + compression="zlib", complevel=4, least_significant_digit=2, +) +temp.units = "degrees_C" +temp.long_name = "Near-surface air temperature" + +# Build a field that's warmer near the equator and drifts over the year. +lat_grid = lat[:][None, :, None] +month_phase = np.arange(n_time)[:, None, None] * 2 * np.pi / 12 +field = ( + 25 * np.cos(np.deg2rad(lat_grid)) + - 5 + + 3 * np.sin(month_phase) + + rng.normal(0, 0.5, size=(n_time, n_lat, n_lon)) +) +temp[:] = field.astype("f4") + +note("The file's structure, including the nested group:") +display(HTML(f"
{climate}
"), append=True) +display(HTML(f"
{observations}
"), append=True) + +# Decode the numeric time axis into Python/cftime datetimes. +dates = num2date(t[:], units=t.units, calendar=t.calendar) +note(f"First three time steps decoded: {[str(d)[:10] for d in dates[:3]]}") + +# Average across longitude to get a zonal-mean Hovmöller diagram. +zonal_mean = temp[:].mean(axis=2) # shape (time, lat) + +fig, ax = plt.subplots(figsize=(8, 4)) +mesh = ax.pcolormesh( + np.arange(n_time), lat[:], zonal_mean.T, cmap="RdBu_r", shading="auto", +) +ax.set_xlabel("Month index") +ax.set_ylabel("Latitude (°N)") +ax.set_title("Zonal-mean air temperature (°C)") +fig.colorbar(mesh, ax=ax, label="°C") +fig.tight_layout() +display(fig, append=True) + +climate.close() diff --git a/examples/netcdf4/groups_and_multidim_grids/config.toml b/examples/netcdf4/groups_and_multidim_grids/config.toml new file mode 100644 index 0000000..05c9898 --- /dev/null +++ b/examples/netcdf4/groups_and_multidim_grids/config.toml @@ -0,0 +1 @@ +packages = ["netcdf4", "numpy", "matplotlib"] diff --git a/examples/netcdf4/groups_and_multidim_grids/setup.py b/examples/netcdf4/groups_and_multidim_grids/setup.py new file mode 100644 index 0000000..b1a892e --- /dev/null +++ b/examples/netcdf4/groups_and_multidim_grids/setup.py @@ -0,0 +1,24 @@ +"""Setup for example 2: same names as cell 1, no IPython shim.""" +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) + + +import numpy as np +import matplotlib.pyplot as plt +from netCDF4 import Dataset, num2date + +rng = np.random.default_rng(0) diff --git a/examples/netcdf4/in_memory_roundtrip/code.py b/examples/netcdf4/in_memory_roundtrip/code.py new file mode 100644 index 0000000..c15eafe --- /dev/null +++ b/examples/netcdf4/in_memory_roundtrip/code.py @@ -0,0 +1,54 @@ +# --------------------------------------------------------------------- +# Round-trip a netCDF dataset through a bytes buffer. +# --------------------------------------------------------------------- +# +# When a Dataset is opened with mode="w" and a `memory` size hint, calling +# .close() returns a memoryview holding the serialized file. Those bytes +# can be sent over the network, stored in a database, or re-opened as a +# read-only Dataset by passing them back via the `memory` kwarg. + +heading("Writing a netCDF file straight into a bytes buffer") + +# Step 1: build a tiny weather station record in memory. +station = Dataset("station.nc", mode="w", memory=4096, format="NETCDF4") +station.station_id = "PSY-001" + +station.createDimension("obs", 24) +hour = station.createVariable("hour", "i4", ("obs",)) +hour.units = "hour of day" +hour[:] = np.arange(24) + +humidity = station.createVariable("humidity", "f4", ("obs",)) +humidity.units = "percent" +humidity[:] = (60 + 15 * np.sin(np.arange(24) * 2 * np.pi / 24) + + rng.normal(0, 2, 24)).clip(0, 100).astype("f4") + +# .close() with memory= returns a memoryview of the serialized dataset. +buffer = station.close() +raw_bytes = bytes(buffer) +note(f"Serialized dataset is {len(raw_bytes):,} bytes " + f"(starts with magic {raw_bytes[:4]!r}).") + +# Step 2: open those bytes again as a read-only Dataset. +reopened = Dataset("station.nc", mode="r", memory=raw_bytes) + +heading("Inspecting the round-tripped dataset") +note("Global attributes survive the round trip:") +display(HTML(f"
station_id = {reopened.station_id}
"), append=True) + +# Use ncattrs() and __dict__ to introspect attributes programmatically. +hum_var = reopened.variables["humidity"] +attrs = {name: hum_var.getncattr(name) for name in hum_var.ncattrs()} +display(HTML( + f"
humidity attributes: {attrs}\n"
+    f"humidity dtype:      {hum_var.dtype}\n"
+    f"humidity shape:      {hum_var.shape}\n"
+    f"first 6 values:      {np.round(hum_var[:6], 2).tolist()}\n"
+    f"daily mean:          {hum_var[:].mean():.2f}%
" +), append=True) + +# get_variables_by_attributes is a handy way to find variables by metadata. +percent_vars = reopened.get_variables_by_attributes(units="percent") +note(f"Variables measured in percent: {[v.name for v in percent_vars]}") + +reopened.close() diff --git a/examples/netcdf4/in_memory_roundtrip/config.toml b/examples/netcdf4/in_memory_roundtrip/config.toml new file mode 100644 index 0000000..ed8e8a7 --- /dev/null +++ b/examples/netcdf4/in_memory_roundtrip/config.toml @@ -0,0 +1 @@ +packages = ["netcdf4", "numpy"] diff --git a/examples/netcdf4/in_memory_roundtrip/setup.py b/examples/netcdf4/in_memory_roundtrip/setup.py new file mode 100644 index 0000000..adeee16 --- /dev/null +++ b/examples/netcdf4/in_memory_roundtrip/setup.py @@ -0,0 +1,23 @@ +"""Setup for example 3: same names as cell 1, no IPython shim.""" +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) + + +import numpy as np +from netCDF4 import Dataset + +rng = np.random.default_rng(0) diff --git a/examples/netcdf4/order.json b/examples/netcdf4/order.json new file mode 100644 index 0000000..18c45e9 --- /dev/null +++ b/examples/netcdf4/order.json @@ -0,0 +1,5 @@ +[ + "create_and_read_dataset", + "groups_and_multidim_grids", + "in_memory_roundtrip" +] From f7b28de4deef0a672a0e0009384f107cdc1edbf8 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Thu, 4 Jun 2026 18:02:41 +0100 Subject: [PATCH 2/2] Fix imports. --- examples/netcdf4/create_and_read_dataset/code.py | 6 ++++++ examples/netcdf4/create_and_read_dataset/setup.py | 6 ------ examples/netcdf4/groups_and_multidim_grids/code.py | 7 +++++++ examples/netcdf4/groups_and_multidim_grids/setup.py | 7 ------- examples/netcdf4/in_memory_roundtrip/code.py | 6 ++++++ examples/netcdf4/in_memory_roundtrip/setup.py | 6 ------ 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/examples/netcdf4/create_and_read_dataset/code.py b/examples/netcdf4/create_and_read_dataset/code.py index 69766fa..5fc860b 100644 --- a/examples/netcdf4/create_and_read_dataset/code.py +++ b/examples/netcdf4/create_and_read_dataset/code.py @@ -9,6 +9,12 @@ """ from IPython.core.display import display, HTML +import numpy as np +from netCDF4 import Dataset + +rng = np.random.default_rng(0) + + # We use diskless=True so the file lives in browser memory rather than # touching a real filesystem. With persist=False the bytes vanish on close. ocean = Dataset("ocean_buoy.nc", mode="w", diskless=True, persist=False, diff --git a/examples/netcdf4/create_and_read_dataset/setup.py b/examples/netcdf4/create_and_read_dataset/setup.py index 51b9911..5986b1d 100644 --- a/examples/netcdf4/create_and_read_dataset/setup.py +++ b/examples/netcdf4/create_and_read_dataset/setup.py @@ -34,9 +34,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - - -import numpy as np -from netCDF4 import Dataset - -rng = np.random.default_rng(0) diff --git a/examples/netcdf4/groups_and_multidim_grids/code.py b/examples/netcdf4/groups_and_multidim_grids/code.py index 769c35f..7301132 100644 --- a/examples/netcdf4/groups_and_multidim_grids/code.py +++ b/examples/netcdf4/groups_and_multidim_grids/code.py @@ -7,6 +7,13 @@ # file). Here we build a tiny synthetic temperature grid and use # `num2date` to turn the numeric time axis into real datetimes. +import numpy as np +import matplotlib.pyplot as plt +from netCDF4 import Dataset, num2date + +rng = np.random.default_rng(0) + + heading("A gridded temperature field with groups") climate = Dataset("climate.nc", mode="w", diskless=True, persist=False, diff --git a/examples/netcdf4/groups_and_multidim_grids/setup.py b/examples/netcdf4/groups_and_multidim_grids/setup.py index b1a892e..270c130 100644 --- a/examples/netcdf4/groups_and_multidim_grids/setup.py +++ b/examples/netcdf4/groups_and_multidim_grids/setup.py @@ -15,10 +15,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - - -import numpy as np -import matplotlib.pyplot as plt -from netCDF4 import Dataset, num2date - -rng = np.random.default_rng(0) diff --git a/examples/netcdf4/in_memory_roundtrip/code.py b/examples/netcdf4/in_memory_roundtrip/code.py index c15eafe..89c1ba2 100644 --- a/examples/netcdf4/in_memory_roundtrip/code.py +++ b/examples/netcdf4/in_memory_roundtrip/code.py @@ -7,6 +7,12 @@ # can be sent over the network, stored in a database, or re-opened as a # read-only Dataset by passing them back via the `memory` kwarg. +import numpy as np +from netCDF4 import Dataset + +rng = np.random.default_rng(0) + + heading("Writing a netCDF file straight into a bytes buffer") # Step 1: build a tiny weather station record in memory. diff --git a/examples/netcdf4/in_memory_roundtrip/setup.py b/examples/netcdf4/in_memory_roundtrip/setup.py index adeee16..e3a1fe5 100644 --- a/examples/netcdf4/in_memory_roundtrip/setup.py +++ b/examples/netcdf4/in_memory_roundtrip/setup.py @@ -15,9 +15,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - - -import numpy as np -from netCDF4 import Dataset - -rng = np.random.default_rng(0)