bijective-gamut 2.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. bijective_gamut-2.1.0/.gitignore +30 -0
  2. bijective_gamut-2.1.0/CHANGELOG.md +450 -0
  3. bijective_gamut-2.1.0/CITATION.cff +34 -0
  4. bijective_gamut-2.1.0/CMakeLists.txt +182 -0
  5. bijective_gamut-2.1.0/CONTRIBUTING.md +104 -0
  6. bijective_gamut-2.1.0/LICENSE +202 -0
  7. bijective_gamut-2.1.0/PKG-INFO +264 -0
  8. bijective_gamut-2.1.0/README.md +219 -0
  9. bijective_gamut-2.1.0/SPEC.md +936 -0
  10. bijective_gamut-2.1.0/examples/00_readme.py +29 -0
  11. bijective_gamut-2.1.0/examples/01_run_table.py +36 -0
  12. bijective_gamut-2.1.0/examples/02_one_mesh.py +55 -0
  13. bijective_gamut-2.1.0/examples/03_geometry_family.py +39 -0
  14. bijective_gamut-2.1.0/examples/04_changing_mesh.py +32 -0
  15. bijective_gamut-2.1.0/examples/05_time_series.py +29 -0
  16. bijective_gamut-2.1.0/examples/06_uncertainty_gradients.py +35 -0
  17. bijective_gamut-2.1.0/examples/07_models.py +81 -0
  18. bijective_gamut-2.1.0/examples/08_selecting_parts.py +38 -0
  19. bijective_gamut-2.1.0/examples/09_combining_files.py +35 -0
  20. bijective_gamut-2.1.0/examples/10_vtk.py +37 -0
  21. bijective_gamut-2.1.0/examples/_cylinder.py +26 -0
  22. bijective_gamut-2.1.0/examples/c/one_mesh.c +97 -0
  23. bijective_gamut-2.1.0/examples/cli/second_entry.json +1 -0
  24. bijective_gamut-2.1.0/examples/cli/second_entry_runs.csv +3 -0
  25. bijective_gamut-2.1.0/examples/cli/sweep.json +9 -0
  26. bijective_gamut-2.1.0/examples/cli/sweep_runs.csv +5 -0
  27. bijective_gamut-2.1.0/examples/cpp/combining.cpp +47 -0
  28. bijective_gamut-2.1.0/examples/cpp/cylinder.hpp +51 -0
  29. bijective_gamut-2.1.0/examples/cpp/family.cpp +54 -0
  30. bijective_gamut-2.1.0/examples/cpp/nearest.cpp +29 -0
  31. bijective_gamut-2.1.0/examples/cpp/one_mesh.cpp +84 -0
  32. bijective_gamut-2.1.0/examples/cpp/parts.cpp +58 -0
  33. bijective_gamut-2.1.0/examples/cpp/refinement.cpp +50 -0
  34. bijective_gamut-2.1.0/examples/cpp/run_table.cpp +46 -0
  35. bijective_gamut-2.1.0/examples/cpp/sweep.cpp +36 -0
  36. bijective_gamut-2.1.0/examples/cpp/transient.cpp +44 -0
  37. bijective_gamut-2.1.0/examples/cpp/uncertain.cpp +44 -0
  38. bijective_gamut-2.1.0/examples/rust/combining.rs +43 -0
  39. bijective_gamut-2.1.0/examples/rust/cylinder.rs +47 -0
  40. bijective_gamut-2.1.0/examples/rust/family.rs +50 -0
  41. bijective_gamut-2.1.0/examples/rust/nearest.rs +23 -0
  42. bijective_gamut-2.1.0/examples/rust/one_mesh.rs +69 -0
  43. bijective_gamut-2.1.0/examples/rust/parts.rs +49 -0
  44. bijective_gamut-2.1.0/examples/rust/refinement.rs +46 -0
  45. bijective_gamut-2.1.0/examples/rust/run_table.rs +41 -0
  46. bijective_gamut-2.1.0/examples/rust/sweep.rs +28 -0
  47. bijective_gamut-2.1.0/examples/rust/transient.rs +37 -0
  48. bijective_gamut-2.1.0/examples/rust/uncertain.rs +39 -0
  49. bijective_gamut-2.1.0/exports.map +4 -0
  50. bijective_gamut-2.1.0/exports.txt +1 -0
  51. bijective_gamut-2.1.0/include/gamut/gamut.h +399 -0
  52. bijective_gamut-2.1.0/include/gamut/gamut.hpp +695 -0
  53. bijective_gamut-2.1.0/licenses/hdf5-lbnl.txt +61 -0
  54. bijective_gamut-2.1.0/licenses/hdf5.txt +106 -0
  55. bijective_gamut-2.1.0/licenses/zlib.txt +22 -0
  56. bijective_gamut-2.1.0/llms.txt +115 -0
  57. bijective_gamut-2.1.0/mkdocs.yml +120 -0
  58. bijective_gamut-2.1.0/overrides/partials/logo.html +3 -0
  59. bijective_gamut-2.1.0/packaging/gamut-format/README.md +6 -0
  60. bijective_gamut-2.1.0/packaging/gamut-format/pyproject.toml +30 -0
  61. bijective_gamut-2.1.0/pyproject.toml +88 -0
  62. bijective_gamut-2.1.0/python/gamut/__init__.py +48 -0
  63. bijective_gamut-2.1.0/python/gamut/__main__.py +6 -0
  64. bijective_gamut-2.1.0/python/gamut/_files.py +44 -0
  65. bijective_gamut-2.1.0/python/gamut/_lib.py +430 -0
  66. bijective_gamut-2.1.0/python/gamut/_models.py +507 -0
  67. bijective_gamut-2.1.0/python/gamut/_study.py +785 -0
  68. bijective_gamut-2.1.0/python/gamut/_units.py +64 -0
  69. bijective_gamut-2.1.0/python/gamut/_validate.py +68 -0
  70. bijective_gamut-2.1.0/python/gamut/cli.py +922 -0
  71. bijective_gamut-2.1.0/python/gamut/vtk.py +554 -0
  72. bijective_gamut-2.1.0/python/tests/_pages.py +156 -0
  73. bijective_gamut-2.1.0/python/tests/test_binding.py +331 -0
  74. bijective_gamut-2.1.0/python/tests/test_cli.py +433 -0
  75. bijective_gamut-2.1.0/python/tests/test_docs.py +140 -0
  76. bijective_gamut-2.1.0/python/tests/test_edges.py +1172 -0
  77. bijective_gamut-2.1.0/python/tests/test_examples.py +54 -0
  78. bijective_gamut-2.1.0/python/tests/test_existing_destinations.py +105 -0
  79. bijective_gamut-2.1.0/python/tests/test_files.py +346 -0
  80. bijective_gamut-2.1.0/python/tests/test_guide_cli.py +61 -0
  81. bijective_gamut-2.1.0/python/tests/test_guide_languages.py +77 -0
  82. bijective_gamut-2.1.0/python/tests/test_layouts.py +156 -0
  83. bijective_gamut-2.1.0/python/tests/test_models.py +604 -0
  84. bijective_gamut-2.1.0/python/tests/test_readme.py +84 -0
  85. bijective_gamut-2.1.0/python/tests/test_validate.py +553 -0
  86. bijective_gamut-2.1.0/python/tests/test_vtk.py +270 -0
  87. bijective_gamut-2.1.0/rust/.gitignore +2 -0
  88. bijective_gamut-2.1.0/rust/Cargo.toml +10 -0
  89. bijective_gamut-2.1.0/rust/gamut/Cargo.toml +59 -0
  90. bijective_gamut-2.1.0/rust/gamut/build.rs +8 -0
  91. bijective_gamut-2.1.0/rust/gamut/src/lib.rs +1176 -0
  92. bijective_gamut-2.1.0/rust/gamut/tests/corpus.rs +265 -0
  93. bijective_gamut-2.1.0/rust/gamut/tests/study.rs +115 -0
  94. bijective_gamut-2.1.0/rust/gamut-sys/Cargo.toml +16 -0
  95. bijective_gamut-2.1.0/rust/gamut-sys/build.rs +10 -0
  96. bijective_gamut-2.1.0/rust/gamut-sys/generate.py +180 -0
  97. bijective_gamut-2.1.0/rust/gamut-sys/src/lib.rs +166 -0
  98. bijective_gamut-2.1.0/src/capi.cpp +1560 -0
  99. bijective_gamut-2.1.0/src/chunk.cpp +58 -0
  100. bijective_gamut-2.1.0/src/chunk.hpp +43 -0
  101. bijective_gamut-2.1.0/src/chunks.cpp +668 -0
  102. bijective_gamut-2.1.0/src/codec.cpp +253 -0
  103. bijective_gamut-2.1.0/src/codec.hpp +36 -0
  104. bijective_gamut-2.1.0/src/copy.cpp +485 -0
  105. bijective_gamut-2.1.0/src/copy.hpp +32 -0
  106. bijective_gamut-2.1.0/src/evaluate.cpp +477 -0
  107. bijective_gamut-2.1.0/src/evaluate.hpp +115 -0
  108. bijective_gamut-2.1.0/src/h5.cpp +1452 -0
  109. bijective_gamut-2.1.0/src/h5.hpp +345 -0
  110. bijective_gamut-2.1.0/src/names.cpp +157 -0
  111. bijective_gamut-2.1.0/src/names.hpp +49 -0
  112. bijective_gamut-2.1.0/src/nearest.cpp +164 -0
  113. bijective_gamut-2.1.0/src/nearest.hpp +49 -0
  114. bijective_gamut-2.1.0/src/reader.cpp +849 -0
  115. bijective_gamut-2.1.0/src/reader.hpp +95 -0
  116. bijective_gamut-2.1.0/src/schema.cpp +86 -0
  117. bijective_gamut-2.1.0/src/schema.hpp +143 -0
  118. bijective_gamut-2.1.0/src/select.cpp +314 -0
  119. bijective_gamut-2.1.0/src/select.hpp +23 -0
  120. bijective_gamut-2.1.0/src/sha256.cpp +150 -0
  121. bijective_gamut-2.1.0/src/sha256.hpp +40 -0
  122. bijective_gamut-2.1.0/src/types.cpp +152 -0
  123. bijective_gamut-2.1.0/src/types.hpp +133 -0
  124. bijective_gamut-2.1.0/src/units.cpp +316 -0
  125. bijective_gamut-2.1.0/src/units.hpp +14 -0
  126. bijective_gamut-2.1.0/src/validate.cpp +1687 -0
  127. bijective_gamut-2.1.0/src/validate.hpp +38 -0
  128. bijective_gamut-2.1.0/src/value.cpp +136 -0
  129. bijective_gamut-2.1.0/src/value.hpp +78 -0
  130. bijective_gamut-2.1.0/src/writer.cpp +1923 -0
  131. bijective_gamut-2.1.0/src/writer.hpp +197 -0
  132. bijective_gamut-2.1.0/tests/header_tests.cpp +543 -0
  133. bijective_gamut-2.1.0/tests/unit_tests.cpp +466 -0
  134. bijective_gamut-2.1.0/tests/writer_reader_tests.cpp +1198 -0
  135. bijective_gamut-2.1.0/tools/build_core.sh +30 -0
  136. bijective_gamut-2.1.0/tools/build_deps.sh +87 -0
  137. bijective_gamut-2.1.0/tools/damaged_files.py +284 -0
