ncarnate 2.2.0__tar.gz → 2.2.2__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.
- ncarnate-2.2.2/.github/workflows/ci.yml +157 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/.github/workflows/publish.yml +8 -7
- {ncarnate-2.2.0 → ncarnate-2.2.2}/.gitignore +1 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/CHANGELOG.md +37 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/CITATION.cff +3 -3
- {ncarnate-2.2.0 → ncarnate-2.2.2}/CODE_OF_CONDUCT.md +1 -1
- {ncarnate-2.2.0 → ncarnate-2.2.2}/PKG-INFO +59 -19
- {ncarnate-2.2.0 → ncarnate-2.2.2}/README.md +51 -14
- ncarnate-2.2.2/brand/README.md +81 -0
- ncarnate-2.2.2/brand/build_lockup.py +90 -0
- ncarnate-2.2.2/brand/build_raster_lockup.py +149 -0
- ncarnate-2.2.2/brand/compose_logo.py +215 -0
- ncarnate-2.2.2/brand/globe.py +216 -0
- ncarnate-2.2.2/brand/ncarnate-lockup-dark.png +0 -0
- ncarnate-2.2.2/brand/ncarnate-lockup-dark.svg +1 -0
- ncarnate-2.2.2/brand/ncarnate-lockup.png +0 -0
- ncarnate-2.2.2/brand/ncarnate-lockup.svg +1 -0
- ncarnate-2.2.2/brand/ncarnate-mark.png +0 -0
- ncarnate-2.2.2/brand/ncarnate-mono.svg +1 -0
- ncarnate-2.2.2/brand/ncarnate-source.png +0 -0
- ncarnate-2.2.2/brand/ncarnate-white.svg +1 -0
- ncarnate-2.2.2/brand/ncarnate-wordmark-white.svg +1 -0
- ncarnate-2.2.2/brand/ncarnate-wordmark.svg +1 -0
- ncarnate-2.2.2/brand/ncarnate.svg +1 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/conda-recipe/recipe.yaml +5 -2
- {ncarnate-2.2.0 → ncarnate-2.2.2}/docs/conf.py +7 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/docs/fidelity-notes.md +39 -29
- ncarnate-2.2.2/docs/index.rst +30 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/__init__.py +28 -0
- ncarnate-2.2.2/ncarnate/atttypes.py +257 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/audit/__init__.py +17 -1
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/audit/classify.py +1 -0
- ncarnate-2.2.2/ncarnate/audit/codes.py +113 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/audit/inspect.py +17 -6
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/cli.py +6 -2
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/constants.py +1 -1
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/convert/__init__.py +133 -61
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/convert/models.py +18 -1
- ncarnate-2.2.2/ncarnate/convert/preflight.py +298 -0
- ncarnate-2.2.2/ncarnate/convert/report.py +114 -0
- ncarnate-2.2.2/ncarnate/core.py +1198 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/errors.py +36 -0
- ncarnate-2.2.2/ncarnate/handoff.py +297 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/hdf4.py +4 -1
- ncarnate-2.2.2/ncarnate/hdf4_runtime.py +81 -0
- ncarnate-2.2.2/ncarnate/result.py +673 -0
- ncarnate-2.2.2/ncarnate/schemas/__init__.py +15 -0
- ncarnate-2.2.2/ncarnate/schemas/handoff.schema.json +276 -0
- ncarnate-2.2.2/ncarnate/stage.py +114 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/paper/paper.md +11 -8
- {ncarnate-2.2.0 → ncarnate-2.2.2}/pyproject.toml +18 -6
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_codes.py +6 -1
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/conftest.py +3 -1
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/convert/test_integrity.py +8 -3
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/amsre_5daysnow_trim.provenance.json +1 -1
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/amsre_seaice12km_trim.hdf +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/amsre_seaice12km_trim.provenance.json +1 -1
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/mod03_trim.hdf +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/mod03_trim.provenance.json +1 -1
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/myd05_trim.hdf +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/myd05_trim.provenance.json +1 -1
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/raingrid_trim.provenance.json +1 -1
- ncarnate-2.2.2/tests/fixtures/operation_result/amsre_seaice12km.result.json +575 -0
- ncarnate-2.2.2/tests/fixtures/operation_result/packed_fill.canonical.json +1 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/trim_hdfeos2.py +2 -2
- ncarnate-2.2.2/tests/test_attribute_types.py +210 -0
- ncarnate-2.2.2/tests/test_convert_collisions.py +336 -0
- ncarnate-2.2.2/tests/test_convert_inplace_collisions.py +237 -0
- ncarnate-2.2.2/tests/test_g5_handoff_sufficiency.py +217 -0
- ncarnate-2.2.2/tests/test_handoff_gate.py +138 -0
- ncarnate-2.2.2/tests/test_handoff_schema.py +148 -0
- ncarnate-2.2.2/tests/test_operation_result.py +178 -0
- ncarnate-2.2.2/tests/test_optional_hdf4.py +413 -0
- ncarnate-2.2.2/tests/test_plan_hash.py +91 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/test_raw_granules.py +1 -1
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/test_recompress_netcdf.py +27 -0
- ncarnate-2.2.2/tests/test_sdist_contents.py +139 -0
- ncarnate-2.2.2/tests/test_stage_api_4b.py +154 -0
- ncarnate-2.2.2/tests/test_stage_api_g4.py +128 -0
- ncarnate-2.2.2/tools/validate_distributions.py +80 -0
- ncarnate-2.2.0/.github/workflows/ci.yml +0 -73
- ncarnate-2.2.0/docs/index.rst +0 -9
- ncarnate-2.2.0/ncarnate/audit/codes.py +0 -56
- ncarnate-2.2.0/ncarnate/convert/report.py +0 -59
- ncarnate-2.2.0/ncarnate/core.py +0 -505
- {ncarnate-2.2.0 → ncarnate-2.2.2}/.gitattributes +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/.github/FUNDING.yml +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/.readthedocs.yaml +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/.zenodo.json +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/CONTRIBUTING.md +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/LICENSE +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/conda-recipe/README.md +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/docs/api.rst +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/docs/requirements.txt +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/__main__.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/audit/models.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/audit/report.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/convert/integrity.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/convert/reader.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/discovery.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/eos/__init__.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/eos/gctp.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/eos/grid.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/eos/structmeta.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/eos/swath.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/formats.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/hashing.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/ncarnate/limits.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/paper/paper.bib +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/__init__.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/record.schema.json +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_agreement.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_audit_path.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_checksum.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_classify.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_cli_dispatch.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_contract.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_emission.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_error_codes.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_inspect.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_models.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_readonly.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/audit/test_scaffold.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/convert/__init__.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/convert/test_agreement.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/convert/test_cli_dispatch.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/convert/test_cli_integration.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/convert/test_convert_manifest.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/convert/test_inplace_skip.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/convert/test_models.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/convert/test_reader.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/convert/test_summary_exit.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/blockers/compound_type.nc +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/amsre_5daysnow_trim.hdf +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/hdfeos2/raingrid_trim.hdf +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/netcdf/endianness.nc +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/netcdf/nested_groups.nc +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/netcdf/packed_fill.nc +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/data/netcdf/unlimited_dim.nc +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/make_blocker_fixtures.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/fixtures/make_fixtures.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/test_cli.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/test_discovery.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/test_eos_units.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/test_formats.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/test_hdf4_conversion.py +0 -0
- {ncarnate-2.2.0 → ncarnate-2.2.2}/tests/test_limits.py +0 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
# The pip-based jobs (test, test-windows-degraded, build) install no system
|
|
10
|
+
# HDF libraries on purpose: the netCDF4 wheels bundle libhdf5/libnetcdf, and
|
|
11
|
+
# the pyhdf >= 0.11.6 manylinux wheels bundle libhdf4 (verified against PyPI
|
|
12
|
+
# 2026-07-08 for cp310-cp313). Platforms without a pyhdf wheel (e.g.
|
|
13
|
+
# linux-aarch64) need a system libhdf4 for the sdist build instead. The
|
|
14
|
+
# one deliberate exception is test-conda-forge, which provisions the whole
|
|
15
|
+
# runtime stack (incl. a system-HDF4-linked pyhdf) from conda-forge to prove
|
|
16
|
+
# the *supported* install channel delivers full HDF4 capability (KD-L7, G2).
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
lint:
|
|
20
|
+
name: ruff
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
|
|
24
|
+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
25
|
+
with:
|
|
26
|
+
python-version: "3.12"
|
|
27
|
+
- name: Install ruff
|
|
28
|
+
# Minor-pinned so a future ruff release can't red a green branch with
|
|
29
|
+
# no code change (new lint rules land in version bumps). Bump
|
|
30
|
+
# deliberately; keep in step with the version developers run locally.
|
|
31
|
+
run: python -m pip install "ruff==0.15.*"
|
|
32
|
+
- name: Run ruff
|
|
33
|
+
run: ruff check .
|
|
34
|
+
|
|
35
|
+
test:
|
|
36
|
+
name: pytest (${{ matrix.os }} py${{ matrix.python-version }})
|
|
37
|
+
runs-on: ${{ matrix.os }}
|
|
38
|
+
strategy:
|
|
39
|
+
fail-fast: false
|
|
40
|
+
matrix:
|
|
41
|
+
os: [ubuntu-latest]
|
|
42
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
43
|
+
# Smoke the self-contained-wheel install story (pyhdf + netCDF4 +
|
|
44
|
+
# pyproj, no system HDF libs) on macOS too. Windows is deliberately
|
|
45
|
+
# excluded from the *full* matrix: pyhdf's Windows wheel ships no
|
|
46
|
+
# HDF4 runtime, so the HDF4 fixtures can't run there. Windows-pip
|
|
47
|
+
# instead gets the dedicated degraded-capability job below
|
|
48
|
+
# (KD-L4/KD-L7), which pins exactly what must keep working — and
|
|
49
|
+
# exactly how the HDF4 path must refuse — without a usable pyhdf.
|
|
50
|
+
include:
|
|
51
|
+
- os: macos-latest
|
|
52
|
+
python-version: "3.12"
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
|
|
55
|
+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
56
|
+
with:
|
|
57
|
+
python-version: ${{ matrix.python-version }}
|
|
58
|
+
- name: Install package with test extras
|
|
59
|
+
run: |
|
|
60
|
+
python -m pip install --upgrade pip
|
|
61
|
+
python -m pip install -e ".[test]"
|
|
62
|
+
- name: Run tests (offline; raw-granule marks self-skip)
|
|
63
|
+
run: python -m pytest -q
|
|
64
|
+
|
|
65
|
+
test-windows-degraded:
|
|
66
|
+
name: pytest degraded-capability (windows pip)
|
|
67
|
+
runs-on: windows-latest
|
|
68
|
+
steps:
|
|
69
|
+
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
|
|
70
|
+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
71
|
+
with:
|
|
72
|
+
python-version: "3.12"
|
|
73
|
+
- name: Install package with test extras (pip wheels only)
|
|
74
|
+
# pyhdf's Windows wheel *installs* but cannot *import* (it ships no
|
|
75
|
+
# HDF4 runtime) — which is precisely the degraded environment this
|
|
76
|
+
# job exists to pin (KD-L4/KD-L7, gate G2).
|
|
77
|
+
run: |
|
|
78
|
+
python -m pip install --upgrade pip
|
|
79
|
+
python -m pip install -e ".[test]"
|
|
80
|
+
- name: Degraded-capability contract (tests/test_optional_hdf4.py)
|
|
81
|
+
# The purpose-built matrix: import / --help / --version / detection /
|
|
82
|
+
# netCDF recompression / audits all work without a usable pyhdf; an
|
|
83
|
+
# HDF4 attempt refuses with the stable HDF4_RUNTIME_UNAVAILABLE
|
|
84
|
+
# message before output creation, never an import traceback.
|
|
85
|
+
run: python -m pytest tests/test_optional_hdf4.py -q
|
|
86
|
+
|
|
87
|
+
test-conda-forge:
|
|
88
|
+
name: pytest full-capability (conda-forge)
|
|
89
|
+
runs-on: ubuntu-latest
|
|
90
|
+
# conda-forge is the *supported* HDF4 install channel (the refusal and
|
|
91
|
+
# the README point Windows/aarch64 users at `conda install -c
|
|
92
|
+
# conda-forge ncarnate`). This lane proves that channel actually
|
|
93
|
+
# delivers the full HDF4/HDF-EOS2 capability — a promise the pip matrix
|
|
94
|
+
# cannot make, since it relies on the bundled-libhdf4 pyhdf wheels
|
|
95
|
+
# rather than the conda-forge pyhdf built against a system HDF4
|
|
96
|
+
# (KD-L7 / priority step 2.5; gate G2 cannot close without it).
|
|
97
|
+
defaults:
|
|
98
|
+
run:
|
|
99
|
+
shell: bash -el {0}
|
|
100
|
+
steps:
|
|
101
|
+
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
|
|
102
|
+
- name: Provision a conda-forge full-capability environment
|
|
103
|
+
uses: mamba-org/setup-micromamba@d7c9bd84e824b79d2af72a2d4196c7f4300d3476 # v3.0.0
|
|
104
|
+
with:
|
|
105
|
+
environment-name: ncarnate-cf
|
|
106
|
+
# The runtime stack comes wholly from conda-forge — pyhdf here is
|
|
107
|
+
# built against a real HDF4 library, unlike the pip wheel's
|
|
108
|
+
# bundled runtime. Pin Python; float the rest to what the channel
|
|
109
|
+
# currently ships (the point is the channel's promise, not a
|
|
110
|
+
# frozen resolution).
|
|
111
|
+
condarc: |
|
|
112
|
+
channels:
|
|
113
|
+
- conda-forge
|
|
114
|
+
channel_priority: strict
|
|
115
|
+
create-args: >-
|
|
116
|
+
python=3.12
|
|
117
|
+
pyhdf
|
|
118
|
+
netCDF4
|
|
119
|
+
numpy
|
|
120
|
+
pyproj
|
|
121
|
+
tqdm
|
|
122
|
+
pytest
|
|
123
|
+
- name: Install ncarnate from source without replacing native deps
|
|
124
|
+
# --no-deps: install only ncarnate; the native stack (pyhdf,
|
|
125
|
+
# netCDF4, numpy, pyproj, tqdm) stays the conda-forge build. Build
|
|
126
|
+
# isolation still fetches the hatchling backend into a throwaway
|
|
127
|
+
# env, which never touches the runtime dependencies above.
|
|
128
|
+
run: python -m pip install -e . --no-deps
|
|
129
|
+
- name: Prove the conda-forge HDF4 runtime imports
|
|
130
|
+
run: |
|
|
131
|
+
python -c "from ncarnate.hdf4_runtime import require_hdf4_runtime; require_hdf4_runtime(); print('conda-forge HDF4 runtime OK')"
|
|
132
|
+
- name: Full suite — HDF4 import, conversion, audit, manifest on real fixtures
|
|
133
|
+
# The committed HDF4/HDF-EOS2 fixtures drive real conversions here
|
|
134
|
+
# (unlike the pip matrix's bundled-wheel path), exercising the
|
|
135
|
+
# supported channel end to end. Raw-granule tests self-skip offline.
|
|
136
|
+
run: python -m pytest -q
|
|
137
|
+
|
|
138
|
+
build:
|
|
139
|
+
name: build + twine check
|
|
140
|
+
runs-on: ubuntu-latest
|
|
141
|
+
steps:
|
|
142
|
+
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
|
|
143
|
+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
144
|
+
with:
|
|
145
|
+
python-version: "3.12"
|
|
146
|
+
- name: Install build tooling
|
|
147
|
+
run: python -m pip install build twine
|
|
148
|
+
- name: Build sdist + wheel
|
|
149
|
+
run: python -m build
|
|
150
|
+
- name: Check distributions
|
|
151
|
+
run: twine check dist/*
|
|
152
|
+
- name: Validate distributions
|
|
153
|
+
run: >-
|
|
154
|
+
python tools/validate_distributions.py
|
|
155
|
+
--allowed-email dev@erickshepherd.com
|
|
156
|
+
--allowed-email ca@trufo.ai
|
|
157
|
+
dist/*
|
|
@@ -31,8 +31,8 @@ jobs:
|
|
|
31
31
|
name: Build sdist + wheel
|
|
32
32
|
runs-on: ubuntu-latest
|
|
33
33
|
steps:
|
|
34
|
-
- uses: actions/checkout@
|
|
35
|
-
- uses: actions/setup-python@
|
|
34
|
+
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
|
|
35
|
+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
36
36
|
with:
|
|
37
37
|
python-version: "3.12"
|
|
38
38
|
- name: Build
|
|
@@ -40,7 +40,8 @@ jobs:
|
|
|
40
40
|
python -m pip install --upgrade build twine
|
|
41
41
|
python -m build
|
|
42
42
|
python -m twine check dist/*
|
|
43
|
-
|
|
43
|
+
python tools/validate_distributions.py --allowed-email dev@erickshepherd.com --allowed-email ca@trufo.ai dist/*
|
|
44
|
+
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
|
44
45
|
with:
|
|
45
46
|
name: dist
|
|
46
47
|
path: dist/
|
|
@@ -54,11 +55,11 @@ jobs:
|
|
|
54
55
|
permissions:
|
|
55
56
|
id-token: write
|
|
56
57
|
steps:
|
|
57
|
-
- uses: actions/download-artifact@
|
|
58
|
+
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
|
58
59
|
with:
|
|
59
60
|
name: dist
|
|
60
61
|
path: dist/
|
|
61
|
-
- uses: pypa/gh-action-pypi-publish@
|
|
62
|
+
- uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
|
|
62
63
|
with:
|
|
63
64
|
repository-url: https://test.pypi.org/legacy/
|
|
64
65
|
skip-existing: true # re-dispatching a dry run for an existing version won't fail
|
|
@@ -72,8 +73,8 @@ jobs:
|
|
|
72
73
|
permissions:
|
|
73
74
|
id-token: write
|
|
74
75
|
steps:
|
|
75
|
-
- uses: actions/download-artifact@
|
|
76
|
+
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
|
76
77
|
with:
|
|
77
78
|
name: dist
|
|
78
79
|
path: dist/
|
|
79
|
-
- uses: pypa/gh-action-pypi-publish@
|
|
80
|
+
- uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
|
|
@@ -4,6 +4,43 @@ All notable changes to this project are documented here. The format is
|
|
|
4
4
|
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
5
5
|
this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [2.2.2] - 2026-07-21
|
|
8
|
+
|
|
9
|
+
Maintenance release with packaging and CI updates. Conversion, recompression,
|
|
10
|
+
audit, and verification behavior are unchanged.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Standardized package metadata and normalized bundled test fixtures.
|
|
14
|
+
- Added automated distribution-policy validation.
|
|
15
|
+
- Pinned GitHub Actions to reviewed immutable commits and current Node 24 action
|
|
16
|
+
releases.
|
|
17
|
+
- Kept the bundled conda recipe on the last published PyPI artifact; its version
|
|
18
|
+
and source hash will be advanced together in a post-publication update after
|
|
19
|
+
the definitive 2.2.2 sdist exists.
|
|
20
|
+
|
|
21
|
+
## [2.2.1] - 2026-07-12
|
|
22
|
+
|
|
23
|
+
A documentation and branding patch. **No functional changes** to conversion,
|
|
24
|
+
recompression, or verification behavior — the fidelity guarantee is unchanged;
|
|
25
|
+
this release only describes it more precisely and dresses up the project.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- Fidelity wording now matches what the verifier enforces. The data-preservation
|
|
29
|
+
guarantee is described as **value-identical** — bit-for-bit for integer, char,
|
|
30
|
+
and packed data; NaN- and signed-zero-insensitive for floating-point/complex — in the
|
|
31
|
+
README, `docs/fidelity-notes.md`, and the HDF4 module docstring, replacing the
|
|
32
|
+
blanket "bit-identical" (which overstated the guarantee for float data, since
|
|
33
|
+
the verifiers compare floats with `numpy.array_equal(equal_nan=True)`).
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
- Brand assets and a theme-aware README logo lockup (light/dark), plus live
|
|
37
|
+
PyPI, conda-forge, and Read the Docs badges.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- Read the Docs build stays green under current `myst-parser` by suppressing the
|
|
41
|
+
`myst.header` advisory (the strict `fail_on_warning` build would otherwise fail
|
|
42
|
+
on the README-derived landing page).
|
|
43
|
+
|
|
7
44
|
## [2.2.0] - 2026-07-11
|
|
8
45
|
|
|
9
46
|
The archive-modernization workflow: a read-only **audit** that plans a
|
|
@@ -9,10 +9,10 @@ type: software
|
|
|
9
9
|
authors:
|
|
10
10
|
- family-names: "Shepherd"
|
|
11
11
|
given-names: "Erick Edward"
|
|
12
|
-
email: "
|
|
12
|
+
email: "dev@erickshepherd.com"
|
|
13
13
|
orcid: "https://orcid.org/0000-0002-4750-6100"
|
|
14
|
-
version: "2.2.
|
|
15
|
-
date-released: "2026-07-
|
|
14
|
+
version: "2.2.2"
|
|
15
|
+
date-released: "2026-07-21"
|
|
16
16
|
doi: "10.5281/zenodo.21288802"
|
|
17
17
|
license: MIT
|
|
18
18
|
repository-code: "https://github.com/ErickShepherd/ncarnate"
|
|
@@ -57,7 +57,7 @@ an individual is officially representing the community in public spaces.
|
|
|
57
57
|
|
|
58
58
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
59
59
|
reported to the community leaders responsible for enforcement at
|
|
60
|
-
**
|
|
60
|
+
**dev@erickshepherd.com**. All complaints will be reviewed and investigated
|
|
61
61
|
promptly and fairly.
|
|
62
62
|
|
|
63
63
|
All community leaders are obligated to respect the privacy and security of the
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ncarnate
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: Convert HDF4/HDF-EOS2 (MODIS, AMSR-E) to CF-annotated netCDF4 and losslessly recompress netCDF/HDF5 files.
|
|
5
5
|
Project-URL: Homepage, https://github.com/ErickShepherd/ncarnate
|
|
6
|
-
Project-URL: Documentation, https://ncarnate.readthedocs.io/en/
|
|
6
|
+
Project-URL: Documentation, https://ncarnate.readthedocs.io/en/stable/
|
|
7
7
|
Project-URL: Source, https://github.com/ErickShepherd/ncarnate
|
|
8
|
-
Project-URL:
|
|
9
|
-
|
|
8
|
+
Project-URL: Changelog, https://github.com/ErickShepherd/ncarnate/releases
|
|
9
|
+
Project-URL: Issues, https://github.com/ErickShepherd/ncarnate/issues
|
|
10
|
+
Author-email: Erick Shepherd <dev@erickshepherd.com>
|
|
10
11
|
License-Expression: MIT
|
|
11
12
|
License-File: LICENSE
|
|
12
13
|
Keywords: AMSR-E,CF conventions,GIS,HDF-EOS,HDF-EOS2,HDF4,HDF5,MODIS,atmospheric science,compression,geolocation,grid,netCDF,netCDF4,recompression,remote sensing,swath,xarray
|
|
13
|
-
Classifier: Development Status ::
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
15
|
Classifier: Intended Audience :: Science/Research
|
|
15
16
|
Classifier: Operating System :: MacOS
|
|
16
17
|
Classifier: Operating System :: Microsoft :: Windows
|
|
@@ -29,12 +30,22 @@ Requires-Dist: pyhdf>=0.11.6
|
|
|
29
30
|
Requires-Dist: pyproj>=3.6
|
|
30
31
|
Requires-Dist: tqdm>=4.66
|
|
31
32
|
Provides-Extra: test
|
|
33
|
+
Requires-Dist: build>=1.0; extra == 'test'
|
|
34
|
+
Requires-Dist: hatchling>=1.27; extra == 'test'
|
|
32
35
|
Requires-Dist: pytest; extra == 'test'
|
|
33
36
|
Description-Content-Type: text/markdown
|
|
34
37
|
|
|
35
|
-
|
|
38
|
+
<p align="center">
|
|
39
|
+
<picture>
|
|
40
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ErickShepherd/ncarnate/main/brand/ncarnate-lockup-dark.png">
|
|
41
|
+
<img alt="ncarnate" src="https://raw.githubusercontent.com/ErickShepherd/ncarnate/main/brand/ncarnate-lockup.png" width="460">
|
|
42
|
+
</picture>
|
|
43
|
+
</p>
|
|
36
44
|
|
|
37
45
|
[](https://github.com/ErickShepherd/ncarnate/actions/workflows/ci.yml)
|
|
46
|
+
[](https://pypi.org/project/ncarnate/)
|
|
47
|
+
[](https://anaconda.org/conda-forge/ncarnate)
|
|
48
|
+
[](https://ncarnate.readthedocs.io/en/latest/)
|
|
38
49
|
[](https://github.com/ErickShepherd/ncarnate/blob/main/LICENSE)
|
|
39
50
|
[](https://pypi.org/project/ncarnate/)
|
|
40
51
|
[](https://doi.org/10.5281/zenodo.21288802)
|
|
@@ -66,7 +77,8 @@ Reach for ncarnate if you are trying to:
|
|
|
66
77
|
filter without altering a single stored value.
|
|
67
78
|
- **Shrink an archive of scientific files** without risking the science: every
|
|
68
79
|
output is verified value-for-value against its source before it replaces
|
|
69
|
-
anything, and stored values round-trip bit-
|
|
80
|
+
anything, and stored values round-trip value-identically (bit-for-bit for
|
|
81
|
+
integer and packed data; NaN- and signed-zero-insensitive for floating point).
|
|
70
82
|
- **Batch-convert a directory tree** of legacy granules to modern netCDF4 in one
|
|
71
83
|
command.
|
|
72
84
|
|
|
@@ -74,12 +86,20 @@ Reach for ncarnate if you are trying to:
|
|
|
74
86
|
|
|
75
87
|
Converting or recompressing a file changes *storage*, never *science data*:
|
|
76
88
|
|
|
77
|
-
- Every variable's stored values are preserved **
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
- Every variable's stored values are preserved **value-identically** — bit-for-bit
|
|
90
|
+
for integer and packed data; for floating-point data, distinct NaN
|
|
91
|
+
bit-patterns and `-0.0`/`+0.0` compare equal. Packed integers stay packed;
|
|
92
|
+
`scale_factor`/`add_offset`/`_FillValue` are carried across as declarations,
|
|
93
|
+
never applied.
|
|
94
|
+
- Every dimension (including unlimited-ness), attribute (including its exact
|
|
95
|
+
storage type — an `NC_STRING` scalar stays `NC_STRING`, verified via netCDF-C
|
|
96
|
+
type inquiry), and group survives. HDF-EOS2 `StructMetadata` is preserved
|
|
97
|
+
verbatim; names netCDF cannot hold are sanitized with the original recorded in
|
|
98
|
+
a companion attribute.
|
|
99
|
+
- **Complex-valued variables (`complex64`/`complex128`) are excluded** from the
|
|
100
|
+
fidelity guarantee: netCDF stores them as compound types, which ncarnate
|
|
101
|
+
**refuses loudly** with the stable `UNSUPPORTED_TYPE` error rather than
|
|
102
|
+
guessing at a lossy copy. Complex support is a later, evidence-backed feature.
|
|
83
103
|
- Geolocation reconstruction is strictly **additive**: the original information
|
|
84
104
|
always rides along, so the conversion never becomes the only copy of the
|
|
85
105
|
truth. Swath coordinates are attached to variables whose first two axes are
|
|
@@ -120,11 +140,18 @@ required. On platforms without a repaired `pyhdf` wheel (e.g. Linux aarch64),
|
|
|
120
140
|
building from sdist requires the system HDF4 library first (Debian/Ubuntu:
|
|
121
141
|
`apt install libhdf4-dev`).
|
|
122
142
|
|
|
123
|
-
**Windows via pip:** the netCDF/HDF5
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
143
|
+
**Windows via pip:** PyPI wheels give you the full netCDF/HDF5 surface —
|
|
144
|
+
`import ncarnate`, the CLI (`--help`/`--version`), format detection, audits,
|
|
145
|
+
manifest runs, and verified recompression — but **not** HDF4/HDF-EOS2
|
|
146
|
+
*conversion*: `pyhdf`'s Windows wheel ships no HDF4 runtime. An HDF4 attempt
|
|
147
|
+
is refused cleanly **before any output is created** with the stable
|
|
148
|
+
`[HDF4_RUNTIME_UNAVAILABLE]` message naming the detected cause, the
|
|
149
|
+
capabilities that still work, and the fix — never an unexplained import
|
|
150
|
+
traceback. An audit of an archive containing HDF4 files still completes,
|
|
151
|
+
recording those files as `unsupported` with the same code. For HDF4 on
|
|
152
|
+
Windows use the conda-forge install above (or **WSL** with the pip
|
|
153
|
+
instructions); a dedicated CI job pins this degraded-capability contract on
|
|
154
|
+
every change.
|
|
128
155
|
|
|
129
156
|
## Command line usage
|
|
130
157
|
|
|
@@ -199,6 +226,19 @@ The end-of-run summary counts converted / skipped / failed with reasons, and the
|
|
|
199
226
|
exit code is non-zero **iff** a selected record failed — so a partial failure on
|
|
200
227
|
a terabyte run surfaces loudly instead of silently mis-converting.
|
|
201
228
|
|
|
229
|
+
**Destination collision preflight.** Before any directory or output file is
|
|
230
|
+
created, every selected record's destination is computed up front — from the
|
|
231
|
+
source's *detected bytes*, never the manifest's declared format — and any
|
|
232
|
+
collision refuses the **entire run** with exit code 2 and a stable
|
|
233
|
+
`[DESTINATION_COLLISION]` message on stderr listing every involved source and
|
|
234
|
+
the contested destination. No last-writer-wins, no auto-rename, no partial
|
|
235
|
+
proceed, and nothing is written. Refused collisions include: two records
|
|
236
|
+
landing on one output path (e.g. an `a.hdf` → `a.nc` conversion next to a real
|
|
237
|
+
`a.nc` sibling), case-fold-equivalent names (one file on NTFS/APFS), duplicate
|
|
238
|
+
records for one source, an output tree overlapping a source tree (symlinks
|
|
239
|
+
resolved), and a pre-existing destination unless you pass `--skip-existing` to
|
|
240
|
+
resume.
|
|
241
|
+
|
|
202
242
|
## Library usage
|
|
203
243
|
|
|
204
244
|
```python
|
|
@@ -228,7 +268,7 @@ The AMSR-E daily 12.5 km sea-ice granule this project grew up around:
|
|
|
228
268
|
| netCDF4 recompression (`--complevel 9`) | 42.6 MB | 19.9 MB |
|
|
229
269
|
| HDF-EOS2 → netCDF4 (+ reconstructed lat/lon) | 60.2 MB | 35.5 MB |
|
|
230
270
|
|
|
231
|
-
Both outputs re-read
|
|
271
|
+
Both outputs re-read value-identically to their sources; the conversion
|
|
232
272
|
additionally carries CF `polar_stereographic` grid mappings and coordinates for
|
|
233
273
|
both hemispheric grids. The northern grid's reconstructed latitudes/longitudes
|
|
234
274
|
agree with The HDF Group's independent conversion of the same granule to within
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ErickShepherd/ncarnate/main/brand/ncarnate-lockup-dark.png">
|
|
4
|
+
<img alt="ncarnate" src="https://raw.githubusercontent.com/ErickShepherd/ncarnate/main/brand/ncarnate-lockup.png" width="460">
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
2
7
|
|
|
3
8
|
[](https://github.com/ErickShepherd/ncarnate/actions/workflows/ci.yml)
|
|
9
|
+
[](https://pypi.org/project/ncarnate/)
|
|
10
|
+
[](https://anaconda.org/conda-forge/ncarnate)
|
|
11
|
+
[](https://ncarnate.readthedocs.io/en/latest/)
|
|
4
12
|
[](https://github.com/ErickShepherd/ncarnate/blob/main/LICENSE)
|
|
5
13
|
[](https://pypi.org/project/ncarnate/)
|
|
6
14
|
[](https://doi.org/10.5281/zenodo.21288802)
|
|
@@ -32,7 +40,8 @@ Reach for ncarnate if you are trying to:
|
|
|
32
40
|
filter without altering a single stored value.
|
|
33
41
|
- **Shrink an archive of scientific files** without risking the science: every
|
|
34
42
|
output is verified value-for-value against its source before it replaces
|
|
35
|
-
anything, and stored values round-trip bit-
|
|
43
|
+
anything, and stored values round-trip value-identically (bit-for-bit for
|
|
44
|
+
integer and packed data; NaN- and signed-zero-insensitive for floating point).
|
|
36
45
|
- **Batch-convert a directory tree** of legacy granules to modern netCDF4 in one
|
|
37
46
|
command.
|
|
38
47
|
|
|
@@ -40,12 +49,20 @@ Reach for ncarnate if you are trying to:
|
|
|
40
49
|
|
|
41
50
|
Converting or recompressing a file changes *storage*, never *science data*:
|
|
42
51
|
|
|
43
|
-
- Every variable's stored values are preserved **
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
- Every variable's stored values are preserved **value-identically** — bit-for-bit
|
|
53
|
+
for integer and packed data; for floating-point data, distinct NaN
|
|
54
|
+
bit-patterns and `-0.0`/`+0.0` compare equal. Packed integers stay packed;
|
|
55
|
+
`scale_factor`/`add_offset`/`_FillValue` are carried across as declarations,
|
|
56
|
+
never applied.
|
|
57
|
+
- Every dimension (including unlimited-ness), attribute (including its exact
|
|
58
|
+
storage type — an `NC_STRING` scalar stays `NC_STRING`, verified via netCDF-C
|
|
59
|
+
type inquiry), and group survives. HDF-EOS2 `StructMetadata` is preserved
|
|
60
|
+
verbatim; names netCDF cannot hold are sanitized with the original recorded in
|
|
61
|
+
a companion attribute.
|
|
62
|
+
- **Complex-valued variables (`complex64`/`complex128`) are excluded** from the
|
|
63
|
+
fidelity guarantee: netCDF stores them as compound types, which ncarnate
|
|
64
|
+
**refuses loudly** with the stable `UNSUPPORTED_TYPE` error rather than
|
|
65
|
+
guessing at a lossy copy. Complex support is a later, evidence-backed feature.
|
|
49
66
|
- Geolocation reconstruction is strictly **additive**: the original information
|
|
50
67
|
always rides along, so the conversion never becomes the only copy of the
|
|
51
68
|
truth. Swath coordinates are attached to variables whose first two axes are
|
|
@@ -86,11 +103,18 @@ required. On platforms without a repaired `pyhdf` wheel (e.g. Linux aarch64),
|
|
|
86
103
|
building from sdist requires the system HDF4 library first (Debian/Ubuntu:
|
|
87
104
|
`apt install libhdf4-dev`).
|
|
88
105
|
|
|
89
|
-
**Windows via pip:** the netCDF/HDF5
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
106
|
+
**Windows via pip:** PyPI wheels give you the full netCDF/HDF5 surface —
|
|
107
|
+
`import ncarnate`, the CLI (`--help`/`--version`), format detection, audits,
|
|
108
|
+
manifest runs, and verified recompression — but **not** HDF4/HDF-EOS2
|
|
109
|
+
*conversion*: `pyhdf`'s Windows wheel ships no HDF4 runtime. An HDF4 attempt
|
|
110
|
+
is refused cleanly **before any output is created** with the stable
|
|
111
|
+
`[HDF4_RUNTIME_UNAVAILABLE]` message naming the detected cause, the
|
|
112
|
+
capabilities that still work, and the fix — never an unexplained import
|
|
113
|
+
traceback. An audit of an archive containing HDF4 files still completes,
|
|
114
|
+
recording those files as `unsupported` with the same code. For HDF4 on
|
|
115
|
+
Windows use the conda-forge install above (or **WSL** with the pip
|
|
116
|
+
instructions); a dedicated CI job pins this degraded-capability contract on
|
|
117
|
+
every change.
|
|
94
118
|
|
|
95
119
|
## Command line usage
|
|
96
120
|
|
|
@@ -165,6 +189,19 @@ The end-of-run summary counts converted / skipped / failed with reasons, and the
|
|
|
165
189
|
exit code is non-zero **iff** a selected record failed — so a partial failure on
|
|
166
190
|
a terabyte run surfaces loudly instead of silently mis-converting.
|
|
167
191
|
|
|
192
|
+
**Destination collision preflight.** Before any directory or output file is
|
|
193
|
+
created, every selected record's destination is computed up front — from the
|
|
194
|
+
source's *detected bytes*, never the manifest's declared format — and any
|
|
195
|
+
collision refuses the **entire run** with exit code 2 and a stable
|
|
196
|
+
`[DESTINATION_COLLISION]` message on stderr listing every involved source and
|
|
197
|
+
the contested destination. No last-writer-wins, no auto-rename, no partial
|
|
198
|
+
proceed, and nothing is written. Refused collisions include: two records
|
|
199
|
+
landing on one output path (e.g. an `a.hdf` → `a.nc` conversion next to a real
|
|
200
|
+
`a.nc` sibling), case-fold-equivalent names (one file on NTFS/APFS), duplicate
|
|
201
|
+
records for one source, an output tree overlapping a source tree (symlinks
|
|
202
|
+
resolved), and a pre-existing destination unless you pass `--skip-existing` to
|
|
203
|
+
resume.
|
|
204
|
+
|
|
168
205
|
## Library usage
|
|
169
206
|
|
|
170
207
|
```python
|
|
@@ -194,7 +231,7 @@ The AMSR-E daily 12.5 km sea-ice granule this project grew up around:
|
|
|
194
231
|
| netCDF4 recompression (`--complevel 9`) | 42.6 MB | 19.9 MB |
|
|
195
232
|
| HDF-EOS2 → netCDF4 (+ reconstructed lat/lon) | 60.2 MB | 35.5 MB |
|
|
196
233
|
|
|
197
|
-
Both outputs re-read
|
|
234
|
+
Both outputs re-read value-identically to their sources; the conversion
|
|
198
235
|
additionally carries CF `polar_stereographic` grid mappings and coordinates for
|
|
199
236
|
both hemispheric grids. The northern grid's reconstructed latitudes/longitudes
|
|
200
237
|
agree with The HDF Group's independent conversion of the same granule to within
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Brand assets
|
|
2
|
+
|
|
3
|
+
The ncarnate mark: a phoenix rising from an orbited globe — legacy satellite and
|
|
4
|
+
geospatial data reincarnated as modern netCDF4. The Earth is drawn as a
|
|
5
|
+
latitude/longitude graticule (the gridded scientific data); an orbital band
|
|
6
|
+
sweeps around it (a nod to the satellite granules ncarnate reincarnates); and the
|
|
7
|
+
phoenix rises from it, an ember crest at its head — the spark of rebirth.
|
|
8
|
+
|
|
9
|
+
## Files
|
|
10
|
+
|
|
11
|
+
| File | Use |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `ncarnate.svg` | Primary mark — full colour on the rounded navy tile (app-icon / avatar). |
|
|
14
|
+
| `ncarnate-mono.svg` | Single-ink navy silhouette, transparent — one-colour contexts on light backgrounds. |
|
|
15
|
+
| `ncarnate-white.svg` | Single-ink off-white silhouette, transparent — for dark backgrounds. |
|
|
16
|
+
| `ncarnate-wordmark.svg` / `-white.svg` | The word alone (Sora SemiBold, outlined). |
|
|
17
|
+
| `ncarnate-lockup.png` / `-dark.png` | **Shipped lockup** — source mark tile (raster) + wordmark, horizontal. `-dark` recolours the wordmark off-white for dark backgrounds (the mark tile reads on both themes). |
|
|
18
|
+
| `ncarnate-mark.png` | The source mark tile alone, with transparent rounded corners (favicon / social / reuse). |
|
|
19
|
+
| `ncarnate-lockup.svg` / `-dark.svg` | Fully-vector lockup — **work in progress**, not yet shipped (see below). |
|
|
20
|
+
|
|
21
|
+
The project README header swaps `ncarnate-lockup.png` ⇄ `-dark.png` by
|
|
22
|
+
`prefers-color-scheme` via `<picture>`. The wordmark inside is glyph **outlines**
|
|
23
|
+
(no font needed to render); it is rasterized into the shipped PNG lockup.
|
|
24
|
+
|
|
25
|
+
### Vector globe: deferred
|
|
26
|
+
|
|
27
|
+
The mark's thin globe **graticule** has not yet re-drawn cleanly as vectors — a
|
|
28
|
+
generated two-tilt globe (`compose_logo.py` + `globe.py`) matches at display scale
|
|
29
|
+
but its acceptance against the source is still open. Rather than block the release on
|
|
30
|
+
it, the **shipped lockup embeds the approved `ncarnate-source.png` mark as raster**
|
|
31
|
+
(`build_raster_lockup.py`), which is crisp at the README's display size. The vector
|
|
32
|
+
mark (`ncarnate.svg`, `ncarnate-lockup.svg`) remains in-repo as work in progress;
|
|
33
|
+
when it is accepted, swap the `<image>` in `build_raster_lockup.py` back to the
|
|
34
|
+
vector paths and the README layout is unchanged. See
|
|
35
|
+
`docs/plans/2026-07-11-logo-fidelity-regen.md`.
|
|
36
|
+
|
|
37
|
+
## Palette
|
|
38
|
+
|
|
39
|
+
| | Hex | Role |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| Deep navy | `#152A47` | Field / primary ink |
|
|
42
|
+
| Warm off-white | `#F2EDE1` | Structure (phoenix + graticule) |
|
|
43
|
+
| Ember | `#E8843C` | Crest flame — the spark of rebirth |
|
|
44
|
+
|
|
45
|
+
Wordmark type: **Sora SemiBold** (weight 600), SIL Open Font License 1.1.
|
|
46
|
+
|
|
47
|
+
## Regenerating
|
|
48
|
+
|
|
49
|
+
`ncarnate-source.png` is the approved raster — a flat, hard-edged emblem made by
|
|
50
|
+
an image model (the pictorial mark), used here as a **placement reference**: the
|
|
51
|
+
phoenix is traced from it, while the globe/orbit geometry (centre, radius, tilt)
|
|
52
|
+
is *measured* from it and then generated as vector primitives, not traced.
|
|
53
|
+
|
|
54
|
+
`compose_logo.py` does the hybrid composition (deps: `vtracer`, `pillow`, `scipy`,
|
|
55
|
+
`numpy`; `globe.py` is pure-stdlib): it splits the raster into field / phoenix /
|
|
56
|
+
ember, masks the globe + orbit out of the structure so only the organic phoenix is
|
|
57
|
+
traced, generates the graticule globe + orbit ring via `globe.py`, and layers them
|
|
58
|
+
(tile → globe → orbit-back → phoenix → orbit-front → ember). The globe geometry
|
|
59
|
+
(`CX, CY, R, TILT, MERIDIANS, PARALLELS`, orbit params) lives in constants at the
|
|
60
|
+
top of `compose_logo.py`; to re-fit it to a new raster, overlay `globe.py --out`
|
|
61
|
+
on the raster and nudge until the limb/graticule line up.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# deps: vtracer, fonttools, pillow, scipy, numpy (+ cairosvg or @resvg/resvg-js to rasterise for the eyeball check)
|
|
65
|
+
python3 compose_logo.py ncarnate-source.png ncarnate.svg # mark + -mono + -white
|
|
66
|
+
|
|
67
|
+
# fetch Sora and instance to weight 600:
|
|
68
|
+
# curl -sL -o sora-var.ttf "https://raw.githubusercontent.com/google/fonts/main/ofl/sora/Sora%5Bwght%5D.ttf"
|
|
69
|
+
# python3 -c "from fontTools import ttLib; from fontTools.varLib.instancer import instantiateVariableFont as I; f=ttLib.TTFont('sora-var.ttf'); I(f,{'wght':600},inplace=True); f.save('sora-600.ttf')"
|
|
70
|
+
python3 build_lockup.py --text ncarnate --font sora-600.ttf --mark ncarnate.svg \
|
|
71
|
+
--field '#152A47' --structure '#F2EDE1' --out-prefix ncarnate # wordmark + light/dark VECTOR lockups (WIP)
|
|
72
|
+
|
|
73
|
+
# shipped lockup: raster source mark + wordmark, light/dark PNG (deps: fonttools, pillow, numpy, scipy, cairosvg)
|
|
74
|
+
python3 build_raster_lockup.py --font sora-600.ttf # ncarnate-lockup{,-dark}.png + ncarnate-mark.png
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Verify on both GitHub themes before committing — render each lockup on `#ffffff`
|
|
78
|
+
and `#0d1117` and confirm the wordmark is legible on each. The globe is generated,
|
|
79
|
+
so also zoom to a meridian×parallel crossing near the central meridian and confirm
|
|
80
|
+
uniform stroke width through the crossing, true concentric arcs, even meridian
|
|
81
|
+
spacing, and clean round caps.
|