Starting from a master CDF¶
Most missions don’t build their data files from scratch. They start from a master CDF: a file with every variable and every attribute already defined, but no data. Filling a master is the easiest way to produce files that always have the same, correct structure.
With pycdfpp, a master is an ordinary CDF file. The recipe is always the same:
Load the master.
Fill its variables with your data.
Update the global attributes that change from file to file.
Save under the data file’s name.
What is in a master¶
NASA publishes the masters of every dataset of its archive in the SPDF 0MASTERS folder. Instrument teams usually keep their own. Here is the master of the ACE magnetometer data used throughout this documentation:
import urllib.request
import numpy as np
import pycdfpp
url = "https://spdf.gsfc.nasa.gov/pub/software/cdawlib/0MASTERS/ac_h0_mfi_00000000_v01.cdf"
urllib.request.urlretrieve(url, "ac_h0_mfi_00000000_v01.cdf")
master = pycdfpp.load("ac_h0_mfi_00000000_v01.cdf")
for name, var in master.items():
print(f"{name:15} {str(var.shape):12} {var.type.name:12} nrv={var.is_nrv}")
Epoch (0, 1) CDF_EPOCH nrv=False
Magnitude (0, 1) CDF_REAL4 nrv=False
BGSEc (0, 3) CDF_REAL4 nrv=False
label_BGSE (1, 3, 6) CDF_CHAR nrv=True
...
Two kinds of variables:
Variables that change with time have 0 records. They wait for your data.
Non-record-varying variables (
nrv=True) are already filled: labels, energy tables, and other constants. Leave them as they are.
Every variable already has its attributes: units, fill value, valid range, the
DEPEND_0 link to the time variable, and so on. You don’t have to set any of them.
Filling the variables¶
Use set_values on each variable to fill. Here, one minute of made-up data, one sample
every 16 seconds:
time = np.arange("2024-01-01", "2024-01-01T00:01", np.timedelta64(16, "s"),
dtype="datetime64[ns]")
field = np.random.default_rng(0).normal(0.0, 5.0, size=(len(time), 3))
cdf = pycdfpp.load("ac_h0_mfi_00000000_v01.cdf")
cdf["Epoch"].set_values(time)
cdf["BGSEc"].set_values(field.astype(cdf["BGSEc"].values.dtype))
cdf["Magnitude"].set_values(np.linalg.norm(field, axis=1).astype(np.float32))
A few rules:
Time: pass
datetime64values. They are converted to the master’s time type,CDF_EPOCHhere.Numbers must have the master’s type.
BGSEcisCDF_REAL4(float32), sofloat64values are refused.var.values.dtypegives the right numpy type, even for an empty variable:data.astype(var.values.dtype).Shapes must match the records.
BGSEcrecords hold 3 values, so give it an(N, 3)array. Scalars likeMagnitudeare declared with records of shape(1,)in this master; a plain 1-D array of N values is fine.Every variable with a
DEPEND_0needs as many records as its time variable.
Note
You don’t need force=True to fill a master: its variables are empty, so nothing is
overwritten. Keep force=True for when you really mean to change a variable’s type
or shape.
Variables you don’t fill¶
A master often defines more variables than one file needs. Variables you leave empty are
saved empty, which ISTP tools accept. To leave them out of the file instead, remove them
with pycdfpp.CDF.filter():
cdf.filter(variables=lambda v: v.is_nrv or len(v) > 0, inplace=True)
Updating the global attributes¶
Some global attributes change with each file: its identifier, its version, when it was
made. Set them with set_values:
from datetime import date
cdf.attributes["Logical_file_id"].set_values(["ac_h0_mfi_20240101_v01"])
cdf.attributes["Data_version"].set_values(["1"])
cdf.attributes["Generation_date"].set_values([date.today().strftime("%Y%m%d")])
Attributes that don’t exist in the master can be added with add_attribute, as in
Writing files.
Saving¶
Save under the data file’s name. Never save over the master: you will need it for the next file.
pycdfpp.save(cdf, "ac_h0_mfi_20240101_v01.cdf")
check = pycdfpp.load("ac_h0_mfi_20240101_v01.cdf")
print(check["BGSEc"].shape, check["BGSEc"].attributes["UNITS"].value) # (4, 3) nT
The compression of each variable comes from the master too. Here, BGSEc is saved
with gzip, as the master asks.
Producing many files¶
Load the master again for each file. A fresh load is fast (only the structure is read) and guarantees that nothing from the previous file leaks into the next one:
def produce(day, time, field):
cdf = pycdfpp.load("ac_h0_mfi_00000000_v01.cdf")
cdf["Epoch"].set_values(time)
cdf["BGSEc"].set_values(field.astype(np.float32))
cdf["Magnitude"].set_values(np.linalg.norm(field, axis=1).astype(np.float32))
cdf.attributes["Logical_file_id"].set_values([f"ac_h0_mfi_{day}_v01"])
pycdfpp.save(cdf, f"ac_h0_mfi_{day}_v01.cdf")
produce("20240101", time, field)
Finally, check the result against the ISTP conventions, as explained in Producing ISTP-compliant files.