DataCanon
DataCanon is a data model for structured scientific data with deterministic,
language-independent identity.
Scientific data often outlives the software that created it. A calculation may
move between programming languages, be stored in several formats, and remain
in use through years of development. DataCanon defines what the data contains
and when two instances are equal, independently of how it is stored or displayed.
Each DataCanon Instance (DCI) belongs to exactly one DataCanon Type (DCT).
Its type and information determine its identity. Every valid DCI has one
canonical byte encoding; equal DCIs have identical encodings across conforming
implementations. SHA-256 of those bytes provides a deterministic fingerprint.
Store, recover, and compare data
The Python reference implementation constructs DCIs from instances of supported
conventional types, writes the DCIs to disk, and reconstructs them with their
DataCanon identity intact. Conventional types include built-in types, library
types, and user-defined types used directly for data and computation.
Canonical binary files (.dcb) contain the complete canonical encoding.
Human-readable Shorthand, YAML interchange, and HDF5 provide other
representations of the same data. Shorthand can mix concise mathematical and
collection notation with explicit constructors. Its longhand form uses
constructors for structured values while retaining ordinary literals such as
1, 1.0, and "hello". HDF5 stores Arrays as native numerical
datasets and can embed externally referenced payloads.
Text layout and storage choices do not determine DCI identity. A change from a
List to a Tuple does: those are different DCTs even when their items agree.
Rigidly structured scientific data
DataCanon supports exact numerical structure alongside finite machine numbers.
Fractions, Sums, Products, OrderedProducts, Powers, Quotients, and Complex values can represent
quantities such as
1/2 * sqrt(3)
quotient(1, sqrt(3) + sqrt(2))
complex(1/2, 1/2 * sqrt(3))
A Fraction and a Power within a Product can exactly represent the irrational
number in the first expression. The second preserves a structured numerator
and denominator. Complex components retain their individual DCTs.
Lists, Tuples, Sets, Multisets, ordered and unordered Mappings, and named
Records provide different ways to organize data. Multisets preserve repeated
members without assigning semantic order. Arrays retain their portable type,
shape, and exact finite element representations, including signed zero.
Bytes contains an opaque byte sequence directly in a DCI. Applications can
place interpretation metadata alongside it in a Record. ExternalFile and
ExternalArray instead preserve a relative path and content digest. Their
metadata can be read without retrieving the external bytes; retrieval and
integrity verification are separate operations.
Identity and conventional types
DataCanon defines structured data, not mathematical equivalence. Integer 1
and Float 1.0 have different identities. Numerical nesting, repeated terms,
and the constituent information prescribed by each DCT are preserved.
Conversion interfaces connect DCIs with instances of conventional types in a
given programming language. Python examples include list, Fraction, and
NumPy arrays. A List DCI represented by a ListDC object and a conventional
list object belong to different types with different equality and operation
rules. Both are Python objects.
Where suitable conventional types are missing, the Python implementation
supplies additional types such as Power, Sum, and Multiset. The numerical
conventional types support arithmetic independently of DCI construction and
persistence. Their operations can simplify structure; conversion through
conventional objects is not guaranteed to retain an arbitrary DCI’s identity.
The DataCanon data model defines no such arithmetic operations.
Optional Shorthand preprocessing supplies compact Lists, textual substitutions,
and explicit scalar eval(...) before strict parsing. It is an input facility,
not an extension of DCI identity.
Using this documentation
- Getting Started: construct data, write binary files, and explore the other representations.
- Guides: conventional types, command-line operations, and Shorthand preprocessing, explained step by step.
- Python API: conversion tables, conventional types, rendering, and persistence.
- Reference: the 24 DCTs, their structure, and representation choices.
- Specifications: seven Version 1 documents, including the optional Conventional Types companion.
Version 1 remains a draft. Outstanding interoperability and conformance items
are recorded in the individual specifications.
1 - Getting Started
Scientific data moves between programs, readable input files, and storage
formats. A different file layout need not mean different data; a changed Array
type or coordinate structure can mean different data even when the numbers look
the same.
DataCanon gives structured scientific data a precisely defined identity,
independent of its programming language or storage representation. Each
DataCanon Instance (DCI) has one unique canonical byte encoding. A fingerprint
derived from those bytes lets you identify the data across representations.
This guide constructs a dataset, identifies it, stores and inspects it, then
shows how to author scientific input directly.
Installation
Clone the DataCanon code repository, then run these commands from its root:
python3 -m venv DC
source DC/bin/activate
python -m pip install .
The Makefile also provides environment setup, editable installation, and tests.
Construct and identify a DCI
Start with a Python dictionary containing a Tuple key, a List of species,
a coordinate Tuple, and a numerical Array:
import numpy as np
from data_canon import as_dc_instance, fingerprint
data = {
("Eg", 0): 2.37,
"species": ["Sr", "Ti", "O"],
"origin": (0.0, 0.0, 0.0),
"lattice": np.eye(3, dtype="float64"),
}
instance = as_dc_instance(data)
identifier = fingerprint(instance)
print(identifier)
Output:
eb8f306c801ca4452e283f052b51a6834a846399cc3aac43720fb7cf48faeca4
The whole dataset is one DCI: a Mapping containing other DCIs. Mapping keys can
be structured values, including the Tuple ("Eg", 0).
Python’s dict, tuple, and NumPy arrays are conventional types used by the
application. Conversion constructs a DCI whose information and equality rules
are defined by DataCanon, independently of those Python types.
The fingerprint is SHA-256 of the DCI’s canonical bytes, written as 64 lowercase
hexadecimal characters. You can record it alongside a calculation to identify
its input data. It is derived from the data’s defined identity, not from the
memory address of a Python object or the layout of a text file.
Type participates in identity. Replacing the origin Tuple with a List changes
the DCI even though its coordinates have the same numerical values:
changed_data = dict(data)
changed_data["origin"] = [0.0, 0.0, 0.0]
changed_instance = as_dc_instance(changed_data)
assert changed_instance != instance
assert fingerprint(changed_instance) != identifier
Array type and shape also participate in identity. Equal DCIs have identical
canonical bytes and fingerprints; DCI equality itself is defined by the data
model, not by comparing fingerprints.
Write a DCI to disk as binary data
Use .dcb to store the canonical bytes directly:
from data_canon import read, write
write(instance, filename="output_data.dcb")
restored = read("output_data.dcb")
assert restored == instance
assert fingerprint(restored) == identifier
The file is binary, not text. Reading reconstructs the same DCI, including
its types, Array shape, and element values. DataCanon decoding reconstructs
DCIs without invoking application-defined code.
Render a DCI as Shorthand
Shorthand is a human-readable representation of a DCI:
from data_canon import parse_shorthand, render_shorthand
text = render_shorthand(instance)
print(text)
Output:
{
( "Eg", 0 ): 2.37,
"species": [ "Sr", "Ti", "O" ],
"origin": ( 0.0, 0.0, 0.0 ),
"lattice":
array(
[[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0]]
)
}
Parsing the text recovers the same identity:
parsed = parse_shorthand(text)
assert parsed == instance
assert fingerprint(parsed) == identifier
Text layout is presentation. Canonical bytes determine the fingerprint, so
spacing does not become part of the data’s identity.
Inspect a DCI via the command line
Installing the package also installs dc. With the environment active, display
the binary file as the Shorthand shown above:
Inspect particular values without writing a Python script:
dc output_data.dcb --get '("Eg", 0)'
dc output_data.dcb --get species --index 0
Output:
Print the same fingerprint calculated in Python:
dc output_data.dcb --fingerprint
The CLI guide covers searches, selection,
and format transfers in more detail.
Preserve identity through YAML and HDF5
The same DCI can be represented in other formats without changing its identity.
YAML uses native values where faithful and a $ marker for embedded strict
Shorthand, including Tuple keys and Array values:
from data_canon import parse_yaml, render_yaml
yaml_text = render_yaml(instance)
print(yaml_text)
Output:
$ ( "Eg", 0 ): 2.37
species:
- Sr
- Ti
- O
origin: $ ( 0.0, 0.0, 0.0 )
lattice: |-
$
array(
[[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0]]
)
from_yaml = parse_yaml(yaml_text)
assert from_yaml == instance
assert fingerprint(from_yaml) == identifier
For numerical storage, HDF5 represents Arrays as native numerical datasets:
write(instance, filename="output_data.h5")
from_hdf5 = read("output_data.h5")
assert from_hdf5 == instance
assert fingerprint(from_hdf5) == identifier
Use .dcs with the same write() and read() functions to store Shorthand
text. The .dcb, Shorthand, YAML, and HDF5 representations recover equal DCIs
and therefore the same canonical bytes and fingerprint. Hashing the YAML or
HDF5 file bytes directly would instead identify that particular file
representation.
Author scientific data directly
So far, we have constructed data in Python. DataCanon also lets us author
structured scientific data directly in Shorthand:
scientific_input = parse_shorthand("""
{
"position": (1/2, 1/4, 0),
"phase": complex(1/2, 1/2 * sqrt(3))
}
""")
The position contains exact Fractions. The Product 1/2 * sqrt(3) exactly
represents an irrational quantity, and the Complex retains the structure of
its real and imaginary components.
DataCanon does not do math. Parsing constructs and preserves Fraction,
Power, Product, and Complex DCIs. Applications can convert DCIs to instances
of conventional types for computation; DC identity belongs to the stored data
structure.
Make scientific input easier to maintain
Optional Shorthand preprocessing lets an input file use named parameters.
An excerpt of a crystal-structure input illustrates the idea: a parameter
controls several coordinates while exact reference positions remain visible.
source = """
u = 0.305 |
{
"basis_atoms": {
"O": [
(u, u, 0),
(1/2 + u, 1/2 + -u, 1/2)
]
}
}
"""
authored = parse_shorthand(source, preprocess=True)
write(authored, filename="structure.dcb")
Preprocessing substitutes u before strict parsing. The resulting DCI stores
the coordinate values and their numerical structure; the parameter definition
is an input convenience. The same identity and persistence operations now
apply to this authored data.
The CLI enables preprocessing for .dcs input files. The
Shorthand Preprocessing guide
develops substitutions, compact Lists, and explicit eval(...) separately,
and includes a complete crystal-structure example.
Next steps
2 - Guides
The guides develop individual features step by step:
- Identity and Fingerprinting: identify calculation inputs, distinguish data changes from presentation changes, and associate results with their inputs.
- Persistence and External Resources: write complete packages, retrieve verified payloads, and transfer compressed external Arrays between directories and HDF5.
- Conventional Types: distinguish conventional objects from DCIs, use numerical and collection operations, and understand conversion.
- Custom Conversion Rules: define application-owned Record conventions and recursively convert conventional objects, using dates as an example.
- Command-Line Interface: inspect files, transfer formats, search nested mappings, and compute fingerprints.
- Shorthand Preprocessing: compact Lists, substitutions, and scalar
eval(...).
Start with Getting Started for binary
persistence, a first conversion example, and the command-line interface.
The Reference describes DCT structure and
identity; conventional objects follow their own type-specific rules.
2.1 - Identity and Fingerprinting
A calculation’s inputs may appear in a readable text file, a binary archive,
and several programming languages. Comparing the files tells you whether their
bytes agree. DataCanon lets you ask a different question: do the representations
describe the same structured data?
This guide identifies a calculation’s inputs, distinguishes changes in data
from changes in presentation, and uses fingerprints to associate results with
their inputs. Start with Getting Started
for installation and the first persistence workflow.
Consider an application that evaluates a response at several sampling points:
import numpy as np
from data_canon import as_dc_instance, canonical_encode, fingerprint
inputs = {
"model": "response-v1",
"temperature": 300.0,
"sampling_points": np.array(
[[0.0, 0.0, 0.0], [0.5, 0.0, 0.0]],
dtype="float64",
),
("solver", "tolerance"): 1e-8,
}
instance = as_dc_instance(inputs)
input_id = fingerprint(instance)
The DCI identifies the whole input structure: its keys, values, types, Array
shape, and Mapping entry order. The fingerprint is SHA-256 of the canonical
encoding, returned as 64 lowercase hexadecimal characters.
Canonical encoding preserves all distinctions in DCI identity. Equal DCIs have
identical canonical bytes; unequal DCIs have different canonical bytes. SHA-256
compresses those bytes to a fixed-size identifier. Fingerprint equality is a
practical identification mechanism based on collision resistance, not the
definition of DCI equality.
Change the presentation without changing the data
Render the input using different layouts, then parse both versions:
from data_canon import parse_shorthand, render_shorthand
readable = render_shorthand(instance)
single_line = render_shorthand(instance, layout="inline")
first = parse_shorthand(readable)
second = parse_shorthand(single_line)
assert first == second == instance
assert canonical_encode(first) == canonical_encode(second)
assert fingerprint(first) == fingerprint(second) == input_id
Whitespace and layout belong to the text representation. They do not change the
DCI. The same principle applies when the DCI is stored in HDF5 rather than
Shorthand: the physical file bytes can differ while the reconstructed data has
the same identity.
Hash a file’s bytes when you need to identify that exact file. Fingerprint its
DCI when you need to identify its DataCanon data across representations.
Recognize changes that matter
A change in type can reveal an unintended change in a calculation pipeline:
changed_inputs = dict(inputs)
changed_inputs["sampling_points"] = inputs["sampling_points"].astype("float32")
changed = as_dc_instance(changed_inputs)
assert changed != instance
assert fingerprint(changed) != input_id
All coordinates in this example are exactly representable in both Array types.
The type change still changes the DCI. DataCanon retains a distinction that a
comparison of numerical values alone could miss.
Other identity rules follow each DCT’s contract:
| Change | Effect on identity |
|---|
| Replace a List with a Tuple containing the same items | Changes identity |
| Reshape an Array while keeping the same element stream | Changes identity |
| Reorder Mapping entries or Record fields | Changes identity |
| Reorder Set members or UnorderedMapping entries | Preserves identity |
| Replace positive Float zero with negative Float zero | Changes identity |
| Change a relative path in an external reference | Changes identity |
| Move a complete package to another directory | Preserves identity |
Choose the DCT that expresses the application’s intended distinctions. If
association order should not matter, an UnorderedMapping expresses that choice
explicitly. The Reference summarizes all DCT
identity rules.
Choose what the identifier covers
An input identifier covers exactly the information placed in its DCI. Adding a
run timestamp changes that DCI even when the calculation inputs stay the same.
Keep operational metadata separate when it should not affect input identity:
run_record = {
"input_fingerprint": input_id,
"inputs": instance,
"operator_note": "Convergence study, second run",
}
run_instance = as_dc_instance(run_record)
The input DCI remains available unchanged inside the run record. The run record
has its own identity, which also covers the note.
Application conventions determine what is required to reproduce a calculation.
Include a model version, solver settings, or other dependencies when they affect
the result. DataCanon cannot infer missing information or decide that two
different parameterizations describe the same physical system. An application
can perform its own normalization before constructing a DCI when it wants such
equivalences to share an identifier.
Records provide named structures for application conventions. See
Custom Conversion Rules for
mapping an application’s classes to explicit Record structures.
A fingerprint is useful as a lookup key for a result cache or provenance record:
results_by_input = {
input_id: {"response": 12.5, "converged": True},
}
result = results_by_input[fingerprint(instance)]
assert result["converged"]
For a cache to be valid, its key must cover every dependency that can affect the
cached result. If a code version or random seed matters, include it in the
identified inputs or in a larger cache-key DCI.
Within one Python process, DCIs can also be dictionary keys directly:
point = as_dc_instance(np.array([0.5, 0.0, 0.0], dtype="float64"))
responses = {point: 12.5}
same_point = as_dc_instance(np.array([0.5, 0.0, 0.0], dtype="float64"))
assert responses[same_point] == 12.5
All DCIs are Python-hashable, including Array and List DCIs. Python’s hash()
supports dictionary lookup; use fingerprint() for a portable identifier to
store or communicate between processes and implementations. Preserve DCI
immutability when using a DCI as a key.
External content has a separate integrity check
An ExternalFile or ExternalArray DCI includes a relative path and a declared
content digest. Its fingerprint covers that metadata. Constructing the DCI or
computing its fingerprint does not retrieve the external bytes.
Materialization verifies the retrieved content against the declared digest.
Missing or corrupt content prevents materialization without changing the
metadata DCI’s identity. The
Persistence and External Resources guide
develops that workflow, including complete packages and cross-format transfer.
For the formal encoding and fingerprint contract, see
DataCanon Core.
2.2 - Persistence and External Resources
A scientific dataset often combines a small description with a large numerical
Array or an existing file. DataCanon can store the data together or preserve an
explicit reference to external content. The DCI retains its identity as a
complete dataset moves between a directory and an HDF5 container.
This guide constructs a dataset with an external Array, writes a complete
package, retrieves its numerical contents, and transfers the package. The
Identity and Fingerprinting guide
explains what its identifier covers.
Decide what belongs inside the DCI
An Array DCI contains its numerical elements. An ExternalArray DCI contains
the information needed to identify and interpret an external element stream:
a relative path, portable Array type, shape, and SHA-256 content digest.
| Requirement | DCT |
|---|
| Keep numerical elements inside the DCI | Array |
| Refer to a separately stored numerical element stream | ExternalArray |
| Refer to an opaque file’s exact bytes | ExternalFile |
| Keep a small opaque byte sequence inside the DCI | Bytes |
Array and ExternalArray are distinct DCTs. Converting an Array into an external
reference changes the DCI. Moving an existing ExternalArray’s payload between
supported storage representations can preserve its identity.
Construct a dataset with an external Array
Force-constant matrices provide a useful example: a calculation may produce a
large matrix with many zeros, accompanied by relatively small descriptive data.
Use a small matrix here to keep the example easy to run:
import numpy as np
from data_canon import ExternalArray, as_dc_instance, fingerprint
forces = np.zeros((32, 32), dtype="float64")
forces[0, 0] = 2.5
forces[0, 1] = -0.0
dataset = as_dc_instance({
"description": "Force-constant example",
"force_constants": ExternalArray(
forces,
filename="forces",
compression=True,
),
})
dataset_id = fingerprint(dataset)
The conventional ExternalArray object supplies the numerical data during
conversion. The resulting ExternalArray DCI records the path
("external_dc", "forces.bin.gz"), type float64, shape (32, 32), and content
digest. The implementation retains the supplied data as a nonsemantic cache so
the writer can persist it.
The content digest covers the uncompressed canonical Array element bytes.
Those bytes contain only elements: no type, shape, or DCI header. A .bin or
.bin.gz payload is therefore different from a .dcb file, which contains one
complete canonical DCI encoding.
Write a complete directory package
from pathlib import Path
from data_canon import read, write
source = Path("force_example") / "dataset.dcs"
write(dataset, filename=source)
The writer creates the Shorthand representation at force_example/dataset.dcs
and the compressed payload at
force_example/external_dc/forces.bin.gz. Together, the representation and all
referenced payloads form a complete package.
Use dataset.dcb instead to store the DCI as canonical binary bytes. Both
directory-backed formats resolve relative paths beneath the representation’s
containing directory by default.
The current Python write() verifies and persists required resources. It does
not expose a metadata-only export option. Missing resources or conflicting
contents can cause a write to fail. An existing external file with conflicting
contents is not silently overwritten.
Read metadata before retrieving content
restored = read(source)
assert restored == dataset
assert fingerprint(restored) == dataset_id
Reading reconstructs the DCI hierarchy without materializing external payloads.
The dataset’s metadata can be read even if the payload is unavailable. The
reader retains the representation’s source location as nonsemantic context for
later retrieval.
Converting the restored dataset to conventional objects requests the external
Array’s numerical contents:
from data_canon import dc_instance_to_python
conventional = dc_instance_to_python(restored)
loaded_forces = conventional["force_constants"]
assert np.array_equal(loaded_forces, forces)
assert np.signbit(loaded_forces[0, 1])
Materialization decompresses the payload, verifies its digest, and validates its
type, shape, required byte length, and element validity. It returns a NumPy
array; the original DCI remains an ExternalArray DCI.
A missing file, malformed compressed stream, incorrect digest, or invalid
element stream causes retrieval to fail. The metadata DCI and its fingerprint
remain unchanged.
Transfer the package to HDF5
container = Path("force_example") / "dataset.h5"
write(restored, filename=container)
embedded = read(container)
assert embedded == dataset
assert fingerprint(embedded) == dataset_id
The HDF5 writer retrieves and verifies the source payload before embedding it.
The container holds the DCI hierarchy and an embedded payload collection, so
the complete package can travel as one file. External references retain their
paths and digests; they are not converted into Array DCIs.
Reading the HDF5 hierarchy still does not materialize its external Arrays.
Subsequent conversion retrieves the embedded payload by its complete relative
path and verifies it.
Export the package back to a directory:
destination = Path("force_export") / "dataset.dcb"
write(embedded, filename=destination)
assert (destination.parent / "external_dc" / "forces.bin.gz").exists()
assert read(destination) == dataset
The writer recreates the compressed external file using the same relative path.
The Shorthand, HDF5, and binary representations recover equal DCIs and therefore
share a fingerprint. Their physical file bytes need not agree.
Understand compression and identity
ExternalArray(..., compression=True) selects .bin.gz storage. With
compression=False, the default, a filename stem receives .bin instead.
Explicit .bin and .bin.gz suffixes must match the compression setting.
In a directory, .bin.gz stores a gzip stream. In HDF5, the same reference uses
a gzip-compressed dataset containing the logical element bytes. The HDF5
library handles dataset decompression; the implementation does not interpret
those returned bytes as another gzip file.
Changing forces.bin to forces.bin.gz changes the path and therefore changes
DCI identity. Once the path is chosen, transferring that reference between
directory and HDF5 storage preserves it. Compression does not change the
content digest’s definition.
The option applies to ExternalArrays. It does not automatically compress every
Array or ExternalFile, or introduce support for .dcb.gz representations.
Keep opaque files opaque
Use an ExternalFile when the payload is a file whose interpretation belongs to
the application:
from data_canon import ExternalFile
notes = as_dc_instance(ExternalFile.from_bytes(
b"Calibration instrument: A17\n",
filename="calibration.txt",
))
write(notes, filename=Path("force_example") / "calibration.dcb")
An ExternalFile digest covers the file’s exact bytes. A .gz filename does not
cause DataCanon to reinterpret that digest as a digest of decompressed content.
The Python conversion interface can apply a filename-based content loader after
verification; for example, .txt content becomes text. Such interpretation is
separate from the external DCI’s identity.
Move resources without changing references
Move a directory package with its relative resource tree intact. Moving only
dataset.dcs or dataset.dcb may leave a valid metadata representation whose
payloads cannot be found in the new directory.
If a DCI was decoded without a source location, supply the namespace explicitly
when converting it:
from data_canon import canonical_decode, canonical_encode
detached = canonical_decode(canonical_encode(dataset))
recovered = dc_instance_to_python(detached, source=source.parent)
assert np.array_equal(recovered["force_constants"], forces)
The source selects where to look. The DCI still determines which bytes are
acceptable through its digest and Array metadata. Source context never changes
equality or fingerprinting.
Within a complete package, references to the same path must agree on one shared
payload. Different paths remain distinct locations, even when their payloads
agree. Writers reject conflicting references rather than rename paths or select
one payload silently.
For method signatures, see the
Python API. The
Persistence specification
and HDF5 specification define the
portable requirements for independent implementations.
2.3 - Conventional Types
DataCanon implementations connect DataCanon Instances (DCIs) with instances of conventional types
in a programming language. Conventional types include built-in types, types
provided by libraries, and user-defined types used directly for data and
computation. Python examples include list, fractions.Fraction, and
numpy.ndarray.
A DataCanon Type (DCT) defines information, invariants, and equality. A
DCI is an instance of a DCT. The Python implementation
represents DCIs using classes such as ListDC and FractionDC. A ListDC
object and a conventional list object are both Python objects, but belong to
different types with different rules. Conversion connects the types; it
does not make their semantics identical.
The examples use Python. Implementations in Julia, Rust, C, or another language
can define conversions involving conventional types from their own language
and libraries. The DataCanon DCTs and DCI equality rules remain independent of
those choices.
The optional Conventional Types specification
defines common construction, arithmetic, comparison, and collection requirements
for independent implementations. Support is declared per conventional type;
Core conformance does not require conventional arithmetic. The guide introduces
the Python binding, including its interfaces and conversion choices.
Distinguish DCTs, their implementations, and conventional types
| DCT | Python class implementing the DCT | Examples of associated conventional types in Python |
|---|
| Integer | IntegerDC | int |
| Float | FloatDC | float |
| List | ListDC | list |
| Tuple | TupleDC | tuple |
| Fraction | FractionDC | fractions.Fraction |
| Array | ArrayDC | numpy.ndarray |
| Complex | ComplexDC | complex, ComplexNumber |
The table identifies conversion relationships, not type equivalences or a
universal one-to-one correspondence. For example, both built-in complex and
ComplexNumber objects can be converted to Complex DCIs, but their component
information differs. The Python API specifies
the conversion directions and resulting types.
Exact named mathematical constants
The conventional Constant type preserves a named mathematical constant.
Pi and E are instances representing pi and Euler’s number; neither stores
a decimal approximation. Conventional arithmetic retains the constant:
import math
from data_canon import Constant, Pi, E, Sum, Product, as_dc_instance, render_shorthand
assert Pi == Constant("pi")
assert Sum(Pi, Pi) == Product(2, Pi)
assert Pi / Pi == 1
assert Pi != math.pi
assert float(E) == math.e
instance = as_dc_instance(2 * Pi)
assert render_shorthand(instance, padding=0) == "2 * Pi"
float(Pi) explicitly requests an approximation. The corresponding Constant
DCI preserves its name, and its shorthand spelling is Pi (E for Euler’s
number). Longhand uses constant("pi") or constant("e"). Physical constants
and user-defined names are not included.
Begin with a List DCI and a conventional list
Construct a List DCI from a conventional list, then convert the DCI to a new
list object:
from data_canon import as_dc_instance, dc_instance_to_python
from data_canon.instance import ListDC
source = [1, 2, 3]
instance = as_dc_instance(source)
converted = dc_instance_to_python(instance)
assert isinstance(instance, ListDC)
assert type(converted) is list
assert converted == source
Both instance and converted are Python objects. The function constructs an
instance of the conventional list type from the List DCI represented by
instance.
The difference affects equality:
assert [1] == [1.0]
assert as_dc_instance([1]) != as_dc_instance([1.0])
Conventional lists use the equality rules of their members. List DCIs compare
the identities of their constituent DCIs, so Integer and Float constituents
remain distinct. Conversion must be understood in terms of both types’ rules.
Use existing conventional types
Most scientific data already has useful conventional types in Python. Use
built-in scalars and containers, Fraction for rational numbers, and NumPy
arrays for machine data:
from fractions import Fraction
import numpy as np
from data_canon.instance import MappingDC
measurements = {
("Eg", 0): 2.37,
"position": (Fraction(1, 2), Fraction(1, 4), 0.199),
"lattice_vectors": np.eye(3, dtype="float64"),
}
measurement_instance = as_dc_instance(measurements)
recovered = dc_instance_to_python(measurement_instance)
assert isinstance(measurement_instance, MappingDC)
assert type(recovered) is dict
assert recovered[("Eg", 0)] == 2.37
assert type(recovered["position"]) is tuple
assert isinstance(recovered["position"][0], Fraction)
assert isinstance(recovered["lattice_vectors"], np.ndarray)
The Tuple key is a DCI within the Mapping DCI and a conventional tuple within
the recovered dictionary. Array type and shape are preserved by the specified
Array conversion. The returned NumPy array is read-only; .copy() creates an
independently mutable array. Mutable buffers or unsupported numerical types may require
explicit conversion before they can be represented by a DCI.
as_dc_instance() accepts an existing DCI too, returning that instance unchanged.
The identity case does not eliminate the distinction between a DCI and a
conventional object of another type.
NumPy scalar values
NumPy scalars convert by value to the corresponding scalar DCTs:
| NumPy scalar | Resulting DCT |
|---|
bool_ | Boolean |
| Signed and unsigned integer scalars | Integer, without a precision limit |
Finite float16, float32, float64 | Float, preserving the value and sign of zero |
Finite complex64, complex128 | Complex, with Float real and imaginary components |
str_ | String |
bytes_ | Bytes |
For example:
from data_canon.instance import FloatDC, IntegerDC
assert as_dc_instance(np.uint64(2**64 - 1)) == IntegerDC(2**64 - 1)
assert as_dc_instance(np.float32(0.5)) == FloatDC(0.5)
Binary16 and binary32 values widen exactly to binary64. Scalar conversions do
not preserve the original NumPy width as identity metadata. For a numerical
element whose type must be preserved, use a rank-zero Array:
from data_canon.instance import ArrayDC
scalar = as_dc_instance(np.float32(0.5))
typed_scalar = as_dc_instance(np.array(0.5, dtype="float32"))
assert isinstance(scalar, FloatDC)
assert isinstance(typed_scalar, ArrayDC)
assert typed_scalar.data.shape == ()
assert typed_scalar.data.dtype == np.dtype("float32")
NaN and infinities remain invalid numerical DCIs. Extended-precision scalar
formats, such as a platform’s wider longdouble or clongdouble, require an
explicit application conversion or custom registry rule. A platform alias of
a supported portable dtype follows that portable dtype’s rule.
NumPy datetime64, timedelta64, and structured or opaque void scalars have
no default conversion. Their units or field structure must be handled explicitly,
for example by a registered conversion to a Record. A duration never implicitly
becomes a unitless Integer. Exact-type registry rules take precedence over the
default NumPy conversions and apply recursively in collections and Mapping keys.
Supply conventional types where suitable ones are missing
A language may lack a conventional type for a structure that DataCanon can
represent. The Python implementation supplies Power, Sum, Product,
Quotient, ComplexNumber, Multiset, and UnorderedDict for such cases.
The additional classes are conventional types, just like types provided by
other libraries; they are distinct from PowerDC, SumDC, and the other
classes implementing DCTs.
DataCanon defines structured data and its identity; it does not define
mathematical operations on that data. Working with structured numbers naturally
creates a need to calculate with them. The additional numerical conventional
types therefore support arithmetic under their own construction, equality,
and simplification rules, similar in spirit to Python’s Fraction.
The arithmetic examples below construct conventional objects directly. No
Shorthand parser, DCI construction, or persistence operation is needed to use
Power, Sum, or the other numerical conventional types. Arithmetic operations
on those objects do not evaluate or simplify stored DCIs.
Arithmetic with structured numbers
Retain a radical exactly
Construct a conventional Power object with a base and a positive rational exponent:
from data_canon import Power
sqrt_two = Power(2, 1, 2)
sqrt_three = Power(3, 1, 2)
cube_root_five = Power(5, 1, 3)
Power(3, 1, 2) represents three raised to the power one half. Its base remains
a conventional int object here. Omitting the denominator gives an integer
exponent: Power(3, 2) means three squared.
Conventional Power reduces the exponent fraction and extracts an exact root
of the base when that makes the exponent an integer. Construction retains the
Power type and the remaining integer exponent:
assert Power(5, 2, 4) == Power(5, 1, 2)
root_four = Power(4, 1, 2)
assert type(root_four) is Power
assert (root_four.base, root_four.numerator, root_four.denominator) == (2, 1, 1)
assert root_four == 2
assert Power(8, 1, 3) == 2
cubed = Power(16, 3, 2)
assert (cubed.base, cubed.numerator, cubed.denominator) == (4, 3, 1)
integer_power = Power(4, 8)
assert (integer_power.base, integer_power.numerator, integer_power.denominator) == (4, 8, 1)
assert integer_power == 65536
assert int(integer_power) == 65536
An integer exponent is preserved, rather than expanded merely to initialize
the object. Equality and explicit int() or float() conversion can still
compute its value. If the requested root is not exact, the constructor retains
the base and reduced exponent: Power(8, 1, 2) keeps those fields.
Float bases follow the same rule, with an exactness check on the replacement
float. For example, Power(4.0, 1, 2) becomes Power(2.0, 1, 1) and
Power(16.0, 3, 2) becomes Power(4.0, 3, 1). Integer powers such as
Power(0.1, 2) remain powers; construction introduces no floating-point rounding.
The behavior follows the model of Fraction: construction retains the requested
conventional type, while equality can identify an equivalent scalar. A Power DCI
has different rules and retains its original base and exponent constituents.
Calculate with exact geometric quantities
For an equilateral triangle of side length one, retain the exact height and
area using conventional Fraction and Power objects:
from fractions import Fraction
height = Fraction(1, 2) * sqrt_three
area = height / 2
assert area == Fraction(1, 4) * sqrt_three
assert height + height == sqrt_three
The multiplication produces a conventional Product object. Division by a conventional int adjusts
the rational factor; adding the two identical heights combines their factors.
No decimal approximation of the square root is needed.
Add terms and combine matching contributions
Python operators construct conventional Sum and Product objects:
from data_canon import Sum
value = sqrt_two + sqrt_three + 3 * sqrt_three
assert value == Sum(sqrt_two, 4 * sqrt_three)
assert sqrt_three - sqrt_three == 0
Conventional Sum combines matching terms and rational constants, and flattens
nested Sum objects. Extractable radical factors are simplified before terms
are collected:
assert Sum(Power(12, 1, 2), sqrt_three).items == (3 * sqrt_three,)
assert Sum(1, Power(4, 1, 2)).items == (3,)
Explicit constructors are useful when assembling a collection of terms:
terms = [sqrt_two, sqrt_three, 3 * sqrt_three]
assembled = Sum(*terms)
assert assembled == value
The constructor retains a conventional Sum object even if simplification leaves one term.
Arithmetic operators may return that remaining term directly:
assert Sum(sqrt_three, 3 * sqrt_three) == Sum(4 * sqrt_three)
assert sqrt_three + 3 * sqrt_three == 4 * sqrt_three
Sum(*items) and Product(*items) take separate operands, rather than one
iterable argument. Conventional Sum and Product objects are immutable and
unhashable, independently of the hashable Sum and Product DCIs.
Multiply factors and combine powers with a common base
Conventional Product combines rational factors and combines exponents for matching bases:
from data_canon import Product
assert Product(Fraction(1, 2), Fraction(2, 3)) == Product(Fraction(1, 3))
assert Power(5, 1, 2) * Power(5, 1, 3) == Power(5, 5, 6)
assert sqrt_two * sqrt_three == Product(sqrt_two, sqrt_three)
The last product can retain its two factors while comparing equal to the
square root of six. Surrounding arithmetic can evaluate an integer power and
combine the result with other terms or factors. Retaining the exponent in a
standalone Power does not prevent that simplification:
assert sqrt_two * sqrt_three == Power(6, 1, 2)
assert sqrt_three * sqrt_three == 3
assert Product(3, Power(4, 1, 2)).items == (6,)
Multiplication distributes over conventional Sum objects:
assert 2 * Sum(sqrt_two, sqrt_three) == Sum(2 * sqrt_two, 2 * sqrt_three)
The simplification rules support exact structured calculations while keeping
the scope of simplification limited and predictable.
Divide by a structured denominator
A reciprocal of a sum needs more than a conventional Fraction. Quotient
retains a structured numerator and denominator:
from data_canon import Quotient
denominator = sqrt_three + sqrt_two
reciprocal = 1 / denominator
assert reciprocal == Quotient(1, Sum(sqrt_three, sqrt_two))
The denominator remains a conventional Sum; construction does not rationalize
it. Equality can nevertheless recognize the rationalized equivalent by exact
cross multiplication:
assert reciprocal == sqrt_three - sqrt_two
assert Quotient(4, 6) == Fraction(2, 3)
assert Quotient(2 * sqrt_three, 4 * sqrt_three) == Fraction(1, 2)
The Quotient constructor reduces rational coefficients and cancels common
factors. It retains the Quotient type even when the result equals a scalar.
Division rejects denominators recognized as zero.
Division by a conventional int or Fraction adjusts exact rational factors,
as in the triangle example. Division by a conventional float follows
floating-point arithmetic.
Keep floating-point contributions separate
Exact rational and floating-point contributions retain their separate roles:
mixed = Sum(Fraction(1, 4), 0.125)
assert mixed.items == (Fraction(1, 4), 0.125)
with_offset = Sum(sqrt_three, 0.25, 0.5)
assert with_offset == Sum(sqrt_three, 0.75)
Conventional float contributions can combine with one another using Python
floating-point arithmetic, with its usual rounding. Float contributions remain
separate from exact rational coefficients and are not converted to large
rational numbers. The numerical conventional types accept finite float
values and reject bool values as numerical operands.
Compare numerical values exactly
Conventional numerical equality uses exact normalization and cross
multiplication. Equality can hold across Sum, Product, Power, Quotient,
int, Fraction, and float; it is independent of presentation order and does
not require matching conventional types.
Float comparison uses the exact binary value already stored by the float,
following Python’s Fraction behavior:
assert Quotient(1, 2) == 0.5
assert Quotient(1, 10) != 0.1
assert Sum(2**53 + 1) != float(2**53 + 1)
assert mixed == Fraction(3, 8)
Keeping float and rational contributions separate during construction does not
require them to compare unequal. No rounded floating-point evaluation is used
as an equality test. Exact comparison forms are cached when needed without
changing the object’s displayed operands or its conversion to a DCI.
Normalization handles rational arithmetic, distributive expressions, rational
radicals, and quotient cross multiplication. It is not a complete procedure for
recognizing every mathematical equivalence. Fractional powers with nonrational
bases remain symbolic. Integer factorization uses trial divisors through 1000
and extracts residual perfect powers through degree 64; a large unresolved
factor stays intact. Comparisons can therefore miss equivalences requiring
further factorization or algebraic identities. No numerical tolerance or
floating-point fallback is used to guess equality.
Choose when to request a machine number
Use float() at the point where a numerical routine or display needs a machine
floating-point result:
import math
assert math.isclose(float(area), math.sqrt(3) / 4)
assert math.isclose(float(reciprocal), 1 / (math.sqrt(3) + math.sqrt(2)))
The conventional numerical object remains unchanged. Fractional powers use the real-valued
domain and rounding behavior of math.pow during explicit evaluation.
int() also evaluates, then truncates toward zero, following Python conventions:
assert float(Sum(1, 1.0)) == 2.0
assert int(Quotient(-7, 3)) == -2
Explicit numerical conversions can raise errors for undefined operations or
overflow. Construction rejects an even root of a known negative scalar; odd
roots of negative scalars use the real branch. A symbolic expression with a
more complicated base may require further evaluation to determine its domain.
Use structured complex components
Use conventional ComplexNumber when real or imaginary components should
retain Fraction, Power, Sum, or other conventional numerical objects:
from data_canon import ComplexNumber
phase = ComplexNumber(Fraction(1, 2), Fraction(1, 2) * sqrt_three)
shifted = phase + 1
assert shifted.real == Fraction(3, 2)
assert shifted.imaginary == phase.imaginary
Component equality follows the same numerical rules. For example,
ComplexNumber(Power(4, 1, 2), 0) compares equal to 2. Comparisons with
built-in complex values compare the actual component values. Conventional
ComplexNumber objects are unhashable.
Addition, subtraction, multiplication, division, negation, and conjugation are
available. For example, multiplication by the imaginary unit rotates the
components:
rotated = phase * ComplexNumber(0, 1)
assert rotated.real == -phase.imaginary
assert rotated.imaginary == phase.real
assert phase.conjugate() == ComplexNumber(phase.real, -phase.imaginary)
Request a built-in Python complex number explicitly when needed:
machine_phase = complex(phase)
assert math.isclose(machine_phase.real, 0.5)
assert math.isclose(machine_phase.imag, math.sqrt(3) / 2)
Component operations follow their underlying Python types. In particular,
division of two conventional int values or float operations can introduce
floating-point results; use Fraction or structured numerical operands when
exact rational division is required.
Array expressions and ordered multiplication
The conventional Sum class supports scalar terms and Array expressions. The
conventional OrderedProduct class preserves factor order, while Product remains
a scalar type.
Construction preserves the expression; .evaluate() performs conventional
arithmetic. For example, combine the Pauli matrices:
import numpy as np
from data_canon import Sum, OrderedProduct, Power, as_dc_instance
sigma_x = np.array([[0, 1], [1, 0]])
sigma_y = np.array([[0, -1j], [1j, 0]])
operator = Sum(OrderedProduct(2, sigma_x), OrderedProduct(3, sigma_y))
np.testing.assert_array_equal(np.asarray(operator, dtype=complex),
2 * sigma_x + 3 * sigma_y)
instance = as_dc_instance(operator)
Factor order matters in conventional matrix multiplication as well as DCI
identity. OrderedProduct(A, B) and OrderedProduct(B, A) can give different
results. Scalar factors may occur anywhere; NumPy array factors currently must have
rank two when an OrderedProduct is evaluated. Conventional Sum supports NumPy arrays of any rank
with exactly matching shapes. Neither operation broadcasts or reshapes data.
Nonzero scalar terms mixed into an Array Sum require an explicit identity Array.
Exact coefficients are retained in object-array entries:
exact = OrderedProduct(Power(2, 1, 2), np.eye(2, dtype=int)).evaluate()
assert exact.dtype == object
assert exact[0, 0] == Power(2, 1, 2)
from fractions import Fraction
exact_y = OrderedProduct(Fraction(1, 3), sigma_y).evaluate()
assert exact_y[1, 0].imaginary == Fraction(1, 3)
Use an explicit dtype when a machine approximation is wanted. An exact object
Array cannot itself be encoded as a portable Array DCI; use a supported machine
dtype or nested Lists to preserve exact per-element DCTs. Scalar-valued fields
such as Power bases and Complex components do not accept Array-valued Sums.
Additional conventional collection types
Retain repeated collection members
A conventional Multiset preserves the number of occurrences without assigning
semantic order. For example, it can describe a composition:
from data_canon import Multiset
composition = Multiset(["Ti", "O", "O"])
assert composition == Multiset(["O", "Ti", "O"])
assert composition != Multiset(["Ti", "O"])
composition.add("O")
assert composition.count("O") == 3
Conventional Multiset is mutable. It accepts an iterable and supports
list-like operations. Its members need not be hashable:
samples = Multiset([[1, 2], [1, 2]])
assert samples == Multiset([[1, 2], [1, 2]])
Member comparison follows Python equality, so members must provide usable
Boolean equality results. This differs from DCI equality; for example, Python
Boolean and integer equality can identify values that DataCanon distinguishes.
Mark an unordered dictionary
Python dict already compares equal dictionaries independently of insertion
order. Its insertion order is nevertheless retained when converting it to a
DataCanon Mapping. Use UnorderedDict to request an UnorderedMapping instead:
from data_canon import UnorderedDict
settings = UnorderedDict({"temperature": 300, "pressure": 0})
settings["temperature"] = 350
assert settings["temperature"] == 350
Conventional UnorderedDict behaves as a Python dictionary, with the same key
hashability and equality rules. Its additional role is to express the intended
DataCanon collection type during conversion.
Construct DCIs from the additional conventional objects
The arithmetic and collection examples above created instances of conventional
types. Use the conversion interface when their results need DataCanon identity:
from data_canon.instance import ProductDC, MultisetDC, UnorderedMappingDC
area_instance = as_dc_instance(area)
assert isinstance(area_instance, ProductDC)
assert dc_instance_to_python(area_instance) == area
assert isinstance(as_dc_instance(composition), MultisetDC)
assert isinstance(as_dc_instance(settings), UnorderedMappingDC)
A conventional Power object follows the arithmetic implementation’s rules.
A Power DCI retains its base DCI and prescribed exponent fields. Likewise,
conventional Sum objects simplify matching terms, while Sum DCIs retain the
specified numerical structure and multiplicity.
The conventional objects can be placed in a dictionary with Tuple keys and
converted together:
from data_canon import render_shorthand
results = {
("triangle", "height"): height,
("triangle", "area"): area,
"phase": phase,
"composition": composition,
"settings": settings,
}
result_instance = as_dc_instance(results)
print(render_shorthand(result_instance))
assert render_shorthand(results) == render_shorthand(result_instance)
render_shorthand() accepts conventional objects by constructing a DCI through
the same conversion interface before rendering. The DCI’s identity is governed
by its DCT, rather than by the original object’s equality rules.
Preserve the distinction during conversion
Conventional types have their own construction, equality, and operation rules.
Conversion can simplify structure, lose distinctions, or fail when a target
type cannot hold the required information. A round trip through conventional
objects is not guaranteed to preserve an arbitrary DCI’s identity.
For example, converting a Sum DCI invokes the conventional Sum constructor:
from data_canon import parse_shorthand, Sum
original = parse_shorthand("sum(1, 1)")
calculated_sum = dc_instance_to_python(original)
assert calculated_sum == Sum(2)
assert as_dc_instance(calculated_sum) != original
No original DCI is modified by the conversion. The newly constructed Sum DCI
has the structure of the conventional result, rather than that of original.
Python’s Fraction reduces ratios, and conventional Power reduces its
rational exponent; their conversion rules can also change DCI structure.
Collection conversion has similar consequences:
first = {"temperature": 300, "pressure": 0}
second = {"pressure": 0, "temperature": 300}
assert first == second
assert as_dc_instance(first) != as_dc_instance(second)
assert as_dc_instance(UnorderedDict(first)) == as_dc_instance(UnorderedDict(second))
Python dictionary equality ignores insertion order, while Mapping DCI equality
includes entry order. UnorderedDict requests an UnorderedMapping DCI instead.
A conventional Multiset uses its members’ conventional equality rules, which
need not coincide with DCI equality.
All DCIs in the reference implementation can be used as Python dictionary keys.
Converting a DCI key to a conventional object can nevertheless produce an
unhashable list or NumPy array. Distinct Boolean, Integer, or Float DCI keys
can also collide under conventional dictionary-key equality. Conversion to a
conventional dictionary is therefore not always a suitable way to retain a
DataCanon Mapping. See the
Python API
for conversion details.
Store and recover the results
Writers accept conventional objects through as_dc_instance(). Readers
reconstruct DCIs from the stored representation:
from data_canon import read, write
write(results, filename="exact_results.dcb")
restored = read("exact_results.dcb")
assert restored == result_instance
recovered_results = dc_instance_to_python(restored)
assert recovered_results[("triangle", "area")] == area
The storage round trip preserves DCI identity. The subsequent conversion from
the Mapping DCI to a conventional dict is a separate operation, subject to
the target types’ rules. Keep the DCI when its complete structural identity is
needed; use conventional objects when their collection or arithmetic operations
are wanted.
ExternalFile and ExternalArray are conventional types for constructing
external references. Their reverse conversions are different operations:
dc_instance_to_python() retrieves and verifies an ExternalFile payload,
returning bytes or a content loader’s result, and materializes an ExternalArray
as a NumPy array. The conversion does not return another ExternalFile or
ExternalArray construction object. Metadata-only work can use the external
DCIs without retrieving the payloads.
For constructor signatures, complete conversion tables, and resource handling,
use the Python API. For numerical DCT
structure, nesting, and identity, use the
Reference and
Core specification.
For the language-independent conventional behavior and its required comparison
cases, use the
Conventional Types specification.
Application-defined Record conventions
An application can connect its conventional types to ordinary Records through
an explicit ConversionRegistry. For example, a conventional date can become a
Record holding year, month, and day fields, while another convention might also
store calendar metadata. DC preserves that structure without choosing a calendar
or defining date operations.
Rules apply recursively through supported collections and Record fields, using
as_dc_instance(obj, registry=registry) and
dc_instance_to_python(node, registry=registry). Reading a stored DCI does not
execute those rules. The Custom Conversion Rules guide
works through a complete date example and explains validation and defaults.
Compressed external Arrays
Use ExternalArray(array, filename="forces", compression=True) to select gzip
storage for zero-heavy numerical data such as force constants. The conventional
constructor finalizes the name as forces.bin.gz; compression defaults to off,
which produces forces.bin. Explicit suffixes must agree with the setting.
Convert to a DCI before writing. Directory-backed packages contain the gzip
file, while HDF5 packages apply dataset compression to the logical element
bytes. The digest covers uncompressed canonical bytes in both representations.
The DCI’s finalized path and identity are preserved through transfers. A name
ending in .bin identifies a different DCI from the corresponding .bin.gz
name. ExternalFiles retain opaque-byte semantics.
See the Python API for filename examples,
validation, and compression behavior.
2.4 - Shorthand Preprocessing
Overview
Shorthand preprocessing provides three conveniences for entering scientific
data by hand: compact List notation, named textual substitutions, and scalar
mathematical evaluation with eval(...).
Preprocessing transforms text into strict Shorthand. Parsing that text then
constructs a DataCanon Instance (DCI). This guide introduces each feature separately
before showing how they work together.
Start with one compact List
Ordinary Shorthand Lists use brackets and commas:
Compact notation lets you enter the same List using angle brackets and spaces:
Use preprocess_shorthand() to expand it:
from data_canon import preprocess_shorthand
source = "<1 2 3>"
prepared = preprocess_shorthand(source)
print(prepared)
The resulting text is:
No DataCanon Instance has been constructed yet. prepared is a Python String.
Parse that String to construct the List DCI:
from data_canon import parse_shorthand
instance = parse_shorthand(prepared)
Preprocessing and strict parsing are separate steps: preprocessing expands input
notation; strict parsing
interprets the resulting Shorthand and validates the data.
A compact group always produces a List, even when it contains only one item:
Empty compact groups are invalid. Use ordinary [] for an empty List.
Enter nested Lists
A comma can separate rows whose items are separated by spaces:
expands to:
A semicolon can introduce another level:
expands to:
[
[
[1, 2],
[3, 4]
],
[
[5, 6],
[7, 8]
]
]
At each level, the preprocessor looks for separators in this order:
- Newline.
- Semicolon.
- Comma.
- Horizontal whitespace: space, tab, or carriage return.
The first separator that splits the contents into multiple nonempty parts
establishes the outer level. Each part is then processed in the same way.
Newlines therefore introduce a level outside semicolons, commas, and spaces.
Explicitly nested angle-bracket groups are also supported.
The layouts above illustrate the resulting structure; exact output spacing
is not important to strict parsing. Avoid repeated or trailing separators
while their treatment remains a Version 1 draft finalization item.
Existing structures remain intact
Separators inside quoted Strings or balanced (), [], and {} do not split
items. For example:
becomes:
The inner [1,2] is one item. Likewise,
becomes:
The spaces inside the String do not introduce List items.
Construct an Array when needed
Compact notation alone creates Lists. To enter an Array, place it inside an
Array expression:
source = 'array(<1.0 0.0, 0.0 1.0>, dtype="float64")'
prepared = preprocess_shorthand(source)
instance = parse_shorthand(prepared)
The prepared text represents:
array([[1.0, 0.0], [0.0, 1.0]], dtype="float64")
The strict Array parser constructs the Array. Compact notation supplies its
numerical body without itself performing Array inference.
Substitute one name
A substitution associates a name with replacement text. Define the name before
|, then use it in the document body:
species="Sr" | {"species": species}
Preprocessing produces:
In Python:
source = 'species="Sr" | {"species": species}'
prepared = preprocess_shorthand(source)
instance = parse_shorthand(prepared)
The name species is an input convenience. After substitution, the parser
receives an ordinary Shorthand Mapping.
Define several names
Separate definitions with whitespace. Whitespace around = is permitted:
species = "Sr"
position = [0.5, 0.5, 0.5] |
{
"species": species,
"position": position
}
becomes:
{
"species": "Sr",
"position": [0.5, 0.5, 0.5]
}
Definitions can contain Strings, Tuples, Arrays, Mappings, or other text:
label = "oxygen"
site = (1/2, 1/2, 0)
lattice = array([[1.0, 0.0], [0.0, 1.0]], dtype="float64") |
{
"label": label,
"site": site,
"lattice": lattice
}
Replacement text is inserted literally. It is not separately parsed as a DCI;
only the final document must be valid strict Shorthand. Replacement text can
therefore also be an expression fragment used later inside eval(...).
Name rules and protected Strings
A name begins with an ASCII letter or underscore, followed by ASCII letters,
digits, or underscores. Names are case-sensitive and must be unique.
Substitution replaces complete identifier-like tokens. A definition for a
does not replace part of alpha or a2, and nothing is substituted inside
quoted Strings:
a=3 | {"value": a, "text": "a"}
becomes:
{"value": 3, "text": "a"}
Build on earlier definitions
Definitions are processed from left to right. Each definition may use externally
supplied names and any earlier local definitions:
a = 4.593
c_over_a = 0.644
c = a * c_over_a |
[c]
This produces [4.593 * 0.644], which represents a List containing a Product.
To compute the lattice parameter for a machine Array, request evaluation:
source = """a=4.593 c_over_a=0.644 c=eval(a * c_over_a) |
array(<a 0 0, 0 a 0, 0 0 c>)"""
instance = parse_shorthand(source, preprocess=True)
The third diagonal entry is 2.957892. An evaluated definition is resolved
before later definitions use it, so d=eval(c * 2) would also work.
Self-references and references to later definitions are rejected; arrange
parameters before the definitions that depend on them.
Replacements remain literal text. For example, a=1 + 2 b=3 * a | b becomes
3 * 1 + 2, without implicit grouping. Use constructors when a nested numerical
structure matters: a=sum(1, 2) b=product(3, a) | b preserves the Sum as one
Product constituent. Ordinary Shorthand parentheses create Tuples, not groups.
Inside eval(...), arithmetic parentheses provide ordinary grouping.
Named parameters make an input file easier to read and revise: repeated
values have one definition, and relationships remain visible in the data.
A crystal structure provides a useful example. Atomic positions are often
specified as fractions of the repeating cell, with several positions sharing
one displacement parameter. Define delta once and write each coordinate
in terms of its reference position and displacement:
from data_canon import parse_shorthand, preprocess_shorthand, render_shorthand
source = """
delta = 0.199 |
{
"basis_atoms": [
(delta, 1/4 + delta, 1/8),
(1/2 + -delta, 3/4 + -delta, 5/8),
(-delta, 3/4 + delta, 3/8),
(1/2 + delta, 1/4 + -delta, 7/8)
]
}
"""
prepared = preprocess_shorthand(source)
instance = parse_shorthand(prepared)
print(render_shorthand(instance, layout="standard", align="left"))
After substitution, the strict parser receives the following structure
(whitespace may differ):
{
"basis_atoms": [
(0.199, 1/4 + 0.199, 1/8),
(1/2 + -0.199, 3/4 + -0.199, 5/8),
(-0.199, 3/4 + 0.199, 3/8),
(1/2 + 0.199, 1/4 + -0.199, 7/8)
]
}
The List contains four Tuple DCIs. Coordinates such as 1/4 + 0.199 are Sum
DCIs whose constituents retain the exact Fraction reference position and the
Float displacement separately. The resulting DCI no longer contains the
substitution name; the source text remains useful for editing the parameter.
The ASCII name delta is used because substitution names currently follow the
ASCII identifier rules. With the positive value defined here, -delta becomes
the negative Float literal -0.199 by textual substitution. This example needs
neither Compact List notation nor eval(...).
Supply substitutions from Python
Instead of placing definitions in the input, supply a mapping of names to
replacement Strings:
prepared = preprocess_shorthand(
'{"species": species, "position": position}',
substitutions={
"species": '"Sr"',
"position": "[0.5, 0.5, 0.5]",
},
)
instance = parse_shorthand(prepared)
The result is the same Mapping as in the inline example. The values in
substitutions must be text. The quotation marks in '"Sr"' are intentional:
they make the replacement a Shorthand String literal.
External substitutions may also be used in inline definitions:
prepared = preprocess_shorthand(
'site=(x, x, 0) | {"position": site}',
substitutions={"x": "1/2"},
)
This produces:
{"position": (1/2, 1/2, 0)}
Each inline definition expands external names and earlier local names in one
pass, then resolves its explicit eval(...) expressions. After definitions are
resolved, external names are substituted once in the body, followed by local
names once. The external mapping itself is not recursively expanded. An inline
name must not conflict with an externally supplied name.
Array presets and linear combinations
Substitutions can name Arrays, making input files concise without changing DCI
identity. For example, the Pauli matrices provide a small linear-operator example:
Sigma_x = array([[0, 1], [1, 0]])
Sigma_y = array([[0, 0-1j], [0+1j, 0]]) |
{
"operator": 2 * Sigma_x + 3 * Sigma_y
}
After substitution, * constructs OrderedProduct because an Array is present;
+ constructs Sum. DC preserves those structures without multiplying or adding
anything. Their conventional counterparts can evaluate the linear combination.
The same mechanism supports 2 * I when I is defined as an identity Array.
Evaluate one scalar expression
Use eval(...) when you want to compute a numerical result before parsing:
prepared = preprocess_shorthand("eval(2 + 3)")
print(prepared)
Output:
Parsing prepared constructs Integer 5:
instance = parse_shorthand(prepared)
Evaluation is explicit. The preprocessor does not perform arithmetic on
expressions outside eval(...).
Supported arithmetic
The evaluator supports real integer and floating-point literals, parentheses,
unary + and -, binary +, -, *, /, and **, and real sqrt(...):
| Input | Resulting Shorthand |
|---|
eval(1/2) | 0.5 |
eval((2 + 3)*4) | 20 |
eval(2**10) | 1024 |
eval(sqrt(9) + 1) | 4 |
eval(-2**2) | -4 |
eval(5+(1.89-2.53)*6/3.72) | 3.967741935483871 |
Operator precedence follows Python. Division / performs true division;
exponentiation uses **, not ^.
Intermediate values use arbitrary-precision integers and binary64
floating-point arithmetic. If a final Float has an exactly integral value,
it is emitted as an Integer. Otherwise, a finite floating-point result is
emitted as a Float literal. No tolerance is used to decide whether a value is
integral.
Evaluation versus stored structure
Strict Shorthand can store an irrational value as a Product containing a
Fraction and a Power:
That expression retains its Product and constituent structure when
preprocessed. To compute a
floating-point approximation instead, request evaluation:
This distinction lets you choose between preserving scientific structure and
performing numerical arithmetic before constructing the DCI.
Strict structure and preprocessing arithmetic
Outside eval(...), 1 + 2 constructs a Sum DCI and 2 * 3 constructs a
Product DCI. Inside eval(...), those operators perform scalar arithmetic
before the result is parsed:
assert parse_shorthand("1 + 2") != parse_shorthand("3")
assert parse_shorthand(preprocess_shorthand("eval(1 + 2)")) == parse_shorthand("3")
The evaluator follows Python operator precedence. For example,
eval(1/2**3) produces 0.125; strict Shorthand rejects 1/2**3 because a
Fraction base requires an explicit power(...) constructor. They are different
input grammars used at different stages.
Evaluation limits
The evaluator supports real scalar arithmetic only. It excludes unresolved
names, arbitrary function calls, Arrays, indexing, attribute access, and
general code execution.
Division by zero, negative real square roots, complex results, and nonfinite
floating-point results are rejected. Implementations may limit expression
length, nesting, and intermediate integer size. Cross-language numerical
conformance details remain recorded in the Version 1 draft specification.
Combine the features
Once each feature is familiar, they can be used together. Consider entering a
symmetric matrix with repeated numerical parameters:
a=1.1 b=0.2 |
<a 0 b, 0 a 0, b 0 eval(-a)>
First, compact List notation expands:
[
[a, 0, b],
[0, a, 0],
[b, 0, eval(-a)]
]
Next, substitutions are inserted:
[
[1.1, 0, 0.2],
[0, 1.1, 0],
[0.2, 0, eval(-1.1)]
]
Finally, eval(...) is evaluated:
[
[1.1, 0, 0.2],
[0, 1.1, 0],
[0.2, 0, -1.1]
]
Strict parsing now constructs a List of Lists. To construct an Array, wrap the
compact body in array(..., dtype="float64").
The order also matters when replacement text contains spaces. For example:
term=1/2 * sqrt(3) | <term 0>
Compact List expansion happens first:
Substitution then produces:
The spaces within the replacement Product do not split it into additional
List items because compact List expansion has already finished.
Longhand and preprocessing
Longhand is the explicit-constructor subset of strict Shorthand. Preprocessing
can insert constructors just as it can insert shorter forms:
prepared = preprocess_shorthand('values=list(1, 2) | tuple(values, fraction(1, 3))')
assert parse_shorthand(prepared) == parse_shorthand('([1, 2], 1/3)')
The longhand renderer flag has no effect on preprocessing. Compact List
notation refers specifically to angle-bracket input groups; it is unrelated
to the renderer’s layout="compact" delimiter policy.
Python parsing and CLI defaults
Call parse_shorthand() directly for strict Shorthand. When preprocessing is
wanted, call preprocess_shorthand() first and parse its result.
Quoted Strings are protected during every stage. Text such as "<1 2>",
"eval(2 + 3)", or "a | b" remains ordinary String content.
Preprocessing does not change DataCanon’s types or identity rules. YAML $
values and .dcs persistence files use strict Shorthand and do not implicitly
perform preprocessing when read through the ordinary Python persistence API.
The CLI is a human-input frontend and enables preprocessing by default:
echo '<1 2, 3 4>' | dc
dc rutile.dcs --dump rutile.dcb
Use --strict to require strict Shorthand input in the CLI. The option does
not change the Python API defaults or enable preprocessing in key/count queries.
For the detailed rules, see the
Shorthand Preprocessing specification.
2.5 - Custom Conversion Rules
DataCanon Records let applications describe structured values without adding
new DataCanon Types. An application can define a Record convention and supply
explicit rules connecting that Record to a conventional type.
A calendar date is a useful example. Python provides datetime.date, but DC
need not prescribe a calendar, year range, or date arithmetic. The application
can make those choices and store the necessary information in a Record.
This guide defines an illustrative "example.Date" convention. The name is
application data, not a built-in DC type or a universally prescribed date format.
1. Choose the Record structure
The example stores three Integer fields in the order year, month, day:
record(
"example.Date",
"year": 2026,
"month": 10,
"day": 22
)
DC defines the Record’s structure and identity. The application defines how
those fields describe a date and which dates are valid for its purposes.
Other applications can choose different names, additional calendar metadata,
or different representations without changing the DC type system.
2. Write the conventional-to-DCI rule
A handler receives the conventional object and a convert() function. Calling
convert() applies the same registry recursively to a constituent object.
from datetime import date
from data_canon import ConversionRegistry, as_dc_instance, dc_instance_to_python
from data_canon.instance import Field, IntegerDC, RecordDC
DATE_RECORD = "example.Date"
DATE_FIELDS = ("year", "month", "day")
def date_to_record(value, convert):
return RecordDC(DATE_RECORD, tuple(
Field(name, convert(getattr(value, name)))
for name in DATE_FIELDS
))
The handler constructs an ordinary Record DCI. Integer field values use the
usual conversion rules. A conventional-to-DCI handler must return a DCI;
returning a conventional dictionary or another unsupported object is an error.
3. Write and validate the reverse rule
A reverse handler receives the original Record DCI and a recursive convert()
function. Validate the expected schema before constructing a conventional object:
def date_from_record(node, convert):
if tuple(field.name for field in node.fields) != DATE_FIELDS:
raise ValueError("example.Date requires year, month, day in that order.")
if not all(isinstance(field.value, IntegerDC) for field in node.fields):
raise TypeError("example.Date fields must be Integer DCIs.")
return date(*(convert(field.value) for field in node.fields))
Python’s date constructor checks its calendar and supported date range.
An invalid calendar date raises an error during this application conversion.
The Record itself can still be a valid DCI: DC does not assign date semantics
to its name or perform the application’s calendar validation.
The example deliberately requires exactly three fields in the stated order.
An application’s own handler can validate additional calendar or provenance
fields according to its documented convention.
4. Register both directions explicitly
registry = ConversionRegistry()
registry.register_type(date, date_to_record)
registry.register_record(DATE_RECORD, date_from_record)
original = date(2026, 10, 22)
instance = as_dc_instance(original, registry=registry)
assert isinstance(instance, RecordDC)
assert dc_instance_to_python(instance, registry=registry) == original
The registry belongs to the application. Registration does not change global
conversion behavior or affect another registry.
Conventional input rules match the exact type. Registering date does not
also register datetime.datetime, which carries additional time information.
Register a separate rule for each supported conventional type. An explicit rule
for an exact built-in type takes precedence over its usual conversion, but
existing DCIs are always returned unchanged by as_dc_instance().
Reverse rules match the full Record name using exact String equality. There
is no import, class lookup, or prefix-based dispatch derived from that name.
Duplicate registrations are rejected so a rule cannot be silently replaced.
5. Convert a nested structure
Rules apply throughout supported collections, including Mapping keys and values:
source = {
("run", 7): {
"recorded_on": date(2026, 10, 22),
"samples": [date(2026, 10, 21), date(2026, 10, 22)],
},
date(2026, 10, 22): "completed",
}
instance = as_dc_instance(source, registry=registry)
restored = dc_instance_to_python(instance, registry=registry)
assert restored == source
A custom handler for a container-like conventional type should call its supplied
convert() function for each constituent that needs conversion. Calling the
public conversion function without registry=registry would instead use the
default rules. Reverse convert() calls also retain the caller’s external
resource source context.
Arrays keep their specialized machine elements. Custom conversion does not
walk raw Array elements, Fraction integers, Record names, or other fixed metadata
as though that information consisted of separate DCIs.
6. Store and recover the DCI
Conversion rules are separate from canonical encoding and persistence:
from data_canon import canonical_decode, canonical_encode
encoded = canonical_encode(instance)
decoded = canonical_decode(encoded)
assert decoded == instance
assert dc_instance_to_python(decoded, registry=registry) == source
Decoding recovers Record DCIs without executing application handlers. Another
implementation can preserve, fingerprint, and inspect the same Records without
knowing the application’s date convention.
The same sequence works with files:
from data_canon import read, write
write(instance, filename="dated_data.dcb")
loaded = read("dated_data.dcb")
assert loaded == instance
assert dc_instance_to_python(loaded, registry=registry) == source
Pass the registry to the conversion functions. read(), write(), renderers,
and canonical codecs do not accept a registry; convert conventional objects to
DCIs first and convert decoded DCIs afterward.
Convert your own class
The same mechanism works for an application-defined class. Consider a simple
DataPoint whose coordinates are stored in underscore-prefixed attributes:
from fractions import Fraction
from data_canon import (
ConversionRegistry, as_dc_instance, dc_instance_to_python,
render_shorthand, read, write,
)
from data_canon.instance import Field, RecordDC
class DataPoint:
def __init__(self, x, y, z):
self._x = x
self._y = y
self._z = z
Choose an application Record name and stable field names. The Record need not
expose the class’s attribute naming convention: x, y, and z describe the
stored data, while _x, _y, and _z remain implementation details.
POINT_FIELDS = ("x", "y", "z")
def point_to_record(value, convert):
return RecordDC("example.DataPoint", tuple(
Field(name, convert(getattr(value, "_" + name)))
for name in POINT_FIELDS
))
def point_from_record(node, convert):
if tuple(field.name for field in node.fields) != POINT_FIELDS:
raise ValueError("example.DataPoint requires x, y, z in that order.")
return DataPoint(*(convert(field.value) for field in node.fields))
point_registry = ConversionRegistry()
point_registry.register_type(DataPoint, point_to_record)
point_registry.register_record("example.DataPoint", point_from_record)
The class imposes no coordinate-type restrictions, so the reverse handler only
validates its field names and order. If your class requires particular coordinate
types or other invariants, validate those requirements in the handler or its
conventional constructor. Recursive convert() calls retain each coordinate’s
usual conventional-to-DCI conversion.
Construct and inspect the DCI:
point = DataPoint(Fraction(1, 2), 0, 1.25)
point_instance = as_dc_instance(point, registry=point_registry)
print(render_shorthand(point_instance, padding=0))
Output:
record(
"example.DataPoint",
"x": 1/2,
"y": 0,
"z": 1.25
)
Write the Record DCI, read it back, and explicitly reconstruct a DataPoint:
write(point_instance, filename="point.dcb")
loaded_point = read("point.dcb")
assert isinstance(loaded_point, RecordDC)
restored_point = dc_instance_to_python(loaded_point, registry=point_registry)
assert type(restored_point) is DataPoint
assert (restored_point._x, restored_point._y, restored_point._z) == (
Fraction(1, 2), 0, 1.25
)
Reading returns a Record DCI. The registered reverse handler calls your class’s
constructor only during the subsequent conventional conversion. A Record name
never causes DC to import or instantiate a class automatically.
The rules also apply inside collections:
cloud = {"points": [point, DataPoint(1, 2, 3)]}
cloud_instance = as_dc_instance(cloud, registry=point_registry)
restored_cloud = dc_instance_to_python(cloud_instance, registry=point_registry)
assert all(type(value) is DataPoint for value in restored_cloud["points"])
assert restored_cloud["points"][1]._z == 3
This example does not define DataPoint.__eq__, so the assertions inspect the
restored class and attributes rather than comparing separately constructed
objects using Python’s default identity-based equality. The runnable file
examples/custom_datapoint.py performs the file round trip in a temporary directory.
Defaults, errors, and limits
With no registry, existing conversion rules are unchanged. An unsupported
conventional object raises TypeError. An unregistered Record follows the
existing generated-namedtuple conversion, which requires suitable Python
class and field names. In particular, a qualified Record name such as
"example.Date" needs an application handler for conventional conversion;
it remains valid for DCI reading, writing, and identity operations without one.
Application handlers are ordinary Python functions, explicitly chosen by the
caller. Handler exceptions propagate; the implementation does not silently
fall back to another rule or repair the Record. A handler is responsible for
its own schema, conversion policy, and any information it discards.
A custom convention does not make every conversion lossless. Conventional
collections still apply their own equality and hashability rules, and a handler
can choose a lossy conventional representation. The example date convention
has a tested round trip because both directions agree on its schema.
The runnable date example is examples/custom_conversion.py; the user-defined
class example is examples/custom_datapoint.py in the code repository.
See the Python API for registry signatures.
2.6 - Command-Line Interface
The dc command loads a DataCanon file and displays, converts, searches, or
selects items from its DCI. It works with Shorthand (.dcs), canonical binary bytes (.dcb), and
DataCanon HDF5 files (.h5 or .hdf5), and YAML interchange files
(.yml or .yaml).
Put an input filename first, or omit it to read Shorthand from standard input.
With no action option, dc prints Shorthand:
Installation and completion
Installing the Python package installs the dc command in the same environment.
Activate that environment before using it. For a development installation from
the code repository:
source DC/bin/activate
make install
dc --help
Enable shell completion in the active environment:
eval "$(register-python-argcomplete dc)"
The Makefile also provides make completion, which prints that command for you
to execute. Add the registration to your shell initialization after environment
activation if you want it applied automatically. Completion suggests command-line
options, filenames, and choices such as rendering styles. It does not inspect
file contents to suggest mapping keys. Option names appear when you type -;
completion does not fill an empty argument position with every CLI option.
After --get, further bare arguments are keys until another option begins.
Omit the filename to try a DCI directly at the command line:
echo 'power(8, 1, 3)' | dc
Output:
The command preserves the Power DCI and displays it in Shorthand. Constructor
names are lowercase. All existing actions and rendering options also work
with piped input:
echo 'power(8, 1, 3)' | dc --longhand
echo '{"outer": {"x": 3}}' | dc --get x
echo '[1, 2, 1]' | dc --count 1
echo 'power(8, 1, 3)' | dc --fingerprint
echo 'power(8, 1, 3)' | dc --dump root.dcb
Use - to select standard input explicitly, including with shell redirection:
dc - --layout standard --align left < structure.dcs
The CLI preprocesses Shorthand input by default, so compact Lists,
substitutions, and explicit eval(...) expressions need no extra option:
echo '<1 2, 3 4>' | dc --dump matrix.dcb
The resulting DCI is a List of Lists. Standard input defaults to Shorthand; select YAML with --input-format yaml.
Use a filename for canonical binary or HDF5 input. A displayed DCI can feed another
dc command:
dc matrix.dcb | dc --longhand
Input may span multiple lines and must contain exactly one complete expression.
The command reads through end-of-file; when typing directly in a terminal,
finish with Ctrl-D on Linux or macOS. Empty input and malformed expressions
are errors.
External-resource paths in standard input resolve from the current working
directory. Piped Shorthand carries the DCI’s metadata; it does not carry the
original file’s resource namespace or its external payloads.
A crystal structure illustrates how named parameters keep a scientific input
file readable. The example describes rutile, a form of titanium dioxide
(TiO₂). Three lattice vectors define a repeating cell; atom positions are
given as fractions of those vectors and grouped by chemical element.
Save the following input as rutile.dcs:
a = 4.593
c_over_a = 0.644
c = a * c_over_a
u = 0.305 |
{
"lattice_vectors": [
(a, 0, 0),
(0, a, 0),
(0, 0, c)
],
"basis_atoms": {
"Ti": [
(0, 0, 0),
(1/2, 1/2, 1/2)
],
"O": [
(u, u, 0),
(1 + -u, 1 + -u, 0),
(1/2 + u, 1/2 + -u, 1/2),
(1/2 + -u, 1/2 + u, 1/2)
]
}
}
The lattice_vectors and basis_atoms keys follow the Principia Materia
(PM) conventions. Lattice vectors and atom positions are written explicitly
as Lists of Tuples. The exact halves remain Fractions, and the parameter u
makes the relationships between oxygen positions visible in the input.
Load the input file directly:
Definitions are resolved in source order, so c can use a and c_over_a.
The definition c = a * c_over_a retains a Product in the third lattice
vector. No evaluation is needed to load or store that structure. Use
c = eval(a * c_over_a) if a computed numerical lattice parameter is wanted
instead.
The CLI preprocesses .dcs and standard-input Shorthand by default. Use
--strict to disable compact Lists, substitutions, and eval(...) and require
strict Shorthand:
dc structure.dcs --strict
echo '<1 2, 3 4>' | dc --strict
The second command reports a syntax error. --strict controls input parsing;
it does not change rendering or mapping-key interpretation. Canonical binary
and HDF5 files use their existing readers in either mode. The Python
parse_shorthand() function, read() function, and YAML airlocks retain their
strict defaults. See
Shorthand Preprocessing
for the individual input features.
Write the resolved DCI as canonical binary bytes:
dc rutile.dcs --dump rutile.dcb
The .dcb file is binary. It stores the DCI, including its numerical structure;
substitution definitions and source formatting are not stored. Subsequent reads
of this file do not need preprocessing.
The destination suffix selects the output format:
| Destination suffix | Output |
|---|
.dcs | UTF-8 strict Shorthand |
.dcb | Canonical binary bytes |
.h5 or .hdf5 | DataCanon HDF5 |
.yml or .yaml | DataCanon YAML interchange |
| No suffix | Shorthand, with .dcs appended |
For example:
dc rutile.dcb --dump rutile.h5
dc rutile.h5 --dump restored.dcs
dc rutile.dcb --dump rutile.yaml
A successful dump prints nothing. The resulting representation preserves the
DCI’s identity. For DCIs with external references, dumping produces a complete
package: resources are verified and copied beside .dcs, .dcb, or YAML output, or
embedded in HDF5. Missing or invalid required resources cause the dump to fail.
ExternalArray paths ending in .bin.gz select gzip storage for directory
packages and compressed embedded datasets for HDF5. The digest covers the
uncompressed canonical element bytes. Transfers retain the same path and
digest; an HDF5-to-directory dump recreates the gzip file. Configure compression
when constructing the conventional ExternalArray; no additional CLI flag is
needed. ExternalFile payloads remain opaque even when their paths end in .gz.
Display YAML
Use --yaml to print the DCI as YAML instead of Shorthand:
dc structure.dcb --yaml
dc structure.dcb --get basis_atoms --yaml
echo '{("Eg", 0): 2.37, "position": (1/2, 0, 0)}' | dc --yaml
The existing YAML interchange rules apply: native YAML values are used where
faithful, and $ introduces strict Shorthand for other DCTs, including Tuple
keys and values. The $ marker must be followed by whitespace. $100 and
${HOME} remain literal Strings. The marker performs no substitution or
evaluation; quoting alone does not escape it. A literal reserved prefix uses
a backslash, such as \$ 1/2. The writer handles literal-prefix escaping.
YAML output uses block-style Lists and Mappings, with one List member per
- entry; empty collections remain [] or {}. Single-line $ values
are not wrapped to a fixed width. YAML quotes scalars only when needed by its
syntax or scalar-resolution rules, and multiline Shorthand uses block scalars.
A .yml or .yaml dump target selects YAML without requiring --yaml:
dc structure.dcb --dump structure.yml
dc structure.dcb --yaml --dump structure
The second command creates structure.yaml. Combining --yaml with an explicit
non-YAML suffix is an error. YAML export preserves the DCI and does not convert
it to conventional Python objects. Terminal output needs only external metadata;
a YAML file dump verifies and copies referenced payloads beside the output file.
--yaml works with display and --get, but not --count or --fingerprint.
YAML output accepts --align left, --align right, --align none, and
--padding N:
dc rutile.dcs --yaml --align left --padding 0
dc rutile.dcb --dump rutile.yaml --align left --padding 1
Alignment is disabled by default. Within a native YAML List, rectangular groups
of nonempty, flat Tuple rows share column widths. Each List aligns independently.
Ragged rows, mixed List/Tuple rows, or cells requiring multiline Shorthand fall
back to normal rendering. Native YAML Lists remain Lists; alignment does not
replace them with Tuple escapes. Embedded Shorthand uses the same alignment
rules as render_shorthand().
Padding defaults to 1 and controls spaces beside same-line delimiters inside
$ escapes, including Tuple keys and numerical constructors. --padding 0
produces $ (1, 2) instead of $ ( 1, 2 ). Padding does not change YAML
indentation or the space separating $ from its value. Fraction literals and
other fixed forms keep their usual spelling. --longhand, --layout,
--custom-layout, and --indent remain Shorthand-only options and are rejected
for YAML output.
Read or write YAML
A .yaml or .yml input suffix selects YAML automatically. The default output
remains Shorthand, so conversion needs no output option:
dc structure.yaml
dc structure.yaml --dump structure.dcb
dc structure.yaml --yaml --align left
For standard input, choose YAML explicitly:
cat structure.yaml | dc --input-format yaml
cat structure.yaml | dc --input-format yaml --yaml --align left --padding 0
--input-format yaml and --input-format shorthand override text input format
selection for stdin or a named file. Input and output choices are independent;
--yaml always selects output, never input. The CLI does not guess format from
text contents. Canonical binary and HDF5 input still require filenames and their
existing readers.
YAML output uses block-style Lists and Mappings. Native multiline Strings use YAML literal block scalars where supported by PyYAML, with quoted fallback when necessary to preserve their contents. Multiline $ escapes likewise use literal block scalars when possible. The --fold-string N option enables width-controlled folded scalars for eligible long single-line Strings. YAML’s native folding and chomping rules apply; these differ from DataCanon Shorthand block-string rules.
YAML $ escapes always use strict Shorthand; they never run preprocessing.
--strict applies only to Shorthand input and is rejected with YAML input.
External paths in a YAML file resolve relative to its containing directory;
YAML received on stdin uses the current working directory. Metadata reads do
not retrieve external payloads. Python YAML import remains parse_yaml();
this CLI support does not change generic Python read()/write() dispatch.
Choose the displayed notation and layout
Choose delimiter layout, column alignment, and indentation independently:
dc rutile.dcb --layout standard --align left
dc rutile.dcb --layout compact --align left --indent 4
dc rutile.dcb --longhand
The default is standard layout with shorter notation where appropriate.
--layout standard puts multiline delimiters on separate lines;
--layout semi-compact shares the opening line with the first item or entry;
--layout compact also shares the closing line with the final content.
--layout inline forces the selected value and its whole subtree onto one line.
dc rutile.dcb --layout inline --padding 0
dc rutile.dcb --custom-layout '{"list":"inline"}'
A List forced inline also renders nested Mappings, Arrays, and other values
inline. Child layout overrides yield to that enclosing inline requirement;
siblings outside the List retain their own layouts. Column alignment and
structural indentation have no effect inside an inline subtree.
--align left, --align right, and --align none control column padding.
--align without a value selects left alignment. Alignment is disabled unless
requested. --indent N controls structural nesting
on new lines, defaulting to 2. It does not add spaces after a same-line opener.
Compact continuation items align beneath the first item; indentation moves
nested blocks below keys rather than changing that alignment.
--padding N controls spaces beside same-line collection and constructor
delimiters, defaulting to 1. It applies to flat values too: [ 1, 2 ] becomes
[1, 2] with --padding 0. Standalone delimiters receive no padding.
Both options accept nonnegative integers, including zero.
echo '{1, 2, [0, 1]}' | dc --layout compact --indent 0 --padding 0
Output:
Use --custom-layout for per-type overrides:
dc rutile.dcb --layout standard --custom-layout '{"mapping":"semi-compact","list":"compact","tuple":"compact","set":"compact"}' --align left
The argument is a strict DataCanon Mapping or UnorderedMapping with String keys
and String values. Quote the entire argument for the shell. Constructor form,
such as mapping("set": "compact"), is also accepted. The CLI does not use
JSON, Python evaluation, or preprocessing to read this configuration.
Every structured type accepts the same four layout contracts. For example,
"set", "multiset", "unordered_mapping", "record", and "array" can be
configured alongside Lists, Tuples, and Mappings. The
Python API lists all accepted type names.
An override affects only its named DCT; unspecified types use --layout
(default standard). No separate per-type command-line flags are needed.
Unknown names, invalid values, and duplicate keys are errors before input loading.
Multiline Mapping values start below their keys unless an enclosing inline
layout suppresses the line breaks.
--longhand selects explicit constructors for structured values. Null, Boolean,
Integer, Float, and String remain literals: sum(1, 1.0, fraction(1, 2)) is
longhand. Fraction uses fraction(...), Bytes keeps bytes(...), and Array
elements retain their specialized syntax. Layout and notation are independent:
dc rutile.dcb --longhand --layout semi-compact --align left
Lists, Tuples, Sets, and Multisets use one row per item when they contain a
List or Tuple, or an item that already spans multiple lines, unless forced inline. Coordinate Tuples
remain inline, so the rutile lattice vectors and atom positions appear as
separate rows. Alignment adds column padding for rectangular rows, including
coordinates containing Sums or Products. For example:
echo '[(1, 0), (0, -1)]' | dc --layout semi-compact --align left
Output:
Using --layout compact also puts the final ] on the last row.
The same options apply when dumping Shorthand:
dc rutile.dcb --dump readable.dcs --layout standard --align left
dc rutile.dcb --dump constructors.dcs --longhand
Rendering options affect textual representation, not DCI identity. The options also apply
to values printed by --get. They are rejected with --count, --fingerprint,
and binary or HDF5 output. YAML accepts alignment and padding as described above;
Shorthand layout options do not apply to YAML.
Render multiline and folded Strings
By default, standard Shorthand layout renders Strings containing LF characters as literal blocks when possible:
{
"message":
"""
First line
Second line
"""
}
Literal blocks preserve the String’s actual line breaks and whitespace. Strings that cannot safely use block syntax retain their ordinary quoted representation.
For long Strings without semantic line breaks, use --fold-string N to request physical wrapping at a preferred width of N characters:
dc input_data.dcs --fold-string 80
dc input_data.dcs --dump readable.dcs --fold-string 80
The renderer uses folded blocks when it can do so without changing the String DCI:
{
"description":
""">
This is a long description that can be wrapped across
physical lines without introducing semantic newlines
into the underlying String.
"""
}
The requested width is a preference, not a strict maximum. Long words and significant whitespace may require longer lines or fallback to quoted rendering.
Block Strings are emitted only under standard Shorthand layout. The semi-compact, compact, and inline policies retain quoted Strings with escapes. Enclosing inline layouts take precedence over child rendering preferences.
The same --fold-string option is available for YAML output:
dc input_data.dcs --yaml --fold-string 80
dc input_data.dcs --dump readable.yaml --fold-string 80
YAML uses its native literal (|) and folded (>) scalar syntax. PyYAML may choose quoted scalars when block formatting is unsuitable, particularly when significant trailing whitespace is present. Width-controlled folding is applied selectively to eligible Strings, not globally to all YAML output.
These options affect textual presentation only. Parsing the output recovers the same DCI and therefore the same canonical bytes and fingerprint.
Find a mapping key
Supply a bare name for a String key. For example, find the titanium positions
in the nested basis_atoms Mapping:
The search visits Mappings and UnorderedMappings throughout the DCI, so it finds
"Ti" inside "basis_atoms". It also visits mappings nested in collections,
Record field values, and mapping keys. It does not require a Mapping at the root.
One matching entry prints its value. If the same key occurs in multiple
mappings, the command prints a List containing all matching values, in traversal
order. Equal repeated values remain separate occurrences. It continues searching
inside a matched value, so deeper matches are included too. Traversal follows
the recovered constituent order; unordered collections do not promise an
application-level ordering of results.
For example, save this strict Shorthand as repeated.dcs:
{"left": {"x": 1}, "right": {"x": 2}}
Then:
prints a List containing 1 and 2. A single match whose value is itself a
List is printed directly; multiple List-valued matches form an outer List.
Keys can belong to any DCT. For example, when a file contains a Tuple key:
dc results.dcb --get '(1/2, 0, 0)'
Valid Shorthand keys retain their DCTs. To select String keys whose text would
otherwise parse as another DCT, include explicit Shorthand quotes:
dc results.dcb --get 1 # Integer key
dc results.dcb --get true # Boolean key
dc results.dcb --get '"1"' # String key
dc results.dcb --get '"true"' # String key
Implicit String handling accepts bare identifiers, including Unicode letters
and names such as basis_atoms or site_1. Names with spaces or punctuation
need explicit Shorthand quotes, for example --get '"site label"'. Malformed
structured keys such as power(1, remain errors; they do not become Strings.
Key comparison uses DCI equality. Integer 1, Float 1.0, and Boolean true
remain distinct keys. Record names and field names are fixed metadata, so
--get does not treat them as mapping keys. A missing key is an error.
Follow a chain of mapping keys
Supply several keys to --get to search for the first and then follow direct
Mapping entries:
dc rutile.dcb --get basis_atoms O
dc results.dcb --get measurements '("Eg", 0)'
The first key is searched recursively using the existing single-key behavior.
Every subsequent key is looked up directly in each matching Mapping or
UnorderedMapping value. Later steps do not search recursively or descend through
Lists. Each key uses the same strict Shorthand or bare-String-name rules.
Negative integer keys work within the chain. For negative expressions that
argument parsing could mistake for options, use constructor form, such as
--get measurements 'fraction(-1, 2)'.
Dots are not separators; a String key containing a dot needs Shorthand quotes,
for example --get '"basis.atoms"'.
Branches missing a subsequent key, or whose intermediate value is not a
Mapping, are skipped. If no branch completes the chain, the command reports
an error. Results stay separate during traversal: one final match is printed
directly, while multiple final matches become a List in traversal order.
Equal repeated matches remain separate, and List-valued matches are not flattened.
For example, this input has two complete chains and one incomplete branch:
echo '[{"a": {"b": 1}}, {"a": {"b": 2}}, {"a": 3}]' | dc --get a b
Output:
Put an input filename before --get; subsequent bare arguments are consumed
as keys. For stdin, omit the filename or use -. Another option ends the key
sequence:
dc rutile.dcb --get basis_atoms O --yaml --align
Pipelines remain useful, but a second dc --get searches recursively again.
A multi-key --get follows direct entries after the first recursive search.
Select or slice a List, Tuple, or Array
--index SELECTOR [SELECTOR ...] applies integers or slices successively.
Integer indices are zero-based and accept negative values from the end:
echo '[(1, 2), (3, 4)]' | dc --index 0
echo '[(1, 2), (3, 4)]' | dc --index=-1
echo '[(1, 2), (3, 4)]' | dc --index 1 0
Without --get, indexing applies to the root DCI. With --get, it applies to
the final result after the entire key chain:
dc rutile.dcb --get basis_atoms O --index 2
dc rutile.dcb --get basis_atoms O --index=-1 --yaml
If there are multiple final matches, --index selects one member of the
aggregate List; it does not index each match individually. A single List-valued
match is indexed directly. Selection order is always --get, then --index,
then rendering, independently of where the options occur on the command line.
For Lists and Tuples, integer indexing selects a contained DCI. For Arrays,
integer indexing selects along the first dimension. If the result is an Array,
it is returned as an Array DCI; if the result is an Array element, that value
is converted to a DCI. Strings, Sets, Multisets, and Mappings are not indexed.
An incompatible target or an out-of-range integer index reports
an error; malformed selectors are command-line usage errors. Empty sequences
have no valid indices.
Slicing operates along the first dimension and preserves the DCT: a List slice is a List, a Tuple slice is a Tuple, and an Array slice is an Array. Out-of-range bounds are clipped, and slices may produce an empty result. A zero step or malformed slice is a usage error before input is loaded. Subsequent selectors operate on the slice result itself, without distributing across its members.
Shorthand formatting and YAML output options apply to the selected DCI.
--index cannot be combined with --dump, --count, or --fingerprint.
Each index is applied to the preceding result: --index 1 0 selects item 1,
then item 0 from that value. Every intermediate target must be a List or Tuple;
otherwise the command fails without printing a partial result. Another option
ends the index sequence. Put the filename before --index as well as --get.
Use --index -1 -1 for several negative integer indices.
Slices use start:stop[:step], with Python’s exclusive stop bound, omitted
bounds, negative bounds, and negative steps:
dc rutile.dcb --get basis_atoms O --index 0:2
echo '(0, 1, 2, 3, 4)' | dc --index ::2
echo '[0, 1, 2, 3, 4]' | dc --index ::-1
Integer indexing selects a contained DCI for Lists and Tuples. For Arrays,
integer indexing selects along the first dimension. If the result is an Array,
it is returned as an Array DCI; if the result is an Array element, that value
is converted to a DCI. Slicing operates along the first dimension and
preserves the DCT: a List slice is a List, a Tuple slice is a Tuple, and an
Array slice is an Array.
Out-of-range bounds are clipped, and slices may produce an empty result.
A zero step or malformed slice is a usage error before input is loaded.
Subsequent selectors operate on the slice result itself, without distributing
across its members:
echo '[(0, 1), (2, 3), (4, 5)]' | dc --index 1: 0 1
That command selects the suffix List, its first Tuple, then the Tuple’s second
item, producing 3.
A slice beginning with - can look like an option to argparse, so attach it
with =. Repeat --index to continue the same selection chain:
echo '[0, 1, 2, 3, 4]' | dc --index=-2:
echo '[[0, 1, 2], [3, 4, 5]]' | dc --index 1 --index=-2: --index 0
Repeated --index options concatenate their selectors in order. Integers and
slices are the entire selection syntax; there is no expression evaluation or
filter language. Put the input filename before the selection arguments.
Approximate and round numerical values
Use --round N to round floating-point values to N decimal places, including
floating-point and complex Array elements. Negative N rounds to powers of
ten. Array type and shape are preserved. Rounding uses Python’s round rule
on binary floating-point values, not exact decimal arithmetic.
dc results.dcb --get reciprocal_vectors --round 3 --align
dc results.dcb --round 3 --yaml
Use --to-float to collapse a complete scalar Fraction, Constant, Sum, Product, Power,
or Quotient value to a Float. Integer and Boolean DCIs remain unchanged;
Complex components are processed individually. Existing Arrays retain their
types. Array-valued Sums and OrderedProducts retain their structure; scalar
subexpressions inside them are approximated without evaluating Array arithmetic. Lists, Tuples, Records, and other collections are processed recursively.
For example:
echo '2 * Pi + 1/2' | dc --to-float --round 3
produces:
Sum and Product operands are evaluated in canonical order so equal DCIs
have equal approximations, independent of their stored operand order.
Selection (--get and --index) happens first, then conversion, then rounding,
regardless of the order of flags. Mapping keys and external references are
unchanged, and no external payload is loaded. A Set transformation that would
produce duplicate members fails explicitly; Multisets retain occurrences.
Both options create a transformed DCI. They work with Shorthand, YAML, and
--dump, including binary or HDF5 output. The original DCI is unchanged;
a transformed DCI can have a different fingerprint. Neither option can be
combined with --count or --fingerprint. Omit --to-float to round Float
leaves while retaining the surrounding numerical structures and exact fields.
Count occurrences of a DCI
--count searches for a complete DCI using its type-defined equality:
dc rutile.dcb --count '1/2'
It prints an integer count. Zero matches is a successful search that prints 0.
The search includes the root and all constituent DCIs: collection members,
mapping keys and values, Record field values, Complex components, Power bases,
and Quotient constituents. Repeated occurrences count separately, including
repeated members of Multisets, Sums, and Products.
Fixed information prescribed directly by a DCT is not a separate DCI. Searching for Integer 1 does
not match a Fraction numerator or a Power exponent field. Likewise, Array
elements, Record names, relative-path components, and byte payload contents
are not searched as constituent DCIs.
Queries are never preprocessed. --count requires strict Shorthand;
--get additionally accepts bare String names as described above. For a
negative numeric query, use an equals sign so argument parsing
keeps the value with its option:
dc results.dcb --count=-1
The same convention works for negative keys: --get=-1.
Print the fingerprint
dc rutile.dcb --fingerprint
The output is the 64-character lowercase hexadecimal SHA-256 fingerprint of the
DCI’s canonical bytes. It is independent of Shorthand layout and HDF5 storage
layout. It is not a hash of the input file’s representation bytes, except when
that file is itself the raw canonical .dcb encoding.
Displaying, searching, and fingerprinting external DCIs use their metadata.
Metadata operations do not retrieve external payloads. Dumping a complete package does require
those resources.
Options and errors
Choose at most one action: --dump, --get, --count, or --fingerprint.
--get accepts one or more keys. --index optionally selects from the root
or the final --get result; it is not valid with the other actions.
With none supplied, the action is to display Shorthand, or YAML with --yaml. Omitting the filename,
or supplying -, reads text from standard input (Shorthand by default;
--input-format yaml selects YAML). Help and version output
do not read input:
Successful output goes to standard output and diagnostics go to standard error.
Displayed Shorthand and YAML are suitable for shell redirection, but use
--dump when creating a complete package with external resources.
| Exit status | Meaning |
|---|
0 | Success, including a count of zero |
1 | Input, data, resource, output, or selection error; including no matching key chain or invalid index target/bounds |
2 | Command-line usage error |
A malformed Shorthand query, invalid input file, or failed resource verification
produces a diagnostic and a nonzero exit status.
3 - Python API
A DataCanon Type (DCT) defines its instances and their identity.
The Python reference implementation provides construction of DataCanon Instances
(DCIs), conversion,
Shorthand, canonical bytes, fingerprints, persistence, and additional
conventional numerical and collection types.
A conventional type is a type used directly for data and computation in a
programming language, including built-in, library-provided, and user-defined
types. Conventional list, Fraction, and Power objects are distinct from
DCIs represented by ListDC, FractionDC, and PowerDC objects. DCIs and conventional objects
are both Python objects; conversion connects types with different contracts.
For a step-by-step introduction, use
Conventional Types.
Public API
| Purpose | Exports |
|---|
| Conversion between conventional objects and DCIs | ConversionRegistry, as_dc_instance, dc_instance_to_python |
| Shorthand | parse_shorthand, render_shorthand |
| Preprocessing | preprocess_shorthand |
| Canonical bytes | canonical_encode, canonical_decode |
| Fingerprinting | fingerprint |
| Persistence | read, write, read_hdf5, write_hdf5 |
| YAML | parse_yaml, render_yaml |
| Explicit numerical transformations | to_float, round_floats |
| Structural queries | iter_dcis, get_values, index_value, count_occurrences |
| Shared output operations | render_output, dump_output |
| Numerical conventional types | Constant, Pi, E, Sum, Product, OrderedProduct, Power, Quotient, ComplexNumber |
| Collection conventional types | Multiset, UnorderedDict |
| External-reference construction types | ExternalFile, ExternalArray |
Classes implementing DCTs, such as RecordDC and BytesDC, are available from
data_canon.instance. Field is a helper class for Record fields. Conversion
and Shorthand parsing can construct DCIs without requiring direct use of
those classes.
The functions below are available from data_canon. No function invokes
argparse or calls the dc executable. Applications can select, transform,
and display a DCI using the same operations as the command-line tool.
| Function | Contract |
|---|
iter_dcis(node) | Visit each DCI occurrence, including the root and Mapping keys; omit fixed information and Array elements. |
get_values(node, key, *following_keys) | Search recursively for the first key, then follow direct Mapping keys. Return one value, or a List DCI of multiple matches; fail if none. Keys are DCIs. |
index_value(node, index) | Select a List/Tuple item with an integer, or preserve its DCT when slicing with a Python slice. |
count_occurrences(node, query) | Count equal DCI occurrences using DCT-defined equality. Both arguments are DCIs. |
to_float(data) | Collapse complete non-Integer scalar numerical values to Float DCIs; recursively process collections and Complex components. Arrays remain unchanged. |
round_floats(data, digits) | Round Float DCIs and floating/complex Array elements to decimal places; negative digits is allowed. Array type and shape are preserved. |
render_output(data, *, format="shorthand", **options) | Render Shorthand or YAML; options are align, padding, longhand, layout, custom_layout, and indent. YAML uses alignment and padding only. |
dump_output(data, filename, *, format="shorthand", **options) | Write a complete package. A filename suffix selects its format; otherwise format supplies .dcs, .yaml, .dcb, or .h5. Text formats accept the rendering options. Return the destination Path. |
Numerical transformations preserve Mapping keys and external references,
without retrieving payloads. Set collisions fail rather than dropping members;
Multiset occurrences are retained. Rounding retains numerical expression
structure; call to_float() first to approximate complete expressions.
Scalar Sum and Product evaluation follows canonical operand order; scalar
OrderedProduct evaluation retains factor order. Array-valued Sums and
OrderedProducts retain their structure while scalar subexpressions are converted.
The input DCI and its cached fingerprint remain unchanged. Evaluation failures,
nonfinite results, and Array overflow are reported as exceptions.
from data_canon import (
parse_shorthand, get_values, index_value, to_float, round_floats,
render_output,
)
from data_canon.instance import StringDC
source = parse_shorthand('{"coordinates": [(Pi, 1/2), (E, 0)]}')
selected = index_value(get_values(source, StringDC("coordinates")), 0)
result = round_floats(to_float(selected), 3)
assert render_output(result, padding=0) == "(3.142, 0.5)"
Conversion between conventional objects and DCIs
as_dc_instance(obj, *, registry=None)
Construct a DCI from an instance of a supported conventional type. Passing an
existing DCI returns that instance unchanged. An optional ConversionRegistry
supplies application-owned conventional-type rules, applied recursively.
from fractions import Fraction
from data_canon import as_dc_instance
instance = as_dc_instance({
("Eg", 0): 2.37,
"position": (Fraction(1, 2), Fraction(1, 4), 0.199),
"label": "O",
"payload": b"\x00\xff",
})
| Conventional input | Resulting DCT |
|---|
None, bool, int, float, str, bytes | Null, Boolean, Integer, Float, String, Bytes, respectively |
fractions.Fraction | Fraction |
Constant (including Pi and E) | Constant |
Sum, Product, OrderedProduct, Power, Quotient | Corresponding numerical DCT |
Built-in complex | Complex with Float components |
ComplexNumber | Complex with separately typed components |
list, tuple, set | List, Tuple, Set |
Multiset | Multiset |
dict | Ordered Mapping |
UnorderedDict | UnorderedMapping |
Portable numpy.ndarray | Array |
ExternalFile, ExternalArray | Corresponding external DCT |
Floats and portable floating-point Array elements must be finite. Conventional
bytes is supported directly; convert mutable byte buffers to bytes
explicitly. Construction of a DCI cannot recover information already lost when
the source conventional object was constructed or manipulated.
dc_instance_to_python(node, source=None, *, registry=None)
Construct an instance of the corresponding conventional type from a DCI,
recursively converting constituent DCIs. The DCI is already a Python object;
the function name identifies the implementation language, not a change from
one language to another.
from data_canon import dc_instance_to_python
converted = dc_instance_to_python(instance)
assert type(converted) is dict
assert type(converted["position"]) is tuple
assert isinstance(converted["position"][0], Fraction)
| DCT | Conventional result |
|---|
| Null | None |
| Boolean | bool |
| Integer | int |
| Float | float |
| String | str |
| Bytes | bytes |
| Fraction | fractions.Fraction |
| Constant | Constant, retaining the exact name |
| Sum, Product, OrderedProduct, Power, Quotient | Corresponding conventional numerical classes |
| Complex | ComplexNumber |
| List, Tuple, Set | list, tuple, set, respectively |
| Multiset | Multiset |
| Mapping | dict |
| UnorderedMapping | UnorderedDict |
| Record | A registered handler’s result, or an instance of a generated collections.namedtuple class |
| Array | numpy.ndarray |
| ExternalFile | Verified payload bytes, or a content loader’s result |
| ExternalArray | A verified, materialized numpy.ndarray |
The conversion directions are not necessarily symmetric. A conventional
Fraction reduces ratios; Sum, Product, and Power apply their own
simplifications. A generated named tuple is accepted as a conventional Tuple
input by as_dc_instance(), so it does not reconstruct the original Record
name and fields through that function.
Conventional dictionaries and sets apply their own hashability and equality
rules. A hashable List DCI key becomes an unhashable conventional list, which
can make conversion to a dict fail. Distinct DCI keys or Set members can also
be merged if their converted conventional objects compare equal.
Without a matching custom Record handler, names and fields must satisfy
namedtuple restrictions for conventional Record conversion to succeed. Array conversion returns the stored read-only NumPy
array; use .copy() when an independently mutable array is wanted. Do not
circumvent the stored array’s read-only protection, because mutation would
invalidate the DCI’s identity and any cached fingerprint or hash.
External resources are materialized during this conversion. Their bytes are
retrieved and verified using retained source context or the supplied source.
ExternalFile conversion can additionally use the implementation’s filename-based
content loaders. Keep the external DCI for metadata-only work.
Conversion to conventional objects and construction of a new DCI do not promise
a lossless round trip for every DCI. The DCI model retains its own information
and equality rules independently of the conventional result types.
NumPy scalar conversion
as_dc_instance() supports NumPy Boolean, signed/unsigned integer, portable
floating-point and complex, Unicode, and byte scalars. The conversions preserve
their values using Boolean, Integer, Float, Complex, String, and Bytes DCIs.
Every supported NumPy integer width converts exactly to the unlimited-precision
Integer DCT. Finite binary16/32 values widen exactly to binary64, preserving
signed zero; complex64/128 become Complex DCIs with Float components.
Scalar width does not become identity metadata. A zero-dimensional ndarray
is still an Array DCI and retains its Array type and empty shape.
Nonfinite numerical scalars are rejected. Wider floating-point/complex formats,
temporal scalars (datetime64, timedelta64), and structured or opaque void
scalars require an explicit conversion or registry handler. Portable dtype aliases
follow their corresponding dtype rules. Custom exact-type handlers take
precedence over the built-in NumPy conversions.
See NumPy scalar values.
ConversionRegistry()
Create an application-owned registry. The conversion functions accept it through
registry=registry; omission retains the existing default rules.
| Method | Rule |
|---|
register_type(conventional_type, converter) | Exact conventional type → DCI |
register_record(name, converter) | Exact Record name → conventional object |
Both methods receive a handler callable as converter(value, convert). The
second argument recursively converts using the same registry. A reverse
handler’s recursive converter also retains the outer source context.
Conventional-to-DCI handlers must return an InstanceDC; reverse handlers
receive the original RecordDC and are responsible for validating their schema.
Rules for exact conventional types take precedence over built-in conversion.
Rules do not apply to subclasses implicitly. Existing DCIs always pass through
as_dc_instance() unchanged, and registering a DCI implementation class is
rejected. Record matching uses the complete name, without import or class lookup.
Duplicate registrations, invalid types or names, and noncallable handlers raise
errors. Handler exceptions propagate without fallback.
Registries do not alter global behavior, DCT definitions, canonical encoding,
or parsing and persistence. No handler is serialized into the DCI. An
unregistered Record retains the generated-namedtuple fallback, including its
name restrictions. Writers and codecs do not accept a registry: convert to a
DCI explicitly before writing and convert decoded DCIs afterward.
The Custom Conversion Rules guide
defines both directions of a date convention and demonstrates nested conversion,
schema validation, and canonical and file round trips.
Shorthand parsing
parse_shorthand(text, *, preprocess=False, substitutions=None)
Parse exactly one complete Shorthand expression into a DCI:
from data_canon import parse_shorthand
phase = parse_shorthand("complex(1/2, 1/2 * sqrt(3))")
The parser accepts explicit constructors and their shorter forms in any mixture:
assert parse_shorthand("fraction(1, 2)") == parse_shorthand("1/2")
assert parse_shorthand("list(1, 2)") == parse_shorthand("[1, 2]")
assert parse_shorthand("tuple(1)") == parse_shorthand("(1)")
assert parse_shorthand("set()") == as_dc_instance(set())
assert parse_shorthand('mapping("x": 1)') == parse_shorthand('{"x": 1}')
Collection constructor arguments are their members or entries. For example,
list([1, 2]) is a List containing one List, not a conversion of that inner List.
set() is the empty Set; {} is the empty Mapping. The old emptyset spelling
is not accepted.
Strict parsing rejects trailing input and invalid DCT information. Numerical
parentheses are not general grouping: (1 + 2) is a singleton Tuple containing
a Sum. Use constructors when nesting needs to be explicit.
Preprocessing can be requested directly:
prepared_instance = parse_shorthand("<1 2 3>", preprocess=True)
The dc CLI preprocesses Shorthand by default and offers --strict; that
frontend policy does not change this function’s strict default.
Supplying substitutions requires preprocess=True. For a clear two-stage
workflow, call preprocess_shorthand() separately as shown below.
Shorthand rendering
render_shorthand(node, *, layout="standard", custom_layout=None, align=False, indent=2, padding=1, longhand=False)
Render a DCI as identity-preserving Shorthand. A conventional input object is
first converted with as_dc_instance(), and the resulting DCI is rendered:
from data_canon import render_shorthand
text = render_shorthand(phase)
assert parse_shorthand(text) == phase
Shorthand has no canonical text layout. Rendering must preserve DCI equality;
fingerprints use canonical bytes rather than this text.
Longhand
longhand is a Boolean, defaulting to False. With True, all structured DCIs
use explicit constructors. Null, Boolean, Integer, Float, and String retain
their literal spellings. Fraction uses fraction(...), and Bytes retains
bytes(...). Array elements retain their specialized numerical syntax.
The boundary is syntactic, not the primitive/atomic/composite classification.
For example, sum(1, 1.0, fraction(1, 2)) is longhand; literals need no
additional wrappers. Version 1 defines neither integer(...), float(...),
nor string(...) wrappers, nor a separate fully wrapped rendering mode.
The same parser accepts both outputs.
value = parse_shorthand("1/2 * sqrt(3)")
assert render_shorthand(value, layout="standard") == "1/2 * sqrt( 3 )"
assert render_shorthand(value, layout="standard", longhand=True) == (
"product( fraction(1, 2), power( 3, 1, 2 ) )"
)
| Structure | Default examples | Longhand |
|---|
| Fraction | 1/2 | fraction(1, 2) |
| Sum | 1 + 2 | sum(1, 2) |
| Product | 2 * sqrt(3) | product(2, power(3, 1, 2)) |
| Power | 5**2, 5**(1/3), sqrt(5) | power(5, 2), power(5, 1, 3), power(5, 1, 2) |
| List | [1, 2] | list(1, 2) |
| Tuple | (1, 2) | tuple(1, 2) |
| Set | {1, 2}, set() | set(1, 2), set() |
| Mapping | {"a": 1} | mapping("a": 1) |
Default rendering uses constructors to retain empty or singleton Sums and
Products, nesting, and unambiguous Power bases. Quotient retains quotient(...).
No separate notation or power_style option is provided.
Independent layout, alignment, indentation, and padding
layout controls line breaking and delimiter placement:
| Layout | Opening delimiter | Closing delimiter |
|---|
standard | Separate line | Separate line |
semi-compact | Shares the first item or entry line | Separate line |
compact | Shares the first item or entry line | Shares the final content line |
inline | Shares the only content line | Shares the only content line |
For example, a semi-compact Mapping renders as:
Scalar-only collections and empty collections normally stay inline. Lists,
Tuples, Sets, and Multisets normally become multiline when they directly
contain a List or Tuple, or an item already spans multiple lines. An inline
subtree suppresses those line breaks. Nonempty Mappings are multiline unless their effective layout is inline.
Multiline Mapping values start below their keys,
so indentation does not depend on key length. Multiline keys remain valid
Shorthand and use the same recursive rendering rules.
align="left" or align="right" pads cells in rectangular List/Tuple rows
and supported Array tables. align=False disables column padding. Alignment can be
combined with every layout and with longhand. Ragged rows receive no column
padding. Alignment does not introduce a line-width wrapping threshold.
indent controls extra indentation when structural nesting places content on
new lines, defaulting to 2. For example, it controls content after a
standalone opener and a multiline value below its Mapping key. It does not
add spacing after an opener that shares its content line. Compact continuation
items align beneath the first item, whose column is determined by the opening
token and padding. Consequently, changing indentation does not move compact
Mapping keys; it moves nested blocks below them.
padding controls gaps beside same-line collection and constructor delimiters,
defaulting to 1. For example, a flat List is [ 1, 2 ]; padding=0 gives
[1, 2]. It also supplies the gap after a shared multiline opener and before
a shared multiline closer. A standalone delimiter receives no padding.
Continuation items begin at relative column len(opening) + padding.
Both options accept nonnegative integers, including zero, and reject Boolean,
negative, and noninteger arguments. Empty collections retain [], (),
set(), and similar spellings. Primitive literals and their fixed constructor
spellings, item separators, and specialized Array element brackets retain their
existing forms. align independently controls table-cell padding.
packed = parse_shorthand('{1, 2, [0, 1]}')
print(render_shorthand(packed, layout="compact", indent=0, padding=0))
Output:
rows = parse_shorthand('{"rows": [(1, 20), (30, 4)]}')
text = render_shorthand(
rows,
custom_layout={"list": "compact", "tuple": "compact", "mapping": "semi-compact"},
align="left",
indent=2,
)
print(text)
assert parse_shorthand(text) == rows
Output:
{ "rows":
[ ( 1 , 20 ),
( 30, 4 ) ]
}
layout is a single global layout name, defaulting to "standard".
custom_layout is an optional conventional mapping from lowercase type names
to layout names. Each override applies only to the named DCT; all unspecified
types use the global layout. The arguments do not change one another.
Accepted override names are list, tuple, mapping, unordered_mapping,
record, set, multiset, sum, product, ordered_product, power, quotient, complex,
array, external_file, and external_array. Sets, Multisets, Records, and
other constructor forms follow the same four layout contracts.
Mapping overrides do not implicitly affect UnorderedMappings or Records.
Ordinary scalar literals remain unchanged. The three multiline layouts do not
force a flat sequence to break; inline suppresses structural line breaks.
text = render_shorthand(
rows,
layout="compact",
custom_layout={"mapping": "semi-compact", "set": "standard"},
align="right",
indent=4,
)
assert parse_shorthand(text) == rows
Constructor continuation arguments align beneath the first argument, at the
opening token’s length plus padding. Indentation does not change that column.
Unknown layouts or type names are rejected. layout does not accept a mapping.
align defaults to False; only False, "left", and "right" are accepted.
Settings are passed per call.
Force one line globally or for a subtree
layout="inline" renders the selected value and every descendant on one line.
An inline parent overrides child layout choices inside that subtree. Alignment
is disabled there, and indentation has no effect; padding still applies.
String line breaks remain escaped within their literals. No input is evaluated
or mathematically simplified.
text = render_shorthand(rows, layout="inline", padding=0)
print(text)
assert parse_shorthand(text) == rows
Output:
{"rows": [(1, 20), (30, 4)]}
Use custom_layout={"list": "inline"} to keep Lists on one line while other
values follow their selected layouts. A Mapping or Array inside such a List
is also inline, even if its own type override requests standard. Siblings
outside that List retain their selected layouts. A type override is consulted
at each node unless an enclosing inline value has already forced the subtree.
Array element brackets retain their specialized numerical syntax; Array
elements are not List DCIs. The Array constructor follows its selected layout,
and structural Array nesting uses the requested indentation. Scalars retain
their existing spellings.
Rectangular rows and line breaking
Outside an inline subtree, Lists, Tuples, Sets, and Multisets become multiline when they directly
contain a List or Tuple, including an empty one, or when an item already renders
across multiple lines. Scalar and inline numerical items do not independently force breaks.
Flat sequences have no automatic line-width threshold.
Alignment recognizes rectangular List or Tuple rows inside any of those four
collection types and preserves each row’s DCT. Short Sums, Products, and Powers can occupy a single cell. Ragged
rows receive no table padding. The rules also apply to longhand constructors.
Column padding may change across a table when one cell grows; use align=False
when minimizing such whitespace changes matters for version-control diffs.
Numerical conventional types
from data_canon import Sum, Product, OrderedProduct, Power, Quotient, ComplexNumber
The numerical conventional types accept int, finite float,
fractions.Fraction, and instances of the supported numerical conventional
types. Boolean is not a numerical operand. Their construction, equality, and
arithmetic rules are distinct from the associated DCT rules.
Sum(*items) and Product(*items)
Construct variadic sums and products:
a = Power(2, 1, 2)
b = Power(3, 1, 2)
assert Sum(b, a, 3*b) == Sum(4*b, a)
assert Product(a, a) == Product(Power(2, 1))
For scalar-only operands, conventional Sum and Product flatten instances of
their own type, combine
matching terms or factors, and preserve first-occurrence presentation order.
Equality compares exact normalized values across supported numerical types and
ignores operand order. Conventional float contributions remain
separate from exact rational quantities; matching float contributions can
combine with one another.
Scalar multiplication distributes over conventional scalar Sum objects.
Array-valued Sums retain structure until explicit evaluation.
.items is the immutable tuple of simplified operands. Conventional Sum and
Product objects are frozen and unhashable. Sum and Product DCIs are hashable
and retain their prescribed DCI structure independently of conventional
simplification.
Power(base, numerator, denominator=1)
Construct a symbolic positive rational power:
root = Power(3, 1, 2)
cube_root = Power(5, 1, 3)
assert Power(5, 2, 4) == Power(5, 1, 2)
The exponent fields must be positive Python int values; conventional Power
reduces their ratio. A Power DCI instead retains its direct integer exponent fields exactly.
Conventional Power extracts an exact root of a rational base when the reduced
exponent’s denominator permits it, retaining the remaining integer exponent.
For example, Power(4, 1, 2) stores base=2, numerator=1, denominator=1,
and Power(16, 3, 2) stores base=4, numerator=3, denominator=1.
Power(4, 8, 1) retains all three fields; construction does not expand it to
Power(65536, 1, 1). Without an exact denominator root, the base and reduced
exponent remain unchanged.
Float bases follow the same construction rule, provided the extracted root is
exactly representable as a float. Even roots of known negative scalar bases are
rejected, and odd roots use the real branch. Nested scalar-base powers combine
exponents with the real branch accounted for.
Equality and explicit numerical conversion still recognize the value of a
retained integer power:
integer_power = Power(4, 8)
assert (integer_power.base, integer_power.numerator, integer_power.denominator) == (4, 8, 1)
assert integer_power == 65536
assert int(integer_power) == 65536
Surrounding Sum, Product, and Quotient arithmetic may evaluate retained powers
when simplifying its operands. DCI conversion stores the conventional object’s
current base and exponent fields; it does not use the cached comparison form.
Quotient(numerator, denominator)
Represent a structured ratio, including a Sum or Product denominator:
ratio = Quotient(1, Sum(a, b))
assert 1 / Sum(a, b) == ratio
The conventional constructor reduces rational coefficients, cancels common
factors, and rejects denominators recognized as zero. It retains the Quotient
type and does not rationalize a Sum denominator during construction. Exact
cross multiplication can recognize a rationalized equivalent during comparison.
The Quotient DCT’s structural validity
rules are separately defined by Core.
Numerical equality
Comparison uses exact rational-expression normalization, with cached comparison
forms, rather than comparing float() results. Float leaves contribute their
actual binary rational values: Quotient(1, 2) == 0.5, but
Quotient(1, 10) != 0.1. Float and rational storage remain separate during
construction.
The supported normalization is not complete mathematical equality for arbitrary
expressions. Fractional powers of nonrational bases remain symbolic; integer
factorization is bounded to trial divisors through 1000 and residual perfect
powers through degree 64. Unresolved factors are preserved exactly, so some
mathematical equivalences may not be recognized. See
Conventional Types for examples
and comparison limits.
Explicit float() and int() conversion
assert float(Sum(1, 1.0)) == 2.0
approximation = float(ratio)
assert int(Quotient(-7, 3)) == -2
Conversions evaluate without modifying the object. int() truncates toward
zero. Exact integer and Fraction intermediates remain exact where possible;
fractional powers follow math.pow. Domain errors, division by zero, and
overflow can raise Python exceptions. Numerical evaluation operates on the
conventional objects, not on DCIs.
ComplexNumber(real, imaginary)
Construct a conventional ComplexNumber object with separately typed numerical
components. Equality compares numerical component values, including against
supported real scalars and built-in complex values. Conventional
ComplexNumber is unhashable. Built-in complex stores floating-point components:
z = ComplexNumber(Fraction(1, 2), Product(Fraction(1, 2), Power(3, 1, 2)))
assert as_dc_instance(z) == phase
approximate_complex = complex(z)
The conventional ComplexNumber type supports arithmetic and conjugation.
complex(z) requests floating-point complex components.
OrderedProduct and Array-valued Sum
OrderedProduct(*factors) stores real conventional scalar values, NumPy
arrays, and other array-expression objects in order. Sum(*terms) also accepts Arrays and OrderedProducts. A scalar-only Sum
retains its existing normalization rules. Scalar Product, Power, Quotient, and
ComplexNumber components reject Array-valued structures.
expression.evaluate() evaluates Array structure into an object-dtype NumPy
Array, retaining exact conventional scalar entries.np.asarray(expression, dtype=float) or dtype=complex explicitly requests
machine values. Without a dtype, exact object entries remain.- Array-valued Sum requires matching shapes at any rank. OrderedProduct requires
rank-two Arrays and compatible multiplication dimensions. Scalar factors scale
the result; no broadcasting, implicit identity insertion, or reshaping occurs.
- Construction does not infer shapes or multiply Arrays. Conversion from a valid
DCI can still fail if a conventional scalar subexpression is invalid, as with
a Quotient having a recognized zero denominator.
- Conventional whole-Array equality returns a Boolean using exact scalar
comparisons. Array-valued expressions cannot be converted to scalar float/int.
is_numerical_dci(node) and is_scalar_numerical_dci(node) are available from
data_canon.instance. Both classify structure without mathematical evaluation.
OrderedProduct DCIs use OrderedProductDC(items_tuple); their immutable operands
are exposed through .items. Sum DCIs remain unordered and preserve multiplicity.
Bytes and collections
Conventional bytes
payload = as_dc_instance(b"\x00\xff\x80")
assert payload == parse_shorthand('bytes("00ff80")')
A Bytes DCI has byte-for-byte identity and no interpretation metadata. To store
metadata alongside a payload, use a Record:
from data_canon.instance import BytesDC, Field, RecordDC, StringDC
blob = RecordDC("Payload", (
Field("media_type", StringDC("application/octet-stream")),
Field("filename", StringDC("sample.bin")),
Field("data", BytesDC(b"\x00\xff\x80")),
))
Record and field names are direct Unicode information. Field values remain
DCIs whose DCTs participate in identity.
Multiset(iterable)
from data_canon import Multiset
members = Multiset(["O", "Sr", "O"])
members.add("O")
assert as_dc_instance(members) == parse_shorthand('multiset("Sr", "O", "O", "O")')
Multiset() is empty. Duplicates are retained, and order is insignificant for
equality. Conventional Multiset uses its members’ equality rules and supports unhashable
members when their equality yields a usable Boolean. It does not impose DCI
equality on conventional members.
UnorderedDict
from data_canon import UnorderedDict
first = as_dc_instance(UnorderedDict({"a": 1, "b": 2}))
second = as_dc_instance(UnorderedDict({"b": 2, "a": 1}))
assert first == second
Conventional dict objects construct ordered Mapping DCIs. UnorderedDict
retains Python dictionary-key restrictions and cannot recover keys already
merged by conventional equality.
Canonical encoding and fingerprints
canonical_encode(node) and canonical_decode(data)
from data_canon import canonical_encode, canonical_decode
encoded = canonical_encode(instance)
assert type(encoded) is bytes
assert canonical_decode(encoded) == instance
Encoding includes every DCI’s one-byte DCT identifier and its prescribed payload.
Variable-type constituents use complete DCI encodings. Fixed integer fields in
Fraction and Power use signed integer payloads directly. Record names and path
components use UTF-8 byte counts and bytes directly, without String identifiers.
Array elements use logical C order and portable element representations, with
multibyte values in big-endian order. Canonical encoding is independent of
Shorthand and NumPy memory layout.
canonical_decode() requires conventional bytes, consumes exactly one complete DCI,
and rejects malformed or noncanonical input. External metadata decodes without
resource retrieval.
fingerprint(node)
from data_canon import fingerprint
identifier = fingerprint(instance)
assert type(identifier) is str
assert len(identifier) == 64
The Python return value is lowercase hexadecimal SHA-256 of the canonical bytes.
The abstract fingerprint is 32 digest bytes. Equal DCIs have equal fingerprints;
a matching digest is not the definition of equality.
Python hashing of DCIs
All 24 DCT implementations support Python hash() and can be used as dictionary
keys, including Arrays, Lists, Sets, and nested structures:
from data_canon.instance import FloatDC, IntegerDC, ListDC
key = ListDC((IntegerDC(1), FloatDC(-0.0)))
lookup = {key: "sample"}
assert lookup[ListDC((IntegerDC(1), FloatDC(-0.0)))] == "sample"
Hash computation is lazy. Constructing a DCI does not compute its hash or
fingerprint. Null, Boolean, Integer, Float, String, Bytes, and Fraction DCIs use
direct hashes; Float uses its binary64 bits. Structured DCIs use their cached
fingerprint bytes, computing the fingerprint on the first hash request if it
is not already available. Nonsemantic source context and payload caches do not
participate in the hash.
Python hashes satisfy the equality requirement that equal DCIs have equal
hashes. Hash values need not distinguish unequal DCIs and are not stable
cross-process identifiers. Use fingerprint() for the portable identifier.
Hashability of a DCI does not imply hashability of its converted conventional
object: a List DCI and a conventional list remain different types.
Persistence
from data_canon import read, write
written = write(instance, filename="output_data.dcb")
assert read(written) == instance
format | Default suffix | Contents |
|---|
"shorthand" | .dcs | UTF-8 strict Shorthand |
"bytes" | .dcb | Raw canonical binary encoding |
"hdf5" | .h5 | DataCanon HDF5 |
A recognized filename suffix overrides format selection. .hdf5 is also
recognized. Without a suffix, the selected format’s suffix is appended.
Without a filename, the fingerprint supplies the filename stem. The return
value is a pathlib.Path.
The current writer verifies and persists referenced external resources. For
.dcs and .dcb, their paths are relative to the output directory; HDF5 embeds
them separately. This public interface has no metadata-only export flag.
A write can fail when required resources are missing or conflicting.
read(filename)
Select the format by suffix and reconstruct a DCI. Reads preserve nonsemantic
source context for later external-resource access. They do not materialize
external payloads.
write_hdf5(node, filename, *, store_fingerprint=False) and read_hdf5(filename)
from data_canon import read_hdf5, write_hdf5
write_hdf5(instance, "example.h5", store_fingerprint=True)
assert read_hdf5("example.h5") == instance
Arrays use native HDF5 numerical datasets; Bytes uses a rank-one uint8 dataset.
Fixed metadata fields reuse integer/string payload storage without DCI kind
attributes. Genuine constituent DCIs retain their required kind attributes.
HDF5 file bytes are not canonical DCI bytes.
store_fingerprint adds optional file-root metadata. It does not add a DCI
constituent. Complete HDF5 writes verify external payloads before embedding them.
External-reference construction types
ExternalFile(path, _data=None)
Construct a conventional ExternalFile object from a relative path to an
existing file, or from contained input bytes or text:
from data_canon import ExternalFile
resource = ExternalFile.from_bytes(b"input data", filename="input.bin")
resource_instance = as_dc_instance(resource)
from_bytes(data, filename=None, directory="external_dc") retains input bytes
as a nonsemantic cache. from_string(text, filename=None, directory="external_dc")
encodes UTF-8. If omitted, the filename is derived from the content digest.
The resulting DCI contains a relative path and digest, not the payload itself.
ExternalArray(data, directory="external_dc", filename=None, compression=False)
import numpy as np
from data_canon import ExternalArray
external_array = as_dc_instance(ExternalArray(
np.array([1.0, -0.0], dtype="float64"),
filename="values.bin",
))
Conversion records portable type, shape, relative path, and SHA-256 of the
canonical Array element stream. Cached data and source context do not affect
identity. Metadata reads need no payload; conversion of the ExternalArray DCI
to a conventional NumPy array materializes and verifies it.
Compress external Array storage
compression is a Boolean and defaults to False. Choose a filename stem;
the constructor appends .bin or .bin.gz and stores the finalized filename:
forces = np.zeros((32, 32), dtype="float64")
compressed = ExternalArray(forces, filename="forces", compression=True)
assert compressed.filename == "forces.bin.gz"
compressed_instance = as_dc_instance(compressed)
assert compressed_instance.path.components[-1] == "forces.bin.gz"
| Filename argument | compression=False | compression=True |
|---|
| Omitted | <digest>.bin | <digest>.bin.gz |
"forces" | forces.bin | forces.bin.gz |
"forces.bin" | Accepted | Error |
"forces.bin.gz" | Error | Accepted |
A filename is a nonempty single component. Explicit suffixes must match the
setting; suffixes other than .bin and .bin.gz are rejected. Use directory
for path prefixes. Compression is encoded by the finalized path, not a new DCI
constituent. The digest always covers the uncompressed canonical element bytes.
write(compressed_instance, filename="forces.dcs")
compressed_restored = read("forces.dcs")
assert compressed_restored == compressed_instance
assert np.array_equal(dc_instance_to_python(compressed_restored), forces)
Directory output writes external_dc/forces.bin.gz; retrieval transparently
decompresses, verifies the digest, and validates shape, type, and elements.
Malformed gzip and excess decompressed data are rejected. Existing conflicting
resources are not overwritten. HDF5 output embeds logical bytes in a chunked
gzip-compressed dataset (level 6), retaining the same DCI path and digest:
write(compressed_instance, filename="forces.h5")
assert read("forces.h5") == compressed_instance
Exporting that HDF5 DCI to a directory recreates the gzip file. Empty and scalar
Arrays are supported. This option does not compress native Array DCIs or opaque
ExternalFiles, and does not add .dcb.gz representation support. A .gz
ExternalFile still means its exact opaque bytes. Raw canonical element payloads
use .bin, not .dcb, because they are not complete DCI encodings. The runnable
examples/external_array_compression.py demonstrates directory/HDF5/directory
transfer with a zero-heavy force-constant Array.
Preprocessing
preprocess_shorthand()
The common call form is:
from data_canon import preprocess_shorthand
prepared = preprocess_shorthand("<1 2 3>")
assert parse_shorthand(prepared) == parse_shorthand("[1, 2, 3]")
Transformations run in order: compact List expansion, external and inline
ordered textual substitution, then explicit scalar eval(...) in the body.
Local definitions may use external names and earlier local definitions. Each
local definition resolves its own explicit eval(...) before becoming available
to later definitions. Self-references and forward references are rejected.
Replacements are literal text; use constructors to preserve nested numerical
expression boundaries. The result is text.
Quoted Strings, including Bytes hexadecimal arguments, are protected.
See the Shorthand Preprocessing guide
for separate introductions to each feature.
YAML interchange
parse_yaml(text) and render_yaml(node, *, align=False, padding=1)
from data_canon import parse_yaml, render_yaml
yaml_instance = parse_yaml("""
'$ ("Eg", 0)': 2.37
name: calculation
phase: $ complex(1/2, 1/2 * sqrt(3))
payload: $ bytes("00ff80")
""")
assert parse_yaml(render_yaml(yaml_instance)) == yaml_instance
assert dc_instance_to_python(yaml_instance)[("Eg", 0)] == 2.37
Native YAML sequences are Lists; mappings become ordered Mappings. The $
prefix is interpreted after YAML String decoding and invokes strict Shorthand.
The $ marker requires a whitespace boundary; $100 and ${HOME} remain
literal Strings. A lone $ is reserved but lacks the required embedded DCI.
Quoting alone does not escape a marker; a backslash does (\$ 1/2).
Literal reserved prefixes are escaped by the writer. Native key collisions or
unhashable converted keys cause the entire Mapping to use the airlock.
Bytes, Multiset, and all structured numerical DCTs use embedded Shorthand.
YAML does not implicitly preprocess embedded text or retrieve external resources.
Its presentation is not canonical and is not fingerprinted directly.
YAML uses block-style Lists and Mappings, keeps single-line scalars unwrapped,
and uses literal blocks for multiline Shorthand. Empty collections retain []
and {}. Scalar quoting is left to YAML’s syntax and scalar-resolution rules.
rows = parse_shorthand("[(1, 2), (30, 4)]")
yaml_text = render_yaml(rows, align="left", padding=0)
assert parse_yaml(yaml_text) == rows
align accepts False (default), "left", or "right". Lists of rectangular,
nonempty, flat Tuple rows share column alignment; ragged or mixed rows retain
ordinary formatting. Embedded Shorthand also receives the alignment setting.
padding is a nonnegative integer, defaulting to 1, with the same same-line
delimiter contract as render_shorthand(). Padding affects $ text rather
than YAML indentation, and applies to embedded keys as well as values.
Specification and draft compatibility
The seven specifications define the
language-independent contracts. The optional
Conventional Types companion
defines common conventional behavior; this API documents the Python binding.
Core conformance does not require conventional arithmetic.
The current fixed-field revision changes
canonical bytes and fingerprints from earlier drafts. Regenerate affected
old-draft .dcb files; automatic legacy-format detection is not provided.
4 - Publication Outline
This outline proposes a paper for the Journal of Open Source Software
(JOSS) describing DataCanon and its Python reference implementation.
The paper should explain the research problem, the software’s
contribution, and the evidence supporting its use. The seven
specifications and documentation carry the detailed type definitions,
encoding rules, and API reference.
Working title
DataCanon: Deterministic Identity and Persistence for Structured
Scientific Data
An alternative retaining the original emphasis is:
DataCanon: Deterministic Semantic Identity for Structured Scientific
Data
A different choice which tries to capture the versatility for exploring
data:
DataCanon: The Last Mile of Scientific Data
Canonical Identity Across Programming Languages and Storage Formats
Central thesis
Scientific software needs a precise way to say what structured data
is, independently of how that data happens to be represented,
stored, or manipulated. DataCanon provides a formal,
language-independent data model for structured scientific data together
with canonical identity.
DataCanon defines DataCanon Types (DCTs), their valid DataCanon
Instances (DCIs), and their equality relations. Every DCI has a unique
canonical byte encoding. Human-readable Shorthand, YAML interchange, and
HDF5 represent that same data without defining its identity.
This distinction should be explicit throughout the paper. DataCanon is
not a database, merely a serialization format, a schema system, or a
programming language. Those systems answer related but different
questions. DataCanon’s core question is: what data object is this?
Representation asks how to communicate it; persistence asks how to store
it; a database asks how to organize, locate, query, and update
collections of it; computation asks what operations to perform on it.
The scientific motivation should lead: exact numerical structures built
from Fractions, Products, Powers, Sums, and Quotients, structured
Complex values, typed multidimensional Arrays, and reliable persistence
of nested scientific data. The paper should then explain how canonical
identity makes those structures portable and reproducibly identifiable.
The current JOSS paper
guidance sets a
750–1,750-word range and identifies sections for Summary, Statement of
need, State of the field, Software design, Research impact statement, AI
usage disclosure, Acknowledgements, and References.
Organize the manuscript around those sections. Aim for approximately
1,400–1,600 words of prose, leaving room to adjust the emphasis during
writing. The word allocations below are planning targets, not journal
requirements.
Section Approximate words
Summary 150
Statement of need 250
State of the field 325
Software design 400
Research impact statement 250
AI usage disclosure and Acknowledgements 100
The submitted paper concerns usable research software. The Python
package, its tests, documentation, and scientific applications should
therefore have a substantial role. Independent implementations
strengthen the portability evidence, but should be described as
completed work only when they have been implemented and validated.
1. Summary
Introduce DataCanon in language accessible to a researcher who has never
used it. Describe it first as a formal, language-independent data
model for structured scientific data with canonical identity, rather
than as a serialization or persistence format.
Explain that DataCanon allows complex scientific data to be constructed,
communicated, compared, fingerprinted, and persisted while retaining an
identity independent of storage layout and implementation language.
Briefly identify the distinctive capabilities:
- Exact structured numerical data, including irrational Powers,
structured Quotients, and Complex values with independently typed
components.
- Portable typed Arrays alongside recursively structured collections.
- Deterministic binary identity and SHA-256 fingerprinting.
- Human-readable input and identity-preserving YAML and HDF5
representations.
- External-resource references with deferred retrieval and integrity
checks.
Describe the Python package as the reference implementation of a
language-independent specification. Avoid an API inventory or encoding
tutorial in the summary.
2. Statement of need
Research problem and audience
Address developers of long-lived computational science and engineering
software, especially workflows that combine exact numerical structure
with large machine Arrays.
Explain that scientific software often has no difficulty storing data,
but still lacks a representation-independent answer to more basic
questions: What type of data object is this? When are two independently
constructed objects the same? Which structural distinctions participate
in identity? Can that identity survive a change of programming language
or storage technology?
Use a concrete scientific object: a crystal structure or symmetry
calculation containing lattice Arrays, fractional coordinates, exact
complex phase factors, parameters, and externally stored numerical
results.
Such an object may need to survive storage changes, support reproducible
identification, and move between implementations while retaining
distinctions that matter to the application.
Scientific example
Use a structured complex phase factor as the central numerical example:
complex(1/2, 1/2 * sqrt(3))
This preserves the exact real and irrational imaginary components
instead of immediately replacing both with floating-point
approximations. Discuss why this is useful for crystallographic or
symmetry data, alongside ordinary floating-point Arrays required by
numerical calculations.
Mention that DataCanon preserves type and constituent structure without
mathematical simplification. Detailed normalization edge cases belong in
the specifications rather than the motivation.
Practical outcome
Explain what the package enables: identity-preserving disk round trips,
format transfers, stable fingerprints of scientifically meaningful data,
and communication of the same DCI across independent implementations.
If a code fragment is needed, use one short persistence example; leave a
full walkthrough in Getting Started.
3. State of the field
Use this section not only to compare features, but to establish what
kind of thing DataCanon is. One of the easiest ways to understand
DataCanon is to distinguish the data model from neighboring layers that
solve different problems.
A useful organizing framework is:
| Layer | Primary question |
|---|
| Data model | What is the data? |
| Representation / serialization | How is the data communicated or encoded? |
| Persistence | How is the data stored and recovered? |
| Database | How are collections of data organized, located, queried, and updated? |
| Computation | What operations are performed on the data? |
DataCanon fundamentally addresses the first question. Its specifications
and software also provide representations and persistence mechanisms,
but those do not define DCI identity. DataCanon can therefore use or
coexist with systems at the other layers rather than replacing them.
Comparison with generic data models
Compare DataCanon first with generic structured-data models such as JSON
and CBOR, not only with their concrete serializations. This comparison
establishes the conceptual neighborhood: recursively structured,
language-independent data whose values can be realized in multiple
programming languages.
A compact comparison table would be valuable in the paper. The exact
entries must be verified against primary specifications during the
literature review; the table below records the questions and intended
comparison rather than final publication claims.
| Capability | JSON | CBOR | Document / NoSQL models | DataCanon |
|---|
| Language-independent structured-data model | Compare | Compare | Compare | Yes |
| Recursive heterogeneous structures | Compare | Compare | Compare | Yes |
| Distinct List and Tuple semantics | Compare | Compare | Compare | Yes |
| Set / Multiset semantics | Compare | Compare | Compare | Yes |
| Structured exact numerical types | Compare | Compare | Compare | Yes |
| Typed multidimensional machine Arrays | Compare | Compare | Compare | Yes |
| Explicit type-and-constituent equality | Compare | Compare | Compare | Yes |
| Canonical identity intrinsic to the model | Compare | Compare | Compare | Yes |
The point is not that other models are deficient. They make different
choices for different purposes. The literature review should identify
which DataCanon distinctions are unusual or useful for scientific
software without claiming that canonicalization, portability, or
structured data are unique inventions.
CBOR as a data model and binary encoding
Treat CBOR as a particularly important comparison, but distinguish its
underlying data model from its binary encoding conventions. CBOR can
represent additional scientific structures through tags and prescribed
conventions. DataCanon’s contribution is not that CBOR is incapable of
representing its DCTs; it is the explicit scientific type system,
constituent structure, equality contract, and canonical identity.
A CBOR-based encoding of DCIs would remain possible without changing the
DataCanon semantic model. However, it would require a fixed mapping of
DCTs to CBOR values, rules for structured numerical types and Arrays,
and a frozen deterministic encoding profile. Avoid claiming that CBOR
cannot meet these requirements. Instead, distinguish the semantic need
for DataCanon from the engineering choice of canonical byte encoding.
Comparison with document / NoSQL systems
Use document databases as a particularly intuitive comparison. DataCanon
shares the document-model intuition that a scientifically meaningful
object may naturally be one recursively structured object rather than
something that must first be decomposed into relational tables.
The abstraction level is nevertheless different. A document database is
primarily concerned with storing, retrieving, indexing, querying, and
updating documents. DataCanon instead specifies the types, constituent
structure, equality, and canonical identity of the data itself. A DCI
could therefore be stored in a document database; the two systems need
not compete.
Avoid treating “NoSQL” as one formal data model. Distinguish document
databases from key-value, wide-column, graph, and other systems when
preparing the final literature review. The document model is the
relevant comparison here.
Then compare against technologies operating at neighboring layers: JSON
canonicalization, deterministic CBOR, schema-driven serialization, YAML
tooling, HDF5, and scientific data containers used by the intended
audience. Select the closest alternatives rather than cataloguing every
serialization format.
Evaluate the distinctions that motivate DataCanon:
Concern Question for the comparison
Identity What equality relation and
canonical encoding are provided?
Scientific types How are structured exact numbers
and typed Arrays represented?
Collections Are ordered and unordered
structures distinguished?
Persistence Can storage layout change while
retaining the same identity?
External data How are payload metadata, content
verification, and retrieval
separated?
Interoperability What agreement is required across
implementations?
Give a concrete reason for developing DataCanon rather than extending a
particular existing package. The proposed contribution is the
combination of an explicit scientific data model, canonical identity,
and multiple faithful representations. Existing formats may provide
related capabilities or support extensions; comparisons must establish
the relevant differences rather than claiming that canonicalization or
portability is unique to DataCanon.
Explain that DataCanon deliberately uses mature facilities such as NumPy
and HDF5 instead of replacing their numerical and storage
infrastructure. The same principle applies more broadly: DataCanon
defines identity at the data-model layer and can interoperate with
representation, persistence, database, and computational systems built
for other concerns.
4. Software design
Organize this section around the design decisions that make the abstract
data model usable in scientific software. Because Section 3 establishes
the data-model / representation / persistence distinction, avoid
repeating that conceptual argument here; show how the software realizes
it. Keep low-level byte tables in the specifications.
Identity is separate from representation
Define DCT and DCI briefly. Equality is specified by types and
constituents; canonical encoding represents that equality injectively:
x = y if and only if E(x) = E(y)
Fingerprinting is:
Shorthand formatting does not determine identity. Distinguish exact
canonical byte equality from digest comparisons, which rely on collision
resistance.
Rationale for the canonical byte encoding
Explain why Core specifies its own canonical bytes instead of adopting
CBOR’s binary encoding or a deterministic CBOR profile. The present
encoding follows the DCT structure directly: a type identifier and a
prescribed payload, with shared rules for lengths, constituents, and
machine values. This favors a small, auditable specification and direct
independent implementations over introducing another encoding layer.
Canonical bytes are normative to DataCanon identity, not an incidental
serialization choice. Once Version 1 is stable, changing the encoding
would change fingerprints even if DCI equality remained unchanged.
An alternative CBOR-based encoding could preserve the same semantics,
but would require an equally precise and version-stable byte contract.
Acknowledge the potential advantages of CBOR-style compact headers and
established encoding tools, particularly for small nested objects. Do
not claim that the current encoding is smaller or faster without
measurements. The justification is presently architectural minimalism,
clarity, and direct control, not demonstrated performance superiority.
Scientific types and machine Arrays
Version 1 defines the DCTs in the Core specification. In the manuscript,
highlight composable numerical structures, Complex, Record, Multiset,
Bytes, Array, and external references rather than explaining every type.
Verify the final DCT count against the publication release rather than
baking a draft count into the prose.
Numerical DCIs include Integer, Float, Fraction, Sum, Product, Power,
and Quotient, together with any additional numerical DCTs present in the
finalized Version 1 specification. This is a category for constituent
restrictions, not an additional DCT. Type membership makes no assertion
about mathematical evaluability.
Explain the composition rule with a concrete example. Fraction’s
prescribed integers are direct information. A Complex component or Power
base is a DCI, whose individual DCT and recursive structure matter.
Encoding reuses shared payload rules for direct fields and complete DCI
encodings for variable-type constituents.
Arrays retain portable type, shape, and element identity. Their elements
are machine values, not individually tagged DCIs. Portable Array types
support efficient numerical containers; canonical elements use logical C
order and prescribed big-endian multibyte representations, independently
of native strides or layout. Nested Lists remain available for
individually typed constituent data.
Distinguish the fully specified Core Array identity from Shorthand
inference and conversion rules where implementation-reference behavior
remains relevant.
Order belongs to the data model
Mapping entry order participates in identity; UnorderedMapping entry
order does not. Likewise, Lists and Tuples are ordered, while Sets are
unordered. Multiset retains repeated members; numerical collection types
likewise retain the multiplicity and nesting prescribed by Core.
Canonical binary encoding orders unordered constituents according to the
finalized Core rules. That encoding order is not a numerical comparison
or a mandated text-rendering order.
Text and conventional-type conversion
One strict Shorthand grammar accepts explicit constructors and shorter
forms. Longhand uses constructors for structured values and ordinary
literals where specified by Shorthand. The boundary is syntactic rather
than simply the Core type classification. Formatting choices preserve
DCI identity.
Define conventional types as types used directly for data and
computation in a programming language, including built-in,
library-provided, and user-defined types. The conversion interface
constructs instances of conventional types from DCIs and constructs DCIs
from supported conventional objects. A ListDC object and a
conventional list object are both Python objects, but their types and
equality contracts differ.
Existing conventional types include list, Fraction, and NumPy
arrays. The reference implementation supplies new conventional types
where suitable types are missing. Arithmetic belongs to the numerical
conventional types, not to the DCT identity rules. Conversion through
conventional objects need not preserve an arbitrary DCI’s identity.
Present conventional arithmetic as an optional companion interface in
the paper, without introducing a separately named arithmetic facility.
The seventh specification, Conventional Types, defines common behavior
for independent implementations, with conformance declared per type.
Core does not require that support. Link the Conventional Types guide
for worked examples and the companion specification for its behavioral
requirements and comparison limits.
Contained and external payloads
Bytes stores an opaque byte sequence directly in the DCI. A Record can
hold its interpretation metadata. This permits a canonical binary file
to contain both structured scientific data and binary payloads without
companion files. External references serve the distinct purpose
described below.
External payloads and persistence
ExternalFile and ExternalArray retain relative paths and content digests
as semantic metadata. ExternalArray also carries portable type and
shape. External bytes are not needed to decode or fingerprint their
metadata.
Source location, cached bytes, and retrieval state are nonsemantic.
Retrieval verifies digests and, for ExternalArray, element-stream
validity. Complete packages preserve those references while placing
payloads in directory-backed storage or embedding them in HDF5.
Seven specifications and the Python implementation
Summarize the specification boundaries:
Specification Responsibility
Core DCTs, equality, canonical bytes,
and fingerprints
Shorthand Strict text parsing and
identity-preserving rendering
Shorthand Preprocessing Optional compact Lists,
substitutions, and explicit
eval(...)
YAML Interchange Native YAML conversion and the $
convention
HDF5 Portable object layout, numerical
datasets, and embedded payloads
Persistence Format selection, resource
namespaces, verification, and
transfer
Conventional Types Optional conventional construction,
arithmetic, comparison,
collections, and conversion
contracts
Connect the contracts to explicit conversion, encoding/decoding,
parser/renderer, and persistence boundaries. The DCTs and DCI equality
are language-independent. The optional Conventional Types companion
specifies a common behavioral foundation. Language bindings document
their supported types, interfaces, additional simplifications, and
conversion choices. Python classes are examples of a binding rather than
normative DCT definitions.
Core does not perform computation. Optional preprocessing performs
explicitly requested arithmetic before strict parsing; it does not alter
DCT identity.
5. Research impact statement
Build this section from specific evidence available at submission time.
Distinguish actual use from proposed applications.
Possible evidence to collect includes:
- A documented Principia Materia integration showing which scientific
objects use DataCanon and what workflow it supports.
- Reproducible crystallographic or symmetry examples using exact
structured Complex values and typed Arrays.
- Verified format transfers preserving canonical bytes and
fingerprints.
- Benchmarks for representative numerical workloads, if performance is
a material reason to adopt the package.
- Independent users, integrations, or publications, if available.
The repository tree shows tests covering conversion, canonical encoding
and vectors, parsing/rendering, YAML, HDF5, and external sources. Report
test results and fixtures only after running and inspecting them;
filenames alone are not validation evidence.
If independent language conformance is completed, include independently
generated matching canonical bytes and fingerprints, all portable Array
types, and bidirectional HDF5 fixtures. Text layout and HDF5 file bytes
need not match. Report completed coverage and remaining limitations
explicitly.
Deferred canonical-encoding evaluation
Record a possible future comparison of the current encoding with a
CBOR-inspired alternative, holding the DCT model and equality rules
fixed. Compare encoded size, encoding and decoding speed, compressed
size, and specification/implementation complexity. Cover every DCT,
small and deeply nested objects, contained Arrays across dtype, rank,
and size, ExternalArrays, and representative scientific datasets. Do not
assume that large Arrays are always externalized or that any one
scientific workload is representative.
This evaluation is worthwhile but is not a prerequisite for completing
the remaining Version 1 implementation work. Revisit the byte encoding
before finalizing the canonical identity contract if evidence reveals a
substantial benefit. Otherwise retain the simpler current design.
6. AI usage disclosure
Prepare an accurate account of AI assistance with software development,
documentation, or manuscript preparation, together with the human review
and verification performed. This documentation and outline have received
AI assistance. The implementation, tests, specifications, and
documentation have also received AI assistance during this redesign.
Record the reviewed scope and the actual human checks performed before
preparing submission text.
Record the tools and available version information when preparing the
submission. Do not describe syntax-only checks as execution,
interoperability validation, or scientific verification.
7. Acknowledgements
Identify contributors, institutional support, and funding actually
associated with the software. Obtain the correct grant identifiers and
attribution before submission.
8. References
Prepare software and primary-source citations for the closest
alternatives, the numerical/storage facilities discussed, and scientific
applications used as evidence. Link the versioned DataCanon
specifications and archive the software release associated with the
paper.
Version 1 draft revisions
Keep this section synchronized with the final Version 1 specification.
The numerical model and DCT inventory have changed during development,
so publication fixtures must use the finalized encodings and terminology
rather than incompatible earlier drafts.
Do not mix results from incompatible draft revisions. Tests should cover
the full finalized numerical structure, longhand and shorter text forms,
byte payloads, multisets, and transfers preserving the current canonical
bytes.
Supporting material and remaining work
The documentation should provide the material omitted from the short
paper: full type definitions, byte encodings, fixed conformance vectors,
API signatures, numerical conventional-type behavior, longhand
rendering, preprocessing details, and persistence examples.
Before finalizing the manuscript, resolve or clearly delimit the
portability items already recorded in the seven drafts, especially
implementation-reference behavior, preprocessing numerical semantics,
and HDF5 datatype validation. Confirm the package’s license, release,
installation instructions, tests, contribution guidance, and cited
scientific evidence against the actual repository. The checklist
identifies items to inspect, not claims that they are currently missing.
A longer technical treatment could develop the full model and
conformance results separately. The JOSS paper should stay centered on
the research software, its design choices, and demonstrated usefulness.
5 - Reference
Version 1 defines 24 DataCanon Types (DCTs). A DataCanon Instance (DCI) is
an instance of one DCT. Instances of different DCTs are never
equal. Each DCT specifies its information and validity rules; canonical bytes
encode that identity independently of textual presentation.
Named mathematical constants
Constant stores the name pi or e, identifying the exact mathematical
constant without a decimal approximation. Shorthand uses Pi and E;
longhand uses constant("pi") and constant("e"). A Constant DCI is distinct
from every Float approximation and is permitted as a Numerical DCI.
Conventional types and DCT implementations
A conventional type is a type used directly for data and computation in a
programming language, including built-in, library-provided, and user-defined
types. Conversion interfaces connect DCIs with instances of conventional types.
DCTs and conventional types have their own rules. For example, Python’s
ListDC class implements the List DCT. A ListDC instance and a conventional
list instance are Python objects of different types. Conversion connects the
types without changing programming languages. A conventional Fraction or
Sum may simplify its contents, while Fraction and Sum DCIs retain their
prescribed structure.
The Conventional Types guide
introduces the relationships and operations. Exact conversion directions and
limitations belong to the Python API.
The optional Conventional Types specification
defines the common behavioral contract for independent language implementations.
Its arithmetic and comparison requirements do not alter the DCT identity rules
summarized below.
Primitive types
Primitive DCTs specify their information directly, without constituent DCIs.
This includes Fraction’s two prescribed integer fields.
| DCT | Information | Example Shorthand |
|---|
| Null | One value | null |
| Boolean | False or true | true |
| Integer | Arbitrary-precision mathematical integer | 42 |
| Float | Finite binary64 value, with bit identity | 1.0 |
| String | Unicode scalar sequence, without normalization | "hello" |
| Bytes | Opaque finite byte sequence | bytes("00ff80") |
| Constant | Exact named mathematical constant | Pi or E |
| Fraction | Integer numerator and positive integer denominator | 1/3 or fraction(1, 3) |
Positive and negative Float zero are distinct. Nonfinite floating-point values
are excluded. Fraction pairs are retained without reduction in the DCI model.
Numerical DCIs
A Numerical DCI is a scalar numerical structure, an Array, a Sum, or an
OrderedProduct. A Scalar Numerical DCI is Integer, Float, Fraction,
Constant, Product, Power, Quotient, or a Sum/OrderedProduct whose every operand
is recursively scalar. Arrays, including rank-zero Arrays, are not scalar in
this classification. Boolean and Complex DCIs are outside these categories.
Classification is structural, without evaluation or shape inference. Sum and
OrderedProduct accept Numerical DCIs; Product, Power bases, Quotient operands,
and Complex components require Scalar Numerical DCIs.
Atomic types
Atomic types have a fixed set of named fields and at least one constituent DCI.
| DCT | Information | Example Shorthand |
|---|
| Power | Scalar Numerical base DCI; positive integer exponent numerator and denominator | 5**(1/3) or power(5, 1, 3) |
| Quotient | Scalar Numerical numerator and denominator DCIs | quotient(1, sqrt(3) + sqrt(2)) |
| Complex | Real and imaginary Scalar Numerical DCIs | complex(1/2, sqrt(3)) |
Power’s exponent defaults to denominator one in its two-argument constructor.
Fraction bases require explicit power(...) syntax. sqrt(base) is the Power
spelling with exponent fields 1 and 2. Exponent fields remain unreduced in DCIs.
Quotient has no mathematical nonzero-denominator invariant in the current Core
draft. Converting a Quotient DCI to a conventional Quotient object can fail
for a recognized zero denominator; explicit numerical evaluation can fail for
other undefined expressions.
Composite types
| DCT | Structure and identity | Example Shorthand |
|---|
| List | Ordered DCIs | [1, 2] or list(1, 2) |
| Tuple | Ordered DCIs, distinct from List | (1, 2) or tuple(1, 2) |
| Set | Unordered distinct DCIs | {1, 2} or set(1, 2) |
| Multiset | Unordered DCI occurrences, retaining multiplicity | multiset(1, 1, 2) |
| Sum | Unordered Numerical DCI occurrences | 1 + 2 or sum(1, 2) |
| Product | Unordered Scalar Numerical DCI occurrences | 2 * sqrt(3) or product(2, sqrt(3)) |
| OrderedProduct | Ordered Numerical DCI occurrences, including Arrays | ordered_product(2, array([[1, 0], [0, 1]])) |
| Mapping | Ordered entries with distinct DCI keys | {"a": 1} or mapping("a": 1) |
| UnorderedMapping | Unordered entries with distinct DCI keys | umapping("a": 1) |
| Record | Unicode name and ordered uniquely named DCI fields | record("Point", "x": 1.0) |
| Array | Portable machine type, shape, and elements | array([1.0, 2.0]) |
| ExternalFile | Relative path and content digest | external_file("x.bin", "DIGEST") |
| ExternalArray | Relative path, Array type, shape, and content digest | external_array("x.bin", "float64", (2), "DIGEST") |
DIGEST is a placeholder for 64 hexadecimal characters, not valid literal
metadata. Resource availability does not change an external DCI’s identity.
Empty Sets use set(); empty Mappings use {} or mapping(). A singleton
Tuple is (1) or tuple(1). Empty and singleton Sums and Products retain their
constructors. Numerical nesting and multiplicity participate in identity.
Numerical structure and presentation
Sum and Product identity ignores operand order but retains repeated operands
and nesting. A flat sum(1, 2, 3) and a nested sum(1, sum(2, 3)) therefore
represent different DCIs. Quotient and Fraction are distinct DCTs even when
their mathematical interpretations agree.
The Python DCT implementations retain supplied operand order for Shorthand
presentation. Canonical encoding sorts complete operand encodings for unordered
DCTs. Canonical decoding may recover a different presentation order without
changing DCI identity. The numerical conventional types have separate
construction and simplification rules.
Strict Shorthand parentheses construct Tuples rather than grouped arithmetic
expressions. Constructors preserve explicit nesting, as in
product(2, sum(3, 4)); binary subtraction is not part of strict Shorthand.
Fixed-domain information is prescribed directly: Fraction integers, Power
exponent integers, Record and field names, relative-path components, Array
metadata, and digests. Their domains do not vary by field value.
A Power base, Complex component, Quotient operand, collection member, or Record
field value is a DCI. Its DCT participates in the containing value’s identity.
Record names do not identify another DCT or instruct an application to create
a class.
Canonical encoding reuses shared payload rules for fixed information and full
DCI encodings for constituent DCIs. Array elements use specialized machine
representations rather than individual DCI encodings.
Portable Array types
| Category | Types |
|---|
| Boolean | bool |
| Signed integers | int8, int16, int32, int64 |
| Unsigned integers | uint8, uint16, uint32, uint64 |
| Floating point | float16, float32, float64 |
| Complex | complex64, complex128 |
Array type, rank, shape, and corresponding element identities all matter.
A rank-zero Array contains one element. Any zero dimension gives zero elements.
Elements are canonically encoded in logical C order, with the rightmost index
varying fastest and multibyte numerical values in big-endian order.
Array construction and dtype promotion in the Python interface follow the
supported NumPy behavior. Explicit dtype conversion can round values; the
resulting Array must satisfy its DCT’s validity rules.
Shorthand, longhand, and preprocessing
Longhand uses explicit constructors for structured values and ordinary literals
for Null, Boolean, Integer, Float, and String. The distinction concerns syntax,
not the Core type classification: Fraction uses fraction(...) despite being
primitive. Bytes keeps bytes(...), and Array elements retain their specialized
syntax. sum(1, 1.0, fraction(1, 2)) is longhand without extra literal wrappers.
The parser accepts both
and any mixture; render_shorthand(..., longhand=True) selects longhand.
Default rendering uses shorter forms with identity-preserving fallbacks.
Layout styles are independent of that choice.
The Python renderer separates layout (standard, semi-compact, compact,
or inline) from column align, structural indent (default 2), and same-line
padding (default 1). Use custom_layout for per-type overrides. Compact
continuation items align with their first item; indentation controls nested
blocks on new lines. Padding also applies to flat collections. An inline
value forces its entire subtree onto one line, overriding child layouts and
ignoring alignment and indentation there. The
Python API defines the options.
Optional preprocessing expands compact Lists, substitutes names, and evaluates
explicit scalar eval(...) expressions before strict parsing. It is not
implicitly used by YAML airlocks or persistence reads.
Representations
| Representation | Role |
|---|
Canonical bytes / .dcb | Unique binary identity encoding |
Shorthand / .dcs | Readable strict text, including longhand |
| YAML interchange | Native YAML plus $ strict Shorthand |
HDF5 / .h5 | Native numerical datasets, structured DCIs, and embedded external payloads |
SHA-256 of canonical bytes is the DCI fingerprint. It is not a hash of Shorthand,
YAML, or HDF5 file bytes. SHA-256 content digests also identify external payloads;
those payloads require separate retrieval and verification.
For normative rules and conformance vectors, use the
Specifications.
6 - Specifications
DataCanon Version 1 is documented in seven complementary specifications. Core defines the data model, equality, canonical encoding, and fingerprinting. The other specifications define representations, optional preprocessing, and persistence behavior. The Conventional Types companion defines optional computational and collection behavior; support is not required for Core conformance.
The specifications are drafts for independent implementation review. Outstanding conformance and interoperability items are recorded in the individual specifications.
This section is generated from the specifications in the DataCanon code repository. Changes should be made there and synchronized here.
6.1 - DataCanon Core
Version 1 Draft
Introduction
DataCanon (DC) is a data model for structured scientific data with a
canonical identity encoding.
DataCanon defines a set of data types and their equality relations, together with a
unique canonical byte encoding for every instance.
The type and encoding definitions provide a common, representation-independent basis for
communicating, identifying, comparing, and preserving structured scientific
data across software implementations and persistence mechanisms.
The identity of a DataCanon Instance is determined solely by its type and its
constituent information. It does not depend on any particular programming
language, textual notation, parser, renderer, or persistence format.
Textual syntaxes and persistence formats may represent DataCanon Instances, but
those representations are outside the core DataCanon specification.
DataCanon is intended for scientific software whose data must remain stable,
portable, reproducible, and meaningful over years or decades of development.
Goals
DataCanon is designed to provide
- rigorously defined data types,
- deterministic data identity,
- implementation-independent comparison,
- reliable interchange between software systems,
- deterministic fingerprinting,
- long-term persistence.
The goals support scientific software whose data must remain robust over the
lifetime of a project rather than the lifetime of a particular programming
language, textual syntax, or persistence format.
Non-Goals
DataCanon intentionally does not provide
- a general-purpose programming language,
- a required textual syntax,
- symbolic algebra,
- general computation,
- variables,
- user-defined functions,
- expression evaluation,
- units,
- dimensional analysis.
DataCanon represents structured data.
It does not manipulate that data or define mathematical operations between
instances except where such structure is explicitly part of a DataCanon Type.
DataCanon canonicalizes identity, not mathematics. Mathematical
relationships between distinct DataCanon Instances are outside the DataCanon
specification.
The non-goals apply to the DataCanon data model and canonical identity.
Optional textual representations and preprocessing facilities may provide
conveniences such as substitutions and expression evaluation without extending
the DataCanon Type system or altering DCI identity.
Terminology
The following terms are used throughout this specification.
DataCanon Type (DCT)
A DataCanon Type (DCT) is a data type defined by the DataCanon
specification.
Each DCT defines
- the information constituting its instances,
- the invariants those instances MUST satisfy, and
- equality between its instances.
A DCT may specify information directly and may contain constituent instances
of other DCTs. Direct information has a domain prescribed by the containing
DCT; a constituent DCI retains its own DataCanon type and identity.
DataCanon Instance (DCI)
A DataCanon Instance (DCI) is an instance of exactly one DCT.
A DCI is a DataCanon value. This specification makes no separate distinction
between a DataCanon instance and a DataCanon value.
DCI equality is defined by the corresponding DCT.
DCI identity is independent of any implementation-language object, textual
representation, persistence format, or other realization of the DCI.
Canonical Encoding
The canonical encoding of a DCI is the unique byte sequence assigned to that
DCI by this specification.
Let E(x) denote the canonical encoding of DCI x.
For any DCIs x and y,
x = y if and only if E(x) = E(y)
where equality on the left is DCI equality as defined by the corresponding
DCTs and equality on the right is byte-for-byte equality.
The canonical encoding does not define DCI equality. Equality is defined by the
DCTs themselves. The canonical encoding provides a unique, portable encoding
of that identity.
The canonical encoding is fully decodable and may be stored or transmitted
directly. No additional representation is required to reconstruct the encoded
DCI. For DCIs referencing external resources, canonical decoding reconstructs
the DCI without requiring access to those resources. Retrieval and verification
of external contents are separate operations.
Representation
A representation of a DCI is any form from which that DCI can be recovered
without loss of DataCanon identity.
Representations may include textual syntaxes, implementation-language objects,
or persistence formats.
No such representation is privileged by the core DataCanon specification.
Conventional Types
A conventional type is a type used directly for data and computation in a
programming language. Conventional types may be built into the language,
provided by libraries, or defined by users. An implementation may supply
additional conventional types where suitable ones are missing.
An implementation may define conversions between DCIs and instances of
conventional types. Each conventional type retains its own construction,
equality, and operation rules, which need not coincide with the rules of an
associated DCT. A conversion relationship does not establish type equivalence,
a one-to-one correspondence, or a lossless round trip.
The term describes the practical types used through an implementation’s
conversion interface; it does not introduce another DCT or a closed type
system. A programming-language object implementing a DCI and an instance of a
conventional type used in conversion can be distinct objects of different
types within the same language.
Conventional-type operations do not change DCI equality or canonical encoding.
Conversion to a conventional object may discard distinctions needed to recover
the original DCI. Such an object is not necessarily a representation of that
original DCI under the definition above.
The optional DataCanon Conventional Types Version 1 companion defines
construction, arithmetic, comparison, collection behavior, and conversion
requirements for implementations claiming support for those conventional
types. Core conformance does not require support for that companion.
Design Principles
Representation Independence
DataCanon identity is independent of the representation used to express,
communicate, or persist a DCI.
Different representations of the same DCI may differ arbitrarily in syntax,
layout, storage organization, or implementation details.
Different representations recover the same DataCanon data when they recover
equal DCIs.
Implementation Independence
The DataCanon specification does not prescribe how a software implementation
represents DCIs internally.
Different implementations may use different classes, structures, memory
layouts, or other implementation-specific mechanisms.
Implementations representing equal DCIs MUST nevertheless agree on DCI
equality and canonical encoding.
Deterministic Identity
Every valid DCI has exactly one canonical encoding.
The canonical encoding depends only on the DCI and its DCT.
It does not depend on the implementation that produced it or on any other
representation of the DCI.
Structural Identity
Unless explicitly stated otherwise by a DCT, DCI identity is determined by the
structure and constituents defined by that DCT rather than by mathematical,
application-specific, or implementation-specific equivalence.
Constituent Structure
A DCT specifies fixed-domain information directly. A field is a constituent
DCI when its DataCanon type or recursive DataCanon structure participates in
the containing value’s identity. For example, Fraction uses direct mathematical
integer fields, while Complex uses Numerical DCIs whose individual DCTs matter.
Encoding rules reuse shared payload definitions for direct information and
complete canonical encodings for constituent DCIs.
Minimality
DataCanon defines only what is required to establish portable structured data
and its identity.
Facilities for textual input, textual rendering, preprocessing, application
semantics, computation, and specialized persistence are defined separately
when needed.
DataCanon Types
DataCanon defines the types described in this section.
Each DataCanon Type (DCT) defines its instances, the invariants those
instances must satisfy, and equality between its instances.
Instances of different DCTs are never equal.
DataCanon Types are classified according to the organization of their
information and constituent DCIs. Classification does not alter identity or
encoding rules.
Primitive Types
A primitive DCT specifies a basic value directly, without constituent
DCIs. Its information may have multiple prescribed fields.
Version 1 defines the following primitive DCTs:
- Null
- Boolean
- Integer
- Float
- String
- Bytes
- Fraction
- Constant
Null
The Null DCT contains exactly one DCI.
All Null DCIs are therefore equal.
Boolean
The Boolean DCT contains exactly two DCIs, representing the Boolean values
false and true.
Two Boolean DCIs are equal if and only if they represent the same Boolean
value.
Integer
The Integer DCT represents the mathematical integers (\mathbb{Z}).
An Integer DCI consists of exactly one mathematical integer. Integer has
unlimited precision.
Two Integer DCIs are equal if and only if they represent the same mathematical
integer.
Float
The Float DCT represents finite IEEE-754 binary64 values.
A Float DCI consists of exactly one finite IEEE-754 binary64 value, including
its sign.
Two Float DCIs are equal if and only if their IEEE-754 binary64 bit
representations are identical.
Positive and negative zero are therefore distinct Float DCIs.
NaN and positive and negative infinity are not Float DCIs in Version 1.
Floating-point values are inherently approximate. Applications requiring an
exact rational representation should use a suitable exact DCT.
String
The String DCT represents finite sequences of Unicode scalar values.
A String DCI consists of exactly one such sequence.
Two String DCIs are equal if and only if they contain identical sequences of
Unicode scalar values.
DataCanon performs no Unicode normalization. Strings that are visually
identical but consist of different sequences of Unicode scalar values are
therefore distinct String DCIs.
Bytes
The Bytes DCT represents finite sequences of bytes. A Bytes DCI consists of
exactly one such sequence, which MAY be empty.
Two Bytes DCIs are equal if and only if their complete byte sequences are
identical, including length and byte order.
Bytes are opaque. DataCanon does not assign a character encoding, file format,
filename, media type, or application-specific interpretation to their contents.
Applications may store such metadata alongside a Bytes DCI, for example in a
Record. That metadata participates in the containing DCI’s identity according
to its DCT, but is not a constituent of the Bytes DCI.
Bytes is distinct from String, Array, and ExternalFile. Its byte sequence is
contained directly in the DCI, rather than referenced by a path and digest.
Fraction
The Fraction DCT represents a structured ratio of two mathematical integers.
A Fraction DCI consists of
- a mathematical integer numerator, and
- a positive mathematical integer denominator.
Two Fraction DCIs are equal if and only if their corresponding constituents
are equal.
Fraction identity is structural rather than mathematical. Distinct numerator
and denominator pairs remain distinct even when they represent the same
mathematical rational number.
DataCanon does not reduce, normalize, or otherwise mathematically simplify
Fractions.
Constant
The Constant DCT contains exactly two DCIs in Version 1, named pi and e.
The name identifies the exact mathematical constant: pi is the ratio of a
circle’s circumference to its diameter in Euclidean geometry; e is Euler’s
number, the base of the natural logarithm. No decimal approximation is stored.
Two Constant DCIs are equal if and only if their names agree. Constant is a
primitive DCT: its prescribed name is direct information, not a String DCI.
A Constant is never equal to a Float containing an approximation of its value.
No other names, aliases, user-defined constants, or physical constants are
included in Version 1.
Numerical Instances
A Numerical DCI is an Integer, Float, Fraction, Constant, Product, Power,
Quotient, Array, Sum, or OrderedProduct DCI. Sum and OrderedProduct operands
MUST themselves be Numerical DCIs. Boolean and Complex are not members of
this category. A Boolean Array is still an Array DCI; category membership does
not promise that a conventional arithmetic implementation supports it.
A Scalar Numerical DCI is one of:
- an Integer, Float, Fraction, or Constant DCI;
- a Product, Power, or Quotient DCI, whose invariants require scalar constituents;
- a Sum or OrderedProduct DCI whose every operand is a Scalar Numerical DCI.
An empty Sum or OrderedProduct satisfies the scalar classification. An Array,
including a rank-zero Array, does not. Classification depends on constituent
structure, without evaluating values or inferring ranks or shapes of results.
For example, a Sum containing an Array through a nested OrderedProduct is not
scalar, regardless of the Array’s shape or the factors’ mathematical values.
The categories specify permitted constituent DCIs; neither is an additional
DCT. Scalar structural validity does not certify that an interpretation is
mathematically defined or real. Types listed here may be defined later.
Atomic Types
An atomic DCT has a fixed set of named fields and at least one constituent
DCI. Fields may contain directly specified information or constituent DCIs,
as prescribed by the DCT.
Version 1 defines the following atomic DCTs:
Power
The Power DCT represents a base with a structured positive rational exponent.
A Power DCI consists of
- a base that is a Scalar Numerical DCI,
- a positive mathematical integer numerator, and
- a positive mathematical integer denominator.
The exponent may be interpreted as the numerator divided by the denominator.
Both exponent constituents MUST be positive and participate independently in
identity. They MUST NOT be reduced or otherwise normalized.
Two Power DCIs are equal if and only if all corresponding constituents are
equal. Power is distinct from its base and from other numerical DCTs, including
when its exponent is one. Nested Powers MUST NOT be collapsed.
The base MUST be a Scalar Numerical DCI. Power does not
evaluate the base, select a mathematical branch, or certify that a real value
exists. Its structural invariants impose no additional sign restriction on the
base.
Quotient
The Quotient DCT represents a structured numerator and denominator, each a
Scalar Numerical DCI. The constituents may belong to different numerical DCTs.
Two Quotient DCIs are equal if and only if corresponding numerators and
corresponding denominators are equal. Constituent roles participate in
identity. Quotient is distinct from Fraction, even when both constituents are
Integers.
DataCanon MUST NOT evaluate, cancel, normalize, or rationalize Quotient
constituents. This draft imposes numerical constituent-type restrictions but
no nonzero-denominator invariant. Structural validity does not certify that
the mathematical division is defined. Any stronger denominator restriction
must be specified explicitly as a DCT invariant.
Complex
The Complex DCT represents a structured complex number consisting of a real
component and an imaginary component.
A Complex DCI consists of
- a real component, and
- an imaginary component.
Each component MUST be a Scalar Numerical DCI.
Two Complex DCIs are equal if and only if their corresponding components are
equal.
Because instances of different DCTs are never equal, the DCT of each component
participates in Complex identity.
DataCanon does not convert Complex components between numerical DCTs or
otherwise mathematically simplify them.
Composite Types
A composite DCT has variable-size constituent structure.
The organization and permitted contents of that structure are defined by the
DCT.
Version 1 defines the following composite DCTs:
- List
- Tuple
- Set
- Multiset
- Sum
- Product
- OrderedProduct
- Mapping
- UnorderedMapping
- Record
- Array
- ExternalFile
- ExternalArray
List
The List DCT represents a finite ordered sequence of DCIs.
A List DCI consists of zero or more DCIs in a defined order.
The constituent DCIs may belong to any DCT and need not belong to the same DCT.
Two List DCIs are equal if and only if
- they contain the same number of items, and
- corresponding items are equal.
Thus, item order participates in List identity.
Lists may recursively contain DCIs of any DCT, including other composite DCTs.
Tuple
The Tuple DCT represents a finite ordered sequence of DCIs.
A Tuple DCI consists of zero or more DCIs in a defined order.
The constituent DCIs may belong to any DCT and need not belong to the same DCT.
Two Tuple DCIs are equal if and only if
- they contain the same number of items, and
- corresponding items are equal.
Thus, item order participates in Tuple identity.
List and Tuple are distinct DCTs. Their instances are therefore never equal,
even when they contain equal constituent DCIs in the same order.
Tuples may recursively contain DCIs of any DCT, including other composite DCTs.
Set
The Set DCT represents a finite unordered collection of distinct DCIs.
A Set DCI consists of zero or more DCIs.
The constituent DCIs may belong to any DCT and need not belong to the same DCT.
No two members of a Set DCI may be equal.
Two Set DCIs are equal if and only if they contain the same members,
independent of member order.
Thus, member order does not participate in Set identity.
Sets may recursively contain DCIs of any DCT, including other composite DCTs.
Multiset
The Multiset DCT represents a finite unordered collection of DCIs in which
members may occur more than once.
A Multiset DCI consists of zero or more member occurrences. Each occurrence is
a DCI of any DCT; members need not belong to the same DCT.
Two Multiset DCIs are equal if and only if, for every DCI, they contain the
same number of occurrences equal to that DCI under DataCanon equality.
Thus, member order does not participate in Multiset identity, while
multiplicity does. An empty Multiset contains no member occurrences.
Multiset and Set are distinct DCTs. Their instances are never equal, even when
a Multiset contains no repeated members.
Multisets may recursively contain DCIs of any DCT, including other Multisets.
Nesting participates in identity; nested Multisets are not flattened.
Sum and Product
Sum and Product are finite unordered collections of operand occurrences.
Multiplicity and nesting participate in identity. Two instances of the same
DCT are equal if and only if their operands match under DCI equality with the
same multiplicities, independent of operand order.
Every Sum operand MUST be a Numerical DCI. Sum therefore accepts scalar
expressions, Arrays, and OrderedProducts, including recursively nested Sums.
Every Product operand MUST be a Scalar Numerical DCI. Product remains an
unordered scalar structure and MUST NOT contain an Array through any path.
No operands are evaluated, flattened, distributed, deduplicated, or simplified.
Array ranks and shape compatibility are not Sum invariants. Scalar terms may
be stored alongside Arrays; Core assigns no implicit broadcasting or identity
matrix convention to their mathematical interpretation.
OrderedProduct
The OrderedProduct DCT is a finite ordered sequence of zero or more Numerical
DCIs. Scalars, Arrays, Sums, and nested OrderedProducts are permitted. Operands
are complete DCIs, not native information fields.
Two OrderedProduct DCIs are equal if and only if they have the same number of
operands and corresponding operands are equal. Order, multiplicity, nesting,
and each operand’s DCT participate in identity. OrderedProduct is distinct
from Product and List, including for empty or scalar-only instances.
Core MUST NOT multiply factors, commute scalars past Arrays, flatten nested
products, remove identities, or check mathematical shape compatibility. No
rank restriction is imposed on Array operands. A rank-three Array is valid
here even when a conventional binding cannot evaluate its ordered product.
Mapping
The Mapping DCT represents a finite ordered collection of key-value entries.
A Mapping DCI consists of zero or more entries in a defined order.
Each entry consists of
- a key DCI, and
- a value DCI.
Keys and values may belong to any DCT.
No two entries in a Mapping DCI may have equal keys.
Two Mapping DCIs are equal if and only if
- they contain the same number of entries,
- corresponding keys are equal, and
- corresponding values are equal.
Thus, entry order participates in Mapping identity.
UnorderedMapping
The UnorderedMapping DCT represents a finite unordered collection of key-value
entries.
An UnorderedMapping DCI consists of zero or more entries.
Each entry consists of
- a key DCI, and
- a value DCI.
Keys and values may belong to any DCT.
No two entries in an UnorderedMapping DCI may have equal keys.
Two UnorderedMapping DCIs are equal if and only if they contain the same
key-value entries, independent of entry order.
Thus, entry order does not participate in UnorderedMapping identity.
Mapping and UnorderedMapping are distinct DCTs.
Record
The Record DCT represents a named structured value consisting of an ordered
sequence of named fields.
A Record DCI consists of
- a name that is a finite sequence of Unicode scalar values, and
- zero or more fields in a defined order.
Each field consists of
- a field name that is a finite sequence of Unicode scalar values, and
- a value DCI.
Field values may belong to any DCT.
Field names within a Record MUST be unique.
Two Record DCIs are equal if and only if
- their Record names are equal,
- they contain the same number of fields,
- corresponding field names are equal, and
- corresponding field values are equal.
Thus, both the Record name and field order participate in Record identity.
Record is distinct from Mapping. A Record represents a named structure with a
defined sequence of named fields, whereas a Mapping represents an ordered
collection of associations between arbitrary DCI keys and values.
The Record name is semantic data. DataCanon does not assign
application-specific meaning to that name and does not interpret it as the name
of another DCT.
A Record name may correspond to a type, class, or other application-defined
concept in an implementation. Such correspondences are defined by the
implementation or application rather than by the Record DCT.
Array
The Array DCT represents dense, regular multidimensional machine data.
An Array DCI consists of
- an Array type,
- a finite multidimensional shape, and
- one element for every position defined by that shape.
The shape is a finite ordered sequence of nonnegative mathematical integers.
The number of entries in the shape is the rank of the Array, and the product
of the entries is the total number of elements.
An empty shape denotes a rank-zero Array containing one element.
The Array type determines the domain and equality relation of its elements.
Two Array DCIs are equal if and only if
- they have the same Array type,
- they have the same shape, and
- corresponding elements are equal according to the equality rule of that
Array type.
Thus, Array type and shape participate in Array identity independently of the
values of the elements.
Arrays are distinct from Lists. A List recursively contains DCIs. The elements
of an Array are instead values of its Array type and are not themselves DCIs.
DataCanon does not define a universal type system or coercion policy for Array
construction. In particular, DataCanon does not prescribe
- which additional native Array types an implementation may support,
- how mixed input values are promoted or coerced, or
- how an implementation represents unsupported or implementation-specific
Array types.
The Array construction choices are delegated to the implementation.
Applications requiring DataCanon-defined type identity for every individual
element should use nested Lists rather than Arrays.
Portable Array Types
DataCanon Version 1 defines the following portable Array types:
boolint8int16int32int64uint8uint16uint32uint64float16float32float64complex64complex128
The portable Array type names denote DataCanon Array types rather than types belonging to any
particular implementation.
The bool Array type contains exactly two values, corresponding to false and
true. Two bool elements are equal if and only if they represent the same
Boolean value.
The signed and unsigned integer Array types represent mathematical integers
within the ranges determined by their respective fixed widths. Two integer
Array elements of the same Array type are equal if and only if they represent
the same mathematical integer.
The floating-point Array types represent finite IEEE-754 binary16, binary32,
and binary64 values, respectively.
Two floating-point Array elements of the same Array type are equal if and only
if their IEEE-754 bit representations are identical.
Positive and negative zero are therefore distinct floating-point Array
elements.
NaN and positive and negative infinity are not elements of the portable
floating-point Array types in Version 1.
The complex64 Array type consists of ordered pairs of float32 elements
representing the real and imaginary components.
The complex128 Array type consists of ordered pairs of float64 elements
representing the real and imaginary components.
Two complex Array elements of the same Array type are equal if and only if
their corresponding real and imaginary components are equal.
DataCanon Version 1 defines Array DCIs only for the portable Array types
listed above. Implementations MAY support additional native Array types, but
values of those types are not Version 1 Array DCIs unless their types and
canonical encodings are defined by an applicable extension to this
specification.
ExternalFile
The ExternalFile DCT represents an externally stored finite sequence of bytes
whose relative path and content digest are part of its identity.
An ExternalFile DCI consists of
- a DataCanon relative path, and
- a SHA-256 content digest.
The path identifies an external resource within a namespace determined by the
representation or application resolving the DCI.
The content digest is the SHA-256 digest of the exact byte sequence contained
in the external file.
The path MUST be relative.
Two ExternalFile DCIs are equal if and only if
- their paths are equal, and
- their content digests are equal.
Thus, both the relative path and content digest participate in ExternalFile
identity.
The contents of an ExternalFile are opaque to DataCanon. DataCanon assigns no
meaning to the byte sequence and does not prescribe how an application
interprets it.
The content digest binds the ExternalFile DCI to a particular byte sequence.
A file found at the specified path whose SHA-256 digest differs from the
content digest is not the external file identified by that DCI.
The existence or availability of the external file does not alter the identity
of the ExternalFile DCI. A missing or inaccessible external file prevents the
external content from being retrieved or verified but does not change the DCI.
ExternalArray
The ExternalArray DCT represents dense, regular multidimensional machine data
stored externally using the DataCanon Array byte representation.
An ExternalArray DCI consists of
- a DataCanon relative path,
- a portable Array type,
- a finite multidimensional shape, and
- a SHA-256 content digest.
The path identifies an external resource within a namespace determined by the
representation or application resolving the DCI.
The path MUST be relative.
The Array type and shape satisfy the same requirements as the corresponding
constituents of an Array DCI.
The logical external element byte stream MUST be the DataCanon-defined byte
representation of the Array elements for the specified Array type and shape.
The content digest is the SHA-256 digest of that logical byte stream, before
storage compression. Persistence may store the stream directly or in a
losslessly compressed representation. Compression does not add a DCI constituent;
the complete relative path, including any suffix, still participates in identity.
Two ExternalArray DCIs are equal if and only if
- their paths are equal,
- their Array types are equal,
- their shapes are equal, and
- their content digests are equal.
Thus, relative path, Array type, shape, and content digest all participate in
ExternalArray identity.
ExternalArray and Array are distinct DCTs. An ExternalArray DCI and an Array
DCI are therefore never equal, even when the external byte sequence represents
the same numerical data as the Array DCI.
The content digest binds the ExternalArray DCI to a particular logical element
byte stream. After any persistence-defined decompression, a stream whose
SHA-256 digest differs from the content digest does not satisfy that DCI.
The existence or availability of the external data does not alter the identity
of the ExternalArray DCI. Missing or inaccessible external data prevents the
Array contents from being retrieved or verified but does not change the DCI.
Relative Paths
External DCTs identify external resources using a DataCanon relative path.
A DataCanon relative path is a finite nonempty ordered sequence of path
components.
Each path component is a nonempty finite sequence of Unicode scalar values.
The components "." and ".." are not permitted.
The characters / and \ are not permitted within a path component.
Two DataCanon relative paths are equal if and only if they contain the same
number of components and corresponding components are equal according to
String equality.
A DataCanon relative path is independent of host operating-system path syntax.
The namespace used to resolve a DataCanon relative path is not part of DCI
identity. The mechanism by which a representation or implementation resolves a
DataCanon relative path to an external resource is outside the core DataCanon
specification.
Canonical Encoding
Every DataCanon Instance has exactly one canonical encoding as a finite
sequence of bytes.
The canonical encoding provides a representation-independent encoding of DCI
identity.
Let E(x) denote the canonical encoding of DCI x. For any DCIs x and y,
x = y if and only if E(x) = E(y)
where equality on the left is DCI equality and equality on the right is
byte-for-byte equality.
The canonical encoding MUST be
- deterministic — a DCI always produces the same byte sequence;
- injective — unequal DCIs produce different byte sequences;
- implementation independent — the encoding does not depend on
implementation-language representations;
- unambiguous — the encoded DCT and all information constituting the DCI
can be recovered uniquely from the byte sequence;
- recursive — constituent DCIs are encoded according to the same canonical
encoding rules;
- stable — the encoding of a Version 1 DCI does not change between
conforming Version 1 implementations.
The canonical encoding is not intended as a human-readable representation,
general-purpose interchange format, or optimized persistence format.
Its purpose is to provide a minimal, stable, and unique byte representation of
DCI identity.
Version Context
The canonical encoding contains no version identifier or framing header. The
applicable DataCanon specification version is determined by the context in
which the encoding is used. Persistence and interchange formats MAY provide
version information independently of the canonical encoding.
Future specifications MUST NOT silently reinterpret Version 1 identifiers or
payload encodings when those bytes are interpreted as Version 1 canonical
encodings.
Unsigned Integer Encoding
Canonical encoding uses a variable-length representation of nonnegative
mathematical integers for lengths, counts, ranks, dimensions, and other
nonnegative integer metadata explicitly designated by this specification.
A nonnegative integer is encoded using unsigned LEB128.
The binary representation of the integer is divided into groups of seven bits,
beginning with the least-significant bits.
Each group is encoded as one byte. The low seven bits contain the group value.
The high bit is the continuation bit.
The continuation bit is
1 when another group follows, and0 for the final byte.
The encoding MUST use the minimum possible number of bytes. Redundant
high-order zero groups are not permitted.
Thus, every nonnegative mathematical integer has exactly one canonical unsigned
integer encoding.
Examples include
| Integer | Encoding |
|---|
| 0 | 00 |
| 1 | 01 |
| 2 | 02 |
| 127 | 7f |
| 128 | 80 01 |
| 129 | 81 01 |
| 255 | ff 01 |
| 256 | 80 02 |
There is no upper bound on the represented integer.
Signed Integer Encoding
Mathematical integers are first mapped bijectively to nonnegative mathematical
integers using zigzag encoding.
For mathematical integer (n), define
$$
Z(n) =
\begin{cases}
2n, & n \ge 0,\
-2n - 1, & n < 0.
\end{cases}
$$
Thus,
| Integer | Zigzag value |
|---|
| 0 | 0 |
| -1 | 1 |
| 1 | 2 |
| -2 | 3 |
| 2 | 4 |
| -3 | 5 |
| 3 | 6 |
The canonical signed integer encoding of (n) is the canonical unsigned
integer encoding of (Z(n)).
This representation has no implementation-defined integer width and therefore
supports the unlimited-precision Integer DCT.
Let S(t) encode a finite sequence of Unicode scalar values t as its UTF-8
byte count using U, followed by the exact UTF-8 bytes:
S(t) = U(number_of_UTF8_bytes) || UTF8(t)
This is the payload encoding used by String, without the String DCT identifier.
It is also used for Record names, field names, and relative-path components.
All UTF-8 validity rules apply; no Unicode normalization is performed.
Encoding Structure
The canonical encoding of every DCI consists of
- a one-byte DCT identifier, followed by
- the canonical payload defined for that DCT.
Thus,
E(x) = DCT(x) || payload(x)
where || denotes byte concatenation.
The DCT identifier determines how the following payload is interpreted.
Direct information is encoded using the payload rules prescribed by its
containing DCT. No DCT identifier is included unless explicitly specified.
Every constituent that is itself a DCI is encoded using its complete canonical
encoding, including its own DCT identifier.
Consequently, nested DCIs are self-describing with respect to their DCTs.
Lengths and counts required to delimit variable-size payloads are encoded using
the canonical unsigned integer encoding defined above.
DCT Identifiers
DataCanon Version 1 assigns the following one-byte identifiers:
| DCT | Identifier |
|---|
| Null | 00 |
| Boolean | 01 |
| Integer | 02 |
| Float | 03 |
| String | 04 |
| Fraction | 05 |
| Complex | 07 |
| List | 08 |
| Tuple | 09 |
| Set | 0a |
| Mapping | 0b |
| UnorderedMapping | 0c |
| Record | 0d |
| Array | 0e |
| ExternalFile | 0f |
| ExternalArray | 10 |
| Multiset | 11 |
| Sum | 12 |
| Product | 13 |
| Power | 14 |
| Quotient | 15 |
| Bytes | 16 |
| Constant | 17 |
| OrderedProduct | 18 |
The DCT identifiers are part of the DataCanon Version 1 specification and MUST
NOT be reassigned within Version 1.
Identifier 06 is reserved and MUST NOT be used for a Version 1 DCI. A
canonical decoder MUST reject that identifier.
The numerical ordering of DCT identifiers has no semantic meaning.
Primitive Types
Null
The canonical encoding of a Null DCI consists solely of the Null DCT
identifier.
Because Null has exactly one DCI, no payload is required.
Boolean
The canonical encoding of a Boolean DCI consists of
- the Boolean DCT identifier, followed by
- one byte identifying the Boolean value.
The Boolean payload is
Thus,
are the complete canonical encodings.
No other Boolean payload byte is valid.
Integer
The canonical encoding of an Integer DCI consists of
- the Integer DCT identifier, followed by
- the canonical signed integer encoding of its mathematical integer value.
Thus,
is the canonical encoding of Integer DCI (n), where I is the signed integer
encoding defined above.
There is no implementation-defined integer width.
Float
The canonical encoding of a Float DCI consists of
- the Float DCT identifier, followed by
- the eight bytes of its IEEE-754 binary64 representation in big-endian byte
order.
Thus every Float DCI occupies exactly nine bytes.
The payload preserves the exact IEEE-754 representation, including the sign of
zero.
Because NaN and positive and negative infinity are not Float DCIs in Version 1,
their IEEE-754 representations are not valid Float payloads.
String
The canonical encoding of a String DCI consists of
- the String DCT identifier,
- the number of bytes in the UTF-8 encoding of the String, encoded using the
canonical unsigned integer encoding, and
- the UTF-8 bytes of the String.
Thus,
where N is the number of bytes in UTF8 and U is the canonical unsigned
integer encoding.
Because a String DCI is a sequence of Unicode scalar values, its UTF-8
representation is uniquely determined.
DataCanon performs no Unicode normalization before encoding.
Bytes
The canonical encoding of a Bytes DCI consists of
- the Bytes DCT identifier,
- the number of bytes, encoded using the canonical unsigned integer encoding,
and
- the exact byte sequence.
No text encoding or transformation is applied. Every byte value from 0 through
255 is permitted. An empty Bytes DCI is encoded as 16 00.
Constant
A Constant DCI is encoded as its DCT identifier 17 followed by exactly one
constant identifier byte: 00 for pi, 01 for e. No length, String DCT
identifier, or name text is included. All other constant identifier bytes are
invalid and MUST be rejected. A missing identifier byte is invalid.
Fraction
A Fraction DCI is encoded as
05 || I(numerator) || I(denominator)
I is the canonical signed integer encoding defined above. Both fields are
mathematical integers; the denominator MUST be positive. Neither field has an
Integer DCT identifier. No reduction is performed.
Atomic Types
Power
A Power DCI is encoded as
14 || E(base) || I(numerator) || I(denominator)
The base is a complete Scalar Numerical DCI. The positive mathematical integer
exponent fields use the signed integer encoding without Integer DCT
identifiers. Both are always encoded, including denominator one, and are not
reduced or otherwise normalized.
Quotient
A Quotient is encoded as its identifier followed by the complete encodings of
its Scalar Numerical numerator and denominator in that order:
15 || E(numerator) || E(denominator)
No constituent is omitted or mathematically transformed.
Complex
The canonical encoding of a Complex DCI consists of
- the Complex DCT identifier,
- the canonical encoding of its real component, and
- the canonical encoding of its imaginary component.
Thus,
07 || E(real) || E(imaginary)
Because each component is encoded as a complete DCI, its DCT participates
directly in the canonical encoding.
For example, a Complex whose real component is an Integer necessarily has a
different canonical encoding from one whose otherwise numerically equal real
component is a Float.
Composite Types
List
The canonical encoding of a List DCI consists of
- the List DCT identifier,
- the number of items encoded using the canonical unsigned integer encoding,
and
- the canonical encoding of each item in semantic order.
Thus, for a List containing (N) items,
08 || U(N) || E(item_0) || E(item_1) || ... || E(item_(N-1))
An empty List therefore consists of
Because each item is self-describing and the number of items is encoded
explicitly, no additional separators or item lengths are required.
Tuple
The canonical encoding of a Tuple DCI consists of
- the Tuple DCT identifier,
- the number of items encoded using the canonical unsigned integer encoding,
and
- the canonical encoding of each item in semantic order.
Thus, for a Tuple containing (N) items,
09 || U(N) || E(item_0) || E(item_1) || ... || E(item_(N-1))
An empty Tuple therefore consists of
List and Tuple use the same payload structure but have different DCT
identifiers and therefore cannot have identical canonical encodings.
Set
The canonical encoding of a Set DCI consists of
- the Set DCT identifier,
- the number of members encoded using the canonical unsigned integer encoding,
and
- the canonical encodings of its members in lexicographic byte order.
To encode a Set, the canonical encoding of each member is first computed
independently.
The member encoding byte sequences are then sorted lexicographically, comparing each byte as
an unsigned integer from 0 through 255.
For a Set containing (N) members whose sorted canonical encodings are
(e_0, e_1, \ldots, e_{N-1}),
0a || U(N) || e_0 || e_1 || ... || e_(N-1)
Because Set members are distinct DCIs and canonical encoding is injective, no
two member encodings may be identical.
An empty Set therefore consists of
The byte ordering used during Set encoding has no semantic meaning and does not
introduce order into the Set DCI.
Multiset
The canonical encoding of a Multiset DCI consists of
- the Multiset DCT identifier,
- the total number of member occurrences encoded using the canonical unsigned
integer encoding, and
- the complete canonical encoding of every member occurrence, sorted in
nondecreasing lexicographic byte order.
Each byte is compared as an unsigned integer from 0 through 255. Duplicate
member encodings MUST be retained, once for each occurrence. The count is the
number of occurrences, not the number of distinct members.
For a Multiset containing (N) occurrences with sorted canonical encodings
(e_0, e_1, \ldots, e_{N-1}),
11 || U(N) || e_0 || e_1 || ... || e_(N-1)
An empty Multiset consists of
The sorting rule provides a unique encoding without introducing semantic
member order. Nested members use their complete canonical encodings and are
not flattened.
OrderedProduct
The encoding is the identifier 18, a canonical unsigned operand count, and
complete operand encodings in their stored order:
18 || U(N) || E(factor_0) || ... || E(factor_(N-1))
No sorting occurs. Every operand MUST be a Numerical DCI. Duplicate factors
and nested OrderedProducts are preserved. The empty instance is 18 00.
Sum and Product
A Sum or Product is encoded as its DCT identifier, the total number of operand
occurrences using canonical unsigned integer encoding, and every complete
operand encoding in nondecreasing lexicographic byte order:
12 || U(N) || e_0 || e_1 || ... || e_(N-1) Sum
13 || U(N) || e_0 || e_1 || ... || e_(N-1) Product
Each byte is compared as an unsigned integer. Duplicate operand encodings MUST
be retained. Sorting does not assign semantic order or flatten nested nodes.
Empty Sum and Product encodings are 12 00 and 13 00, respectively.
Mapping
The canonical encoding of a Mapping DCI consists of
- the Mapping DCT identifier,
- the number of entries encoded using the canonical unsigned integer encoding,
and
- the canonical encodings of each entry in semantic entry order.
Each entry is encoded as
Thus, for a Mapping containing (N) entries,
0b
|| U(N)
|| E(key_0) || E(value_0)
|| E(key_1) || E(value_1)
|| ...
|| E(key_(N-1)) || E(value_(N-1))
where the entries appear in the order belonging to the Mapping DCI.
An empty Mapping therefore consists of
No separate entry delimiter or entry-length encoding is required because each
key and value is itself an unambiguously decodable DCI.
UnorderedMapping
The canonical encoding of an UnorderedMapping DCI consists of
- the UnorderedMapping DCT identifier,
- the number of entries encoded using the canonical unsigned integer encoding,
and
- the canonical encodings of its entries in canonical key order.
To encode an UnorderedMapping, the canonical encoding of each key is first
computed independently.
Entries are then sorted lexicographically by their canonical key encodings,
comparing each byte as an unsigned integer from 0 through 255.
Each entry is encoded as
Thus, for an UnorderedMapping containing (N) entries whose canonically ordered
keys are (k_0, k_1, \ldots, k_{N-1}),
0c
|| U(N)
|| E(k_0) || E(value_0)
|| E(k_1) || E(value_1)
|| ...
|| E(k_(N-1)) || E(value_(N-1))
Because keys within an UnorderedMapping are distinct DCIs and canonical
encoding is injective, no two key encodings may be identical. The canonical
key ordering therefore cannot contain ties.
An empty UnorderedMapping therefore consists of
The byte ordering used during encoding has no semantic meaning and does not
introduce entry order into the UnorderedMapping DCI.
Record
A Record DCI with N fields is encoded as
0d
|| S(record_name)
|| U(N)
|| S(field_name_0) || E(field_value_0)
|| S(field_name_1) || E(field_value_1)
|| ...
|| S(field_name_(N-1)) || E(field_value_(N-1))
S is the Unicode string information encoding. Record and field names have no
String DCT identifiers. Field values are complete DCIs. Fields retain their
semantic order; names MUST be unique according to Unicode scalar sequence
equality. A Record with no fields is 0d || S(record_name) || 00.
Array
The canonical encoding of an Array DCI consists of
- the Array DCT identifier,
- a one-byte Array type identifier,
- the Array rank encoded using the canonical unsigned integer encoding,
- each dimension of the Array shape encoded using the canonical unsigned
integer encoding, and
- the canonical element byte stream.
Thus, for an Array of rank (R) with shape
$$
(d_0,d_1,\ldots,d_{R-1}),
$$
the canonical encoding is
0e
|| ARRAY_TYPE
|| U(R)
|| U(d_0) || U(d_1) || ... || U(d_(R-1))
|| ELEMENT_BYTES
The number of Array elements is determined by the shape and is therefore not
encoded separately.
For a rank-zero Array, the rank is zero and no dimensions follow. A rank-zero
Array contains exactly one element.
Array Type Identifiers
DataCanon Version 1 assigns the following one-byte Array type identifiers:
| Array type | Identifier |
|---|
bool | 00 |
int8 | 01 |
int16 | 02 |
int32 | 03 |
int64 | 04 |
uint8 | 05 |
uint16 | 06 |
uint32 | 07 |
uint64 | 08 |
float16 | 09 |
float32 | 0a |
float64 | 0b |
complex64 | 0c |
complex128 | 0d |
The Array type identifiers are part of the DataCanon Version 1 specification and MUST
NOT be reassigned within Version 1.
The numerical ordering of Array type identifiers has no semantic meaning.
Element Ordering
Array elements are encoded in C-contiguous logical order.
For an Array with shape
$$
(d_0,d_1,\ldots,d_{R-1}),
$$
the rightmost index varies fastest.
The canonical element ordering depends only on the logical Array shape and
element values. It does not depend on the memory layout, strides, contiguity,
or storage representation used by an implementation.
Boolean Elements
Each bool element is encoded as exactly one byte:
No other byte value is valid.
Integer Elements
Fixed-width integer elements are encoded using exactly their specified number
of bits.
Signed integer types use two’s-complement representation.
Unsigned integer types use ordinary unsigned binary representation.
Types wider than one byte are encoded in big-endian byte order.
Thus, each element occupies
| Array type | Bytes |
|---|
int8 | 1 |
int16 | 2 |
int32 | 4 |
int64 | 8 |
uint8 | 1 |
uint16 | 2 |
uint32 | 4 |
uint64 | 8 |
Floating-Point Elements
Floating-point elements are encoded using their exact IEEE-754 representations.
float16, float32, and float64 use binary16, binary32, and binary64,
respectively.
Types wider than one byte are encoded in big-endian byte order.
The exact bit representation is preserved, including the sign of zero.
NaN and positive and negative infinity are not valid elements of portable
floating-point Arrays in Version 1.
Complex Elements
A complex64 element consists of
- its real component encoded as a
float32 element, followed by - its imaginary component encoded as a
float32 element.
A complex128 element consists of
- its real component encoded as a
float64 element, followed by - its imaginary component encoded as a
float64 element.
Thus, a complex64 element occupies exactly 8 bytes and a complex128
element occupies exactly 16 bytes.
Canonical Element Byte Stream
The canonical element byte stream is the concatenation of the canonical
element encodings in C-contiguous logical order.
No separators, element counts, or element lengths occur within the byte
stream because the Array type and shape determine both the size of each
element and the total number of elements.
An Array containing zero elements therefore has an empty canonical element
byte stream.
External Types
Relative Path Encoding
A relative path with N components is encoded as
U(N) || S(component_0) || S(component_1) || ... || S(component_(N-1))
Each component is Unicode string information, without a String DCT identifier.
The path MUST be nonempty and each component MUST satisfy all relative-path
invariants. No path separator is encoded.
SHA-256 Digest Encoding
A SHA-256 digest is encoded as exactly 32 bytes.
The bytes appear in the order defined by SHA-256.
No length field is encoded because the digest length is fixed.
The canonical encoding contains the digest bytes themselves, not a hexadecimal,
Base64, or other textual representation.
ExternalFile
The canonical encoding of an ExternalFile DCI consists of
- the ExternalFile DCT identifier,
- the canonical encoding of its relative path, and
- the 32-byte SHA-256 digest of the external file contents.
Thus,
where PATH is the relative-path encoding defined above and SHA256 is the
digest of the exact external file byte sequence.
No file contents are included in the canonical encoding of the ExternalFile
DCI.
The content digest participates directly in ExternalFile identity. Different
external byte sequences are expected to have different SHA-256 digests under
the cryptographic collision-resistance assumption; this is not a mathematical
guarantee.
The path also participates directly in ExternalFile identity. Two
ExternalFile DCIs with identical external contents but different relative
paths are distinct.
ExternalArray
The canonical encoding of an ExternalArray DCI consists of
- the ExternalArray DCT identifier,
- the canonical encoding of its relative path,
- the one-byte portable Array type identifier,
- the Array rank encoded using the canonical unsigned integer encoding,
- each dimension of the Array shape encoded using the canonical unsigned
integer encoding, and
- the 32-byte SHA-256 digest of the external Array element byte stream.
Thus, for an ExternalArray of rank (R) with shape
$$
(d_0,d_1,\ldots,d_{R-1}),
$$
the canonical encoding is
10
|| PATH
|| ARRAY_TYPE
|| U(R)
|| U(d_0) || U(d_1) || ... || U(d_(R-1))
|| SHA256
where
PATH is the canonical relative-path encoding,ARRAY_TYPE is the one-byte portable Array type identifier,U is the canonical unsigned integer encoding, andSHA256 is the digest of the canonical Array element byte stream.
The logical external payload is exactly the canonical Array element byte
stream defined for the specified Array type and shape. That stream contains
no DCT identifier, Array type identifier, rank, shape, digest, header, or other
framing metadata. A storage compression envelope is outside the logical stream.
The Array type and shape determine the exact logical byte length, after any
decompression. SHA-256 is computed over that logical stream. Persistence defines
uncompressed .bin, gzip .bin.gz, and HDF5 storage; the canonical DCI encoding
itself is unchanged.
Two ExternalArray DCIs are equal if and only if their
- relative paths,
- Array types,
- shapes, and
- content digests
are equal.
ExternalArray and Array are distinct DCTs and therefore never have equal
instances, even when an ExternalArray refers to the same numerical element
values represented by an Array DCI.
The path and digest both participate in ExternalArray identity.
A resource found at the specified path whose byte sequence has a different
SHA-256 digest does not satisfy the ExternalArray DCI.
A missing or inaccessible external resource does not change the identity of
the ExternalArray DCI; it prevents the external contents from being retrieved
or verified.
Canonical Encoding Validity
A byte sequence is a valid canonical encoding of a DCI if and only if it
satisfies all encoding rules defined by this specification and encodes a valid
instance of the identified DCT.
A decoder of canonical DataCanon encoding MUST reject any byte sequence that
violates these requirements.
Complete Consumption
When a byte sequence is presented as the canonical encoding of exactly one DCI,
decoding MUST consume the entire byte sequence.
Bytes remaining after a complete DCI has been decoded make the input invalid as
the canonical encoding of exactly one DCI.
Thus,
is not the canonical encoding of x.
DCT Identifiers
The first byte of every DCI encoding MUST be a DCT identifier defined by the
applicable version of the DataCanon specification.
An unassigned DCT identifier is invalid.
The payload following the identifier MUST satisfy the encoding rules of that
DCT.
Canonical Unsigned Integer Encoding
Every unsigned LEB128 value MUST use the minimum number of bytes required to
represent its value.
An encoding containing a redundant high-order zero group is invalid.
A continuation bit indicating another byte MUST be followed by another byte.
An unterminated unsigned integer encoding is invalid.
Canonical Signed Integer Encoding
A signed Integer payload MUST consist of the canonical unsigned integer
encoding of a value produced by the zigzag mapping.
Because the zigzag mapping is bijective over the mathematical integers, every
canonically encoded nonnegative zigzag value determines exactly one
mathematical integer.
Floating-Point Values
A Float payload MUST contain exactly eight bytes encoding a finite IEEE-754
binary64 value.
A payload representing NaN or positive or negative infinity is invalid in
Version 1.
The same requirement applies at the corresponding width to portable
floating-point Array elements.
Strings
A String payload MUST contain
- a canonical unsigned integer specifying the number of UTF-8 bytes, followed
by
- exactly that number of bytes.
Those bytes MUST form a valid UTF-8 encoding of a sequence of Unicode scalar
values.
Overlong UTF-8 encodings, surrogate code points, malformed UTF-8 sequences, and
other byte sequences not representing Unicode scalar values are invalid.
No Unicode normalization is performed.
Bytes
A Bytes payload MUST contain a canonical unsigned integer byte count followed
by exactly that number of bytes. Every byte value is valid; UTF-8 validation
does not apply. Truncated payloads and nonminimal length encodings are invalid.
Fraction
A Fraction payload MUST contain two minimally encoded signed integer fields.
The denominator MUST be positive. Missing fields, nonminimal encodings, and
nonpositive denominators are invalid.
Atomic Types
The payload of an atomic DCI MUST contain exactly the number and DCTs of
constituents required by its DCT.
Each constituent MUST itself have a valid canonical encoding and MUST satisfy
all invariants required by the containing DCT.
For example, Power exponent fields MUST be positive mathematical integers.
Direct fields use their prescribed payload encodings, not nested DCI encodings.
Ordered Composite Types
The encoded item, entry, or field count MUST equal the number of constituents
that follow.
Each constituent DCI MUST have a valid canonical encoding.
For Mapping and Record, all uniqueness requirements defined by the
corresponding DCT MUST also be satisfied.
Compositional numerical types
Every Sum and Product operand MUST have a valid complete canonical encoding.
Sum operands MUST be Numerical DCIs; Product operands MUST be Scalar Numerical
DCIs, including recursively scalar Sum and OrderedProduct operands. Operand encodings MUST be in
nondecreasing lexicographic byte order; equal adjacent encodings are permitted
and MUST retain their multiplicities.
An OrderedProduct contains its declared number of Numerical DCIs in order,
without sorting or deduplication.
A Power MUST contain a Scalar Numerical base DCI followed by two minimally encoded
positive signed integer fields. A Quotient MUST contain two Scalar Numerical DCIs in
numerator/denominator order.
Decoding MUST preserve constituent types, exponent structure, multiplicity,
and nesting without evaluation or mathematical normalization.
Set
A Set payload MUST contain exactly the encoded number of members.
Each member MUST be a valid canonically encoded DCI.
Member encodings MUST occur in strictly increasing lexicographic byte order.
Because equal DCIs have identical canonical encodings, strict ordering also
ensures that no duplicate Set members occur.
A Set whose members are not in canonical byte order is not canonically encoded.
Multiset
A Multiset payload MUST contain exactly the encoded number of member
occurrences. Each occurrence MUST be a valid canonically encoded DCI.
Member encodings MUST occur in nondecreasing lexicographic byte order.
Adjacent equal encodings are valid and represent repeated members. Decreasing
member encodings are invalid.
A decoder MUST preserve every occurrence and MUST NOT deduplicate or flatten
members.
UnorderedMapping
An UnorderedMapping payload MUST contain exactly the encoded number of entries.
Each key and value MUST be a valid canonically encoded DCI.
Entries MUST occur in strictly increasing lexicographic order of their complete
canonical key encodings.
No two entries may have equal keys.
An UnorderedMapping whose entries are not in canonical key order is not
canonically encoded.
Record
A Record payload MUST contain exactly the encoded number of fields.
The Record name and every field name MUST use valid Unicode string information
encoding without a DCT identifier.
Field names MUST satisfy the uniqueness requirement of the Record DCT.
Field values MUST be valid canonically encoded DCIs.
Array
An Array payload MUST use a portable Array type identifier defined by this
specification.
The encoded rank MUST equal the number of encoded dimensions.
Each dimension MUST be a canonically encoded nonnegative integer.
The number of elements is determined by the product of the dimensions, with an
empty shape defining a rank-zero Array containing one element.
The canonical element byte stream MUST contain exactly
number_of_elements * bytes_per_element
bytes for the specified Array type.
Every element encoding MUST satisfy the requirements of that Array type.
In particular, floating-point and complex elements MUST NOT contain NaN or
positive or negative infinity in Version 1.
The Array decoder consumes exactly the number of element bytes determined by its type and shape.
Relative Paths
A canonically encoded DataCanon relative path MUST contain at least one
component.
The encoded component count MUST equal the number of components that follow.
Each component MUST be valid Unicode string information satisfying the DataCanon relative
path requirements.
In particular,
- a component MUST NOT be empty,
- a component MUST NOT equal
"." or "..", and - a component MUST NOT contain
/ or \.
ExternalFile
An ExternalFile payload MUST contain
- a valid DataCanon relative path, followed by
- exactly 32 bytes representing a SHA-256 content digest.
The external file itself is not required to be available in order for the
ExternalFile DCI or its canonical encoding to be valid.
Availability and verification of the referenced external resource are separate
operations.
ExternalArray
An ExternalArray payload MUST contain
- a valid DataCanon relative path,
- a portable Array type identifier,
- a valid rank and shape, and
- exactly 32 bytes representing the SHA-256 digest of the external canonical
Array element byte stream.
The expected external byte-stream length is determined by the Array type and
shape.
The external resource itself is not required to be available in order for the
ExternalArray DCI or its canonical encoding to be valid.
Availability, byte-length verification, and content-digest verification of the
referenced resource are separate operations.
Decoding
Canonical decoding is the inverse of canonical encoding.
For every DCI x,
A conforming canonical decoder MUST reject any byte sequence that is not the
canonical encoding of exactly one valid DCI.
Canonical decoding performs no mathematical simplification, type coercion,
Unicode normalization, or other transformation of the represented DCI.
Fingerprinting
Every DataCanon Instance has a deterministic fingerprint.
The fingerprint of a DCI is the SHA-256 digest of its canonical encoding.
For DCI x with canonical encoding E(x),
$$
F(x) = \operatorname{SHA256}(E(x)).
$$
A fingerprint is a sequence of exactly 32 bytes. Its conventional textual
representation is the corresponding sequence of 64 lowercase hexadecimal
characters. The textual representation is not itself the fingerprint.
Because equal DCIs have identical canonical encodings, equal DCIs necessarily
have identical fingerprints.
Fingerprint equality does not define DCI equality. DCI equality is defined by
the corresponding DCT.
The canonical encoding, rather than the fingerprint, is the complete
representation of DCI identity.
Cryptographic Digests and Identity
DataCanon uses SHA-256 in two distinct roles:
- to identify the contents of external resources, and
- to compute fingerprints of DataCanon Instances.
SHA-256 is not mathematically injective. Distinct byte sequences may therefore
have identical SHA-256 digests.
The equality of DataCanon Instances is defined exactly by their DCTs and
constituents. For external DCTs, the content digest is a constituent of the DCI;
the external byte sequence itself is not.
Consequently, the identification of external contents by their digests, and the
use of fingerprints as practical identifiers, rely on the cryptographic
collision resistance of SHA-256.
The cryptographic assumptions do not alter the definitions of DCI equality or
canonical encoding.
Appendix A: Canonical Encoding Test Vectors
This appendix specifies fixed Version 1 canonical byte encodings for selected
DCIs. Hexadecimal byte pairs are separated by spaces for readability; the
spaces are not part of the encoded byte sequences. The test vectors provide
independent conformance checks and do not change the encoding rules above.
| DCI | Canonical encoding (hexadecimal bytes) |
|---|
| Null | 00 |
| Boolean(false) | 01 00 |
| Boolean(true) | 01 01 |
| Integer(0) | 02 00 |
| Integer(-1) | 02 01 |
| Integer(42) | 02 54 |
| Integer(128) | 02 80 02 |
| Float(+0.0) | 03 00 00 00 00 00 00 00 00 |
| Float(-0.0) | 03 80 00 00 00 00 00 00 00 |
| String(“A”) | 04 01 41 |
| OrderedProduct(empty) | 18 00 |
| OrderedProduct(Integer 2, Integer 3) | 18 02 02 04 02 06 |
| Constant(pi) | 17 00 |
| Constant(e) | 17 01 |
| Bytes(empty) | 16 00 |
Bytes(hexadecimal 00 ff) | 16 02 00 ff |
| Fraction(numerator=2, denominator=4) | 05 04 08 |
| Complex(real=Integer(1), imaginary=Float(-0.0)) | 07 02 02 03 80 00 00 00 00 00 00 00 |
| List() | 08 00 |
| List(Integer(1), Boolean(true)) | 08 02 02 02 01 01 |
| Tuple() | 09 00 |
| Set() | 0a 00 |
| Multiset() | 11 00 |
| Sum() | 12 00 |
| Product() | 13 00 |
| Sum(Integer(1), Integer(1)) | 12 02 02 02 02 02 |
| Product(Integer(2), Integer(1)) | 13 02 02 02 02 04 |
| Power(base=Integer(5), numerator=1, denominator=3) | 14 02 0a 02 06 |
| Quotient(Integer(2), Integer(3)) | 15 02 04 02 06 |
| Multiset(Integer(1)) | 11 01 02 02 |
| Multiset(Integer(1), Boolean(true), Integer(1)) | 11 03 01 01 02 02 02 02 |
| Multiset(Multiset(), Multiset()) | 11 02 11 00 11 00 |
| Set(Integer(1), Boolean(true)) | 0a 02 01 01 02 02 |
| Mapping() | 0b 00 |
| Mapping(String(“a”) → Integer(1)) | 0b 01 04 01 61 02 02 |
| UnorderedMapping() | 0c 00 |
| UnorderedMapping(String(“b”) → Integer(2), String(“a”) → Integer(1)) | 0c 02 04 01 61 02 02 04 01 62 02 04 |
| Record(name=“R”, field “x” → Integer(1)) | 0d 01 52 01 01 78 02 02 |
| Array(type=bool, shape=(2,), elements=[false, true]) | 0e 00 01 02 00 01 |
| Array(type=float64, shape=(1,), elements=[-0.0]) | 0e 0b 01 01 80 00 00 00 00 00 00 00 |
The following additional vectors are displayed as concatenated fields to make
their structure explicit.
ExternalFile with path components ( "x.bin" ) and a 32-byte all-zero
digest:
0f
01
05 78 2e 62 69 6e
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
ExternalArray with path components ( "x.bin" ), Array type float64,
shape (2,), and a 32-byte all-zero digest:
10
01
05 78 2e 62 69 6e
0b
01 02
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
The all-zero digests in the last two vectors are illustrative digest
constituents. The test vectors specify DCI metadata encoding and do not assert
the existence of external resources with those digests.
6.2 - Shorthand
DataCanon Shorthand — Version 1 Draft
Status: Draft for independent implementation review
Depends on: DataCanon Core Version 1
Scope: Strict Shorthand parsing and identity-preserving rendering
Out of scope: Optional Shorthand preprocessing, YAML interchange, HDF5, and persistence conventions
Introduction
DataCanon Shorthand is a human-readable textual representation of DataCanon Instances (DCIs). It provides notation for all 24 DataCanon Types (DCTs) defined by DataCanon Core Version 1.
Shorthand does not define DCI identity. The core specification defines DCTs, equality, canonical binary encoding, and fingerprinting. A Shorthand document represents exactly one DCI; its whitespace and textual layout do not participate in that DCI’s identity.
A conforming Shorthand parser MUST recover the DCI specified by a valid Shorthand document. A conforming Shorthand renderer MUST produce valid Shorthand that a conforming parser recovers as a DCI equal to the original.
Shorthand is not a canonical textual encoding. Multiple texts may represent the same DCI, and no particular rendering layout is required.
The terms MUST, MUST NOT, SHOULD, and MAY indicate requirements and recommendations for implementations of this specification.
The definitions, invariants, and equality relations of DataCanon Core Version 1 take precedence over any convenience afforded by Shorthand syntax. A syntactically well-formed expression that violates a DCT invariant is invalid Shorthand.
A conforming strict parser MUST:
- parse exactly one complete DCI and reject unexpected trailing input;
- interpret the constructs specified here according to their defined DCTs;
- preserve constituent identity, including unreduced Fractions and floating-point signed zero;
- reject invalid DCT constituents and malformed syntax; and
- operate without performing substitutions, mathematical evaluation, or application-specific transformations.
A conforming renderer MUST preserve DCI equality across a strict render–parse round trip. It MAY choose whitespace, indentation, line breaks, and other identity-preserving layout conventions.
The separately specified Shorthand preprocessor MAY be applied before strict parsing. It is not part of the strict grammar.
Lexical rules
Shorthand is Unicode text. Strings may contain Unicode scalar values as defined by DataCanon Core Version 1.
Outside quoted strings, the recognized whitespace characters are the ASCII space (U+0020), horizontal tab (U+0009), carriage return (U+000D), and line feed (U+000A). Whitespace may appear between syntactic tokens except where this specification explicitly requires adjacent characters.
Strict Shorthand does not define comments. An empty or whitespace-only document is invalid.
Digits and identifiers
A digit is one of the ASCII characters 0 through 9.
An identifier begins with an ASCII letter (A–Z, a–z) or underscore (_), followed by zero or more ASCII letters, digits, or underscores.
The strict grammar recognizes only the identifiers explicitly assigned meaning below. Arbitrary identifiers are not variables and MUST be rejected in strict parsing.
Identifiers and keywords are case-sensitive. In particular, true, false, and null are lowercase.
Strings
A String literal is delimited by double quotation marks ("). Its contents are a sequence of Unicode scalar values, optionally using the following escapes:
| Source characters | Decoded character |
|---|
\" | quotation mark |
\\ | backslash |
\n | line feed |
\r | carriage return |
\t | horizontal tab |
No other backslash escapes are defined. A backslash followed by any other character MUST be rejected. Literal Unicode characters, including literal line breaks, may appear inside a quoted string, provided the decoded result is a valid DataCanon String. No Unicode normalization occurs.
A renderer MUST escape quotation marks and backslashes so that they cannot terminate or alter a String literal; it MAY escape the three supported control characters above.
Block Strings
In addition to ordinary quoted Strings, Shorthand supports literal and folded block Strings. Both forms represent the String DCT and do not introduce new DataCanon Types.
A literal block begins with """ followed immediately by LF:
"""
first line
second line
"""
This represents the String first line\nsecond line.
A folded block begins with """> followed immediately by LF:
""">
This is a long String
written across several
physical lines.
"""
This represents This is a long String written across several physical lines.
Structural indentation. The opening delimiter MUST be the first non-space content on its physical line. Its indentation consists exclusively of spaces and defines the block’s structural indentation. The closing """ MUST begin at exactly the same indentation. It MAY be followed by surrounding Shorthand syntax, such as a comma, colon, or closing collection delimiter.
Each nonempty content line MUST contain at least the structural indentation. Exactly that indentation is removed. Completely empty physical lines are permitted without indentation. Any additional indentation is content.
Literal blocks. After structural indentation is removed, all content characters are preserved, including leading and trailing whitespace and physical LF characters between content lines. The newline immediately following the opening delimiter and the newline immediately preceding the closing delimiter are structural and are not included in the String.
Folded blocks. After structural indentation is removed, whitespace-only lines are discarded, trailing whitespace is removed from nonblank content lines, and the remaining lines are joined with exactly one U+0020 SPACE. Additional leading whitespace on nonblank lines is preserved. Consecutive blank lines do not introduce additional spaces or newlines.
Neither block form interprets escape sequences. Backslashes are literal characters. Strings that cannot be represented using block syntax can always be expressed using ordinary quoted Strings.
Block formatting does not participate in DCI equality, canonical byte encoding, or fingerprinting. Different textual representations that produce the same String DCI have identical canonical identity.
Numerical literals
An integer literal consists of an optional leading - followed immediately by one or more ASCII digits. It denotes an arbitrary-precision mathematical integer in decimal notation. A leading + is not supported.
A floating-point literal consists of an optional leading -, a nonempty sequence of ASCII digits, and at least one of:
- a decimal point followed by one or more ASCII digits; or
- an exponent introduced by
e or E, followed by an optional + or - and one or more ASCII digits.
A floating-point literal may have both a decimal part and an exponent, such as 1.25e-3. The scalar syntax does not require support for a trailing decimal point without following digits (for example, 1.). Array element syntax has a separate allowance for that form.
A floating-point literal denotes the finite IEEE-754 binary64 value obtained by correctly rounding its decimal value. The parser MUST reject values that cannot produce a valid finite Float DCI. Positive and negative zero are distinct.
A Fraction literal consists of an integer literal, /, and one or more ASCII digits denoting a positive denominator, with no intervening whitespace. The numerator and denominator are preserved exactly; no reduction occurs.
Examples:
| Text | DCT | Interpretation |
|---|
42 | Integer | integer 42 |
-0 | Integer | integer 0 |
-0.0 | Float | negative binary64 zero |
1e3 | Float | binary64 1000.0 |
2/4 | Fraction | numerator 2, denominator 4 |
Strict Shorthand constructs
Primitive DCTs
| DCT | Syntax | Example |
|---|
| Null | null | null |
| Boolean | true or false | true |
| Integer | integer literal | -42 |
| Float | floating-point literal | 3.5 |
| String | quoted String literal | "hello" |
Named mathematical constants
Pi denotes Constant pi; E denotes Constant e. Both tokens are
case-sensitive and are accepted wherever a Numerical DCI is permitted.
They are constant tokens, not function calls or variables.
The explicit constructor is constant("pi") or constant("e"), with exactly
one quoted String argument. The String supplies the Constant’s direct name
information; it does not become a constituent DCI. Unknown names and bare
name arguments such as constant(pi) MUST be rejected.
The default renderer emits Pi or E; longhand emits constant("pi") or
constant("e"). Numerical exponent syntax such as E**2 constructs a Power
DCI, not another spelling of Constant e. Quoted "Pi" remains a String.
Array element syntax does not accept constants or structured numerical DCIs.
Bytes
A Bytes DCI is written using one quoted hexadecimal String:
bytes("")
bytes("00ff80")
The decoded String MUST contain an even number of ASCII hexadecimal digits
(0–9, a–f, or A–F). Each pair denotes one byte in sequence order.
An empty String denotes an empty byte sequence. Separators, whitespace within
the decoded String, 0x prefixes, and non-ASCII digits are invalid.
Exactly one String argument is required. Whitespace between syntactic tokens
and an optional trailing comma are permitted. A renderer emits lowercase
hexadecimal digits. The text describes the exact opaque byte sequence; it does
not perform character decoding or interpret a file format.
Fraction
A Fraction is written as a Fraction literal, for example 1/3 or 2/4, or
using the explicit constructor fraction(numerator, denominator).
Both constructor arguments MUST be integer literals; the denominator MUST be
positive. Exactly two arguments are required, and an optional trailing comma
is permitted. Constituents are preserved without reduction. Fraction is
distinct from Integer and Float even when their mathematical values agree.
Sum and Product
sum()
sum(1, 2, 3)
product()
product(1/2, sqrt(3))
Sum arguments are Numerical DCIs; Product arguments are Scalar Numerical
DCIs. Empty and singleton instances are valid,
repeated operands are retained, and an optional trailing comma is permitted.
Operand order is not semantic; multiplicity and nesting are semantic.
Bounded infix forms construct the same flat nodes:
1 + 2 + 3 -> sum(1, 2, 3)
1/2 * sqrt(3) -> product(1/2, sqrt(3))
1 + 2 * 3 -> sum(1, product(2, 3))
Multiplication binds more tightly than addition. Explicit constructor operands
are not flattened: sum(1, 2) + 3 represents sum(sum(1, 2), 3).
The numerical forms construct structure; they perform no arithmetic.
Binary subtraction and general infix division are not supported. Negative
numeric literals remain available, for example 1 + -2. Parentheses retain
Tuple syntax; grouping is expressed through constructors, not grouping
parentheses. Thus (1 + 2) is a singleton Tuple containing a Sum.
OrderedProduct and array expressions
ordered_product(item, ...) constructs an OrderedProduct DCI, preserving
operand order and nesting. Zero or more Numerical DCI operands are accepted;
an optional trailing comma is permitted. No rank, shape, or mathematical
compatibility checks are performed during parsing.
The infix + always constructs Sum and accepts all Numerical DCIs. Infix *
constructs Product when every factor is a Scalar Numerical DCI; otherwise it
constructs OrderedProduct. Operand classification is structural, not numerical
evaluation. Existing precedence is unchanged: ** binds before *, which
binds before +. Complex DCIs and collection DCIs other than Array remain
invalid numerical operands.
2 * array([[1, 0], [0, 1]])
2 * array([[0, 1], [1, 0]]) + 3 * array([[1, 0], [0, -1]])
ordered_product(2, 3)
Explicit product(...) remains scalar-only. Scalar-only OrderedProducts MUST
render using ordered_product(...), because their infix form would construct
a different DCT. Renderers MUST also retain explicit constructors wherever
needed to preserve nested Sum, Product, and OrderedProduct boundaries.
Longhand always uses ordered_product(...); ordinary Shorthand may use *
when reparsing preserves the exact DCI. ordered_product is a collection
layout name accepted by per-type layout configuration.
Power
power(base, numerator)
power(base, numerator, denominator)
The base is a Scalar Numerical DCI; numerator and denominator are positive
mathematical integers written as integer literals. The omitted denominator
defaults to one. Optional
trailing commas are permitted. Exponent constituents are not reduced.
A bounded infix form accepts a positive Integer literal without parentheses,
or a positive Integer/Fraction literal inside exponent-specific parentheses:
An infix power MUST NOT have a Fraction base. Expressions such as
1/2**3 and 1/2**(1/3) are invalid because they resemble arithmetic division
with a different precedence convention. Use the explicit constructor instead:
power(1/2, 3)
power(1/2, 1, 3)
This restriction applies only to the infix spelling. Fraction remains a valid
Power base, and Fraction literals remain permitted in parenthesized exponents.
Power binds more tightly than multiplication and addition. The two stars MUST
be adjacent. Whitespace around the operator and inside exponent parentheses
is permitted. Exponent parentheses are part of this construct and do not
introduce general expression grouping.
Unparenthesized rational exponents, Float exponents, and chains such as
5**2**3 are invalid. Nested powers use explicit power(...) constructors.
sqrt(base) is a convenience spelling for power(base, 1, 2) and constructs
a Power DCI without evaluation. The denominator always participates in the
resulting DCI, whether or not it was written explicitly.
Quotient
quotient(numerator, denominator)
quotient(2, sqrt(3) + sqrt(2))
Both arguments are Scalar Numerical DCIs. Exactly two arguments are required;
an optional trailing comma is permitted. The parser performs no division,
cancellation, or denominator rationalization. Fraction literal syntax remains
unchanged and constructs Fraction, not Quotient.
Complex
A Complex is written:
Each component MUST parse to a Scalar Numerical DCI. The two components retain their DCTs and their individual identities.
Example:
complex(1/2, 1/2 * sqrt(3))
This notation is distinct from the special complex-number syntax used inside Arrays.
List and Tuple
A List uses square brackets or list(...); a Tuple uses parentheses or
tuple(...). Constructor arguments are the items themselves, rather than a
single collection to convert. Elements are arbitrary DCIs separated by commas.
Both forms may be empty and may have an optional trailing comma.
[]
[1, "x", 2/4]
(1, 2)
(1)
()
A single-element Tuple is (1); no trailing comma is required. List and Tuple remain distinct DCTs regardless of their contents.
Set and Mapping
Curly braces denote a nonempty Set or a Mapping, distinguished by the presence of : following the first parsed value.
{1, 2, 3}
{"a": 1, "b": 2}
{}
{} is the empty Mapping. The empty Set is written set().
The identifier emptyset is not part of strict Shorthand.
A Set may also be written set(item, ...), including set() and singleton
Sets. Constructor arguments are individual members. An optional trailing
comma is permitted in the constructor form.
A Mapping may also be written mapping(key: value, ...), including
mapping(). The constructor follows the same entry order, key uniqueness,
and optional trailing-comma rules as the brace form.
Set members are arbitrary DCIs separated by commas. They MUST be pairwise distinct under DCI equality. Set member order is not semantic. The brace form does not admit a trailing comma; the set(...) constructor does.
A Mapping consists of comma-separated key: value entries. Keys and values are arbitrary DCIs, and keys MUST be pairwise distinct under DCI equality. Entry order is semantic. A Mapping may have an optional trailing comma.
Multiset
A Multiset is written as comma-separated DCI members enclosed in
multiset(...):
multiset()
multiset(1, 1, 2)
multiset("Sr", [1, 2], [1, 2])
multiset(multiset(1, 1), 2)
Members may belong to any DCT. Repeated members are permitted and MUST be
retained. Member order is not semantic; multiplicity and nesting are semantic.
For example, multiset(1, 1, 2) and multiset(2, 1, 1) represent equal DCIs,
while multiset(1, 2, 2) represents a different DCI.
An optional trailing comma is permitted. multiset() is the empty Multiset.
A Multiset is distinct from a Set, List, or Tuple with otherwise equal members.
A renderer MAY choose any member order. It MUST preserve every occurrence and
MUST NOT flatten nested Multisets. Canonical byte sorting imposes no textual
member-order requirement.
UnorderedMapping
An UnorderedMapping is written:
umapping("b": 2, "a": 1)
umapping()
It follows Mapping entry syntax and uniqueness requirements, but entry order is not semantic. An optional trailing comma is permitted.
Record
A Record is written with a quoted name followed by zero or more quoted field names and DCI values:
record("Point", "x": 1.0, "y": 2.0)
record("Empty")
The Record name and each field name are DataCanon Strings. Field names MUST be distinct. Field order and the Record name participate in DCI identity. A trailing comma after the Record name or final field is permitted.
Record names and field names need not be valid Shorthand identifiers because they are quoted Strings.
Arrays
Array construct
An Array is written:
array(DATA)
array(DATA, dtype="TYPE")
array(DATA, shape=(DIMENSIONS))
array(DATA, dtype="TYPE", shape=(DIMENSIONS))
The dtype and shape options may occur in either order, at most once each. An optional comma before the closing ) is permitted.
DATA is either a single Array scalar (rank zero) or a nested bracketed collection of Array scalar elements. Array elements are not DCIs. A nested Array body is not a DataCanon List.
Examples:
array(1.0)
array([1, 2, 3])
array([[1.0, 2.0], [3.0, 4.0]])
array([1.0, -0.0], dtype="float32")
array([], dtype="float64", shape=(0, 3))
The bracketed numerical body uses comma-separated entries and rectangular nesting. The current strict parser does not permit a trailing comma inside a bracketed Array body.
Array scalar syntax
Array scalar elements may be true, false, real numeric literals, or complex numeric literals of the form:
REAL+MAGNITUDEj
REAL-MAGNITUDEj
REAL and MAGNITUDE use Array numeric syntax. The real part may be negative; the imaginary magnitude is unsigned. No whitespace is permitted within the complex literal. Exponents are permitted in either component, and the decimal point in an Array numeric literal may have no following digit (1.).
Examples:
array([true, false])
array([1., 2.5])
array([1.0+2.0j, 3.0-4.0j])
array([1e-3+2e4j, -3e2-4e-1j])
1.0+2.0j is not a valid top-level Complex DCI expression.
Portable Array types
The resulting Array MUST have one of the 14 portable Array types defined by DataCanon Core Version 1:
bool, int8, int16, int32, int64, uint8, uint16, uint32, uint64, float16, float32, float64, complex64, and complex128.
A dtype option is a quoted type name. The portable type names above are the interoperable spelling; the Python reference parser also accepts dtype aliases recognized by NumPy when they produce a portable type. Whether to standardize aliases beyond the portable names is open for Version 1 finalization.
Inference and explicit conversion
When dtype is omitted, the reference behavior is NumPy’s array construction from the parsed Array scalar values. When shape is omitted, the reference behavior is the shape inferred from that construction.
When dtype is present, the reference behavior is NumPy Array construction with that dtype. NumPy’s conversion behavior, including any permitted rounding or lossy conversion, is intentionally retained. Shorthand does not impose a separate losslessness rule on explicit dtype conversion. The final Array MUST nevertheless satisfy the portable Array DCT’s invariants, including finiteness of floating-point and complex elements.
When shape is present, the constructed Array is reshaped using NumPy-compatible reshape semantics; the requested shape MUST be valid, have nonnegative dimensions, and preserve the element count.
Reference-version item pending: Before this draft becomes a final cross-language conformance standard, the NumPy reference version MUST be fixed. Julia and other implementations need only reproduce the resulting DCI, not use NumPy internally. The detailed inference and conversion algorithm is deliberately not reproduced in this draft.
Array identity and rendering
Array type, shape, and the exact portable element values participate in identity. Renderers MUST include dtype and shape annotations whenever omitting them would cause a conforming parser to infer a different DCI.
A renderer MUST preserve the bit identities of floating-point Array elements, including positive and negative zero, and the corresponding components of complex Array elements.
The canonical element byte stream and canonical Array encoding are defined only by DataCanon Core Version 1, not by Shorthand formatting.
External resources
Relative paths
Shorthand writes a DataCanon relative path as one quoted String whose components are joined by the ASCII slash (/). The parser splits that String on / to reconstruct the abstract DataCanon relative path.
Each component MUST satisfy the relative-path invariants of DataCanon Core Version 1. In particular, empty components, . and .., and components containing backslashes are invalid.
Parsing an external DCI does not access its external resource.
ExternalFile
external_file("inputs/INCAR", "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
The first argument is a relative path String. The second is a quoted hexadecimal SHA-256 digest decoding to exactly 32 bytes. A renderer SHOULD use 64 lowercase hexadecimal characters. A conforming parser MUST accept that spelling and MAY accept uppercase hexadecimal digits. Whitespace within a hexadecimal digest String is not part of the portable normative spelling.
ExternalArray
external_array("data/forces.bin", "float64", (100, 3), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
The arguments, in order, are a relative path String, a portable Array type String, a Tuple of nonnegative Integer dimensions, and a quoted SHA-256 digest decoding to exactly 32 bytes.
The external byte stream is defined by DataCanon Core Version 1. Shorthand carries the path, Array type, shape, and digest, but not the external contents.
Rendering requirements
A conforming renderer MUST satisfy, for every valid DCI x:
parse_shorthand(render_shorthand(x)) = x
Here equality means DCI equality as defined by DataCanon Core Version 1. The equation describes the behavior of conforming parser and renderer operations, not mandated function names in other programming languages.
Shorthand and longhand
Longhand is the form of Shorthand that uses explicit constructors for
structured values and ordinary literals for Null, Boolean, Integer, Float,
and String. For example, sum(1, 1.0, fraction(1, 2)) is longhand: the
integer and floating-point literals already identify their DCTs. Longhand
does not wrap literals as integer(1), float(1.0), or string("hello");
those wrappers are not part of the Version 1 grammar.
The distinction is syntactic, not the Core classification of primitive,
atomic, and composite DCTs. Fraction and Constant are primitive but use fraction(...) and
constant(...), respectively, in longhand. Longhand is a subset of the same grammar, accepted by the same
parser. Shorthand permits constructors and shorter spellings to be freely
mixed. No additional fully wrapped rendering mode is defined.
The Python renderer accepts longhand=False (the default) or longhand=True.
With False, it uses shorter spellings where appropriate, falling back to
constructors to preserve structure. With True, it produces longhand.
The option MUST be a Boolean. Longhand selection is independent of layout
layout, alignment, and indentation. Both settings produce valid
strict Shorthand representing equal DCIs.
| DCT | Shorthand examples | Longhand |
|---|
| Fraction | 1/2 | fraction(1, 2) |
| List | [1, 2] | list(1, 2) |
| Tuple | (1, 2) | tuple(1, 2) |
| Set | {1, 2} | set(1, 2) |
| Mapping | {"a": 1} | mapping("a": 1) |
| Sum | 1 + 2 | sum(1, 2) |
| Product | 2 * sqrt(3) | product(2, power(3, 1, 2)) |
| Power | 5**2, 5**(1/3), or sqrt(5) | power(5, 2), power(5, 1, 3), or power(5, 1, 2) |
Longhand applies recursively to constituent DCIs. Null, Boolean, Integer,
Float, and String retain their literal spellings. Fraction uses fraction(...)
even though it is classified as primitive; Bytes retains bytes("...").
Constructs having only one spelling retain that spelling.
Array bodies continue to use their specialized element syntax; their brackets
do not represent List DCIs. Shape Tuples may use either Tuple spelling.
Shorthand rendering MUST use constructors wherever necessary to preserve identity.
In particular, empty and singleton Sums and Products retain constructors;
nested Sums and Products MUST NOT be flattened by rendering. A Sum used as a
Product operand requires an explicit constructor because Shorthand does not
provide general grouping parentheses. Fraction bases, signed literal bases,
and complicated bases may use power(...) to avoid precedence ambiguity.
Multiline output may also use constructors for clearer layout.
For example:
1 + sum(2, 3)
2 * product(3, 4)
2 * sum(3, 4)
power(1/2, 3)
Longhand selection and any constructor fallback do not participate in DCI
identity or canonical binary encoding.
Renderers MAY choose their own whitespace, indentation, line breaking, column alignment, and compactness policies. The Python binding’s formatting options are not distinct dialects or normative cross-language layout requirements.
The Python binding provides independent layout, align, indent, and
padding options, plus custom_layout per-type overrides. For multiline output,
standard places both delimiters on separate lines, semi-compact shares the
opening line with the first item or entry, and compact also shares the closing
line with the final content. inline forces the selected value and all its
descendants onto one line. An enclosing inline requirement overrides child
layout selections and disables table alignment within that subtree.
Indentation controls structural nesting on new lines, defaulting to two spaces.
Padding controls same-line gaps beside collection and constructor delimiters,
defaulting to one space, including flat values. Shared-opener continuation
items align with the first item at relative column len(opening) + padding;
indentation does not add another gap there. Standard multiline delimiters
receive no padding because they occupy separate lines. Compact shared closers
receive the requested padding before them. Both options are nonnegative
integers. Primitive literal forms and specialized Array element syntax remain
unchanged; Array constructors can still be rendered inline.
Lists, Tuples, Sets, and Multisets normally become multiline when they directly
contain a List or Tuple, or an item already spanning multiple lines. Inline
layout overrides that rule. Rectangular List/Tuple rows can be aligned inside
any of those four collection types. Empty collections retain their empty forms.
All layout choices preserve DCI identity and the same strict grammar; the
Python API specifies their precise presentation behavior.
Renderers MUST NOT normalize numerical structures mathematically, erase Float signed zero, reorder ordered collections, or change any other constituent that participates in DCI identity.
A renderer of an unordered collection MAY choose any entry/member order accepted by the strict parser. The canonical ordering of the binary encoding does not impose a textual ordering requirement.
A conforming parser MUST reject malformed syntax, unsupported identifiers, invalid numeric values, invalid escape sequences, duplicate keys or fields, invalid Array shapes or types, invalid external paths or digests, and input remaining after a complete DCI has been parsed.
An implementation MAY use its own error types and diagnostics. No specific error-message text is required by this specification.
Implementations MAY impose resource limits on text size, nesting depth, Array allocation, or integer magnitude. Such limits are implementation constraints and do not change DCI identity.
Relationship to other DataCanon specifications
- DataCanon Core V1 defines the DCTs, equality, canonical encoding, and fingerprinting.
- Shorthand Preprocessing V1 (separate document) will define compact List notation, general substitutions, and restricted scalar
eval(...) transformations before strict parsing. - YAML Interchange V1 (separate document) may embed strict Shorthand using the
$ convention. - HDF5 V1 and Persistence V1 (separate documents) will define storage and external-resource conventions.
Neither the preprocessor nor a persistence mechanism alters the strict Shorthand grammar defined here.
The conformance examples identify the DCT or property that a conforming strict parser MUST recover. They are not prescribed renderer output.
| Input | Expected result |
|---|
null | Null |
false | Boolean false |
-42 | Integer -42 |
-0.0 | Float negative zero |
bytes("") | Empty Bytes |
bytes("00FF") | Bytes containing 00 ff |
"a\\nb" | String with a line feed |
2/4 | Fraction with numerator 2, denominator 4 |
sum(1, 1, 2) | Sum retaining duplicate terms |
1/2 * sqrt(3) | Product containing Fraction and Power |
sqrt(3) | Power with numerator 1 and denominator 2 |
power(5, 2) | Power with denominator 1 |
quotient(1, sum(2, 3)) | Quotient with a Sum denominator |
complex(1, 2.0) | Complex(Integer, Float) |
[] | Empty List |
() | Empty Tuple |
(1) | One-element Tuple |
{} | Empty Mapping |
set() | Empty Set |
{1, 2} | Two-member Set |
multiset() | Empty Multiset |
multiset(1, 1, 2) | Multiset with three occurrences |
multiset(2, 1, 1) | Equal to multiset(1, 1, 2) |
multiset(1,) | Multiset with one occurrence |
{"a": 1, "b": 2} | Ordered Mapping |
umapping("b": 2, "a": 1) | UnorderedMapping |
record("Point", "x": 1) | Record |
array([1.0, 2.0]) | Array with inferred portable type and shape |
The following are invalid strict Shorthand:
1 2
'hello'
+1
1/0
1/-2
-sqrt(2)
power(5, 0)
power(5, 1, 0)
5**2**3
1/2**3
1/2**(1/3)
quotient(1)
sum(true)
{1, 1}
{"a": 1, "a": 2}
array([1.0 + 2.0j])
Appendix B — Outstanding Version 1 finalization items
The finalization items are deliberately recorded rather than silently resolved by assumptions about other languages:
- Pin the NumPy reference version governing Array inference, dtype conversion, and shape construction. This is the principal remaining cross-language portability dependency.
- Decide whether NumPy dtype aliases beyond the 14 portable Array type names are normative input or only permissive Python extensions.
- Confirm the exact treatment of optional noncanonical hexadecimal digest spellings, if interoperability with hand-authored external references warrants it.
- Publish cross-implementation vectors pairing Shorthand input with canonical Core V1 bytes, especially for Arrays, compositional numerical types, and external references.
None of these items changes the 24 core DCT definitions or the Core V1 canonical byte encoding.
6.3 - Shorthand Preprocessing
DataCanon Shorthand Preprocessing — Version 1 Draft
Status: Draft for independent implementation review
Depends on: DataCanon Shorthand Version 1; DataCanon Core Version 1
Scope: Optional textual transformation of augmented Shorthand into strict Shorthand
Out of scope: DCI construction, canonical encoding, persistence, application-specific mathematical aliases
Introduction
DataCanon Shorthand preprocessing is an optional text-to-text transformation intended for human-authored Shorthand. It supports three conveniences:
- compact List notation;
- named textual substitutions; and
- explicitly requested scalar arithmetic with
eval(...).
The preprocessor does not construct, inspect, or validate DataCanon Instances (DCIs). Its output is intended to be parsed by a strict Shorthand parser, which remains responsible for all syntax and DCT validity checks. Preprocessing does not change DataCanon Core identity or canonical encoding.
An implementation MAY offer a combined parsing interface. A strict Shorthand
parser MUST NOT implicitly enable preprocessing. A human-facing command-line
interface MAY preprocess Shorthand input by default, provided it documents that
behavior and offers an explicit mode requiring strict Shorthand without
preprocessing. Such a frontend does not change the strict grammar or the
requirements on persisted strict Shorthand representations.
MUST, MUST NOT, SHOULD, and MAY indicate normative requirements and recommendations.
A conforming preprocessor accepts Unicode text and optionally a mapping from substitution names to replacement text. For inputs within its supported language and resource limits, it MUST apply the transformations in this order:
- Expand compact Lists, throughout the document, including substitution definitions.
- Resolve substitutions, processing inline definitions in source order using external names and earlier inline definitions. Evaluate explicit
eval(...) expressions in each resolved definition before making it available to later definitions, then substitute names in the document body. - Evaluate explicit
eval(...) expressions remaining in the body, replacing each with an ordinary Shorthand numeric literal.
The result is text, not a DCI. An output that fails strict Shorthand parsing is not necessarily a preprocessing error: textual substitution values are not individually required to be valid Shorthand.
The preprocessor MUST NOT silently perform arbitrary mathematical multiplication, interpret application-defined symbols such as I or 2I, or evaluate expressions that are not explicitly enclosed in eval(...).
Lexical conventions
Strings and protected regions
A double-quoted string is protected during all three transformations. Compact List delimiters, substitution names, |, and eval(...) appearing inside a quoted string are treated as ordinary text.
The preprocessor recognizes a backslash as escaping the immediately following character for the purpose of locating the end of a quoted string. Validation of permitted String escapes belongs to the strict Shorthand parser.
For example:
"<1 2>"
"eval(1+2)"
"a | b"
are left unchanged by preprocessing.
Bytes literals
The hexadecimal String argument of bytes("...") is protected like every
other quoted String. Preprocessing MUST NOT substitute names, expand Compact
Lists, or evaluate text inside that String. Bytes parsing and hexadecimal
validation belong to the strict Shorthand parser.
Substitution identifiers
A substitution name consists of an ASCII letter or underscore, followed by zero or more ASCII letters, digits, or underscores:
Names are case-sensitive. The preprocessor does not attach DataCanon meaning to them.
Delimiters and nesting
The preprocessor recognizes quoted strings and balanced (), [], and {} when locating top-level delimiters and splitting compact List contents. It recognizes matching parentheses when locating the end of an eval(...) expression.
No comments are defined by this preprocessing specification.
Compact List notation
A compact List is enclosed in angle brackets:
It expands to ordinary Shorthand List syntax using square brackets. A single item remains a one-element List:
<6> -> [6]
<1 2 3> -> [1, 2, 3]
<1 2; 3 4> -> [[1, 2], [3, 4]]
<1 2, 3 4> -> [[1, 2], [3, 4]]
Compact Lists may occur anywhere outside quoted strings, including inside Array syntax, Mapping values, and inline substitution definitions. Nested angle-bracket groups are expanded from the inside outward.
An empty compact List (<> or a whitespace-only group) is invalid. Use ordinary [] for an empty List.
Splitting rules
Within each compact List group, delimiters are considered at the top level of that group: separators inside quoted strings or balanced (), [], and {} do not split an item.
The reference transformation applies the following separator precedence, from highest to lowest:
- line feed (
\n); - semicolon (
;); - comma (
,); - horizontal whitespace (space, tab, carriage return).
At the first level having more than one nonempty part, each part is recursively transformed and the results are enclosed in [...]. If no level splits the contents, the contents form a single item. The outermost compact group always yields a List.
This is a textual convenience, not Array inference. In particular, <1 2; 3 4> expands to a List of Lists. array(<1 2; 3 4>) may subsequently be parsed as an Array by strict Shorthand.
Examples:
<[1,2] 3> -> [[1,2], 3]
<eval(1 + 2) 0> -> [eval(1 + 2), 0]
<"a > b" 3> -> ["a > b", 3]
The eval(...) in the second example is evaluated only in the third preprocessing stage.
Compatibility note: the current reference implementation discards empty parts when splitting compact Lists. The treatment of repeated/trailing separators should be fixed explicitly before Version 1 is finalized; independent implementations SHOULD not rely on such inputs in the meantime.
Textual substitutions
External substitutions
An application MAY supply a mapping of substitution names to replacement text. Every name MUST satisfy the identifier rule above, and every replacement MUST be text.
For example, with external substitutions S = '"Sr"' and P = '[0.5, 0.5, 0.5]':
{"species": S, "position": P}
becomes:
{"species": "Sr", "position": [0.5, 0.5, 0.5]}
Substitution replaces complete identifier-like tokens outside quoted strings; it MUST NOT replace a substring of a longer ASCII identifier. For example, a substitution for a does not change alpha or a2.
Replacement text is inserted literally. It is not parsed or rendered as a DCI, and its whitespace is not normalized.
Inline substitutions
A document MAY begin with inline definitions separated from the document body by a single top-level vertical bar (|):
becomes:
A top-level | is one that is outside quoted strings and outside balanced (), [], and {}. A vertical bar within a quoted string is not a delimiter.
The definition region consists of one or more whitespace-delimited assignments of the form name = replacement-text. Each subsequent top-level name = begins another definition. Definitions may span lines and may contain bracketed structures or quoted strings. Replacement text extends up to the next definition or the | delimiter, with surrounding whitespace removed.
Definition names MUST be unique. An inline name MUST NOT conflict with an externally supplied name. Empty replacement values, text before the first definition, and more than one top-level | are invalid.
If no top-level | is present, the entire text is the document body; only external substitutions are applied.
Expansion order and dependencies
External substitutions are supplied as literal replacement text; the external
mapping is not itself processed as a sequence of dependent definitions.
Inline definitions MUST be processed in source order. For each definition:
- Replace external names and earlier inline names in a single textual pass.
- Evaluate explicit
eval(...) expressions in the resulting replacement text. - Store that resolved text for use by later definitions and the document body.
An unquoted reference to the current or a later inline name MUST be rejected,
even in an unused definition. This includes such a reference exposed by an
external replacement. Quoted occurrences are ordinary String contents, not
dependencies. Names not defined anywhere in the document are left as text; the
strict parser remains responsible for rejecting unknown constructs.
For example:
a = 4.593
c_over_a = 0.644
c = a * c_over_a |
[c]
produces:
No multiplication is performed. To obtain a machine numerical value instead,
write c = eval(a * c_over_a). The stored replacement for c is then
2.957892. A later definition such as d = eval(c * 2) uses that numerical
result, rather than creating a nested eval(...) call. Explicit evaluation in
an unused inline definition is also performed and MUST reject invalid arithmetic.
After all inline definitions have been resolved, external names are replaced
once in the body, followed by resolved inline names once. Explicit eval(...)
expressions remaining in the body are evaluated in the final stage. Replacement
text inserted during one pass is not recursively scanned during that pass.
There is no dependency graph resolution or repeated expansion to convergence.
Compact Lists are expanded before definitions are resolved. Consequently,
a=1/2 * sqrt(3) row=<a 0> | row produces [1/2 * sqrt(3), 0] without splitting
the spaces inside the replacement into additional List items.
Expression boundaries
Substitution is literal text replacement. It does not add parentheses or
constructor calls, and it does not guarantee that an inserted infix fragment
will form one constituent in the surrounding expression. For example:
a=1 + 2 b=3 * a | b
-> 3 * 1 + 2
To preserve a Sum as one Product constituent, use explicit constructor syntax:
a=sum(1, 2) b=product(3, a) | b
-> product(3, sum(1, 2))
Within eval(...), ordinary arithmetic grouping parentheses may be included in
the replacement text. Outside eval(...), strict Shorthand parentheses denote
Tuples, not grouping. Applications SHOULD use substitutions at syntactic value
boundaries rather than relying on token concatenation.
Multiset constructor syntax belongs to strict Shorthand and requires no
preprocessing. Compact angle-bracket notation still expands to Lists, not
Multisets.
Substitutions may be used within a Multiset constructor without removing
repeated occurrences:
a=3 | multiset(a, a, 2)
-> multiset(3, 3, 2)
The preprocessor does not inspect Multiset membership or equality. The strict
parser constructs and validates the resulting Multiset DCI.
Scalar eval(...)
Purpose and placement
After resolving the names in an inline definition, and again after substitution in the document body, the preprocessor locates unquoted eval(...) expressions, allowing whitespace between eval and (. It finds the matching closing parenthesis, including nested parentheses, and replaces the complete expression with a numeric literal.
The evaluator is not a general Python evaluator. It supports only real scalar arithmetic. Arrays, DCIs, symbols, arbitrary functions, attribute access, indexing, comprehensions, Boolean expressions, comparisons, and arbitrary code execution are excluded.
Within eval(...), the following Python-like forms are supported:
- integer and floating-point numeric literals;
- parentheses;
- unary
+ and -; - binary
+, -, *, /, and **; sqrt(expression) with exactly one positional argument.
Operator precedence and associativity follow Python expression semantics. In particular, exponentiation binds more tightly than unary minus on its left (-2**2 is -4), and / performs true division (1/2 is 0.5). The caret ^ is not exponentiation and is unsupported.
The sqrt operation has the semantics of Python’s real-valued math.sqrt, including rejection of negative arguments. No names other than the permitted sqrt call are evaluated.
Results
Intermediate arithmetic uses Python-compatible arbitrary-precision integers and IEEE-754 binary64 floating-point behavior. Every intermediate and final value MUST be a real int or float (not Boolean, complex, or another type). Nonfinite floating-point values are invalid.
If the final value is a finite Float with exactly integral value, it is converted to an Integer before rendering. This conversion uses no numerical tolerance. Consequently:
eval(1/2) -> 0.5
eval(1/2 + 1/2) -> 1
eval(2**10) -> 1024
eval(sqrt(9) + 1) -> 4
eval(-2**2) -> -4
eval(5+(1.89-2.53)*6/3.72) -> 3.967741935483871
The replacement MUST be a valid strict Shorthand Integer or Float literal representing the computed result. A Python-compatible shortest round-tripping decimal representation is RECOMMENDED for finite binary64 results; an implementation MUST preserve the intended binary64 value when subsequently parsed as strict Shorthand.
The evaluator MUST NOT use unrestricted host-language eval on untrusted input. Implementations MAY use an expression parser or a restricted abstract syntax tree evaluator.
Resource limits
Implementations MAY reject expressions that exceed reasonable limits on length, nesting depth, intermediate integer size, or exponentiation magnitude. The limits are resource-protection policies, not extensions to the permitted arithmetic language.
The current Python reference implementation uses a maximum expression length of 1,000 characters, maximum AST recursion depth of 64, and a 4,096-bit intermediate integer limit, with additional exponentiation checks. The thresholds are reference-implementation limits, not required values for other languages.
Portability note: fully identical last-bit results for every floating-point arithmetic expression and sqrt across languages/platforms are not yet guaranteed by this draft. A Version 1 conformance profile should fix the reference numerical semantics and test vectors before claiming universal byte-identical evaluated results.
Error handling and integration with strict Shorthand
A conforming preprocessor MUST reject malformed compact List delimiters, invalid or duplicate substitution definitions, conflicting external/inline names, self-references or forward references between inline definitions, unterminated quoted strings encountered during scanning, malformed eval(...) syntax, and unsupported or invalid evaluated operations.
A preprocessor MAY use implementation-specific exception classes and messages. It does not need to detect ordinary strict Shorthand errors such as unknown DCT identifiers or invalid Fraction denominators. Those errors belong to the strict parser.
For a conforming combined interface, preprocessing followed by strict parsing MUST be equivalent to calling the two operations separately:
parse_shorthand(text, preprocess=True, substitutions=S)
= parse_shorthand(preprocess_shorthand(text, substitutions=S))
The notation above describes behavior, not mandatory function names in other languages.
Strict parsing without preprocessing MUST remain available and MUST reject preprocessor-only syntax that is not valid strict Shorthand.
Relationship to other DataCanon specifications
- DataCanon Core Version 1 defines DCIs, equality, canonical encoding, and fingerprinting.
- DataCanon Shorthand Version 1 defines the strict textual grammar and identity-preserving rendering.
- This specification defines optional text transformations performed before strict Shorthand parsing.
- YAML interchange, HDF5 persistence, and other representations are not required to perform preprocessing automatically.
The Python reference implementation provides preprocess_shorthand(...) and
the explicit parse_shorthand(..., preprocess=True) option. The Python parser
and ordinary .dcs persistence reader remain strict by default. The dc
command-line frontend preprocesses .dcs and standard-input Shorthand by
default; --strict disables preprocessing. Queries are not preprocessed.
Other implementations MAY expose different interfaces while preserving the
same transformations and access to strict parsing.
The output strings below illustrate the intended transformations. Where leading/trailing whitespace is not material to strict Shorthand parsing, it is omitted from the examples.
| Input | Preprocessed output |
|---|
<1 2 3> | [1, 2, 3] |
<6> | [6] |
<1 2; 3 4> | [[1, 2], [3, 4]] |
<1 2, 3 4> | [[1, 2], [3, 4]] |
<[1,2] 3> | [[1,2], 3] |
<eval(1 + 2) 0> | [3, 0] |
"<1 2>" | "<1 2>" |
a=1/2 * sqrt(3) | <a 0> | [1/2 * sqrt(3), 0] |
a=1/2 | eval(a + 1) | 1.5 |
a=2 b=a * 3 | b | 2 * 3 |
a=eval(2 + 3) b=eval(a * 2) | b | 10 |
eval((2 + 3)*4) | 20 |
eval(sqrt(9) + 1) | 4 |
With external substitutions S='"Sr"' and P='[0.5, 0.5, 0.5]':
{"species": S, "position": P}
-> {"species": "Sr", "position": [0.5, 0.5, 0.5]}
Invalid examples include:
<> # empty compact List
<1 2 # unterminated compact List
a=1 | b=2 | [a] # multiple top-level delimiters
a=1 a=2 | [a] # duplicate inline name
a=a | a # self-reference
a=b b=2 | a # forward reference
eval(1/0) # division by zero
eval(2^3) # unsupported operator
eval([1,2]) # non-scalar expression
eval(sqrt(-1)) # invalid real square root
eval(__import__("os")) # arbitrary code is not allowed
Appendix B — Outstanding Version 1 finalization items
The following details should be resolved deliberately rather than inferred from implementation accidents:
- Compact List empty-part behavior: whether repeated or trailing separators are rejected, ignored, or permitted as empty items.
- Inline definition boundaries: precise treatment of unusual top-level
name = sequences within replacement text. - Numerical conformance: a designated reference Python version/semantics for
eval(...), including float formatting and sqrt edge cases, plus cross-language numerical test vectors. - Identifier scanning consistency: align the preprocessor’s
eval token recognition with the ASCII identifier convention adopted for strict Shorthand and substitution names.
The finalization items do not affect DataCanon Core identity or canonical bytes. They affect only the optional preprocessing language and can be settled independently.
6.4 - YAML Interchange
DataCanon YAML Interchange — Version 1 Draft
Status: Draft for independent implementation review
Depends on: DataCanon Core Version 1; DataCanon Shorthand Version 1
Scope: Interchange between YAML documents and DataCanon Instances (DCIs)
Out of scope: Canonical binary encoding rules, HDF5 persistence, Shorthand preprocessing, and application-specific YAML tags
Introduction
DataCanon YAML Interchange permits ordinary YAML values to represent DataCanon Instances (DCIs), while preserving DataCanon-specific structures through an embedded Shorthand convention called the $ airlock.
The interchange is not a new DataCanon Type system and does not define DCI identity. All DCI validity and equality requirements come from DataCanon Core Version 1. Embedded values use strict DataCanon Shorthand Version 1, without preprocessing.
A conforming writer MUST produce YAML from which a conforming reader reconstructs an equal DCI. YAML documents need not be byte-for-byte identical across implementations. Indentation, quoting, flow versus block style, and other identity-preserving YAML presentation choices are not standardized.
A conforming reader is required to support the portable YAML subset specified here; it is not required to accept every construct permitted by the broader YAML language.
The terms MUST, MUST NOT, SHOULD, and MAY indicate normative requirements and recommendations.
Let Y(x) denote any conforming YAML rendering of DCI x, and let P_Y denote YAML interchange parsing. The primary conformance requirement is:
where equality is DataCanon equality. A conforming implementation MUST preserve, among other things:
- distinctions between DCTs, such as Integer and Float, or List and Array;
- unreduced Fraction and unevaluated numerical structure;
- the sign of floating-point zero;
- order in ordered collections;
- member and key identity in unordered collections; and
- the paths, types, shapes, and digests of external DCIs.
The interchange reader MUST NOT implicitly perform Shorthand substitutions, compact List expansion, or eval(...) processing on $ content.
Portable YAML data model
YAML-native values
The following YAML-native values are mapped to DCIs:
| YAML value | DataCanon result |
|---|
| Null | Null DCI |
| Boolean | Boolean DCI |
| Integer | Integer DCI |
| Floating-point number | Float DCI |
| String | String DCI, subject to the $ convention |
| Sequence | List DCI |
| Mapping | Mapping DCI |
YAML sequences MUST be interpreted as Lists, never inferred to be Arrays, regardless of their dimensions or numerical contents.
YAML mappings MUST preserve the order in which their entries are presented when converted to Mapping DCIs. YAML mapping keys and values are converted recursively using the same rules as other YAML values, subject to the portable-subset limitations below.
The interchange defines no native YAML representation for Bytes, Fraction, Sum, Product, Power, Quotient, Complex, Tuple, Set, Multiset, UnorderedMapping, Record, Array, ExternalFile, or ExternalArray. Such DCIs are represented using the $ airlock.
YAML scalar resolution
For the portable subset, implementations MUST agree on whether a scalar is Null, Boolean, Integer, Float, or String before applying the $ convention. The Python reference implementation uses PyYAML’s SafeLoader scalar resolution and SafeDumper emission behavior.
In particular, the following scalar forms have the indicated interpretations:
| YAML source | Resulting DCI |
|---|
null | Null |
true | Boolean true |
false | Boolean false |
42 | Integer 42 |
-3 | Integer -3 |
1.25 | Float 1.25 |
-0.0 | Float negative zero |
"42" | String 42 |
"true" | String true |
"null" | String null |
The reference loader also recognizes some additional YAML scalar spellings, including yes, no, on, and off as Booleans. Such spellings are not recommended for portable hand-authored documents. A conforming writer SHOULD use the unambiguous forms in the table and MUST quote strings that would otherwise be resolved as a different YAML scalar type.
The exact PyYAML reference version and the complete boundary of portable input scalar spellings remain Version 1 finalization items. Independent implementations SHOULD use the reference behavior and the conformance vectors rather than silently applying a different YAML schema. In particular, YAML 1.1-style and YAML 1.2-style implicit resolution are not always interchangeable.
Any resolved numeric value MUST satisfy the invariants of its corresponding DataCanon DCT. In particular, nonfinite floating-point values are invalid DataCanon Floats.
YAML features outside the portable subset
The portable subset does not standardize arbitrary YAML tags, application-defined object construction, timestamps, binary tags, merge keys, recursive aliases, or duplicate YAML mapping keys. Implementations MAY reject such input. An implementation’s acceptance of these constructs does not expand the required portable subset.
The Python reference implementation may accept additional YAML forms through yaml.safe_load; this does not make those forms normative DataCanon interchange syntax. Implementations SHOULD reject or clearly document any input whose conversion would silently lose DCI-relevant information.
The $ airlock
Purpose
The embedded DataCanon marker is $. After optional leading whitespace, the marker MUST be followed by at least one whitespace character before the strict Shorthand value. Whitespace is recognized as Unicode whitespace, consistently with removal of leading whitespace. A decoded YAML String beginning with this marker and boundary represents one embedded strict DataCanon Shorthand DCI, unless escaped as defined below. A marker at the end of the String is also reserved, but is invalid as embedded DC because the required DCI is missing.
The marker performs neither substitution nor evaluation. $100, ${HOME}, $HOME, $1/2, and $$ 1/2 are ordinary Strings because the initial $ has no whitespace boundary. A dollar sign elsewhere in a String has no special meaning. Only $ is recognized as the embedded DataCanon marker.
Examples:
fraction: $ 2/4
product: $ 1/2 * sqrt(3)
array: $ array([1.0, 2.0])
The resulting values are respectively a Fraction DCI, a Product DCI, and an Array DCI. They are not YAML Strings in the resulting DataCanon Mapping.
Quoting alone does not escape the marker. The prefix is recognized in the decoded YAML string value, not by searching raw YAML source characters. Consequently, YAML quoting and block-scalar formatting do not alter the airlock semantics.
Embedded Shorthand parsing
For an unescaped airlock String:
- Remove leading whitespace before
$. - Remove the
$ prefix, whose whitespace boundary MUST have been recognized. - Remove leading whitespace from the remainder.
- Parse the remainder as one complete strict Shorthand DCI.
The embedded text MUST satisfy the strict Shorthand specification, including complete consumption. An invalid embedded DCI makes the interchange input invalid.
For example, the following YAML scalar can be emitted as a literal block:
Its decoded YAML String begins with $ and contains a Shorthand Mapping. The |- marker is YAML presentation syntax, not part of the embedded Shorthand.
Escaping literal Strings
The prefix $ is reserved at the whitespace boundary defined above, or at the end of the String, including after leading whitespace. To preserve an ordinary DataCanon String that would otherwise be interpreted as embedded Shorthand, a writer MUST add one backslash immediately before the prefix, after any existing backslashes.
More precisely, for a decoded YAML String, divide its beginning into:
- zero or more leading whitespace characters;
- zero or more consecutive backslashes; and
- the remaining text.
If the remaining text begins with $ followed by whitespace, or consists solely of $:
- zero preceding backslashes means the remainder after
$ is embedded Shorthand; - one or more preceding backslashes means an ordinary String: remove exactly one of those backslashes and preserve all other characters.
If that boundary condition is not satisfied, the String is unchanged, including all preceding backslashes. In particular, \$100 and \${HOME} are preserved literally.
Thus, at the level of decoded YAML String values (not YAML source quoting):
| YAML String value | DataCanon result |
|---|
$ 1/2 | Fraction DCI 1/2 |
\$ 1/2 | String $ 1/2 |
\\$ 1/2 | String \$ 1/2 |
\$ 1/2 | String $ 1/2 |
$100 | String $100 |
${HOME} | String ${HOME} |
\$100 | String \$100 |
ordinary text | String ordinary text |
The writer MUST apply this escaping convention to String DCIs in all positions, including mapping keys. YAML itself may additionally quote or escape backslashes; those YAML-level escapes are decoded before applying the DataCanon rule.
Airlock rendering
To represent a DCI through the airlock, a writer MUST:
- Render the DCI as valid strict Shorthand that preserves its identity.
- Prefix the resulting text with
$ and whitespace separating the prefix from the Shorthand value. - Emit the result as a YAML String scalar.
A writer MAY use inline YAML scalar presentation or YAML literal-block presentation. A writer SHOULD use a block scalar when the embedded Shorthand contains line breaks.
The Python reference implementation uses $ for single-line Shorthand and $\n for multiline Shorthand. Its embedded Shorthand uses explicit standard layout. Callers may select alignment and same-line delimiter padding without changing DCI identity. Other conforming renderers MAY choose any identity-preserving Shorthand layout.
The reference writer emits nonempty native YAML Lists and Mappings in block style and does not wrap single-line scalars to a fixed width. Empty collections retain [] and {}. When alignment is requested, rectangular groups of nonempty, flat Tuple rows within a native YAML List share column widths; ragged rows and mixed List/Tuple rows retain ordinary rendering. Native YAML Lists remain native Lists. Same-line padding applies inside embedded Shorthand, including keys, and does not control YAML indentation. YAML syntax and scalar-resolution rules may still require quoting. Layout, alignment, padding, and quoting are presentation choices, not identity constituents.
An airlock String MUST NOT be interpreted using optional Shorthand preprocessing.
DataCanon-to-YAML rendering
Native primitive DCIs
Null, Boolean, Integer, Float, and String DCIs SHOULD be rendered using their corresponding native YAML scalar forms, subject to identity preservation.
A writer MUST distinguish native Integer and Float values, including integral-valued Floats such as 1.0. It MUST preserve floating-point signed zero. It MUST quote or otherwise mark Strings so that YAML scalar resolution does not change their DCT.
A String DCI additionally undergoes the $ prefix-escaping rule above.
List DCIs
A List DCI MUST be rendered as a YAML sequence whose members are recursively rendered in their semantic order. An empty List is represented as an empty YAML sequence.
A Tuple DCI MUST NOT be rendered as an ordinary YAML sequence, because that would change its DCT. It uses the airlock instead.
Mapping DCIs
A Mapping DCI MAY be rendered as a native YAML mapping only if all of its keys and values can be recursively converted and the converted keys can be represented without loss or collision.
For compatibility with the Python reference implementation, the native-mapping decision is made by constructing the recursively converted key/value pairs, then checking that every converted key is hashable in the reference mapping model and that no two converted keys compare equal as mapping keys.
If any converted key is unhashable, or any two keys collide under the reference mapping’s equality rules, the writer MUST render the entire Mapping DCI through the $ airlock instead of silently dropping or merging entries.
In particular, the following distinct DataCanon keys collide as Python dictionary keys:
- Boolean
true and Integer 1; - Integer
1 and Float 1.0; - Float
0.0 and Float -0.0.
Such a Mapping MUST use the airlock, even though it is a perfectly valid DataCanon Mapping.
A writer MUST preserve the semantic entry order of an ordered Mapping, whether using native YAML or the airlock. An UnorderedMapping DCI always uses the airlock; it MUST NOT silently become an ordered Mapping DCI.
Bytes DCIs
Bytes DCIs MUST use the $ airlock, for example:
payload: $ bytes("00ff80")
The complete byte sequence is embedded in the document. A native YAML String
is not inferred to be Bytes. YAML binary tags remain outside the portable
subset; the explicit Shorthand construct provides the portable representation.
Other DCTs
Every DCI not explicitly given a faithful native YAML representation above MUST be rendered through the $ airlock. This includes Bytes, Fraction, Sum, Product, Power, Quotient, Complex, Tuple, Set, Multiset, UnorderedMapping, Record, Array, ExternalFile, and ExternalArray.
The airlock preserves the DCI, not necessarily any referenced external payload. An ExternalFile or ExternalArray airlock value contains its Shorthand path and digest metadata; resource retrieval and verification are governed separately by the DataCanon persistence specification.
YAML-to-DataCanon parsing
A conforming interchange reader MUST:
- Parse the YAML document into the portable YAML value model, preserving scalar types and mapping order.
- Convert native Null, Boolean, Integer, and Float values to their corresponding DCIs.
- Process each YAML String using the
$ airlock and escaping rules. - Convert YAML sequences recursively to List DCIs.
- Convert YAML mappings recursively to ordered Mapping DCIs.
- Validate the resulting DCIs under DataCanon Core Version 1.
The reader MUST NOT infer Arrays from nested YAML sequences. It MUST NOT simplify embedded numerical structures. It MUST NOT apply the conversion rules for conventional programming-language types to coerce distinct DCTs into one another.
A reader MAY report a malformed document as a YAML parsing error, an invalid embedded Shorthand error, or a DataCanon invariant error. Exact error-message wording is not normative.
Identity and interoperability
The YAML interchange is not a canonical textual encoding. Different YAML byte sequences can represent equal DCIs. Conforming implementations MUST agree on the resulting DCI for documents within the portable subset.
In particular, a conforming implementation MUST preserve the DCI across a YAML write–read round trip, including any $ embedded values:
No requirement is imposed on matching YAML whitespace, key quoting, block versus flow layout, or exact embedded Shorthand formatting.
A YAML document does not contain canonical DataCanon bytes merely by virtue of representing a DCI. Fingerprints are always computed from the DCI’s canonical binary encoding, not from YAML source text.
Security and resource handling
YAML readers SHOULD use safe data-only parsing and MUST NOT execute arbitrary application objects or code merely because a YAML tag or scalar requests it.
Embedded $ content MUST be processed by strict Shorthand parsing; it MUST NOT invoke the Shorthand preprocessor or arbitrary evaluation.
Implementations MAY enforce resource limits on YAML document size, nesting depth, aliases, and embedded Shorthand size. Such limits are implementation policy and do not change DCI equality.
Reading a YAML document containing ExternalFile or ExternalArray DCIs MUST NOT require the referenced resources to exist or be loaded. Resource resolution, integrity checking, and copying are separate operations.
The following examples illustrate the portable interchange semantics. The exact YAML presentation produced by a writer may differ.
Native values and nested Lists
name: calculation
energy: -12.5
positions:
- [0.0, 0.0, 0.0]
- [0.5, 0.5, 0.5]
The result is a Mapping DCI containing String, Float, and nested List DCIs. The positions are not an Array DCI.
Embedded mathematical structures
fraction: $ 2/4
product: $ 1/2 * sqrt(3)
complex: $ complex(1/2, 1/2 * sqrt(3))
The result contains Fraction, Product, and Complex DCIs, respectively. The Fraction remains unreduced.
Literal reserved prefix
After YAML string decoding and DataCanon prefix unescaping, message is a String DCI whose value is $ 2/4, not a Fraction DCI.
Mapping-key collision
A Mapping DCI containing both Boolean true and Integer 1 as keys cannot be safely represented as a native YAML mapping under the reference key semantics. A writer may emit the entire Mapping as a block-scalar airlock value:
|-
$
{
true: "boolean",
1: "integer"
}
The reader recovers the two distinct keys and their values.
UnorderedMapping
value: $ umapping("a": 1, "b": 2)
The value is an UnorderedMapping DCI, not a native YAML Mapping DCI.
Multiset
A Multiset uses the $ airlock rather than a native YAML sequence:
samples: $ multiset(1, 1, 2)
The value contains three member occurrences, including two equal Integer
members. A writer and reader MUST preserve multiplicity and nested structure.
Reordering members does not change Multiset identity.
An ordinary YAML sequence such as [1, 1, 2] remains a List DCI, not a
Multiset. Multisets may also be represented through the airlock in mapping-key
positions, subject to the same native-key collision rules as other DCIs.
External resource
payload: $ external_file("inputs/config.txt", "0000000000000000000000000000000000000000000000000000000000000000")
The value is an ExternalFile DCI with a relative path and 32-byte digest. No file is accessed while parsing this value.
Invalid and nonportable cases
The following MUST be rejected as valid portable interchange, or are explicitly outside its guaranteed portable subset:
- A
$ scalar containing malformed or incomplete strict Shorthand: invalid. - An embedded DCI violating a core invariant, such as a zero Fraction denominator: invalid.
- A native YAML floating-point value that is NaN or infinity: invalid as a DataCanon Float.
- A native YAML value that cannot be mapped to a DataCanon DCT under the portable subset: outside the portable subset.
- Arbitrary executable YAML tags or application-object constructors: outside the portable subset and MUST NOT be executed by a conforming safe reader.
- Duplicate YAML mapping keys or input relying on merge-key resolution: outside the portable subset; behavior is not guaranteed by this draft.
Version 1 finalization items
The following are intentionally left for a final compatibility pass rather than expanded into a separate YAML standard:
- Reference YAML scalar resolver: Pin the PyYAML reference version and establish portable scalar-resolution vectors, particularly for legacy Boolean spellings, exponent notation, and timestamp-like Strings.
- Mapping-key collisions: Verify cross-language behavior against a compact set of distinct DCI keys that collide in Python’s conventional dictionary model.
- YAML parser subset: Decide whether duplicate YAML mapping keys should be explicitly rejected by all conforming readers rather than left outside the portable subset.
- Additional interchange vectors: Test YAML produced by Python against an independent implementation, checking resulting DCI canonical bytes rather than YAML text.
The finalization items do not alter the DataCanon Core DCTs or canonical binary encoding.
6.5 - HDF5
DataCanon HDF5 — Version 1 Draft
Status: Draft for independent implementation review
Depends on: DataCanon Core Version 1
Scope: Identity-preserving HDF5 representation of DataCanon Instances (DCIs), including embedded external resources
Out of scope: Canonical byte encoding, Shorthand and YAML syntax, filesystem packaging conventions, and application-specific source-resolution APIs
Introduction
DataCanon HDF5 defines a portable representation of DataCanon Instances (DCIs) in HDF5 files. Its purpose is to permit independent implementations, including Python and Julia, to exchange structured scientific data while preserving DataCanon identity.
This specification defines the logical HDF5 objects, required names, attributes, data types, ordering conventions, and handling of externally referenced resources. It does not define new DataCanon Types or change their equality relations. Those are defined by DataCanon Core Version 1.
A conforming HDF5 writer MUST produce a file from which a conforming reader reconstructs an equal DCI. Files representing equal DCIs need not have identical HDF5 bytes. Compression, chunking, storage order, object addresses, and other physical details MAY differ without changing DCI identity.
This specification describes the interoperable layout used by the Python reference implementation (h5py and NumPy). It is written in terms of HDF5 objects and values, not Python classes.
The terms MUST, MUST NOT, SHOULD, and MAY indicate normative requirements and recommendations.
Let H(x) be a conforming HDF5 representation of DCI x, and P_H its interpretation as a DCI. The primary requirement is:
Equality here is DataCanon equality, including DCT distinctions, structural numerical constituents, signed floating-point zeros, Array types and shapes, ordering of ordered collections, and external-resource metadata.
A reader MUST NOT change a DCI merely to fit the conventions of its implementation language. A reader MAY reject malformed or unsupported HDF5 input; it MUST NOT silently substitute a different DCI.
File layout
A DataCanon HDF5 file contains a required top-level object named root and may contain a top-level group named _external.
/
├── root required; representation of one DCI
└── _external optional; embedded external byte resources
├── 0
│ ├── path
│ │ ├── 0
│ │ └── ...
│ └── data
└── ...
The HDF5 file root group MAY have a fingerprint attribute as described below.
The root object MUST represent exactly one DCI, which may contain arbitrarily nested DCIs. It may be an HDF5 group or dataset, depending on its DCT.
Except for the structural helper groups explicitly identified in this specification, each HDF5 object representing a DCI MUST have a scalar string attribute named kind, with the exact value specified for its DCT. Structural helper groups (for example, mapping-entry groups and path groups) do not represent DCIs and do not require kind.
Object names and DCT kind strings are case-sensitive.
General storage conventions
Ordered children
Where a group represents an ordered sequence, its children are named by consecutive zero-based decimal indices:
Indices MUST be written without leading zeros, except for 0. Semantic order is numeric index order, not HDF5 group iteration order or lexicographic name order. Empty sequences have no indexed children.
The same convention applies to List and Tuple items, Set and Multiset member occurrences, Mapping and UnorderedMapping entries, Record fields, path components, and Array shape dimensions stored as children.
Strings
A String DCI is represented as a one-dimensional HDF5 dataset of unsigned 8-bit integers containing the exact UTF-8 bytes of the string, with kind = "string".
No terminating NUL is added. Embedded NUL bytes are permitted as part of the UTF-8 encoding of a valid String DCI. An empty String is represented by an empty dataset of shape (0,).
A reader MUST decode the bytes as valid UTF-8 and apply the String invariants from Core Version 1. No Unicode normalization is performed.
This byte-array representation is intentional: it avoids restrictions of HDF5 variable-length string interfaces on embedded NUL characters.
Bytes
A Bytes DCI is represented as a rank-one HDF5 dataset of unsigned 8-bit
integers containing the exact byte sequence, with kind = "bytes".
An empty Bytes DCI has dataset shape (0,). No text decoding, terminating
NUL, digest, or framing metadata is applied to its contents.
A reader MUST reject Bytes objects that are groups, have a null dataspace,
have rank other than one, or use a datatype other than unsigned 8-bit integers.
Enumeration datatypes, including Boolean enums, are not valid Bytes datasets.
The kind distinguishes Bytes from String and uint8 Array even when their
stored element bytes happen to agree. Bytes payloads are stored directly in
the DCI hierarchy, not in the external-resource collection.
Arbitrary-precision integers
An Integer DCI is represented as a scalar HDF5 UTF-8 string dataset containing its value in base-ten notation, with kind = "integer".
A conforming writer MUST use ordinary decimal notation: optional leading - for negative values, followed by ASCII digits; no leading +, exponent, separators, or unnecessary leading zeros. Zero is written as 0. The value is not restricted to a fixed machine-integer width.
A reader MUST recover the mathematical integer exactly. This representation is deliberately different from an HDF5 native integer dataset.
Fixed-width numerical values
Float DCIs use scalar HDF5 IEEE-754 binary64 datasets. The exact finite binary64 value, including the sign of zero, MUST be preserved.
For native numerical datasets, HDF5 datatype endianness and storage layout MAY differ, provided the logical values and their required IEEE-754 bit identities are recovered exactly. Nonfinite values are not valid Version 1 DCIs.
Fixed-domain information fields
Prescribed integer and Unicode fields are not DCI objects and carry no required
kind attribute. Writers MUST omit kind on those fields. Integer information
uses scalar UTF-8 decimal datasets with the same payload rules as Integer;
string information uses rank-one uint8 UTF-8 datasets with the same payload
rules as String. Readers validate the payload directly rather than recursively
interpreting the field as a DCI. Additional nonsemantic attributes may be ignored.
This applies to Fraction numerator/denominator, Power exponent fields, Record
and field names, path components (including embedded-resource paths), external
Array type names, and external Array shape dimensions. The Power base, Record
field values, Complex components, Quotient operands, and collection members
remain full DCI representations with required kind attributes.
Required structure
The tables below define the layout produced by a conforming writer. A conforming reader MUST recognize these layouts and MUST reject input that cannot be interpreted as a valid DCI without loss of identity. Additional application-specific HDF5 metadata MAY be present, but MUST NOT change the interpretation of the specified DataCanon objects.
Primitive DCTs
| DCT | kind | HDF5 representation |
|---|
| Null | null | Group, no DCI constituents |
| Boolean | boolean | Scalar Boolean dataset (HDF5 enum FALSE=0, TRUE=1) |
| Integer | integer | Scalar UTF-8 decimal-string dataset |
| Float | float | Scalar IEEE-754 binary64 dataset |
| String | string | Rank-one uint8 dataset containing UTF-8 bytes |
| Bytes | bytes | Rank-one uint8 dataset containing arbitrary bytes |
| Fraction | fraction | Group with direct numerator and denominator integer information |
The Boolean dataset MUST represent exactly false or true; arbitrary nonzero integers are not a valid substitute for the Boolean DCT. Implementations using the HDF5 Boolean enumeration convention MUST recognize FALSE and TRUE.
Constant
A Constant is a group with kind = "constant" and exactly one child dataset,
name. The dataset contains direct String information, stored as rank-one
uint8 UTF-8 bytes using the String information rules; it is not a String DCI.
Its decoded value MUST be exactly pi or e. Unknown names, missing fields,
extra child objects, and incorrect dataset layouts MUST be rejected.
Fraction
A Fraction is a group with kind = "fraction" and two fixed integer fields:
fraction/
├── numerator Decimal integer information
└── denominator Decimal integer information
The denominator MUST be positive. Neither constituent is reduced or normalized.
Atomic DCTs
Power
A Power is a group with kind = "power", one base DCI, and two fixed integer fields:
power/
├── base Scalar Numerical DCI
├── numerator Positive decimal integer information
└── denominator Positive decimal integer information
All children MUST be present, including denominator one. A reader MUST verify
the base DCT and exponent positivity. Exponent constituents MUST NOT be
reduced and nested base structure MUST be preserved.
Quotient
A Quotient is a group with kind = "quotient":
quotient/
├── numerator Scalar Numerical DCI
└── denominator Scalar Numerical DCI
Both children MUST be present and have permitted numerical DCTs. Readers MUST
preserve their roles and structure without division or simplification. The
Core Quotient invariants determine validity; HDF5 introduces no additional
mathematical denominator check.
Complex
A Complex is a group with kind = "complex":
complex/
├── real Scalar Numerical DCI
└── imaginary Scalar Numerical DCI
The real and imaginary components are themselves complete DCI representations. A Complex DCI is distinct from an Array element of a complex Array type.
Composite collections
List, Tuple, and Set
List, Tuple, and Set DCIs use groups whose kind values are respectively list, tuple, and set. Each contains indexed child DCIs named 0, 1, and so forth.
collection/ kind = "list", "tuple", or "set"
├── 0 DCI
├── 1 DCI
└── ...
For List and Tuple, numeric child order is semantic and MUST be preserved. For Set, member order is not semantic; a conforming writer MAY choose any indexed member order, and a reader MUST construct a Set DCI with distinct members according to Core Version 1.
The HDF5 representation of a Set does not need to use canonical byte ordering. Canonical ordering belongs to the canonical binary encoding, not to HDF5 storage.
Sum and Product
Sum and Product are groups with kind = "sum" and kind = "product",
respectively. Each operand occurrence is a complete DCI child named by its
consecutive zero-based index.
An empty instance has no indexed children. Each duplicate occurrence MUST be
stored separately. Child order is not semantic and need not match canonical
byte order. Every Sum operand MUST be a Numerical DCI. Every Product operand MUST be a
Scalar Numerical DCI, including the recursive restrictions on Sum and
OrderedProduct. Array rank and shape are not Sum invariants.
Readers MUST preserve nested nodes and MUST NOT deduplicate or simplify them.
OrderedProduct
An OrderedProduct is a group with kind = "ordered_product". Each operand is
stored as a complete indexed DCI child named 0, 1, and so forth. The
consecutive index order is semantic and MUST be retained. Every operand MUST
be a Numerical DCI. Empty, repeated, scalar-only, and nested operands are
permitted. No sorting or evaluation is performed. No Array rank or shape
compatibility condition is imposed on this representation.
Multiset
A Multiset is a group with kind = "multiset". Each indexed child represents
one complete member DCI:
multiset/ kind = "multiset"
├── 0 DCI
├── 1 DCI
└── ...
Child names follow the consecutive numeric-index convention. An empty
Multiset is a group with no indexed children. Each repeated occurrence MUST
have its own indexed child; readers MUST NOT deduplicate equal members.
Numeric child order does not participate in Multiset identity. A conforming
writer MAY choose any member order, and canonical byte ordering is not
required for HDF5 storage. Nested Multisets MUST retain their DCT and structure.
External DCIs within Multiset members MUST be discovered for payload
collection and source-context attachment, including recursively nested members.
Repeated references to one relative path preserve all member occurrences while
sharing one embedded payload under the existing path-consistency rules.
Mapping and UnorderedMapping
Mapping and UnorderedMapping DCIs use groups with kind = "mapping" and kind = "unordered_mapping", respectively. Each indexed child is a structural entry group containing two complete DCIs:
mapping/ kind = "mapping" or "unordered_mapping"
├── 0/
│ ├── key DCI
│ └── value DCI
├── 1/
│ ├── key DCI
│ └── value DCI
└── ...
For Mapping, numeric entry order is semantic. For UnorderedMapping, entry order is not semantic. Keys MUST be distinct according to DataCanon equality, which need not coincide with the equality rules for keys of a conventional dictionary type in the implementation language.
Entry groups do not require a kind attribute.
Record
A Record is a group with kind = "record":
record/
├── name UTF-8 byte information
└── fields/
├── 0/
│ ├── name UTF-8 byte information
│ └── value DCI
├── 1/
│ ├── name UTF-8 byte information
│ └── value DCI
└── ...
The Record name is semantic. Fields are ordered numerically; their names MUST be unique according to String equality. The fields group and its indexed entry groups are structural helpers without required kind attributes.
Array
An Array DCI is represented as a native HDF5 numerical dataset with kind = "array". Its HDF5 datatype, dataspace shape, and element values together determine the DataCanon Array type, shape, and elements.
A conforming reader and writer MUST support the 14 portable Array types of Core Version 1:
| DataCanon Array type | HDF5 logical representation |
|---|
bool | Boolean enum with FALSE=0, TRUE=1 |
int8, int16, int32, int64 | Signed integers of corresponding widths |
uint8, uint16, uint32, uint64 | Unsigned integers of corresponding widths |
float16, float32, float64 | IEEE-754 binary16, binary32, binary64 |
complex64 | Compound datatype with fields r and i, each IEEE-754 binary32 |
complex128 | Compound datatype with fields r and i, each IEEE-754 binary64 |
For the complex types, the compound fields r and i represent real and imaginary components respectively, in that order. The field names and component widths match the Python h5py reference representation and are part of the portable interchange layout. A reader MUST reconstruct the corresponding DataCanon complex Array type, not a Record or a structured Array of unrelated fields.
The Array rank and shape are taken from the HDF5 dataspace. Rank-zero Arrays use scalar dataspaces. Zero-sized dimensions are permitted and MUST be preserved. A rank-zero Array is not the same DCI as a rank-one Array containing one element.
The element ordering is the logical ordering of the HDF5 dataspace, consistent with the Core Version 1 Array definition. An implementation MUST preserve the exact logical numerical elements, including signed floating-point zeros and the real and imaginary components of complex elements. HDF5 chunking, compression, and physical data ordering do not participate in DCI identity.
A conforming writer MUST NOT store a portable Array as a nested collection of DCI groups merely because that is easier for its implementation. The native numerical dataset is a defining feature of this representation.
External DCIs
General principle
An ExternalFile or ExternalArray remains an external DCT even when its bytes are embedded in an HDF5 file. Embedding payloads MUST NOT transform ExternalArray into Array or ExternalFile into String or Array.
The semantic constituents are represented in the root DCI tree. The external payload bytes are stored separately in the top-level _external group.
Relative path representation
A DataCanon relative path is represented as a structural group named path, containing indexed UTF-8 byte datasets for its path components:
path/
├── 0 UTF-8 byte information
├── 1 UTF-8 byte information
└── ...
A path MUST have at least one component and MUST satisfy the Core Version 1 relative-path invariants. The path group itself has no kind attribute. The path is not stored as a host-operating-system path string.
Digest representation
A SHA-256 content digest is stored in a dataset named digest containing exactly 32 unsigned 8-bit integers, in digest byte order. The digest is not stored as hexadecimal text.
ExternalFile
An ExternalFile is a group with kind = "external_file":
external_file/
├── path/ Relative-path component group
└── digest uint8 dataset, shape (32,)
The content digest is the SHA-256 digest of the exact external file bytes.
ExternalArray
An ExternalArray is a group with kind = "external_array":
external_array/
├── path/ Relative-path component group
├── array_type UTF-8 byte information
├── shape/ Indexed decimal integer datasets
└── digest uint8 dataset, shape (32,)
The array_type UTF-8 byte dataset contains one of the 14 portable Array type names. The shape group contains indexed decimal integer datasets, one per dimension; an empty group denotes rank zero.
The content digest is the SHA-256 digest of the canonical Array element byte stream, not the complete canonical encoding of an Array DCI. The logical element bytes contain no dtype, shape, header, or other framing information. Directory gzip envelopes are not embedded as the logical payload.
Embedded external payloads
Structure
When external resources are present, a conforming HDF5 writer embeds their verified bytes under the top-level group _external:
_external/
├── 0/
│ ├── path/ Indexed UTF-8 byte datasets
│ └── data Rank-one uint8 dataset
├── 1/
│ ├── path/
│ └── data
└── ...
Each indexed _external child is a structural group, not a DCI. Its path group uses the same relative-path component encoding as above. Its data dataset contains the logical resource bytes as a rank-one uint8 dataset, including empty payloads when applicable. ExternalFiles contribute their opaque bytes; ExternalArrays contribute their verified, uncompressed canonical element streams.
The numeric index of an embedded payload is not semantic. A reader locates an embedded resource by matching the full sequence of relative-path components, not by assuming that a particular DCI points to a particular numeric index.
ExternalArray compression
The reference writer applies HDF5 gzip compression (level 6, chunked storage)
to embedded ExternalArray payload datasets whose logical path ends in .bin.gz.
It does not store a gzip-file envelope in the dataset. Dataset reads return the
canonical element bytes directly; the content digest covers those bytes.
Scalar Arrays and empty Arrays still use rank-one byte payload datasets, so the
same storage rule applies. .bin ExternalArray payloads and native Array DCIs
remain uncompressed by default.
Compression filters and physical layout are nonsemantic. Readers reconstruct
the logical bytes through HDF5 and validate them normally, regardless of the
physical filters in a valid input dataset. Readers MUST NOT perform another
gzip decompression based on the logical resource path. Directory export uses
the path’s .bin/.bin.gz suffix to recreate the required storage representation.
Uniqueness and consistency
A conforming writer MUST embed each distinct referenced relative path at most once. If multiple external DCIs refer to the same relative path, their digests MUST agree and the referenced bytes MUST be consistent with each DCI’s requirements.
If two references use the same path but require conflicting bytes or digests, a conforming writer MUST reject the attempted file creation rather than silently select one payload.
An embedded ExternalArray logical payload MUST satisfy its declared Array type and shape, including element-byte length, canonical Boolean bytes, and finite floating-point/complex elements. A compressed ExternalArray and an opaque ExternalFile MUST NOT share a relative path; their directory storage requirements conflict even if their digests agree.
Writing and verification
Before embedding external bytes, a writer MUST verify that their SHA-256 digest equals the digest stored in the corresponding DCI. It MUST NOT embed unverified bytes as though they satisfy the reference.
The Python reference writer collects and verifies all referenced external payloads before creating or overwriting the destination HDF5 file. This preflight behavior is recommended because it prevents missing or conflicting resources from destroying an existing file. Other implementations MAY use different transactional mechanisms providing equivalent integrity safeguards.
A writer may obtain bytes from a verified in-memory cache, an existing filesystem resource, or another supported representation containing the resource. The choice of source does not change the resulting DCI.
Reading and deferred access
Reading the HDF5 DCI hierarchy MUST reconstruct ExternalFile and ExternalArray DCIs from their semantic metadata without automatically materializing their external payload bytes.
A conforming reader SHOULD retain sufficient nonsemantic source context to resolve embedded resources on later request. Such context does not participate in DCI equality or canonical encoding.
When an implementation does retrieve an embedded payload, it MUST verify its digest against the DCI before accepting the bytes. For ExternalArray it MUST additionally validate the declared Array type, shape, byte length, and element validity.
A missing or corrupt embedded payload does not change the identity of the metadata DCI; it prevents successful retrieval or verification of the referenced content.
The general semantics of resolving external resources across different storage representations belong to the separate DataCanon Persistence specification.
Optional fingerprint attribute
A writer MAY store a scalar file-root attribute named fingerprint. Its value is the 64-character lowercase hexadecimal textual representation of the SHA-256 fingerprint of the DCI stored at /root, as defined by Core Version 1.
The attribute is metadata, not an additional constituent of the DCI. Its presence or absence does not affect identity.
The Python reference reader does not currently use this attribute to validate the file. A conforming reader MAY verify it and MUST NOT treat a mismatching fingerprint as evidence that two unequal DCIs are equal.
Reading, writing, and validity
A conforming writer MUST produce the DCI layouts and semantic types specified above. A conforming reader MUST support every required Version 1 layout and reconstruct the same DCI identity.
Readers MUST reject absent /root, unknown kind values, missing required constituents, invalid constituent DCTs, invalid relative paths, and numerical or structural data that cannot represent a valid DCI. They MUST NOT silently repair or simplify invalid DCIs.
A reader MAY ignore additional nonsemantic attributes or groups, provided they do not create ambiguity about the required DCI structure. The strictness of rejecting all extraneous structural objects is not yet fully specified; conforming writers SHOULD emit only the defined structural children.
External payload verification is distinct from decoding the DCI hierarchy. A reader need not access embedded payload bytes merely to construct a metadata-only external DCI.
Illustrative HDF5 trees
Scientific Mapping
/
└── root/ kind = "mapping"
├── 0/
│ ├── key kind = "string", UTF-8 bytes "lattice"
│ └── value kind = "array", shape (3, 3), float64
└── 1/
├── key kind = "string", UTF-8 bytes "scale"
└── value/ kind = "fraction"
├── numerator decimal integer text "1"
└── denominator decimal integer text "2"
ExternalArray
/
├── root/ kind = "external_array"
│ ├── path/
│ │ ├── 0 UTF-8 bytes "external_dc"
│ │ └── 1 UTF-8 bytes "forces.bin"
│ ├── array_type UTF-8 bytes "float64"
│ ├── shape/
│ │ ├── 0 decimal integer text "100"
│ │ └── 1 decimal integer text "3"
│ └── digest uint8[32]
└── _external/
└── 0/
├── path/
│ ├── 0 UTF-8 bytes "external_dc"
│ └── 1 UTF-8 bytes "forces.bin"
└── data uint8[2400]
The example assumes float64 elements (8 bytes each), shape (100, 3), and therefore 2,400 canonical element bytes.
Implementations SHOULD test at least the following cases:
- All eight primitive DCTs, including both named Constants, unreduced Fractions, empty and arbitrary Bytes sequences, arbitrary-precision Integers, Unicode Strings, embedded NULs, and positive versus negative Float zero.
- All three atomic DCTs: Power with a Scalar Numerical base and positive integer exponent fields, Quotient with Scalar Numerical operands, and Complex with components of different DCTs.
- All collection DCTs, including empty collections, ordered versus unordered semantics, Multiset and numerical Sum/Product multiplicities and nesting, and keys that would collide under ordinary programming-language equality. OrderedProduct tests MUST distinguish reversed operand order and preserve Array operands without rank checks.
- Record names and field names containing Unicode and spaces; preservation of field order.
- All 14 portable Array types; scalar Arrays; empty dimensions; signed zeros; complex components; native HDF5 dataset interoperability.
- ExternalFile and ExternalArray metadata round trips without eager payload loading.
- Embedded payload retrieval, digest verification, shared paths, and rejection of conflicting paths.
- Optional fingerprint metadata, without making it part of identity.
- Rejection of malformed files, including missing
/root and unknown kind attributes.
Cross-language test fixtures SHOULD include files written independently by Python and Julia. HDF5 file bytes need not match; reconstructed DCIs and their canonical encodings MUST match.
Version 1 finalization notes
The following points warrant focused interoperability testing before declaring this document final; they are not reasons to redesign the representation:
- Native HDF5 datatypes: confirm that Julia’s HDF5 implementation reads and writes the
h5py Boolean enum and r/i complex compound layouts with the intended semantics, especially across endianness and floating-point widths. - Reader strictness: the Python reference reader currently relies on HDF5 conversion and DCI constructors for some validation and does not explicitly reject every possible malformed HDF5 datatype or extra child. The normative reader requirements may therefore require additional validation tests or a narrower portable-input statement.
- Embedded payload lookup: confirm that source-aware loading resolves the
/_external entry by complete relative path, independently of enumeration order, and verifies digest and Array element validity on retrieval. - Version metadata: the current layout contains no mandatory HDF5 format-version attribute. If one is introduced later, it should be nonsemantic and backward-compatible with existing Version 1 files.
- Exact string datatype details: the Integer DCI uses an HDF5 scalar UTF-8 variable-length string, while String DCI uses a
uint8 byte dataset. Independent implementations must not confuse the two.
The finalization items should be settled by conformance tests and targeted reference-implementation changes, rather than by altering the DataCanon core identity model.
6.6 - Persistence
DataCanon Persistence — Version 1 Draft
Status: Draft for independent implementation review
Depends on: DataCanon Core Version 1; DataCanon Shorthand Version 1; DataCanon HDF5 Version 1
Related: DataCanon YAML Interchange Version 1; DataCanon Shorthand Preprocessing Version 1
Scope: Storage-format selection, reading and writing, external-resource namespaces, deferred retrieval, integrity verification, and identity-preserving transfer
Out of scope: DCT definitions, canonical byte encoding, Shorthand grammar, HDF5 object layout, YAML scalar semantics, and implementation-language APIs
Introduction
DataCanon Persistence defines how DataCanon Instances (DCIs) are read, written, and transferred among supported storage representations while preserving their identity. It also defines how the relative paths belonging to ExternalFile and ExternalArray DCIs are interpreted within different resource namespaces.
DataCanon Core Version 1 alone defines DCI equality, canonical encoding, and fingerprints. Persistence MUST NOT redefine or alter those properties.
A DCI may be represented in memory, as canonical binary bytes, as Shorthand text, or within HDF5. External resources may be stored as separate filesystem files or embedded in an HDF5 container. The source from which an implementation can retrieve such resources is not part of the DCI.
This specification concerns the behavior of conforming implementations, not their choice of internal classes, APIs, or filesystem utilities. The Python reference implementation provides operations commonly named read and write, but those names are not normative.
The terms MUST, MUST NOT, SHOULD, and MAY indicate normative requirements and recommendations.
A representation is a stored form from which a DCI can be reconstructed. A resource namespace is the context in which an external DCI’s DataCanon relative path is resolved. A source is an implementation-level handle or description of such a namespace. A payload is the byte sequence referred to by an external DCI.
A metadata read reconstructs a DCI without necessarily retrieving external payloads. Materialization retrieves and validates a referenced payload. A complete package consists of a DCI representation together with all external payloads needed to satisfy its external references.
For every supported format, a conforming writer and reader MUST satisfy the identity-preserving round trip:
where equality is DataCanon equality. The equation assumes that the write succeeds and the stored representation is subsequently available and intact. It does not imply that a metadata read materializes external resources.
Distinct representations of equal DCIs need not have equal file bytes. In particular, Shorthand formatting and HDF5 physical layout are not canonical encodings.
Supported representations and filenames
Version 1 defines the following conventional file suffixes:
| Suffix | Representation | Governing specification |
|---|
.dcs | Strict DataCanon Shorthand text | Shorthand Version 1 |
.dcb | Raw canonical DCI byte encoding | Core Version 1 |
.h5 | DataCanon HDF5 container | HDF5 Version 1 |
A .dcs file MUST contain exactly one valid strict Shorthand DCI expression, encoded as UTF-8 text. The file MAY contain permitted Shorthand whitespace. The file MUST NOT require substitutions, compact List expansion, or eval(...) to be interpreted.
A .dcb file MUST contain exactly one complete canonical Version 1 DCI encoding. It MUST NOT prepend a magic header, length prefix, or version marker to those bytes. Its bytes are precisely the canonical encoding defined by Core Version 1.
A .h5 file MUST follow the HDF5 Version 1 layout, including its required /root object and the rules for embedded external payloads.
The suffixes above are conventional format-selection signals, not semantic DCI constituents. An implementation MAY provide explicit format selection, and MAY recognize additional suffix aliases, but MUST interpret a selected format according to its governing specification.
YAML interchange is separately specified and is not a required format for the generic Version 1 persistence dispatch. An implementation MAY provide explicit YAML import and export operations. It MUST NOT silently treat arbitrary YAML as strict Shorthand or vice versa.
Reading a DCI
A conforming read operation MUST reconstruct the DCI described by the selected representation and MUST validate its semantic constituents according to Core Version 1.
In particular, a reader MUST preserve:
- the DCT of every constituent, including distinctions between Integer, Float, Fraction, Sum, Product, Power, Quotient, List, Tuple, and Array;
- ordered collection and Record field order;
- unordered collection membership and key/value identity;
- Multiset member multiplicities and nested structure;
- unreduced Fraction and Power exponent structure, and unevaluated Sum, Product, and Quotient constituents;
- floating-point bit identity, including signed zero;
- Array type, shape, and portable element values; and
- ExternalFile and ExternalArray relative paths, metadata, and SHA-256 digests.
A reader MUST NOT change an ExternalArray into an Array, or an ExternalFile into another DCT, merely because the referenced bytes are available.
Contained byte payloads
Bytes DCIs contain their entire byte sequences. Writers MUST preserve those
bytes directly in the selected representation: raw length-delimited bytes in
.dcb, hexadecimal Shorthand in .dcs, or kind = "bytes" datasets in .h5.
No external-resource namespace or companion file is required for Bytes.
Reading a Bytes DCI reconstructs its byte sequence. Applications may attach
interpretation metadata using a containing Record or another suitable DCT;
persistence does not interpret or execute byte contents.
External references are not eagerly loaded
Reading a DCI MUST NOT require external resources to exist, be opened, or be verified. An ExternalFile or ExternalArray DCI is valid independently of the availability of its referenced payload.
Thus, a metadata read of a .dcs, .dcb, or .h5 representation containing external DCIs MUST be possible without first materializing those external resources, provided the DCI representation itself is valid.
For HDF5, reading the DCI hierarchy does not require reading the /_external/*/data payload datasets. An implementation MAY read nonsemantic metadata needed to establish future source context.
A reader MAY expose a separate operation that verifies or materializes external payloads. Such an operation is not part of ordinary metadata reading.
Source context
A reader SHOULD retain sufficient source context to resolve external resources later without requiring the application to resupply the location of the representation. This context MAY be associated with the returned DCI tree, with its external nodes, or with a separate source handle.
Source context is nonsemantic. It MUST NOT participate in DCI equality, canonical encoding, or fingerprinting. Attaching, replacing, or discarding source context MUST NOT change the DCI.
The source context may identify a directory containing external files, an HDF5 file containing embedded payloads, or another implementation-supported resource namespace.
The representation’s source context does not modify the relative path stored in any external DCI.
External resource namespaces
Abstract relative paths
ExternalFile and ExternalArray DCIs contain DataCanon relative paths, defined by Core Version 1 as nonempty ordered sequences of path components. A path is not a host operating-system pathname and does not include an absolute location or a source identifier.
Two equal external DCIs retain the same identity even when resolved against different source namespaces. The choice of namespace determines which bytes may be found; it does not determine DCI equality.
A resource is accepted as satisfying an external DCI only after the required digest and, for ExternalArray, element-stream checks succeed.
Directory-backed representations
For a .dcs or .dcb representation stored in a filesystem, the default resource namespace is the directory containing that representation file.
A DataCanon relative path with components ("inputs", "config.txt") is resolved by joining those components beneath the representation’s containing directory, using the host filesystem’s path conventions.
For example:
runs/run42/
├── calculation.dcs
└── inputs/
└── config.txt
An ExternalFile whose DataCanon relative path is ("inputs", "config.txt") refers by default to runs/run42/inputs/config.txt when calculation.dcs is the source representation.
The same convention applies to a .dcb file in place of calculation.dcs. No particular name for the representation file or containing directory is required.
An implementation MAY accept an explicit directory source to override the default namespace. Such an override MUST NOT change the DCI’s stored relative path or its identity.
A conforming implementation MUST NOT interpret DataCanon relative path components . or .., absolute paths, or embedded separators as permissible alternate spellings. The Core Version 1 relative-path validation rules apply before resolution.
Host filesystem behavior, including permissions and symbolic links, is implementation-specific. Implementations SHOULD avoid unintended resolution outside an explicitly trusted resource namespace, particularly when handling untrusted input.
HDF5-backed representations
For a DCI read from a DataCanon HDF5 file, the default external resource namespace is that HDF5 container’s embedded /_external collection.
The reader resolves a DataCanon relative path by matching its complete ordered component sequence against the path stored with an embedded resource entry. The numeric name of the /_external entry is not semantic and MUST NOT be used as the resource identifier.
The HDF5 Version 1 specification defines the precise layout, uniqueness rules, and payload datasets. Persistence MUST NOT reinterpret those structures.
A source-aware implementation MAY support an explicitly supplied alternative resource namespace, but MUST NOT silently substitute a different payload for a missing or invalid embedded resource.
No source available
A DCI constructed directly in memory, or decoded from canonical bytes without an associated representation location, may have no source context.
Such a DCI remains valid. Materializing an uncached external resource requires an appropriate source to be supplied or otherwise made available. Failure to locate a resource MUST NOT mutate the DCI or change its fingerprint.
External payload materialization and verification
General requirements
Before returning or otherwise accepting retrieved external bytes as the content identified by an ExternalFile or ExternalArray DCI, an implementation MUST compute SHA-256 over the retrieved bytes and compare the resulting 32-byte digest to the DCI’s declared digest.
For ExternalArray, retrieved payload bytes mean the logical canonical element stream after any storage decompression. For ExternalFile, they mean the exact opaque stored bytes.
If the digest differs, the operation MUST fail. It MUST NOT return the mismatching bytes as verified content.
A missing, inaccessible, truncated, or corrupt resource MUST cause the attempted materialization to fail. Materialization failures do not invalidate the metadata DCI.
The cryptographic limitations of SHA-256 are those described in Core Version 1: digest equality is a practical integrity check based on collision resistance, not a proof of byte-sequence identity.
ExternalFile
An ExternalFile payload is an opaque byte sequence. Materialization MUST preserve its exact bytes. DataCanon Persistence does not mandate any text decoding, YAML parsing, or application-specific interpretation of those bytes.
An implementation MAY provide extension-based or user-supplied content loaders as a convenience. Such interpretation occurs after digest verification and is not part of DCI identity or portable persistence semantics.
ExternalArray
An ExternalArray payload is the canonical Array element byte stream, not the complete canonical encoding of an Array DCI. It contains no Array type, shape, or other framing metadata.
After any storage decompression, an implementation MUST validate the digest and the payload against the external DCI’s portable Array type and shape, including:
- the exact required byte length;
- canonical Boolean element bytes (
00 and 01 only); - finite floating-point and complex elements; and
- the element ordering and representation prescribed by Core Version 1.
A materialized Array value MAY be provided as an instance of a conventional numerical container type, such as a NumPy array in Python. This does not change the original ExternalArray DCI into an Array DCI.
ExternalArray storage compression
ExternalArray logical payloads are canonical Array element streams. Directory
storage MUST use a relative path whose final component ends in .bin for an
uncompressed stream or .bin.gz for a gzip-compressed stream. Suffixes are
case-sensitive. Other suffixes are not supported by this directory storage
profile. Core relative-path validity is unchanged: metadata decoding does not
need to retrieve a resource or apply this storage profile.
The digest ALWAYS covers the uncompressed canonical element bytes. A reader
MUST decompress .bin.gz resources before accepting their digest and Array
contents. It MUST enforce the exact logical byte length determined by the Array
type and shape; it SHOULD reject excess decompressed output while reading,
rather than allocating an unbounded decompressed buffer. Missing resources,
malformed or truncated gzip, digest mismatch, invalid Boolean bytes, nonfinite
elements, and incorrect logical lengths cause materialization to fail.
Compressed and uncompressed paths identify distinct DCIs even if their logical
payloads agree. A transfer MUST preserve the stored path and digest. In
particular, HDF5-to-directory export MUST recreate a gzip file when the path
ends in .bin.gz. A directory-to-HDF5 transfer embeds the verified logical
stream, rather than a gzip envelope. HDF5 dataset filters handle storage
compression transparently; readers MUST NOT decompress the returned logical
bytes a second time because a resource path ends in .gz.
ExternalFile payloads remain opaque. A .gz ExternalFile is verified over its
stored bytes and is not implicitly decompressed. One path MUST NOT be shared
by an opaque ExternalFile and a compressed ExternalArray, even when their
declared digests agree, because their directory storage requirements conflict.
Uncompressed ExternalArrays may share paths with ExternalFiles when all byte
and validity requirements agree. Writers MUST reject conflicting references
before producing a complete package.
The Python conventional constructor accepts compression=False or True.
A suffixless filename receives .bin or .bin.gz; an omitted filename is the
logical digest in hexadecimal plus the selected suffix. Explicit suffixes must
agree with the selected compression setting. The constructor finalizes the
path before DCI construction. There is no additional semantic compression field.
The reference writer uses gzip level 6 for directory streams and embedded HDF5
ExternalArray datasets selected by .bin.gz. Compression is off by default.
Native Array datasets are unaffected by this option.
Cached data
An implementation MAY retain external payloads in memory, including data supplied when an external DCI is constructed. Such caches are nonsemantic.
A cache MUST NOT be trusted merely because it is associated with a DCI. Cached bytes MUST satisfy the same digest and, where applicable, ExternalArray validation requirements before they are returned as verified content or persisted as a valid external resource.
An implementation MAY reuse the result of a prior successful verification when it can ensure that the verified bytes have not changed.
Writing and packaging
Representation writing
A conforming writer MUST emit the selected representation of the DCI according to its governing format specification. It MUST NOT change semantic constituents, simplify mathematical structures, or replace external DCTs with their materialized values.
A writer MAY accept an instance of a conventional type and first convert it to a DCI under implementation-defined conversion rules. That conversion is outside this specification; the persistence requirements apply to the resulting DCI.
Filesystem-backed complete packages
When asked to produce a complete .dcs or .dcb package, a writer MUST make each referenced external payload available beneath the destination representation’s containing directory at the DataCanon relative path specified by its DCI.
The writer MUST verify each payload before accepting it for output. It MUST preserve the relative path, digest, Array type, and shape constituents of every external DCI.
A writer MAY obtain the payload from a verified cache, an existing filesystem source, or a supported container source. Copying the bytes to a new namespace MUST NOT change the DCI.
A writer MUST NOT silently omit required external payloads while claiming to have produced a complete package.
Writing only a .dcs or .dcb DCI representation without copying its external payloads MAY be supported as a metadata-only export. Such an export can be a valid representation of the DCI, but it is not necessarily a complete package. An implementation SHOULD make this distinction clear to its caller.
HDF5 complete packages
A conforming HDF5 writer MUST follow the HDF5 Version 1 rules for collecting, verifying, and embedding referenced external bytes in /_external. In particular, it MUST reject incompatible references to the same relative path.
A complete HDF5 package contains the DCI hierarchy and all required embedded external payloads. The semantic external DCI paths and digests remain unchanged by embedding.
A reader MAY reconstruct the DCI without accessing those embedded bytes. Completeness of the package and eagerness of the reader are separate concerns.
Multiset traversal
External-resource discovery MUST include every member occurrence of a
Multiset, recursively through nested constituent structures. Retained source
context MUST also reach external DCIs inside Multisets.
Persistence MUST preserve member multiplicity and nesting. Multiple occurrences
of the same external reference do not require duplicate payload files or
embedded payload entries: one resource location satisfies all consistent
references to its relative path. This resource sharing MUST NOT remove or
merge member occurrences in the represented DCI.
Repeated paths and conflicting references
Within one complete package, two external DCIs with the same relative path refer to one resource location. A conforming writer MUST ensure that all references to that path are consistent with one shared byte sequence.
In particular, if their declared digests differ, the writer MUST reject complete-package creation. Even when digests agree, every ExternalArray reference to that path MUST be compatible with the shared payload’s type, shape, and element-stream validity.
Identical payload bytes referenced by different relative paths remain different resource locations. A writer MUST preserve the paths; it MUST NOT silently merge their semantic references into one path.
Overwriting and failure safety
Writers SHOULD verify all required source payloads and detect conflicting references before overwriting a destination representation or external resource. A writer SHOULD use temporary files, atomic replacement where supported, or equivalent safeguards to avoid leaving partially written packages.
The exact transaction mechanism is implementation-specific. A failed write MUST NOT be reported as a successful complete-package write.
A writer SHOULD avoid destructive overwrites of unrelated files in the destination resource namespace. Applications are responsible for choosing appropriate destination directories and overwrite policies.
Identity-preserving transfers
A transfer between supported representations consists conceptually of:
- reconstructing a DCI from the source representation;
- retaining or supplying a source namespace for any external resources;
- verifying required payloads when preparing a complete destination package; and
- writing an identity-preserving destination representation and its required resources.
For example:
runs/run42/calculation.dcs
|
v
DCI + source context
|
v
archive/calculation.h5
If the source DCI contains an ExternalArray, the destination HDF5 file embeds its verified element bytes but retains the ExternalArray DCT, relative path, Array type, shape, and digest.
The reverse transfer from HDF5 to .dcs or .dcb materializes verified embedded resources into files under the destination directory when a complete package is requested. Their relative paths are preserved.
For equal source and destination DCIs x and y:
where E and F are the canonical encoding and fingerprint defined by Core Version 1. The surrounding file bytes need not match across formats.
A transfer MUST NOT silently resolve conflicts by changing paths, digests, or DCTs. If an identity-preserving complete transfer cannot be performed, it MUST fail or explicitly produce a metadata-only export when the caller has selected that behavior.
A persistence format MAY carry a DCI fingerprint as nonsemantic metadata. The fingerprint is always SHA-256 of the DCI’s canonical encoding, not of the bytes of the .dcs or .h5 file.
The HDF5 Version 1 specification defines its optional file-root fingerprint attribute. This Persistence specification does not introduce an additional required fingerprint field for .dcs or .dcb.
A reader MAY check a stored fingerprint. It MUST NOT treat the fingerprint as an additional DCI constituent or use it to replace the DCT-defined equality relation.
The fingerprint of an ExternalFile or ExternalArray DCI covers its path and declared content digest, not the external payload bytes directly. External payload verification remains a separate operation.
Errors and resource limits
A conforming reader MUST reject invalid representation syntax, invalid canonical bytes, or invalid HDF5 DCI structure according to the governing format specification. It MUST NOT silently coerce malformed data into a different DCI.
An implementation MAY enforce practical limits on input size, nesting depth, Array allocation, number of external references, or payload size. Such limits do not redefine DataCanon identity.
External content MUST be treated as untrusted until verified. A verified digest does not make an external file’s application-level contents safe to execute or interpret. DataCanon Persistence does not require execution of external content or of any code contained in Shorthand or YAML input.
Error types, diagnostic wording, caching strategy, concurrency controls, and source-handle representation are implementation details.
Interoperability examples
Directory-backed Shorthand
experiment/
├── structure.dcs
└── data/
└── forces.bin
structure.dcs contains an ExternalArray with relative path ("data", "forces.bin"), a portable Array type and shape, and the SHA-256 digest of forces.bin’s canonical element bytes.
Reading structure.dcs reconstructs the ExternalArray metadata without reading forces.bin. Materialization resolves the relative path under experiment/, verifies its digest and element validity, and produces the numerical data.
Raw canonical bytes
A .dcb file containing the canonical encoding of ExternalFileDC is valid even if the external file is absent. Decoding recovers the relative path and digest. The missing payload prevents materialization, not decoding or fingerprinting.
HDF5 embedded resources
experiment.h5
/root DCI hierarchy
/_external/0/path Relative-path components
/_external/0/data Verified payload bytes
The HDF5 reader recovers external DCIs from /root without eagerly loading /_external/0/data. When materialization is requested, the resource is located by its stored path components and its digest is verified.
experiment/structure.dcs
|
v
experiment.h5
|
v
archive/structure.dcb
archive/data/forces.bin
A complete transfer preserves the DCI’s canonical encoding and fingerprint, despite differences in the storage representation and external-payload location.
Independent implementations, including Python and Julia, SHOULD test:
- Round trips of all 24 DCTs through
.dcs, .dcb, and .h5 where supported by the corresponding representation specification. - Preservation of DCI identity and canonical fingerprints across format changes.
- Metadata-only reads of external DCIs when referenced payloads are missing.
- Directory-relative resolution for
.dcs and .dcb sources. - HDF5 path-based lookup of embedded payloads, independent of entry enumeration order.
- ExternalFile digest verification and ExternalArray digest, length, and element validation.
- Cached-data verification and explicit source overrides.
- Complete transfer between directory-backed and HDF5-backed representations.
- Detection of shared-path conflicts and rejection of incomplete complete-package writes.
- Distinction between valid metadata-only exports and complete packages.
- Multiset multiplicity and nesting across all formats, including deferred retrieval, shared resources, and conflicting external references within Multiset members.
- Preservation of Sum/Product multiplicity and nesting, unreduced Power exponent constituents, Quotient constituent roles, and Complex components using all permitted numerical DCTs.
Cross-language fixtures SHOULD compare reconstructed DCIs and canonical encodings, not the physical bytes of HDF5 files or the whitespace of Shorthand documents.
Version 1 finalization notes
The following points should be confirmed against the current Python reference reader.py, writer.py, and external.py before this draft is declared final:
- Format dispatch: verify exactly which suffix aliases and explicit-format overrides are supported. This draft standardizes
.dcs, .dcb, and .h5 as the portable convention. - Directory-backed packaging: verify whether the current generic writer always copies external payloads, offers metadata-only export, or supports both. The distinction in this draft is normative for implementations claiming complete-package support; it does not assert that the Python API currently exposes both modes.
- Default source precedence: verify how cached bytes, retained
_source context, explicitly supplied sources, and destination directories are selected. Source precedence is not specified as a universal rule here; every accepted payload must still be verified. - HDF5 source fallback: confirm whether an explicit alternate source can override an unavailable embedded payload, and whether that should be a portable option or remain application policy.
- Filesystem transactionality: test that conflicts and missing resources are detected before destructive destination writes, especially for directory-backed output.
- Source metadata propagation: confirm that external nodes read from any supported format retain enough nonsemantic source context for deferred materialization and later complete-package transfer.
- Source namespace security: define implementation guidance for symlinks and path traversal through filesystem indirection without changing the Core relative-path DCT.
- YAML persistence: decide whether
.yaml/.yml should be added to generic persistence dispatch or remain explicit YAML interchange operations.
The finalization notes identify focused interoperability and implementation-audit items, not reasons to modify DCI identity or canonical bytes.
Draft encoding revision
The current draft encodes fixed integer and Unicode information directly,
without redundant Integer or String DCT identifiers. Canonical bytes and
fingerprints of affected DCIs differ from earlier drafts, including DCIs that
contain affected values. No automatic detection or migration of previous draft
.dcb encodings is defined. Regenerate those files from the corresponding DCI
or another identity-preserving representation. HDF5 fixed fields retain their
payload storage types and no longer require kind attributes.
6.7 - Conventional Types
DataCanon Conventional Types — Version 1 Draft
Status: Draft for independent implementation review
Depends on: DataCanon Core Version 1
Related: DataCanon Persistence Version 1
Scope: Optional conventional numerical and collection types, their operations, and conversion relationships with DCIs
Out of scope: DCT definitions, canonical encoding, textual syntax, and complete symbolic algebra
Purpose and boundaries
DataCanon Core defines data and identity. Core does not perform arithmetic.
An implementation can also provide conventional types used directly for
data and computation in its programming language. Conventional types include
built-in, library-provided, and user-defined types.
An Integer DCI and a conventional integer are different objects with different
contracts. The same distinction applies to a Sum DCI and a conventional Sum:
the DCI preserves its constituent information, while the conventional object
supports arithmetic and construction-time simplification.
Many conventional types already exist in programming languages and libraries.
This companion specification defines a common behavioral foundation for
additional types, including Power, Sum, Product, OrderedProduct, Quotient, ComplexNumber,
Multiset, and an unordered mapping. An implementation MAY use an existing
type when it satisfies the relevant requirements.
Support for this companion specification is optional. Conformance to Core,
Shorthand, or a persistence specification does not require conventional
arithmetic. Supporting conventional arithmetic MUST NOT change DCI equality,
canonical encoding, or fingerprints.
The terms MUST, MUST NOT, SHOULD, and MAY express requirements,
recommendations, and permissions.
Conformance is declared per conventional type. An implementation MUST identify
which types it supports and any dependencies needed for their operations.
For example, a Power implementation need not provide Multiset, but a numerical
implementation claiming the operations below must support their result types.
Construction-only adapters MUST NOT claim the arithmetic conformance defined
here.
A language binding specifies class or type names, constructors, operators
or equivalent functions, error reporting, and conversions in a particular
language. Names such as Power(base, numerator, denominator) in this document
describe a language-independent interface. A binding need not reproduce Python
classes, exceptions, method names, or operator overloading.
A conforming binding MUST document:
- supported conventional types and scalar domains;
- construction, arithmetic, and explicit numerical conversion interfaces;
- mappings to and from DCIs, including possible loss and failure;
- additional simplifications and limits of exact comparison; and
- practical resource limits and how exhausted limits are reported.
Additional conventional types and stronger exact simplification MAY be
provided. Extensions MUST preserve the requirements below. No universal
canonical representation of conventional objects is defined: independently
implemented conventional objects can have different stored forms while
representing the same numerical value.
Common numerical model
Operands and scalar domains
Scalar real numerical operands comprise conventional integers, exact rational
numbers, finite floating-point numbers, and supported conventional
Product, Power, Quotient, and Constant objects, plus recursively scalar Sum
and OrderedProduct objects. Complex numbers are handled separately. Sum and
OrderedProduct also accept numerical Arrays and array-valued expressions.
Product, Power, Quotient, and ComplexNumber components remain scalar-only.
Booleans MUST NOT be accepted as arithmetic operands merely because a language
treats them as integers.
The portable scalar domains are mathematical integers, rational numbers with
positive denominators, and finite IEEE-754 binary64 numbers. A binding SHOULD
reuse appropriate existing integer and rational types. Fixed-width bindings
MUST detect unsupported ranges rather than silently wrap exact arithmetic.
Additional floating-point formats MAY be supported as documented extensions.
A rational with denominator one MAY be stored as an integer. Exact rational
arithmetic MUST preserve the mathematical result, subject to reported resource
limits. Nonfinite floats MUST be rejected as operands and MUST NOT be silently
introduced into valid numerical objects by arithmetic.
Stored structure and numerical value
Conventional numerical equality concerns numerical values, rather than DCI
identity or equality of stored constituents. Construction can simplify stored
structure, but construction does not promise a unique normal form for every
mathematically equivalent expression.
An explicit constructor MUST retain the requested outer conventional type.
For example, constructing a Power whose value is two still produces a Power.
Arithmetic operations MAY instead return an integer, rational, float, or
another supported numerical type when simplification permits it.
Sum and Product MUST retain the first-occurrence order of surviving operand
groups for traversal and presentation. Combining an operand with an earlier
matching group does not move that group. Equality MUST ignore the order of
Sum terms and Product factors. A binding MUST NOT sort stored operands merely
to obtain commutative equality.
Exactness and floating-point arithmetic
Construction and arithmetic MUST NOT approximate an unresolved rational power
by a floating-point value. Explicit numerical conversion is a separate
operation.
Existing floats remain floating-point operands. Rational and floating-point
coefficient groups MUST remain separate during collection of terms and
factors. An implementation MUST NOT replace a float with a large rational
merely to obtain an exact stored coefficient.
Arithmetic combining floats uses the binding’s documented floating-point
operations and rounding. Such arithmetic can round even though an unresolved
Power remains exact. Retaining a float in a structured object does not create
arbitrary-precision floating-point arithmetic.
Exact comparison MAY interpret a finite float as its exact binary rational
value internally. Comparison MUST NOT modify the stored float or use a
rounded numerical evaluation as evidence of equality.
Constant
A conventional Constant stores exactly one name, pi or e, representing the
same exact mathematical constant as the corresponding Constant DCI. A binding
MUST reject other names. The Python binding provides Constant("pi") and
Constant("e"), with Pi and E as conventional instances.
Construction and arithmetic MUST preserve the named constant exactly, rather
than replacing it with an approximate float. Explicit floating-point conversion
MAY approximate its value. Named constants participate as real numerical
operands in Sum, Product, Power, Quotient, and ComplexNumber.
Comparison MUST distinguish a named constant from any finite rational or float
approximation. Bindings MUST recognize identical names, cancellation of an
identical nonzero constant, and matching symbolic terms. Bindings are not
required to decide arbitrary identities involving transcendental constants.
For example, Pi + Pi == 2 * Pi, Pi / Pi == 1, and Pi != float(Pi).
No stored approximation participates in exact comparison.
Power
A Power has a real numerical base and a positive rational exponent supplied
as a positive integer numerator and positive integer denominator. The
denominator defaults to one. The exponent pair MUST be reduced by its greatest
common divisor during construction.
For real values, a negative base with an odd reduced denominator uses the real
root. A negative base with an even reduced denominator has no real value and
MUST be rejected when the sign is established. A binding MAY defer a domain
decision for a compound base whose sign its exact machinery cannot establish;
an attempted numerical evaluation outside the real domain MUST fail.
Construction MUST extract an exact denominator-th root of an integer or
rational scalar base when the entire required root is exact. The extracted
root becomes the base and the reduced numerator becomes the integer exponent.
The integer exponent MUST then remain stored, rather than being evaluated
solely by the Power constructor.
| Input constructor | Required stored result |
|---|
Power(5, 2, 4) | Power(5, 1, 2) |
Power(4, 1, 2) | Power(2, 1, 1) |
Power(16, 3, 2) | Power(4, 3, 1) |
Power(4, 8) | Power(4, 8, 1) |
Power(8, 1, 2) | Power(8, 1, 2) |
Power(-8, 2, 3) | Power(-2, 2, 1) |
For a float base, a replacement base MUST preserve the exact value of the
stored power. An exact representable root MAY be extracted; an approximate
root MUST NOT be substituted. For example, the Python binding extracts the
square root of 4.0 as 2.0, retaining a float base.
Nested powers MAY be combined only when the real branch is preserved. Blindly
multiplying exponents is incorrect for negative bases: the square root of the
square of negative two has value positive two. Additional exact power
simplification is allowed in surrounding arithmetic; retaining an exponent in
a directly constructed Power does not prohibit its evaluation in a Sum or
Product.
A binding MAY provide exponentiation by zero and negative rational exponents
as arithmetic operations. Zero exponent gives one wherever the operation is
defined. A negative exponent is represented using a reciprocal Quotient and
a positive Power. Such operations do not permit zero or negative exponent
constituents in a Power constructor.
OrderedProduct and array-valued Sum
OrderedProduct preserves a sequence of real scalar and Array-expression
operands. Array operands in this section are instances of the numerical array
type provided by the binding. Construction MUST NOT evaluate Array products, approximate exact
coefficients, infer result shapes, or flatten nesting. Implementations MUST
preserve multiplication order. Scalar-only OrderedProducts are also valid.
Bindings MAY declare scalar-only Sum support separately from Array-expression
arithmetic. Unsupported operands MUST be rejected explicitly. For bindings
supporting Array-expression arithmetic, the Version 1 contract is:
- Sum accepts Arrays of any rank with exactly matching shapes. No broadcasting
or automatic reshaping is performed.
- OrderedProduct accepts rank-two Arrays, multiplying them in order. Real
scalar factors scale the result and may appear anywhere in the sequence.
- Nonzero scalar terms mixed into an Array-valued Sum are rejected during
evaluation; a user can supply an explicit identity Array. Scalar zero is
accepted as an additive identity, supporting ordinary summation interfaces.
- Unsupported ranks, incompatible dimensions, and nonfinite floating-point
results cause evaluation to fail. Structural DCI validity is unaffected.
- Exact rational, radical, and named-constant coefficients MUST remain exact
during evaluation. Explicit floating-point or machine-complex conversion may
approximate them. Fixed-width integer multiplication MUST NOT silently wrap.
The Python binding supports integer, floating-point, and complex numerical
Arrays for this arithmetic. Boolean Arrays are not arithmetic operands.
Object Arrays may contain supported conventional scalar entries. Exact
coefficients remain exact when scaling complex Array elements as well.
The Python binding offers .evaluate() on conventional numerical objects.
Array-expression evaluation returns an object-dtype NumPy Array containing
conventional scalar values, including ComplexNumber where needed. Such an
object Array is not a portable DataCanon Array. To encode its result, explicitly
choose a supported machine dtype, or convert its nested contents to Lists so
individual exact values retain their DCTs.
np.asarray(expression, dtype=float) or dtype=complex explicitly requests a
machine approximation. Without a dtype, exact object entries remain. Evaluation
requires allocation; copy=False is rejected. An Array-valued expression cannot
be converted to scalar float or int. Array division by a real scalar may
construct reciprocal scaling; division by an Array is not supported.
Python * and @ involving a conventional array-expression object preserve
factor order and construct OrderedProduct. Ordinary NumPy operations involving
only ordinary NumPy Arrays retain NumPy’s own semantics. Expressions retain
owned read-only copies of supplied Arrays; callers MUST NOT circumvent that
protection. Equality of conventional array expressions compares whole shapes
and scalar entries, returning one Boolean rather than an Array of Booleans.
Sum
The following normalization rules apply to scalar-only Sums. Array-valued
Sums may retain operand boundaries until explicit evaluation as described above.
A Sum contains zero or more real numerical terms. Its value is their sum;
an empty Sum has value zero. Construction MUST flatten directly nested Sums
and combine exact rational constants.
Matching symbolic terms with exact rational coefficients MUST combine their
coefficients. The mandatory comparison cases below establish a minimum set of
recognized matches; complete algebraic matching is not required. Floating-point
coefficient groups MUST be collected separately from exact rational groups.
For example, two occurrences of the square root of three, one multiplied by
three, combine to four times the square root of three. A separate floating-point
offset remains a float. Exact cancellation MAY remove terms and can leave an
empty Sum; the explicit constructor still returns a Sum.
Construction MUST preserve surviving term-group order. An implementation MAY
perform additional exact term simplifications within the documented comparison
limits. It MUST NOT evaluate an irrational term to collect it with a float.
Product
A Product contains zero or more real numerical factors. Its value is their
product; an empty Product has value one. Construction MUST flatten directly
nested Products and combine rational scalar factors. Float factors form a
separate coefficient group.
Construction MUST combine powers with an identical supported scalar base by
adding their rational exponents when the operation preserves the real value.
An exact zero factor reduces the product’s value to zero. A floating-point
zero factor remains floating-point when no exact zero determines the result.
Recognized nonzero identity factors MAY be omitted.
An explicit Product constructor need not distribute over Sum factors. The
arithmetic multiplication operation MUST support distribution when a scalar Sum is
an operand. Retaining a factored explicit Product and expanding multiplication
are compatible: exact comparison recognizes the required distributive cases.
Quotient
A Quotient contains a real numerical numerator and denominator. Unlike the
Fraction DCT, either operand can be a compound numerical object. Its value is
the numerator divided by the denominator.
A denominator established to be zero MUST be rejected. A binding MAY defer a
zero decision that lies outside its exact comparison capabilities; numerical
division by zero MUST fail when encountered.
Construction MUST reduce a quotient of rational scalars, flatten directly
nested Quotients, and cancel recognized matching nonzero factors. A zero
numerator with an established nonzero denominator reduces to zero over one.
Equal established nonzero numerator and denominator reduce to one over one.
Construction retains the Quotient outer type.
A denominator containing a scalar Sum is permitted. Rationalizing such a denominator
is not a construction requirement. For example, the reciprocal of the sum of
the square roots of three and two can remain stored as that Quotient.
An explicit Quotient containing a float identifies division by the exact
stored float. Arithmetic division involving floats MAY use ordinary rounded
floating-point division or a rounded reciprocal according to the binding.
The binding MUST document that distinction. An implementation MUST NOT claim
that a rounded reciprocal is an exact algebraic rewrite.
Arithmetic and numerical conversion
Conforming numerical implementations MUST provide addition, subtraction,
negation, multiplication, and division with supported real operands, including
scalar operands on either side. Operator spelling is language-specific.
Unsupported operands MUST produce an explicit failure or the host language’s
unsupported-operation protocol; they MUST NOT be silently approximated.
Arithmetic MUST preserve exact integer, rational, and unresolved power values,
apart from explicitly documented operations involving floats. A result MAY
unwrap an empty or singleton Sum or Product to a conventional scalar or
remaining numerical object. Explicit constructors retain their outer type.
A binding MUST provide an explicit conversion to its floating-point numerical
type. The conversion evaluates the numerical value and permits approximation.
Overflow, an invalid real domain, and division by zero MUST be reported rather
than yielding an accepted nonfinite structured operand.
An integer conversion, if provided, MUST document its rounding rule. The Python
binding evaluates and truncates toward zero, following Python’s int behavior.
Evaluation of an irrational expression may first round; the conversion does
not promise an exact floor or truncation algorithm for arbitrary algebraic
numbers near an integer boundary.
Numerical equality
Numerical equality MUST use exact reasoning for supported comparisons. Equality
MUST NOT be implemented by converting both operands to floats, by tolerance,
or by comparing approximate decimal renderings.
The mandatory comparison foundation includes rational arithmetic, the actual
binary rational values of finite floats, operand permutation, directly nested
Sum/Product flattening, distribution for finite rational combinations, exact
scalar root extraction, and the radical identities in the following table.
R(n, d) denotes an exact conventional rational; sqrt(n) denotes
Power(n, 1, 2), not a floating-point square-root function.
| Comparison | Required result |
|---|
Power(4, 1, 2) == 2 | true |
Power(0, 1, 2) == 0 | true |
Power(4, 8) == 65536 | true |
sqrt(12) == 2 * sqrt(3) | true |
sqrt(2) * sqrt(3) == sqrt(6) | true |
sqrt(3) * sqrt(3) == 3 | true |
Quotient(1, Sum(sqrt(3), sqrt(2))) == sqrt(3) - sqrt(2) | true |
Sum(R(1, 2), 0.25) == R(3, 4) | true |
Quotient(1, 2) == 0.5 | true |
Quotient(1, 10) == 0.1 | false |
Sum(2**53, 1) == float(2**53) | false |
No complete decision procedure for all mathematical identities is required.
For identities beyond the mandatory foundation, a binding MAY conservatively
return false when equality is not established, or report an unsupported
comparison. The choice MUST be documented. Such a false result is not a proof
of mathematical inequality. A resource-limit failure MAY be reported explicitly.
Factorization algorithms, search budgets, and cached comparison forms are
implementation choices. A binding MUST NOT publish a factorization cutoff as
a universal mathematical definition of equality. Cross-language fixtures MUST
include the mandatory cases and identify any additional promised cases.
Conventional numerical objects MAY be unhashable. If hashing is provided,
objects that compare equal MUST hash equally under the language’s hash
contract, including comparisons with existing scalar types. Hashing arbitrary
stored syntax is insufficient for mathematical equality. DCI hashing is a
separate matter governed by DCI identity.
ComplexNumber
A ComplexNumber contains real and imaginary conventional numerical components.
The components MAY be integers, rationals, floats, Constant, Product, Power,
Quotient, or recursively scalar Sum and OrderedProduct objects. Construction MUST preserve supported exact components rather
than converting them unconditionally to floats.
Equality compares corresponding component values. A binding SHOULD support
comparison with a real scalar by treating its imaginary component as zero,
and with an existing machine complex type using its actual stored components.
Addition, subtraction, negation, multiplication, and division MUST follow the
usual complex formulas using the binding’s component arithmetic. Division by
an established zero complex value MUST fail. Native scalar arithmetic may
affect result types and rounding; the binding MUST document that behavior.
An explicit conversion to a machine complex type MAY approximate components.
Multiset
A conventional Multiset represents a finite unordered collection with
multiplicity. Equality requires equal member multiplicities under the
conventional member equality relation. Member order does not affect equality.
Construction and iteration SHOULD preserve insertion order for presentation.
A binding MUST provide construction from a finite collection, iteration,
total occurrence count, and insertion of one occurrence. Mutability and
additional collection operations are binding choices.
Membership comparison uses conventional equality, not DCI equality. For
example, a language that considers integer one and floating-point one equal
can treat their occurrences as belonging to the same equality group.
Converting the Multiset to a Multiset DCI still converts the actual stored
members individually; distinct conventional storage can produce distinct DCIs.
A Multiset SHOULD support unhashable members when their equality is usable.
A binding MUST document restrictions on member comparison, including members
whose equality does not return a usable Boolean result. A mutable Multiset
need not be hashable.
Unordered mapping
A conventional unordered mapping supplies the language’s ordinary key/value
operations with equality independent of entry order. An existing mapping type
MAY satisfy this contract. The Python binding uses UnorderedDict to identify
conversion to the UnorderedMapping DCT, while ordinary dict converts to the
ordered Mapping DCT.
Key hashability and equality follow the conventional mapping’s rules. A
binding MUST NOT claim that an ordinary mapping can retain arbitrary distinct
DCI keys after converting them to conventional objects. Key collisions and
unsupported keys are possible conversion outcomes.
Conversion to and from DCIs
Conversion MUST distinguish an instance of a conventional type from an object
implementing a DCT. Sharing a programming language does not make their rules
identical.
| DCT | Typical conventional counterpart |
|---|
| Null, Boolean, Integer, Float, String, Bytes | Existing language or library scalar types |
| Fraction | Exact rational type |
| Constant | Named conventional Constant |
| Power, Sum, Product, Quotient, OrderedProduct | Corresponding conventional numerical types |
| Complex | Structured conventional ComplexNumber or a machine complex type |
| List, Tuple, Set | Existing conventional collection types |
| Multiset | Conventional Multiset |
| Mapping, UnorderedMapping | Suitable conventional mapping types or adapters |
| Record | Binding-defined record or application type |
| Array | Numerical array type |
| ExternalFile, ExternalArray | Resource adapters or verified materialized content |
An implementation claiming conversions for a supported conventional type MUST
document both directions independently. Conversion to a DCI MUST use the
conventional object’s current stored information. It MUST NOT substitute an
approximate evaluated scalar for a structured numerical object merely because
the numerical values are close.
Conversion from a DCI MAY invoke a conventional constructor and simplify its
information. Conversion through conventional objects is therefore not
universally identity preserving. A conventional Power constructor can reduce
an exponent or extract a root that its source Power DCI stores structurally.
A conventional rational constructor can normalize its numerator and denominator.
Mappings and Sets can merge members under conventional equality, reject
unhashable members, or fail under binding policy. A binding MUST document
whether collisions fail or follow ordinary collection construction. Conversion
MUST NOT be advertised as lossless when such outcomes are possible.
Not every valid numerical DCI has a defined real numerical value. A conversion
to a conventional numerical type MAY fail even when decoding the DCI succeeds.
An implementation MUST NOT repair the DCI to hide that failure.
Array type, shape, signed zeros, and copy or mutability policy MUST be stated
by the binding. External payload retrieval and verification follow Persistence;
no conversion rule in this document weakens digest or element validation.
Metadata-only external adapters MAY be provided. Converting an external DCI
into materialized conventional content does not change the original DCI.
Python binding examples
The Python reference binding exports the conventional numerical and collection
classes from data_canon. Python’s fractions.Fraction supplies exact rationals.
from fractions import Fraction
from data_canon import (
Power, Sum, Product, Quotient, ComplexNumber, Multiset, UnorderedDict,
as_dc_instance, dc_instance_to_python,
)
root_two = Power(2, 1, 2)
root_three = Power(3, 1, 2)
assert Sum(root_three, 3 * root_three) == 4 * root_three
assert Product(root_two, root_three) == Power(6, 1, 2)
assert Quotient(1, Sum(root_three, root_two)) == root_three - root_two
assert Quotient(1, 10) != 0.1
power = Power(16, 3, 2)
assert (power.base, power.numerator, power.denominator) == (4, 3, 1)
retained = Power(4, 8)
assert (retained.base, retained.numerator, retained.denominator) == (4, 8, 1)
assert retained == 65536
assert ComplexNumber(Fraction(1, 2), root_three) == ComplexNumber(0.5, root_three)
assert Multiset(["O", "Ti", "O"]) == Multiset(["Ti", "O", "O"])
assert UnorderedDict({"a": 1, "b": 2}) == UnorderedDict({"b": 2, "a": 1})
Conventional construction and DCI construction have distinct outcomes:
from data_canon.instance import IntegerDC, PowerDC
source = PowerDC(IntegerDC(16), 3, 2)
conventional = dc_instance_to_python(source)
converted = as_dc_instance(conventional)
assert converted == PowerDC(IntegerDC(4), 3, 1)
assert converted != source
Application-defined Record conventions
Bindings MAY provide explicit application-defined conversions between
conventional types and Record DCIs. Such conventions do not define additional
DCTs. For example, an application can choose a date Record with year, month,
and day fields, or include calendar metadata according to its requirements.
This specification does not prescribe a date schema or calendar.
A binding offering custom conversion SHOULD support recursive conversion within
supported constituent structures and document handler selection, validation,
precedence, and error behavior. The application is responsible for its Record
schema and for the meaning and validity of the conventional result. A valid
Record DCI need not satisfy an application’s schema.
Application conversion MUST NOT change DCI equality or canonical encoding.
Canonical decoding MUST reconstruct the DCI independently of application
conversion handlers. A Record name alone MUST NOT cause implicit execution of
application code during decoding. Custom conversion is a separate explicit
operation. Registries and executable handlers are not DCI constituents.
The Python binding provides an explicit ConversionRegistry with exact
conventional-type matching and exact Record-name matching. Other bindings may
use idiomatic mechanisms with separately documented policies. Cross-language
interoperability of an application convention requires agreement on the Record
name, field names, DCTs, order, and application meaning; matching language class
names is not sufficient.
Independent implementation review
Cross-language tests SHOULD cover the required stored Power results, mandatory
equality cases, operand-order preservation, empty and singleton constructors,
float separation and rounding, zero denominators, negative-base domains,
collection multiplicities, and conversions that change DCI identity.
Tests MUST distinguish numerical agreement from equal stored conventional
forms and from equal DCIs. Matching numerical values alone do not imply matching
canonical bytes after conversion.
Before finalization, the Python reference implementation and independent
bindings should be audited for component validation in ComplexNumber,
documented behavior beyond the mandatory exact-comparison foundation,
floating-point overflow and signed zero, and resource-limit reporting.
Reference implementation behavior is evidence for conformance, not permission
to silently relax a normative requirement.