Skip to main content

ShapeCheck

Build & Tests codecov

Framework-agnostic library for checking array/tensor shapes at runtime.

Finding the root of shape mismatches can be troublesome, especially with broadcasting rules and mutable arrays. Comments documenting shapes can easily become out of date as code evolves. This library aims to solve both of those problems by ensuring function input/output shape expectations are met. The concise syntax for expressing shapes serves to document code as well, so new users can quickly understand what's going on.

With frameworks like JAX or TensorFlow, "runtime" is actually "compile" or "trace" time, so you don't pay any cost during execution. For frameworks like PyTorch, asynchronous execution will hide the cost of shape checking. You only pay a small overhead with synchronous, eager frameworks like numpy.

Install Library

From PyPI:

pip install --upgrade shapecheck

To install the latest version:

pip install --upgrade git+https://github.com/n2cholas/shapecheck.git

Usage

import numpy as np
from shapecheck import check_shapes

@check_shapes({'imgs': 'N,W,W,-1', 'labels': 'N,1'}, aux_info=None, out_='')
def loss_fn(batch, aux_info):
    # do something with aux_info
    diff = (batch['imgs'].mean((1, 2, 3)) - batch['labels'].squeeze())
    return np.mean(diff**2)

loss_fn({'imgs': np.ones((3, 2, 2, 1)), 'labels': np.ones((3,1))}, np.ones(1))
loss_fn({'imgs': np.ones((5, 3, 3, 4)), 'labels': np.ones((5,1))}, 'any')
# Below line fails:
loss_fn({'imgs': np.ones((3, 5, 2, 1)), 'labels': np.ones((3,1))}, 'any')

Error message:

shapecheck.exception.ShapeError: in function loss_fn.
Named Dimensions: {'N': 3, 'W': 5}.
Input:
    Argument: batch  Type: <class 'dict'>
        MisMatch: Key: imgs Expected Shape: ('N', 'W', 'W', -1) Actual Shape: (3, 5, 2, 1).
        Match:    Key: labels Expected Shape: ('N', 1) Actual Shape: (3, 1).
    Skipped:  Argument: aux_info.

In the above example, we compute the loss with a batch of data, which is a dictionary with images and labels. We specify that we want N square images which can have any number of channels (indicated by the -1). Inputs to check_shape can be arbitrarily nested dicts/lists/tuples, as long as the structure of the shape specification matches the structure of the inputs to the decorated function.

We also specify that aux_info shouldn't be checked. Equivalently, we could've excluded it from the definition:

@check_shapes({'imgs': 'N,W,W,-1', 'labels': 'N,1'}, out_='')

or passed it as a positional argument.

@check_shapes({'imgs': 'N,W,W,-1', 'labels': 'N,1'}, None, out_='')

Finally, we specify the output shape should be a scalar via out_=''. All non-input shape arguments to check_shape have an underscore after them so they don't conflict with the decorated function's arguments (for now, just out_ and match_callees_).

If you have a function with shape-checking that calls many other functions with shape-checking, you can optionally enforce that dimensions with the same letter name in the caller correspond to the same sized dimension in the callees via match_callees_=True. That is, you can check that a function's input named dimensions match the same named dimensions of all checked functions higher in the call stack. For example:

@check_shapes('M', 'N', 'O', out_='M')
def callee(a, b, c):
    return a

@check_shapes('M', 'N', 'R')
def caller_fn_1(x, y, z):
    return callee(y, x, z)

@check_shapes('M', 'N', 'R', match_callees_=True)
def caller_fn_2(x, y, z):
    return callee(y, x, z)

caller_fn_1(np.ones(5), np.ones(6), np.ones(7))  # succeeds
caller_fn_2(np.ones(5), np.ones(6), np.ones(7))  # fails

Here, we (accidentally) swapped x and y when calling callee. caller_fn_1 succeeds because the inputs are compatible when considering the named dimensions for callee_fn alone. But caller_fn_2 fails because the named dimensions are inconsistent between the caller and the callee. The following error would be produced:

