Skip to main content

keras-hexagdly

Keras 3 port of HexagDLy: convolution and pooling methods for hexagonally sampled data, originally written for PyTorch by Tim Lukas Holch and Constantin Steppa (ai4iacts).

This port reproduces HexagDLy's hexagonal addressing scheme and sub-kernel decomposition exactly (bit-for-bit equivalent outputs, cross-checked against the upstream PyTorch package in hexagdly-oracle), but is built on Keras 3 so it runs on any backend (TensorFlow, JAX, PyTorch) and uses a channels-last (NHWC/NDHWC) tensor layout instead of PyTorch's channels-first.

It also adds three functionalities that do not exist in upstream HexagDLy:

  • share_neighbors (Conv2d, Conv3d): ties the weights of several cells of a hexagonal kernel together instead of giving every cell its own independent weight, which cuts the parameter count and imposes a geometric symmetry on the learned kernel. Three modes -- "ring", "diag", "sym" -- are illustrated below.
  • depth_padding="same" (Conv3d only): zero-pads the depth/time axis so the temporal kernel is centred on each time step and the output depth equals the input depth, instead of HexagDLy's "valid"-only behaviour (output depth shrinks by kernel - 1).
  • hls4ml export for FPGA synthesis, currently limited to io_stream and the 2D layers.

Weights trained with pytorch-hexagdly can be loaded into these layers -- see keras_hexagdly.torch_interop and notebooks/pytorch_to_keras_example.ipynb. No torch dependency is required: a plain .npz of the state dict works.

See NOTICE.md for attribution details and citation information.

Installation

pip install keras-hexagdly

For development (running the test suite, which also checks parity against upstream PyTorch HexagDLy, and the example notebooks):

pip install keras-hexagdly[dev]

Usage

import keras
import keras_hexagdly as hgly

kernel_size, stride = 1, 4
in_channels, out_channels = 1, 3

hexconv = hgly.Conv2d(in_channels, out_channels, kernel_size, stride)
x = keras.random.uniform((1, 21, 21, 1))  # channels-last: (N, H, W, C)
y = hexconv(x)

in_channels can be omitted; it is then inferred from the input on first call, like a standard Keras layer: hgly.Conv2d(out_channels, kernel_size=kernel_size, stride=stride).

New: share_neighbors -- weight sharing across kernel cells

Available on Conv2d and Conv3d. share_neighbors reduces the number of learnable parameters by grouping kernel cells that share a single weight. Three modes are available, illustrated below for kernel_size=2 (19 cells):

share_neighbors="ring" share_neighbors="diag" share_neighbors="sym"
ring diag sym
3 weights -- cells at the same hex distance from center share one weight (concentric rings). 10 weights -- visually opposite (antipodal) cells share one weight. 10 weights -- geometrically adjacent 60 degree pairs share one weight.
  • "ring": the most aggressive reduction. All 6 direct neighbours share one weight, all 12 outer cells share another. Enforces exact 6-fold rotational symmetry of the learned kernel.
  • "diag": antipodal symmetry -- each cell and its mirror image through the center share a weight. Useful when the kernel should be point-symmetric.
  • "sym": 60 degree adjacent pairs -- consecutive neighbours along the kernel boundary share a weight. Useful when the kernel should reflect local rotational symmetry.

For kernel_size=1 (7 cells): ring=2 weights, diag=4, sym=4. For kernel_size=2 (19 cells): ring=3 weights, diag=10, sym=10. Compare to the default share_neighbors=None, which gives 7 and 19 independent weights. "ring" works at any kernel size; "diag" and "sym" are defined for kernel_size 1 and 2 only, matching pytorch-hexagdly, which does not define them beyond n=2 either.

hexconv = hgly.Conv2d(in_channels, out_channels, kernel_size=2, share_neighbors="ring")

share_neighbors=True is accepted as an alias for "ring".

New: same-padded temporal convolution (Conv3d)

conv3d = hgly.Conv3d(in_channels, out_channels, kernel_size=(depth_k, hex_k),
                      depth_padding="same")  # output depth == input depth

Before applying these layers, your data must already be arranged on the square-tensor layout HexagDLy expects (zig-zag columns); see notebooks/keras_hexagdly_addressing_scheme.ipynb for how to get there from raw detector coordinates, and notebooks/keras_hexagdly_2d_example.ipynb for a worked convolution/pooling example, including the new features above.

Notebooks

Ported from HexagDLy's own notebooks, one-to-one where the content is framework-specific, lightly adapted where it depends on a torch-specific dataloader/training loop:

FPGA export via hls4ml

The hex layers can be synthesised to HLS C++ through hls4ml. The layers are replaced by a fused line-buffer kernel that keeps only the resident rows of the frame in a shift register, rather than materialising the whole gathered tensor.

import hls4ml
from keras_hexagdly.hls4ml_handler import register_hex_gather_layers
from keras_hexagdly.hls4ml_ext import patch_model_for_hls, hex_reuse_config, check_hls_config

register_hex_gather_layers("Vitis")
hls_ready = patch_model_for_hls(model)                 # strategy="linebuffer"

config = hls4ml.utils.config_from_keras_model(hls_ready, granularity="name")
hex_reuse_config(config, hls_ready)                    # per-layer ReuseFactor
check_hls_config(config, hls_ready, io_type="io_stream")

hls_model = hls4ml.converters.convert_from_keras_model(
    hls_ready, hls_config=config, io_type="io_stream", backend="Vitis",
)

Supported scope

Only io_stream and the 2D layers (Conv2d, MaxPool2d) are supported for now. That is the combination that is covered by C-simulation and validated by RTL co-simulation.

