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..5fc860b --- /dev/null +++ b/examples/netcdf4/create_and_read_dataset/code.py @@ -0,0 +1,67 @@ +""" +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 + +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, + 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..5986b1d --- /dev/null +++ b/examples/netcdf4/create_and_read_dataset/setup.py @@ -0,0 +1,36 @@ +"""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) 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..7301132 --- /dev/null +++ b/examples/netcdf4/groups_and_multidim_grids/code.py @@ -0,0 +1,85 @@ +# --------------------------------------------------------------------- +# 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. + +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, + 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..270c130 --- /dev/null +++ b/examples/netcdf4/groups_and_multidim_grids/setup.py @@ -0,0 +1,17 @@ +"""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) diff --git a/examples/netcdf4/in_memory_roundtrip/code.py b/examples/netcdf4/in_memory_roundtrip/code.py new file mode 100644 index 0000000..89c1ba2 --- /dev/null +++ b/examples/netcdf4/in_memory_roundtrip/code.py @@ -0,0 +1,60 @@ +# --------------------------------------------------------------------- +# 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. + +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. +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..e3a1fe5 --- /dev/null +++ b/examples/netcdf4/in_memory_roundtrip/setup.py @@ -0,0 +1,17 @@ +"""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) 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" +]