@@ -0,0 +1,30 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.egg-info/
5
+ dist/
6
+ wheelhouse/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+
11
+ # Build trees and built libraries
12
+ build/
13
+ build-*/
14
+ python/gamut/_native/
15
+ matlab/*.mex*
16
+
17
+ # Julia
18
+ julia/Manifest.toml
19
+
20
+ # Files the tests and benchmarks write
21
+ *.gamut
22
+ !docs/**/*.gamut
23
+ bench/results/raw/
24
+
25
+ .DS_Store
26
+ matlab/libgamut.*
27
+ matlab/gamut.dll
28
+ matlab/gamut.lib
29
+ *.mltbx
30
+ site/
@@ -0,0 +1,450 @@
1
+ # Changelog
2
+
3
+ ## 2.1.0 (2026-10-30)
4
+
5
+ **Upgrading from 2.0.** The file format (`gamut/1`) is unchanged, and the
6
+ C interface only adds calls, so programs written for 2.0 build and run
7
+ against 2.1; what `evaluate` now does differently is listed below. Install
8
+ `bijective-gamut` where you installed `gamut-format`.
9
+
10
+ - The Python package is now `bijective-gamut` on PyPI, and the Rust crates
11
+ are `bijective-gamut` and `bijective-gamut-sys`; the code still says
12
+ `import gamut` and `gamut::`. `gamut-format`, the name through 2.0.0,
13
+ installs `bijective-gamut` at the same version, extras included.
14
+ - Rust: the `bijective-gamut` crate (`gamut` in code), over
15
+ `bijective-gamut-sys`, the declarations of `gamut.h` that
16
+ `rust/gamut-sys/generate.py` writes from it. It writes and reads
17
+ data files with the calls of the other languages, evaluates model files
18
+ whose models are the built-in nearest neighbour, and is held to the
19
+ corpus. Every example has a Rust tab, and the reference a Rust page.
20
+ The crates are added from this repository's release tag (not yet from
21
+ crates.io).
22
+ - `related` in every language, read in the core: what describes a result
23
+ (its standard deviation, quantiles and derivatives, and the parts of its
24
+ vector) comes from `gamut_related` in the C interface. MATLAB
25
+ (`study.related(name)`) and C++ (`study.related(name)`, a
26
+ `gamut::Related`) gain it, and Python's and Julia's now read it from the
27
+ core, so the four agree.
28
+ - `evaluate` is one implementation, in the core: the `gamut_evaluation_*`
29
+ calls check the inputs (E2, E5, E9, and the W4 finding, with one
30
+ message in every language), run the built-in models and write the data
31
+ file. Python, MATLAB and Julia only call the registered model types, and
32
+ a C program can evaluate a model file itself. `gamut_evaluate_begin`
33
+ stays for 2.0 callers. Along the way:
34
+ - coordinates given to `evaluate` are the data file's, per run, as
35
+ section 7.4 says; they were dropped for a model file whose mesh is
36
+ fixed;
37
+ - an int64 input past 2^53 is compared with its bounds exactly and
38
+ evaluated, where it was refused (E4);
39
+ - W4 says in how many rows an input lies outside, the same in every
40
+ language;
41
+ - a model file whose results read one model output at two widths (a
42
+ scalar and a field) is refused (E17) before any model runs;
43
+ - in Python, a complex, boolean or text input or coordinate is refused
44
+ (E4), where it was cast.
45
+
46
+ ## 2.0.0 (2026-10-29)
47
+
48
+ **Upgrading from 1.3.** The file format is unchanged (`gamut/1`), so
49
+ files move between 1.3 and 2.0, short of the new limits below. Code
50
+ written for 1.3 can stop at, or behave differently after, these changes;
51
+ the fixes further down also correct results that were wrong, such as a
52
+ `select` on int64 values past 2⁵³.
53
+
54
+ - Creating a file never replaces an existing one: `create`,
55
+ `create_model`, `extract`, `write_copy`, `evaluate` with a path,
56
+ `gamut build`, `gamut-vtk import`, and `gamut get` without `--force`.
57
+ - A parameter is float64 unless declared int64; integers given to it no
58
+ longer make it int64.
59
+ - In MATLAB, `[]` as rows, nodes or cells selects none; `':'`, or leaving
60
+ them out, selects every one.
61
+ - Input that was stored wrongly is refused: a field given sites × runs (a
62
+ square array cannot be told apart, and is taken as runs × sites), a
63
+ label given `None` or NaN, a fractional, out-of-range, text or boolean
64
+ value for an int64 model input, an integer that float64 would round, a
65
+ group member given two different geometries in one batch, and a
66
+ negative `--rows` position on the command line.
67
+ - A file whose category tables together, or a model whose arrays
68
+ together, pass the cap one table or array had is refused (E26) when it
69
+ is opened or read; 1.3 allocated for them without limit.
70
+ - In C, every array argument comes with its length, and every array the
71
+ core fills with its capacity: `gamut_batch_values` takes `first_axis`;
72
+ `gamut_geometry_points`, `gamut_geometry_cells`, `gamut_tree_set_array`
73
+ and `gamut_nearest_call` take counts; `gamut_shape` and
74
+ `gamut_tree_array_info` take `rank, shape, capacity`.
75
+ `gamut_units_parse` is `gamut_units_valid`.
76
+ - On the command line, every failure under `--json` is
77
+ `{"problems": [{"rule", "path", "message"}]}`; it was `{"error": {...}}`
78
+ or `{"problems": [{"key", "message"}]}` by command, and argparse's own
79
+ mistakes printed text (`validate` still prints its findings). Success
80
+ documents no longer carry an empty `problems`. A misused `gamut get`
81
+ exits 2 before creating its output.
82
+ - In Python, `related(name)["vector"]` lists the vector's parts only, as
83
+ Julia did; a statistic or derivative of a part that carries the same
84
+ `vector` was listed among them.
85
+ - Python 3.11 or later is required (3.10 reaches its end of life this
86
+ month), NumPy 1.26 or later (it was 1.24, never tested) and Julia 1.10
87
+ or later (it was 1.9, never tested). MATLAB R2023b or later is
88
+ unchanged. Each minimum is now tested, with Python 3.14 too, and **none
89
+ of them rises within 2.x**.
90
+ - In C++, `get_parameter<T>` and `get_constant<T>` name the type: `double`,
91
+ or `int64_t` for a value declared int64. Another type is refused rather
92
+ than rounded or truncated.
93
+ - Two rules each split in two: appending to a file whose growable
94
+ dimensions are fixed is E25 (it was E20, which keeps storage the format
95
+ forbids), and a limit reached is E26 (it was E23, which keeps storage a
96
+ reader cannot read). Code that tests for E20 or E23 checks the new
97
+ codes too.
98
+
99
+ The rest of this section says what changed and why.
100
+
101
+ Creating a Gamut file never replaces an existing one. `create`,
102
+ `create_model`, `evaluate`, `extract` and `write_copy`, in every language
103
+ and in C, `gamut build` and `gamut-vtk import` refuse a destination that
104
+ exists and leave it as it was; 1.3.0 and earlier replaced it. A script that
105
+ writes the same name on each run now stops at that refusal on its second
106
+ run: choose a new name, or remove the previous output yourself first. To
107
+ add runs to an existing file in Python, open it with
108
+ `gamut.open(path, mode="a")`. A `gamut build` that failed part way used to
109
+ delete the file it had replaced.
110
+
111
+ A parameter is float64 unless declared int64 (`"dtype": "int64"`; in C++,
112
+ `declare_parameter<int64_t>`). 1.3.0 and earlier stored an undeclared
113
+ parameter as int64 when its first values were integers, so a Python sweep
114
+ begun with `range(0, 11, 2)` refused 2.5 when appended to, and the same
115
+ program wrote int64 from Python and float64 from MATLAB. Files are
116
+ unchanged: an int64 parameter stays valid and reads as before. Code that
117
+ gives integers to an undeclared parameter now writes float64; declare
118
+ int64 where whole numbers are meant.
119
+
120
+ A refused replacement of an attribute (a value of about 64 KiB or more)
121
+ keeps the attribute's previous value. Before, the previous value was
122
+ deleted first, so the refusal lost it.
123
+
124
+ A field given the wrong way round, sites × runs instead of runs × sites,
125
+ is refused. It was stored scrambled, each run holding a mix of every run's
126
+ values. The C interface's `gamut_batch_values` takes a new `first_axis`
127
+ argument, the length of the caller's first axis or 0 for a flat array,
128
+ and the core refuses a first axis that is not the batch's runs. A square
129
+ array cannot be told apart and is taken as runs × sites.
130
+
131
+ A label given as `None` or NaN in Python is refused (E5); it used to
132
+ become a category named "None" or "nan". A category name that is not valid
133
+ UTF-8 is refused when the file opens (E4), in every language, where Python
134
+ raised `UnicodeDecodeError`. A position above 2⁶³ − 1 is reported as given
135
+ rather than wrapped to a negative number. `get_points` on a file with no
136
+ mesh says so, in every language, instead of asking for a row.
137
+
138
+ A group member given two different geometries in one batch is refused by
139
+ the core, in every language. MATLAB kept the first geometry silently.
140
+
141
+ `open` with a refused `threads` value closes the file it opened, in Python
142
+ and MATLAB; the file stayed open, and a later append to it failed. MATLAB's
143
+ `'compression', false` works; it always failed with an unknown command.
144
+
145
+ Appending two files in which one name is a parameter in one and a label
146
+ in the other is refused; it crashed the process. A `where` text with
147
+ parentheses nested deeper than 64 levels is refused; deep enough nesting
148
+ overflowed the stack and crashed. Appending to a file whose label codes
149
+ are int64, as §10.1 allows another writer to store them, works; it failed
150
+ with an internal error and left the file's columns of different lengths.
151
+
152
+ Values read from the file that locate others (a changing mesh's run
153
+ starts and instance numbers, cell offsets) are range-checked before any
154
+ arithmetic on them, so a damaged file is refused or reported (E21, E10)
155
+ instead of overflowing into wrong answers. `select` compares an int64
156
+ parameter or scalar exactly with an integer literal, one outside int64's
157
+ range included; as doubles, values above 2⁵³ that differ compared equal.
158
+ Appending a changing mesh's geometry to int64 mesh labels takes codes
159
+ beyond int32. BitRound no longer rounds a value near the largest finite float up
160
+ to infinity. Names are checked against ASCII letters and digits whatever
161
+ locale the host program has set.
162
+
163
+ E20 and E23 each meant two things a caller handles differently. E20 is
164
+ now storage the format forbids, and appending to a valid file whose
165
+ growable dimensions are fixed is E25: the file is read-only, and
166
+ `write_copy` makes one that grows. E23 is now an object a reader cannot
167
+ read (a chunk index it cannot use, a chunk of the wrong size, a filter
168
+ that fails), and E26 is a count past one of the caps in
169
+ `docs/security.md`. SPEC.md, the corpus and every language follow.
170
+
171
+ The C interface states the length of every array it reads and the
172
+ capacity of every array it fills, so the core checks what a binding passes
173
+ instead of trusting it: the points' count, the offsets' count (one more
174
+ than the cells), a model array's count against its shape, and each
175
+ nearest-model column's length. `gamut_shape` and `gamut_tree_array_info`
176
+ take the rank, the shape and the shape's capacity, in that order, and
177
+ refuse a capacity below the rank. `gamut_units_parse` is renamed
178
+ `gamut_units_valid`, which says what its 1 means; it is the one function
179
+ that does not return 0 on success.
180
+
181
+ A group reachable by more than one hard link is walked once; a chain of
182
+ such groups made validation, append, extract and `write_copy` take time
183
+ that doubled with every level. `/private` may hold them, as §8 lets it
184
+ hold any structure; model data, a tree, refuses them (E18). The category
185
+ tables read when a file opens share one budget, the cap one table had, and
186
+ so does validation; a model's arrays read together share one budget, the
187
+ cap one array had (E26). Many tables, or many arrays, each under its own
188
+ cap could make opening a file, or reading or validating a model,
189
+ allocate gigabytes.
190
+
191
+ The command line reports malformed input as `error: …` with the exit
192
+ status the guide gives instead of a Python traceback: a build or append
193
+ document that is not JSON exits 1, and `--zone` without `=`, a zone name
194
+ not in the label's table, or a `--rows` position outside the file exits 2.
195
+ `--rows` positions are checked by the core for every output type; for a
196
+ `.gamut` output, `-1` meant the last row.
197
+
198
+ The validator checks what SPEC.md says and it did not: a model-data
199
+ array without its dimension scale (E19); an integer that is not int64 or
200
+ a number that is not float64 in model data (E18, which reading model data
201
+ now refuses too); a string attribute stored wider than its UTF-8 bytes
202
+ (E3); a derivative with respect to `coordinates` along an axis the
203
+ coordinates do not have (E13); and the vector and axis of a result whose
204
+ statistic this version does not define. A required object or attribute
205
+ that is absent is E2, whatever rule its contents would break: a label's
206
+ `category`, the mesh's `kind` and `support_id`, a changing mesh's
207
+ `instance_nodes`, the cell arrays. A table's repeated entry is reported
208
+ once.
209
+
210
+ SPEC.md settles where an independent reader could diverge. A units string
211
+ holding a space outside ASCII, such as a no-break space pasted from a web
212
+ page, does not parse (E7). Names use ASCII letters and digits. A scale's
213
+ encoding is what a writer produces, not a rule. A root dataset without a
214
+ `role` is ignored (W5). A file whose growable dimensions are fixed, as
215
+ some netCDF writers leave them, is valid and read-only: appending says so
216
+ and points to `write_copy`, which makes a copy that grows.
217
+
218
+ The corpus requires each rule case to break exactly its rules, in Python,
219
+ Julia and MATLAB; it accepted any extra finding. Its rule cases no longer
220
+ break rules by accident, every valid data file is appended to itself, and
221
+ it gains a valid file of int64 columns and hostile files of several large
222
+ tables and a deeply nested `where`.
223
+
224
+ MATLAB takes `"double-quoted"` strings wherever it takes text, notes and
225
+ model numbers included, refuses sparse arrays, and takes cell arrays given
226
+ as any integer class, read by their class; integer arrays
227
+ that were not double were read as int64, past the end of their buffer.
228
+ An open study keeps the MATLAB gateway loaded, so `clear mex` no longer
229
+ orphans it, and MATLAB finishes open files when it exits. A refused call
230
+ frees what it built before MATLAB reports the error. In Julia, the
231
+ library's symbols are looked up under a lock from any thread, and a study
232
+ left to the garbage collector removes its temporary file as `close` does.
233
+
234
+ The guide and SPEC.md say what an append costs when it is interrupted: a
235
+ batch refused for its own data leaves the file as it was, every such
236
+ check being made before the first write, but a write that fails or a
237
+ process killed while it appends can leave the whole file unreadable. Keep a copy of a file you cannot regenerate, or append each
238
+ job's own file at the end.
239
+
240
+ In MATLAB, `[]` as rows, nodes or cells selects none and `':'` (or leaving
241
+ them out) every one; `[]` meant every row, so the rows of a `select` that
242
+ matched nothing read the whole file. MATLAB passes integer arrays to the
243
+ core in their own class, so int64 codes above 2⁵³ are not rounded through
244
+ double. An int64 model input given fractional or out-of-range values is
245
+ refused in Python and MATLAB, as in Julia, instead of being truncated; an
246
+ unsigned array above int64's range is refused instead of wrapping. Julia's
247
+ `open` closes the file when its `threads` value is refused. An integer
248
+ written to a float64 parameter, scalar, field or coordinate, or given to a
249
+ float64 model input, that a float64 cannot hold exactly (past 2⁵³) is
250
+ refused (E4) in every language; it was rounded silently. Opening a file that does not exist says so, rather than
251
+ that it is not HDF5. A column with the wrong number of values names it and
252
+ both counts. A Python `Study` prints its path, kind, type and row count.
253
+
254
+ A group member repeated with the same values in another shape (4 × 2 and
255
+ 2 × 4 points) is refused as a different geometry. Replacing an attribute
256
+ works however many one-character names the object already holds. The
257
+ validator checks a model-data scale's extent and attribute creation order
258
+ as it checks every scale. A `where` nested past the cap is E26, as the
259
+ limits in `docs/security.md` are. `gamut get` refuses to write over an
260
+ existing `.csv`, `.npy` or `.npz` output unless given `--force`, and
261
+ checks `--rows` without reading the rows.
262
+
263
+ A file whose `DIMENSION_LIST` attribute is not HDF5's list of dimension
264
+ scales (an integer, a string) is reported (E19) instead of crashing
265
+ `validate` and `open`: HDF5's scale calls follow the attribute as
266
+ references, and Gamut now checks its type before they read it.
267
+
268
+ ## 1.3.0 (2026-10-25)
269
+
270
+ A file may carry a portion marking: a new optional root attribute,
271
+ `portion_marking`, holding free text the model's author set, which the
272
+ format does not interpret (specification sections 8 and 10.1). `evaluate`
273
+ copies it into every file evaluated from a model file that has one
274
+ (section 7.4), and `extract` and `write_copy` keep it. It is set with
275
+ `create` and `create_model` in Python (`portion_marking`), MATLAB
276
+ (`'portionMarking'`) and Julia (`portion_marking`), with `set_attribute`
277
+ in C and C++, and with the `portion_marking` key of a `gamut build`
278
+ document; `gamut info` shows it. Readers of earlier versions report it as
279
+ an attribute this version does not define (W5) and go on; they do not copy
280
+ it into an evaluation or a copy, and their writers refuse it.
281
+
282
+ ## 1.2.0 (2026-10-23)
283
+
284
+ A file can no longer make a reader or a writer touch another path. A
285
+ dataset whose values HDF5 keeps outside the file, through external storage
286
+ or as a virtual dataset, passed validation and was read from that other
287
+ file (a constant as soon as the file opened), and appending wrote to it,
288
+ creating it if need be. Such a dataset is now an error (E22): the
289
+ validator reports it, opening refuses the file, and appending refuses it
290
+ before the file is opened to write. Appending also refuses a run-table
291
+ column that cannot grow (E20), before writing anything.
292
+
293
+ A read is refused (E23) before any buffer is sized when it would return
294
+ more than 2³⁰ values (8 GiB at 8 bytes a value); a file declaring billions
295
+ of rows made the Python, Julia and MATLAB layers allocate for them first.
296
+ The threaded reader no longer reads past its arrays when a field has fewer
297
+ axes than its selections.
298
+
299
+ The core reads compressed datasets itself, a chunk at a time, with one
300
+ path for one thread or many, and HDF5 is never handed a chunk it has not
301
+ checked. A chunk that is not stored whole, or that gzip does not inflate
302
+ to its full size, is refused (E23) on reading and before appending
303
+ rewrites it; HDF5 trusts these sizes, and a crafted chunk made it read past
304
+ its memory and return what lay there, or write past it when appending. A
305
+ filter other than gzip and shuffle is not read (E20). Reading a few
306
+ scattered rows or nodes is several times faster, up to twenty times on a
307
+ million-row table, and other reads and writes take the time they did.
308
+ Building the core needs HDF5 1.14 or later (every package carries 1.14.6).
309
+
310
+ MATLAB installs from a toolbox, `Gamut.mltbx`, attached to each release:
311
+ MATLAB R2023b or later on Linux, Windows or an Apple silicon Mac, and no
312
+ compiler. Each release also carries the core on its own for each platform
313
+ (`gamut-core-<version>-<platform>`, with its headers and CMake package) for
314
+ Julia, C and C++. Every package now includes the licences of HDF5 and zlib,
315
+ which the core links in.
316
+
317
+ Evaluating a damaged model file is refused in every language when the file
318
+ lacks what evaluation reads: a nearest model's numbers (`columns`, `lower`,
319
+ `upper`, `x`, `outputs`; E17), or a declaration such as a parameter's
320
+ bounds, a result's model or output, or a model's type or inputs. Python
321
+ and Julia raised a key error and MATLAB a field error instead. In Python
322
+ and Julia, `related` likewise refuses a quantile without its `q`.
323
+
324
+ Calls from several threads no longer crash. HDF5 keeps state for the whole
325
+ process and is built without its thread-safety option, so two threads each
326
+ reading its own file could bring it down; every call that reaches a file
327
+ now takes its turn. `threads=` remains the way to read one large field
328
+ faster.
329
+
330
+ Appending from C, C++ or MATLAB refuses a file of another major format
331
+ version, as reading already did, and does so before opening the file to
332
+ write, which leaves it byte for byte as it was.
333
+
334
+ Messages say more about what to fix. A number left as text (`"4.0"` from
335
+ a spreadsheet) is named as such, rather than reported as a missing value;
336
+ an illegal name is refused with what a legal one is; W2 counts the rows
337
+ of each status and says when no status is named `converged`, the one
338
+ name that marks a run as usable; W1 names the group a split divides.
339
+
340
+ The languages agree in more places. `gamut_create` from C, or
341
+ `gamut::create` from C++, with a mesh and no `per` is refused (E6), as
342
+ Python, MATLAB and Julia already refused it, where it took `fixed`. Cell
343
+ offsets of the wrong length are refused (E5) in Python, MATLAB and Julia
344
+ before the core reads them, as the C++ layer already did, and so are a
345
+ nearest model's inputs of unequal length. A parameter given only one of
346
+ `lower` and `upper` is refused (E16) in Python, MATLAB and Julia, where the
347
+ other was taken as 0 or the one given was dropped. An evaluation that
348
+ fails leaves no file behind, in every language. In Julia, a `true` note is stored as the integer 1,
349
+ and `related` keys a derivative along each coordinate axis separately, as
350
+ in Python.
351
+
352
+ A run table from a spreadsheet goes straight in: Python's `add_runs`
353
+ takes a pandas DataFrame and `add_run` one of its rows, and MATLAB's
354
+ `addRuns` takes a table, as `readtable` gives, with labels as text or
355
+ categorical.
356
+
357
+ The README and the guide start from the simplest study, a table of runs,
358
+ shown in Python, MATLAB, Julia and C++, and step up to a mesh from there;
359
+ the guide opens on the pattern every file follows and lists its pages by
360
+ what a study has. It shows how to publish a train and test split, says
361
+ that only `converged` marks a run as usable, that a new category name
362
+ extends a label's categories, and that unit names are checked only when a
363
+ read converts them. MATLAB can read a file with `ncread` and nothing
364
+ installed. Installing covers xarray, Xcode for MATLAB on macOS, and adds
365
+ the Julia package at the release tag that matches the wheel.
366
+
367
+ `help(gamut)` opens on the whole pattern, `help(gamut.create)` documents
368
+ every declaration, and `llms.txt` gives coding agents what Gamut is for,
369
+ the pattern, the mistakes that are easy to make, and every page.
370
+
371
+ A model in a file has its own step in the README and leads the models
372
+ page: a quadratic, of a model type defined in a few lines, is fitted in
373
+ Python to the README's sweep, saved, and evaluated in MATLAB by a MATLAB
374
+ class of the same type, with no Python involved; the built-in nearest
375
+ model is evaluated from Julia and C++. `help(gamut.create_model)` and
376
+ `llms.txt` show the whole model pattern and its easy mistakes, and
377
+ `help gamut.registerModelType` in MATLAB says what `call` receives and
378
+ returns. A model's `save` that returns an object which is not model data
379
+ is refused with the object named, where Python reported a
380
+ zero-dimensional array; so are a list mixing text with other values and
381
+ text with more than one axis, which Python flattened into strings (E18).
382
+ `gamut info` names the studies a model was trained on, and a model's
383
+ one-line description. `bench/scaling.py` times writes and reads as run
384
+ tables, one mesh and a mesh per run grow.
385
+
386
+ An answer from `evaluate` says, row by row, which inputs lie outside the
387
+ range the model was fitted on, and how far. The answer already kept the
388
+ model's bounds on its parameters; `outside(name)` measures each row's
389
+ value against them, in the parameter's units: 0 inside or on a bound,
390
+ negative below, positive above, NaN for a missing value. A parameter with
391
+ no bounds is refused rather than reported as inside. The core computes it
392
+ once (`gamut_outside` in C) for any data file, and it is `study.outside`
393
+ in Python, MATLAB and C++ and `Gamut.outside` in Julia. `evaluate` still
394
+ warns as well (W4). The models page and the MATLAB, Julia and C++ pages
395
+ show the call.
396
+
397
+ The validator holds a mesh's computed coordinates to the writer's rules: a
398
+ computing model only in a model file whose mesh is per run, which must
399
+ name one (E16), and that model existing with its output named (E17). The
400
+ writer now also refuses a computing model without an output. An array
401
+ holding more values than the validator reads, 2²⁶, is now reported as
402
+ unchecked (W11) rather than passed over in silence.
403
+
404
+ Opening a file takes about a third less time (0.81 to 0.51 ms on the
405
+ benchmarks' capsule study): the reader opens each object once, for its
406
+ attributes, type and shape together, and leaves the storage settings to the
407
+ validator.
408
+
409
+ ## 1.1.0 (2026-10-19)
410
+
411
+ A model file whose coordinates are computed by a model now evaluates. The
412
+ reader looked for the model's name only where coordinates are a group,
413
+ and section 10.3 stores them as a zero-length dataset, so evaluation asked
414
+ the caller for coordinates instead, and a copy of the file lost which
415
+ model computes them. The format is unchanged.
416
+
417
+ `study.model_numbers(id)` reads a model's stored numbers back as the
418
+ dictionary its `save` gave, whether or not its type is registered:
419
+ `modelNumbers` in MATLAB and `Gamut.model_numbers` in Julia. C programs
420
+ already had `gamut_model_numbers`.
421
+
422
+ ## 1.0.0 (2026-10-18)
423
+
424
+ Version 1 of the format, `gamut/1`, and a new implementation. Before
425
+ version 1 the format and its libraries were called mestra (the 0.1 series
426
+ on PyPI).
427
+
428
+ The format ([SPEC.md](https://bijective-systems-org.github.io/gamut/reference/specification.html)) is redesigned around the study: a run
429
+ table with roles, statuses and splits; a shared mesh whose geometry is
430
+ fixed, per run or per member of a family, or a mesh per run; fields as one
431
+ block of runs × nodes; uncertainty and derivatives as relations; and model
432
+ files of the same shape as data. Files of the earlier `mestra/0` are
433
+ refused with a message naming their version.
434
+
435
+ A C++ core with a C interface implements the format, and Python, MATLAB
436
+ and Julia are layers over it, so every language gives the same answers.
437
+ The Python wheels link HDF5 statically, so the core never meets another
438
+ HDF5 in the same process.
439
+
440
+ Python has `create`, `open`, `append`, `extract`, `write_copy`,
441
+ `validate`, `create_model` and `evaluate`; the `gamut` command line
442
+ (`info`, `validate`, `build`, `append`, `get`); and `gamut-vtk`, which
443
+ turns a folder of VTK runs into a study and back. MATLAB, Julia and C++
444
+ (the header-only `gamut/gamut.hpp`) have the same calls in each language's
445
+ spelling, and C programs use the C interface, `gamut/gamut.h`.
446
+
447
+ Readers check sizes before using memory, never follow links and load no
448
+ HDF5 plugins ([security](https://bijective-systems-org.github.io/gamut/security.html)). The guide has fourteen pages,
449
+ each held to a runnable example by the tests, and the
450
+ [benchmarks](https://bijective-systems-org.github.io/gamut/benchmarks.html) cover two studies.
@@ -0,0 +1,34 @@
1
+ cff-version: 1.2.0
2
+ message: >-
3
+ If you use Gamut in published work, please cite it as below, and
4
+ name the format version your files declare (the root attribute
5
+ `format`, for example "gamut/1").
6
+ title: "Gamut: an open file format for simulation studies"
7
+ type: software
8
+ version: 2.1.0
9
+ date-released: 2026-10-30
10
+ authors:
11
+ - name: "Bijective Systems"
12
+ repository-code: "https://github.com/bijective-systems-org/gamut"
13
+ url: "https://github.com/bijective-systems-org/gamut"
14
+ license: Apache-2.0
15
+ abstract: >-
16
+ Gamut is an open file format for the results of a parametric
17
+ simulation study and the models fitted to it. One HDF5 file, laid
18
+ out as a valid netCDF-4 file, holds the run table (parameters with
19
+ roles, labels, statuses, splits and scalar results, every number with
20
+ its units), the mesh the fields live on, the fields themselves as one
21
+ block of runs by nodes when the runs share a mesh, uncertainty and
22
+ gradients beside the values they describe, and fitted models in the
23
+ same terms as the data. One C++ core, with Python, MATLAB, Julia, C and
24
+ Rust interfaces, implements the specification, and a shared corpus of
25
+ valid, broken and hostile files holds every language to it.
26
+ keywords:
27
+ - simulation data
28
+ - parametric study
29
+ - surrogate model
30
+ - HDF5
31
+ - netCDF-4
32
+ - mesh
33
+ - scientific machine learning
34
+ - uncertainty quantification