space::cdf
cheatah-space v0.1.0-alpha — Biome Standard 0.6.5-alpha
Classes
CdfError— The exception every malformed-file path raises.File— An opened CDF.LeapSecondEntry— One row of NASA'sCDFLeapSeconds.txt, held as exact scaled integers.
Functions
The number of entries in the leap-second table.
42 as shipped; grows only when a leap second is added upstream.
O(1).
none.
systests/test_leapseconds.purrimport io
import space.cdf as cdf
io.print(cdf.leap_second_count()) # 42How many leading entries lie in the pre-1972 "rubber second" era, where TAI-UTC drifts continuously with the day rather than stepping by whole seconds.
14, matching NERA1 in NASA's own implementation.
O(1).
none.
systests/test_leapseconds.purrimport io
import space.cdf as cdf
io.print(cdf.leap_second_drift_rows()) # 14 — entries 0..13 driftOne entry of the leap-second table, by index.
index | 0-based; anything outside [0, leap_second_count()) yields a zeroed entry. |
the entry, by value.
O(1).
none.
systests/test_leapseconds.purrimport io
import space.cdf as cdf
# The most recent entry: TAI-UTC became 37 s on 2017-01-01.
let e = cdf.leap_second_entry(41)
io.print(e.year, e.month, e.day) # 2017 1 1
io.print(e.delta_at_e7) # 370000000 — 37.0 s, in units of 1e-7 sTAI-UTC in EXACT nanoseconds for a UTC calendar date.
Pure integer arithmetic. Before 1972 the offset drifts with the day, which is what the drift columns encode; the MJD at noon is a half-integer, so the drift term is evaluated at twice scale and halved by folding the 2 into the 1e-7 -> 1e-9 conversion. Nothing rounds.
This is EXACT, and NASA's library is not. NASA computes the same quantity in double and truncates toward zero, so on 430 of the 4384 days between 1960-01-01 and 1972-01-01 its answer is 1 ns lower than this one. From 1972-01-01 onward the two agree on every day, because the offset is a whole number of seconds and the drift term vanishes. Use tai_minus_utc_ns_nasa_compat() when bit-identical agreement with NASA matters more than being right; the divergence is enumerated day by day in tests/space_cdf_leapseconds_test.cpp so it stays a reviewed fact, not a surprise.
year | UTC year. |
month | UTC month, 1-12. |
day | UTC day of month. |
TAI-UTC in nanoseconds; 0 before 1960-01-01, where the table does not reach.
O(leap_second_count()) — a linear scan of 42 entries.
none.
systests/test_leapseconds.purrimport io
import space.cdf as cdf
# Since 2017-01-01, TAI runs 37 seconds ahead of UTC.
io.print(cdf.tai_minus_utc_ns(2026, 8, 28)) # 37000000000TAI-UTC in nanoseconds, computed the way NASA's CDF library computes it.
A faithful reproduction of the reference implementation: accumulate in double, then truncate toward zero. It exists so bit-identical agreement with NASA is available when that is what is wanted — and so the difference between the two is a tested, enumerated fact rather than a suspicion.
year | UTC year. |
month | UTC month, 1-12. |
day | UTC day of month. |
TAI-UTC in nanoseconds as NASA would compute it; 0 before 1960-01-01.
O(leap_second_count()) — a linear scan of 42 entries.
none.
systests/test_leapseconds.purrimport io
import space.cdf as cdf
# Identical to the exact form from 1972 onward; both are 37 s in 2026.
io.print(cdf.tai_minus_utc_ns_nasa_compat(2026, 8, 28)) # 37000000000Where the table comes from.
NASA's published leap-second table URL.
O(1).
none.
systests/test_leapseconds.purrimport io
import space.cdf as cdf
io.print(cdf.leap_seconds_url())The Updated: stamp carried inside the upstream file itself.
the upstream stamp, YYYYMMDD.
O(1).
none.
systests/test_leapseconds.purrimport io
import space.cdf as cdf
io.print(cdf.leap_seconds_upstream_updated()) # 20161025SHA-256 of the NORMALIZED upstream table — comments and blank lines dropped, each remaining line's whitespace collapsed and trimmed.
Normalizing means a reformat or an edited comment raises no false alarm, while any change to the data does.
the 64-character lowercase hex digest.
O(1).
none.
systests/test_leapseconds.purrimport io
import space.cdf as cdf
# scripts/check_leapseconds.sh recomputes this from the live file and compares.
io.print(cdf.leap_seconds_sha256())The date this copy of the table was last checked against upstream.
the verification date.
O(1).
none.
systests/test_leapseconds.purrimport io
import space.cdf as cdf
let d = cdf.leap_seconds_verified_on()
io.print(d.year, d.month, d.day) # 2026 8 28The date through which this table is guaranteed COMPLETE.
Derived, not guessed: IERS announces a leap second at least six months ahead in Bulletin C. Upstream carried no entry after 2017-01-01 when we checked on leap_seconds_verified_on(), so none can occur before six months after that date. Past this date the table may be merely current rather than provably complete — ask leap_seconds_covers() before trusting a conversion of a future timestamp.
the last date the table is provably complete through.
O(1).
none.
systests/test_leapseconds.purrimport io
import space.cdf as cdf
let d = cdf.leap_seconds_known_good_through()
io.print(d.year, d.month, d.day) # 2027 2 28Whether a date falls in the range this table is provably complete for.
A conversion outside it is not necessarily wrong — it is merely unverifiable, because a leap second announced after leap_seconds_verified_on() would not be in this copy. Callers converting future timestamps should branch on this rather than take a silently stale answer.
year | UTC year. |
month | UTC month, 1-12. |
day | UTC day of month. |
true when the date is on or before leap_seconds_known_good_through().
O(1).
none.
systests/test_leapseconds.purrimport io
import space.cdf as cdf
io.print(cdf.leap_seconds_covers(2026, 8, 28)) # True
io.print(cdf.leap_seconds_covers(2099, 1, 1)) # False — beyond the guaranteeOpen a CDF by path.
Maps the file and parses every descriptor. Only CDF 3.x single-file, uncompressed-container, IEEE-encoded files are accepted; anything else is refused here with the ErrorCode that says why, so a later read cannot fail for a reason that was knowable at open.
path | the |
the opened file.
O(v) in the number of variables; no record data is read.
one descriptor list and name index; the file itself is memory-mapped, not read.
systests/test_cdf_read.purrimport io
import space.cdf as cdf
let f = cdf.open("omni_hro2_1min_20150101_v01.cdf")
io.print(len(cdf.var_names(f))) # 47Open a CDF from bytes already in memory.
The bytes are copied and owned by the File. This is the path every hostile-input test uses: ASan sees each byte of a heap buffer, but cannot see an over-read that stays inside a mapped page, so a bounds bug is only reliably caught here.
data | the file's bytes. |
size | how many. |
the opened file.
O(size) to copy, then O(v) in the number of variables.
one copy of the bytes, plus the descriptor list and name index.
systests/test_cdf_read.purrimport io
import space.cdf as cdf
# From purr, prefer cdf.open(path); this entry point exists for tests and embedders.
let f = cdf.open("omni_hro2_1min_20150101_v01.cdf")
io.print(cdf.record_count(f, "Epoch")) # 44640The names of every variable in the file, rVariables first, in file order.
f | an opened file. |
the names.
O(v).
one list of v strings.
systests/test_cdf_read.purrimport io
import space.cdf as cdf
let f = cdf.open("omni_hro2_1min_20150101_v01.cdf")
io.print(cdf.var_names(f)[0]) # EpochHow many records a variable has.
f | an opened file. |
name | the variable. |
the record count; 0 for a variable with nothing written.
O(1) after the name lookup.
none.
systests/test_cdf_read.purrimport io
import space.cdf as cdf
let f = cdf.open("omni_hro2_1min_20150101_v01.cdf")
io.print(cdf.record_count(f, "F")) # 44640 — one day at one-minute cadenceThe CDF data type of a variable, by its format name.
f | an opened file. |
name | the variable. |
e.g. "CDF_REAL4", "CDF_EPOCH", "CDF_TIME_TT2000".
O(1) after the name lookup.
one short string.
systests/test_cdf_read.purrimport io
import space.cdf as cdf
let f = cdf.open("omni_hro2_1min_20150101_v01.cdf")
io.print(cdf.data_type_name(f, "Epoch")) # CDF_EPOCHThe shape values() will return for a variable.
Records first, then each dimension that varies per record, then one trailing axis when a value is itself several elements — the string length of a CHAR variable, or 2 for EPOCH16.
f | an opened file. |
name | the variable. |
the shape.
O(d) in the number of dimensions.
one short list.
systests/test_cdf_read.purrimport io
import space.cdf as cdf
let f = cdf.open("omni_hro2_1min_20150101_v01.cdf")
io.print(cdf.shape(f, "F")) # [44640] — a scalar per recordinline ::cheatah::ndarray::basic_ndarray< double > values(const File &f, const std::string &name)
#
Every record of a variable, as float64.
Lossless for every CDF type except CDF_INT8 and CDF_TIME_TT2000, which a double cannot hold exactly; those are refused with ErrorCode::LossyConversion — use values_i64(). CHAR data comes back as byte values with the string length as the last axis; EPOCH16 as a trailing axis of 2. This is the array cheatah-plot draws directly.
f | an opened file. |
name | the variable. |
an ndarray shaped as shape() describes. A copy of the file's data.
O(n) in the number of values — one decode pass.
the result array, exactly once; nothing intermediate.
systests/test_cdf_read.purrimport io
import ndarray
import space.cdf as cdf
let f = cdf.open("omni_hro2_1min_20150101_v01.cdf")
let b = cdf.values(f, "F") # IMF magnitude, nT, one value per minute
io.print(b[0], b[1], b[2]) # 6.92 5.84 5.71The whole point of returning an ndarray: it goes straight into cheatah-plot.
import ndarray
import space.cdf as cdf
import plot
import plot.figure as figure
let f = cdf.open("omni_hro2_1min_20150101_v01.cdf")
let t = cdf.values(f, "Epoch")
let b = cdf.values(f, "F")
# Rebase onto hours: cheatah-plot has no time axis, and a raw CDF_EPOCH of 6.3e13 ms would
# render as one repeated tick label.
let n = ndarray.size_of(b)
let hours = ndarray.zeros([n])
for i in range(n) { hours[i] = (t[i] - t[0]) / 3600000.0 }
let fig = figure.line(figure.new_figure(), hours, b)
fig = figure.ylabel(fig, "|B| (nT)")
plot.save(fig, "omni_imf.png")inline ::cheatah::ndarray::basic_ndarray< long long > values_i64(const File &f, const std::string &name)
#
Every record of an integer variable, as int64 — the exact path for CDF_INT8 and TT2000.
Accepts every integer type; refuses REAL4/REAL8/EPOCH/EPOCH16 with ErrorCode::LossyConversion because truncating a float is not a conversion anyone asked for.
f | an opened file. |
name | the variable. |
an ndarray of int shaped as shape() describes. A copy of the file's data.
O(n) in the number of values — one decode pass.
the result array, exactly once; nothing intermediate.
systests/test_cdf_read.purrimport io
import ndarray
import space.cdf as cdf
# TT2000 is nanoseconds since J2000; an int64 holds it exactly, a double does not.
let f = cdf.open("rbsp-a_density_emfisis-l4_20130101_v1.5.17.cdf")
let t = cdf.values_i64(f, "Epoch")
io.print(t[0])Constants & variables
First magic number of a CDF 3.x file, at byte offset 0.
First magic number of a CDF 2.6/2.7 file — the 32-bit-offset generation.
First magic number of a pre-2.6 file. Not supported; see ErrorCode::UnsupportedPreV26.
Second magic number: the file's records are stored uncompressed.
Second magic number: the whole file is compressed and a CCR follows the magic.
Byte offset of the CDR, which always follows the two 4-byte magic numbers.
Bytes a checksummed file carries past GDR.eof — an MD5 digest, not a CDF record.
True when this build targets a little-endian host.
CDF stores no encoding meaning "native", so the Host selector has to be resolved against the machine actually doing the reading.
CDR flags bit 0: set when the file's majority is row-major.
CDR flags bit 1: set when all data lives in this one file rather than .v*/.z* companions.
CDR flags bit 2: set when the file carries a trailing checksum.
CDR flags bit 3: set when that checksum is MD5. Meaningful only with kCdrFlagChecksum.
How many ErrorCode enumerators exist, including None.
The unit tests assert one crafted failure per code against this, so a new code cannot be added without a test that provokes it.
Types
The internal record types, as written in the 4-byte big-endian field at record offset 8.
UnusedInternal is -1 and marks a hole left by a rewrite; such records are skipped, never parsed. Every other value is a real record with a defined layout.
The CDF data types.
The numbering is the format's, not ours — see the file docs for why REAL4/FLOAT and REAL8/DOUBLE are distinct values with identical layouts.
The byte orders and floating-point formats a CDF's values may be stored in.
"NETWORK" is not XDR despite the name — it is plain big-endian IEEE-754. That is worth stating because assuming otherwise invents a whole conversion layer that is not needed.
The four ways a CDF's bytes can actually need decoding.
Twenty-one encodings collapse to four decode behaviours, which is the single simplification that keeps the decoder small: what matters is integer byte order and whether floats are IEEE or one of the two VAX formats.
How a multi-dimensional variable's values are laid out within one record.
What a variable's unwritten records contain.
The compression algorithms CDF defines. Only Gzip occurs in the public archive.
Everything that can be wrong with a CDF we are asked to read.
One flat enumeration rather than exception subtypes: a caller almost always wants to report what happened and where, not to branch on the kind, and a closed set can be table-tested for reachability — every code below is provoked by a crafted file in the unit tests, so none of them is aspirational.