io_stream io_parallel
Conv2d, MaxPool2d supported not supported
Conv3d, MaxPool3d not supported not supported

The unsupported paths are not silently wrong -- they raise. patch_model_for_hls raises NotImplementedError on a 3D layer, and check_hls_config raises on io_type="io_parallel". Both accept allow_unvalidated=True if you want to experiment with them anyway, but nothing about their numerics or resource usage is guaranteed.

hex_reuse_config matters more than it looks: hls4ml's config_from_keras_model(granularity="name") writes ReuseFactor=1 into every layer entry it recognises, which overrides the model-level value -- but custom layers get no entry and inherit the model-level one instead. Without an explicit per-layer setting, a single model ends up mixing two different reuse factors.

Testing

pip install -e .[dev] --no-build-isolation   # see note below
pytest tests/

(--no-build-isolation: only needed if your pip is old -- pip 22.0.2's isolated build environment was observed to pick up a setuptools version that mis-names the built wheel UNKNOWN. Verified clean with a modern pip (>=23) in a fresh venv: plain pip install . works with no workaround. Either way, pytest tests/ works without installing anything -- conftest.py puts src/ and tests/ on sys.path.)

Most of the suite no longer lives here. It has moved to hexagdly-oracle, the shared test repo for this library and pytorch-hexagdly: hand-verified layer outputs, share_neighbors weight-sharing oracles, depth_padding, mixed precision, serialization, edge cases, indexed-equivalence and the hls4ml export tests (including C-simulation). tests/ here keeps only what is genuinely local.

To run the full suite, check the oracle out as a sibling directory:

git clone https://github.com/YugnatD/hexagdly-oracle
PYTHONPATH=hexagdly-oracle/src pytest tests/ hexagdly-oracle/tests/

Verified to pass on all three Keras 3 backends (set KERAS_BACKEND=tensorflow|torch|jax before importing keras; tensorflow is the default if unset):

KERAS_BACKEND=tensorflow   # 891 passed,  7 skipped
KERAS_BACKEND=torch        # 881 passed, 17 skipped
KERAS_BACKEND=jax          # 806 passed, 92 skipped (slower: per-shape JIT compile)

A GitHub Actions workflow (.github/workflows/test.yml) runs this matrix (3 backends x 3 Python versions) plus ruff check/ruff format --check on every push and PR, checking out the oracle repo as part of the job.

Note for GPU users on the torch backend: the Keras torch backend runs on CUDA when a GPU is visible, and PyTorch defaults to cudnn.allow_tf32 = True, so convolutions are computed in TF32 (~1e-3 relative precision). That is enough to break equivalence assertions on hex kernels, which are wide by construction because dilation is done by zero insertion. The oracle's conftest.py pins the flag off for the test session; the library itself never touches global torch settings.

Disclaimer

Like upstream HexagDLy, this is a prototyping tool: it favors flexibility over performance. Once a model's architecture (kernel size, stride, input shape) is fixed, hard-coding those parameters would yield a faster implementation.

Performance

See benchmarks/ for a speed comparison against upstream PyTorch HexagDLy. Short version: run eagerly on CPU, this port is 1-7x slower than upstream for the same reason upstream itself is slow (the hex sub-kernel decomposition costs several op-dispatches per call -- a design choice, not a regression). Wrapped in a compiled call (jax.jit/ tf.function, which model.fit/model.predict do automatically) it is typically faster than upstream's eager PyTorch, sometimes by an order of magnitude. torch.compile support is currently unreliable for this layer (see the benchmarks README for why); eager execution on a GPU is the recommended way to get speed on the torch backend.

Changelog

See CHANGELOG.md.

License

MIT, see LICENSE. This is a derivative work of HexagDLy (Copyright (c) 2018 ai4iacts); see NOTICE.md.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

keras_hexagdly-0.3.0.tar.gz (2.6 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

keras_hexagdly-0.3.0-py3-none-any.whl (85.3 kB view details)

Uploaded Python 3

File details

Details for the file keras_hexagdly-0.3.0.tar.gz.

File metadata

  • Download URL: keras_hexagdly-0.3.0.tar.gz
  • Upload date:
  • Size: 2.6 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for keras_hexagdly-0.3.0.tar.gz
Algorithm Hash digest
SHA256 f816d912580b0ae6a3b3c49ad067820d84ae3bbe31aa68688d4014ae30e58927
MD5 b4747ddc70645b6590e3f39f19fed7a9
BLAKE2b-256 3073ffc59077e441cc87129abc560cf09d966ead14e631523361597e4db5b8eb

See more details on using hashes here.

Provenance

The following attestation bundles were made for keras_hexagdly-0.3.0.tar.gz:

Publisher: publish.yml on YugnatD/keras-hexagdly

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file keras_hexagdly-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: keras_hexagdly-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 85.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for keras_hexagdly-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9e89c4a3114e1d3f4261f9b681edd51adf5c41aaf3f98db5cf1e9adb92074b2e
MD5 811e0ba20c07ae08beaa7a5ea3923bac
BLAKE2b-256 cb2f220f6eacd6bddb668b6165fdd0b75867f55746878f5dc9c1baf623232800

See more details on using hashes here.

Provenance

The following attestation bundles were made for keras_hexagdly-0.3.0-py3-none-any.whl:

Publisher: publish.yml on YugnatD/keras-hexagdly

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.4.2

2 files

0.4.1

2 files

0.4.0

2 files

0.3.1

2 files

This release

0.3.0 This release

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page