Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/netcdf4/README.md
Original file line number Diff line number Diff line change
@@ -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.
67 changes: 67 additions & 0 deletions examples/netcdf4/create_and_read_dataset/code.py
Original file line number Diff line number Diff line change
@@ -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"<pre>{ocean}</pre>"), append=True)

heading("Reading variables back")
note(
"netCDF variables behave like NumPy arrays. Slicing returns a NumPy "
"(masked) array, and attributes like <code>units</code> are accessible "
"as Python attributes."
)
display(HTML(
f"<p><b>sst.units:</b> {sst.units}<br>"
f"<b>sst.shape:</b> {sst.shape}<br>"
f"<b>First 6 hours:</b> {np.round(sst[:6], 3).tolist()}<br>"
f"<b>Mean SST:</b> {sst[:].mean():.3f} °C</p>"
), append=True)

ocean.close()
1 change: 1 addition & 0 deletions examples/netcdf4/create_and_read_dataset/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["netcdf4", "numpy"]
36 changes: 36 additions & 0 deletions examples/netcdf4/create_and_read_dataset/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)
85 changes: 85 additions & 0 deletions examples/netcdf4/groups_and_multidim_grids/code.py
Original file line number Diff line number Diff line change
@@ -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"<pre>{climate}</pre>"), append=True)
display(HTML(f"<pre>{observations}</pre>"), 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()
1 change: 1 addition & 0 deletions examples/netcdf4/groups_and_multidim_grids/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["netcdf4", "numpy", "matplotlib"]
17 changes: 17 additions & 0 deletions examples/netcdf4/groups_and_multidim_grids/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)
60 changes: 60 additions & 0 deletions examples/netcdf4/in_memory_roundtrip/code.py
Original file line number Diff line number Diff line change
@@ -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"<pre>station_id = {reopened.station_id}</pre>"), 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"<pre>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}%</pre>"
), 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()
1 change: 1 addition & 0 deletions examples/netcdf4/in_memory_roundtrip/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["netcdf4", "numpy"]
17 changes: 17 additions & 0 deletions examples/netcdf4/in_memory_roundtrip/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)
5 changes: 5 additions & 0 deletions examples/netcdf4/order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
"create_and_read_dataset",
"groups_and_multidim_grids",
"in_memory_roundtrip"
]