shapecheck.exception.ShapeError: in function callee.
Named Dimensions: {'M': 5, 'N': 6, 'R': 7, 'O': 7}.
Input:
    MisMatch: Argument: a Expected Shape: ('M',) Actual Shape: (6,).
    MisMatch: Argument: b Expected Shape: ('N',) Actual Shape: (5,).
    Match:    Argument: c Expected Shape: ('O',) Actual Shape: (7,).

This library also supports variadic dimensions. You can use '...' to indicate 0 or more dimensions:

@check_shapes('dim, ..., 1', '..., dim, 1')
def g(a, b):
    pass

g(np.ones((2, 3, 4, 1)), np.ones((5, 2, 1)))  # succeeds
g(np.ones((3, 1)), np.ones((3, 1)))  # succeeds
g(np.ones((2, 3, 4, 1)), np.ones((1, 1)))  # fails

The last statement fails with the following error, since dim doesn't match:

shapecheck.exception.ShapeError: in function g.
Named Dimensions: {'dim': 2}.
Input:
    Match:    Argument: a Expected Shape: ('dim', '...', 1) Actual Shape: (2, 3, 4, 1).
    MisMatch: Argument: b Expected Shape: ('...', 'dim', 1) Actual Shape: (1, 1).

You can also name the variadic dimensions, to ensure that a contiguous sequence of dimensions match between arguments. For example:

@check_shapes('batch,variadic...', 'variadic...')
def h(a, b):
    pass

h(np.ones((7, 1, 2)), np.ones((1, 2)))  # succeeds
h(np.ones((6, 2)), np.ones((1, 1)))  # fails
h(np.ones((6, 2)), np.ones((1)))  # fails

You can enable/disable shapechecking globally as shown below:

from shapecheck import is_checking_enabled, set_checking_enabled

assert is_checking_enabled()
set_checking_enabled(False)
assert not is_checking_enabled()
set_checking_enabled(True)
assert is_checking_enabled()

Or via a context manager:

assert is_checking_enabled()
with set_checking_enabled(False):
    assert not is_checking_enabled()
assert is_checking_enabled()

If you have any questions or issues with the library, please raise an issue on GitHub. Hope you enjoy using the library!

Release files for shapecheck 0.0.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for shapecheck 0.0.2
File Size Uploaded
shapecheck-0.0.2.tar.gz 12.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for shapecheck 0.0.2
File Interpreter ABI Platform
shapecheck-0.0.2-py2.py3-none-any.whl Python 2, Python 3 none any Details

Total release size: 23.9 kB

Release files / shapecheck-0.0.2.tar.gz

Download URL shapecheck-0.0.2.tar.gz
Size 12.6 kB
Tags Source
SHA-256 checksum
How to use checksums
0e2baf8c50c664258e5a426fc17a025456144277c4b366763a53be4d21a867b2
BLAKE2b-256 checksum
How to use checksums
1cc97f37121dc9faf2889383f963cf9aab2e95b5d5b13b0798fdc50c9f93eed3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.3.0 pkginfo/1.6.1 requests/2.25.1 setuptools/51.1.1 requests-toolbelt/0.9.1 tqdm/4.55.0 CPython/3.8.5

Release files / shapecheck-0.0.2-py2.py3-none-any.whl

Download URL shapecheck-0.0.2-py2.py3-none-any.whl
Size 11.2 kB
Tags Python 2 Python 3
SHA-256 checksum
How to use checksums
7f20e66cd3bdd99dae4d1fedf63087fe109afe0e2c903139cd80b4e078fc1759
BLAKE2b-256 checksum
How to use checksums
8ee6fb52c53abe03282e71e4149e84374afc1ad5f017e57986686f6224761fb7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.3.0 pkginfo/1.6.1 requests/2.25.1 setuptools/51.1.1 requests-toolbelt/0.9.1 tqdm/4.55.0 CPython/3.8.5

Release history Release notifications | RSS feed

This release

0.0.2 This release

2 release files

0.0.1

2 release 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