Compare commits

..
34 Commits
Author SHA1 Message Date
Patrick Kidger 165065756f Static type-checking fix (#34) 2022-09-24 18:27:22 -07:00
ebrevdo dcd73e3431 Add support for e.g. jaxtyping.Float[Union[...], ...] in py3.8 (#31)
* Add support for e.g. jaxtyping.Float[Union[...], ...] in py3.8

Turns out that python3.8, Union lacks the __name__ attribute.  Use repr()
in these cases.

* Fix linter.

* Remove implicit cast to bool in favor of try/except.
2022-09-22 13:25:29 -07:00
Patrick Kidger f175c7f315 Fixed py.type not being packaged (#30) 2022-09-22 12:16:25 -07:00
Patrick Kidger da8300ec6c tidyness tweak (#28) 2022-09-20 15:16:52 -07:00
Patrick Kidger 39439c2790 More fixes (#27)
* Edge-case doc fixes for parameterising types with PyTrees or AbstractDtypes

* Fixes for threading

* version bump
2022-09-20 15:12:21 -07:00
Patrick Kidger 6202dcc639 doc fix (#26) 2022-09-19 23:30:29 -07:00
Patrick Kidger c2e9d913d5 Fixed jaxtyped breaking descriptors. Fixed long module names. (#25) 2022-09-19 22:40:25 -07:00
Patrick Kidger 62ddcc25b5 Threading fix (#24) 2022-09-16 08:04:28 -07:00
Patrick Kidger a89ebe356b Merge pull request #22 from google/v021
Bump version
2022-09-15 08:19:08 -07:00
Patrick Kidger 98133f5e1e Bump version 2022-09-15 16:15:28 +01:00
Patrick Kidger 2c7dbbd593 Merge pull request #21 from google/pytyped
Create `py.typed`
2022-09-15 08:15:03 -07:00
Patrick Kidger 1291a90192 Merge pull request #20 from google/bool-checking
Fix for boolean checking not working
2022-09-15 08:14:56 -07:00
Patrick Kidger 14117804aa Create py.typed 2022-09-15 16:07:02 +01:00
Patrick Kidger e61a37f0a3 Fix for boolean checking not working (#19) 2022-09-15 16:05:49 +01:00
Patrick Kidger 3c7e4e11ee Merge pull request #16 from google/rewrite
Rewrite!
2022-09-07 12:30:59 -07:00
Patrick Kidger 01f8f20bf5 Fixed a few names 2022-09-07 12:20:42 -07:00
Patrick Kidger cb11a93b22 doc tweaks 2022-09-07 12:06:15 -07:00
Patrick Kidger c82fbbea4e doc tweaks 2022-09-07 12:00:58 -07:00
Patrick Kidger f28b0c789a doc tweaks 2022-09-07 11:32:14 -07:00
Patrick Kidger a53fe6af57 Changes from feedback:
- Float32 introduced to replace f32 (etc.)
- Old f32 aliases were previously around for backward-compatibility, but
  the code is pretty hideous, and all stakeholders are now on board
  through RFC #13.
- Overall feedback was that Int{Sign,Unsign} was too long and not enough
  like numpy. Changed to Int, UInt, and Integer.
2022-09-07 11:09:48 -07:00
Patrick Kidger 140be9ecec Doc updates 2 2022-08-30 13:26:41 -07:00
Patrick Kidger 07e735797e Doc updates 2022-08-30 13:14:50 -07:00
Patrick Kidger 48e8131247 Added jaxtyping.Array=jnp.ndarray 2022-08-30 12:56:47 -07:00
Patrick Kidger 903000f3d5 Rewrote syntax 2022-08-30 12:47:45 -07:00
Patrick Kidger 3f9fad59bd Merge pull request #11 from google/v010
Version 0.1.0
2022-08-01 01:09:25 +01:00
Patrick Kidger 8f64c99649 Pre-commit fixes 2022-08-01 01:06:45 +01:00
Patrick Kidger 3e86c704ae Bump version 2022-08-01 01:01:59 +01:00
Patrick Kidger 07259aa5c8 Added support for symbolic expressions in axes. Closes #9. 2022-08-01 01:00:18 +01:00
Patrick Kidger 6ff4620d1a Broadcast+variadic is now more lenient, and supports anything broadcastable together. Closes #8 2022-07-31 23:59:20 +01:00
Patrick Kidger 6e2837e5b7 Tidied up _#* into acting as prefixes, regardless of order. Closes #10 2022-07-31 23:45:40 +01:00
Patrick Kidger 7b698ae215 Merge pull request #3 from google/test-tidy
Test tidy
2022-07-12 10:11:50 +01:00
Patrick Kidger e162e1281a Made missing beartype result in test skips rather than tests not running 2022-07-12 10:07:42 +01:00
Patrick Kidger 9f75958b2d Make test/helpers.py a relative import 2022-07-12 10:03:19 +01:00
Patrick Kidger 81238e38e8 Update tests to make beartype optional 2022-07-11 19:27:16 +01:00
24 changed files with 840 additions and 352 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
[flake8]
max-line-length = 120
max-line-length = 88
ignore = W291,W293,W503,W504,E123,E126,E203,E402,E701,E731,F722
per-file-ignores = __init__.py: F401
+98 -65
View File
@@ -2,60 +2,78 @@
## Annotating array types
Each array is denoted by a type `dtype[shape]`, such as `f32["batch channels"]`.
Each array is denoted by a type `dtype[array, shape]`, such as `Float[Array, "batch channels"]`.
### Shape
The shape should be a string of space-separated symbols, such as "a b c d". Each symbol can be:
- `int`: fixed-size axis, e.g. `f32["28 28"]`.
- `str`: variable-size axis, e.g. `f32["channels"]`.
- `_`: anonymous axis, e.g. `f32["batch channels _ _"]`.
- `...`: anonymous zero or more axes, e.g. `f32["... c h w"]`
- `*name`: zero or more variable-size axes, e.g. `f32["*batch c h w"]`
- Append `#` to a dimension size to indicate that it can be that size *or* equal to one -- i.e. broadcasting is acceptable.
The shape should be a string of space-separated symbols, such as `"a b c d"`. Each symbol can be either an:
- `int`: fixed-size axis, e.g. `"28 28"`.
- `str`: variable-size axis, e.g. `"channels"`.
- A symbolic expression (without spaces!) in terms of other variable-size axes, e.g. `def remove_last(x: Float[Array, "dim"]) -> Float[Array, "dim-1"]`.
When calling a function, variable-size axes will be matched up across all arguments and checked for consistency. (See [runtime type checking](#runtime-type-checking) below.)
When calling a function, variable-size axes and symbolic axes will be matched up across all arguments and checked for consistency. (See [runtime type checking](#runtime-type-checking) below.)
In addition some modifiers can be applied:
- Prepend `*` to a dimension to indicate that it can match multiple axes, e.g. `"*batch c h w"` will match zero or more batch axes.
- Prepend `#` to a dimension to indicate that it can be that size *or* equal to one -- i.e. broadcasting is acceptable, e.g. `def add(x: Float[Array, "#foo"], y: Float[Array, "#foo"]) -> Float[Array, "#foo"]`.
- Prepend `_` to a dimension to disable any runtime checking of that dimension (so that it can be used just as documentation). This can also be used as just `_` on its own: e.g. `"b c _ _"`.
When using multiple modifiers, their order does not matter.
As a special case:
- `...`: anonymous zero or more axes (equivalent to `*_`) e.g. `"... c h w"`
Some notes:
- To denote a scalar shape use `""`, e.g. `f32[""]`.
- To denote an arbitrary shape (and only check dtype) use `"..."`, e.g. `f32["..."]`.
- You cannot have multiple variadic axes, i.e. you can only use `...` or `*name` at most once in each array.
- An example of broadcasting in one dimension: `add(x: f32["foo#"], y: f32["foo#"]) -> f32["foo#"]`.
- An example of broadcasting multiple dimensions: `add(x: f32["*foo#"], y: f32["*foo#"]) -> f32["*foo#"]`.
- To denote a scalar shape use `""`, e.g. `Float[Array, ""]`.
- To denote an arbitrary shape (and only check dtype) use `"..."`, e.g. `Float[Array, "..."]`.
- You cannot have more than one use of multiple-axes, i.e. you can only use `...` or `*name` at most once in each array.
- An example of broadcasting multiple dimensions: `def add(x: Float[Array, "*#foo"], y: Float[Array, "*#foo"]) -> Float[Array, "*#foo"]`.
- A symbolic expression cannot be evaluated unless all of the axes sizes it refers to have already been processed. In practice this usually means that they should only be used in annotations for the return type, and only use axes declared in the arguments.
### Dtype
The dtype should be any one of (imported from `jaxtyping`):
- Any dtype at all: `Array`
- Boolean: `b`
- Any integer, unsigned integer, floating, or complex: `n` (for <ins>n</ins>umber)
- Any floating or complex: `x` (for ine<ins>x</ins>act)
- Any floating point: `f`
- Floating point: `bf16`, `f16`, `f32`, `f64` (`bf16` is bfloat16)
- Any complex: `c`
- Complexes: `c64`, `c128`
- Any integer or unsigned intger: `t` (for in<ins>t</ins>eger)
- Any unsigned integer: `u`
- Unsigned integer: `u8`, `u16`, `u32`, `u64`
- Any signed integer: `i`
- Signed integer: `i8`, `i16`, `i32`, `i64`
- Any dtype at all: `Shaped`
- Boolean: `Bool`
- Any integer, unsigned integer, floating, or complex: `Num`
- Any floating or complex: `Inexact`
- Any floating point: `Float`
- Of particular precision: `BFloat16`, `Float16`, `Float32`, `Float64`
- Any complex: `Complex`
- Of particular precision: `Complex64`, `Complex128`
- Any integer or unsigned intger: `Integer`
- Any unsigned integer: `UInt`
- Of particular precision: `UInt8`, `UInt16`, `UInt32`, `UInt64`
- Any signed integer: `Int`
- Of particular precision: `Int8`, `Int16`, `Int32`, `Int64`
Unless you really want to force a particular precision, then for most applications you should probably allow any floating-point, any integer, etc. That is, use
```python
from jaxtyping import f
f["some_shape"]
from jaxtyping import Array, Float
Float[Array, "some_shape"]
```
rather than
```python
from jaxtyping import f32
f32["some_shape"]
from jaxtyping import Array, Float32
Float32[Array, "some_shape"]
```
### Array
The array should typically be a `jaxtyping.Array`, which is an alias for `jax.numpy.ndarray`.
But you can use other types as well. `jaxtyping` has support for JAX, NumPy, TensorFlow, and PyTorch, e.g.:
```python
Float[np.ndarray, "..."]
Float[tf.Tensor, "..."]
Float[torch.Tensor, "..."]
```
## PyTrees
### `jaxtyping.PyTree`
Each PyTree is denoted by a type `PyTree[LeafType]`, such as `PyTree[int]` or `PyTree[Union[str, f32["b c"]]]`.
Each PyTree is denoted by a type `PyTree[LeafType]`, such as `PyTree[int]` or `PyTree[Union[str, Float32[Array, "b c"]]]`.
You can leave off the `[...]`, in which case `PyTree` is simply a suggestively-named alternative to `Any`. ([By definition all types are PyTrees.](https://jax.readthedocs.io/en/latest/pytrees.html))
@@ -67,7 +85,7 @@ To enable multi-argument consistency checks (i.e. that shapes match up between a
Regardless of your choice, **this approach synergises beautifully with `jax.jit`!** All shape checks will be performed at trace-time only, and will not impact runtime performance.
### `jaxtyping.jaxtyped`
### Option 1: `jaxtyping.jaxtyped`
Decorate a function with this to have shapes checked for consistency across multiple arguments.
@@ -75,7 +93,7 @@ Example:
```python
# Import both the annotation and the `jaxtyped` decorator from `jaxtyping`
from jaxtyping import f32, jaxtyped
from jaxtyping import Array, Float32, jaxtyped
# Use your favourite typechecker: usually one of the two lines below.
from typeguard import typechecked as typechecker
@@ -84,7 +102,9 @@ from beartype import beartype as typechecker
# Write your function. @jaxtyped must be applied above @typechecker!
@jaxtyped
@typechecker
def batch_outer_product(x: f32["b c1"], y: f32["b c2"]) -> f32["b c1 c2"]:
def batch_outer_product(x: Float32[Array, "b c1"],
y: Float32[Array, "b c2"]
) -> Float32[Array, "b c1 c2"]:
return x[:, :, None] * y[:, None, :]
```
@@ -102,35 +122,44 @@ this function use the same axes sizes as the function it was called from.
Likewise, this means you can use `isinstance` checks inside a function body
and have them contribute to the same collection of consistency checks performed
by a typechecker against its arguments. (Or even forgo a typechecker altogether,
and just do your own manual `isinstance` checks.)
by a typechecker against its arguments. (Or even forgo a typechecker that analyses arguments,
and instead just do your own manual `isinstance` checks.)
Only `isinstance` checks that pass will contribute to the store of axis name-size pairs; those
that fail will not. As such it is safe to write e.g. `assert not isinstance(x,
f32["foo"])`.
Float32[Array, "foo"])`.
### `jaxtyping.install_import_hook`
### Option 2: `jaxtyping.install_import_hook`
It can be a lot of effort to add `@jaxtyped` decorators all over your codebase.
(Not to mention that double-decorators everywhere are a bit ugly.) The easier
option is usually to use the import import hook.
(Not to mention that double-decorators everywhere are a bit ugly.)
Example:
The easier option is usually to use the import hook.
This can be used via a `with` block; for example:
```python
from jaxtyping import install_import_hook
# Plus either one of the following:
install_import_hook("foo", ("typeguard", "typechecked")) # decorate @jaxtyped and @typeguard.typechecked
install_import_hook("foo", ("beartype", "beartype")) # decorate @jaxtyped and @beartype.beartype
install_import_hook("foo", None) # decorate only @jaxtyped (if you have manually applied typechecking decorators)
# Plus any one of the following:
# decorate @jaxtyped and @typeguard.typechecked
with install_import_hook("foo", ("typeguard", "typechecked")):
import foo # Any module imported inside this `with` block, whose name begins
import foo.bar # with the specified string, will automatically have both `@jaxtyped`
import foo.bar.qux # and the specified typechecker applied to all of their functions.
# decorate @jaxtyped and @beartype.beartype
with install_import_hook("foo", ("beartype", "beartype")):
...
# decorate only @jaxtyped (if you want that for some reason)
with install_import_hook("foo", None):
...
```
Any module imported **afterwards**, whose name begins with the specified string, will automatically have both `@jaxtyped` and the specified typechecker applied to all of their functions. (E.g. in the above example `foo`, `foo.bar`, `foo.bar.qux` would all be hook'd).
The import hook may be uninstalled after you've imported all the modules you're interested in:
If you don't like using the `with` block, the hook can be used without that:
```python
hook = install_import_hook(...)
... # perform imports
hook = install_import_hook(...):
import ...
hook.uninstall()
```
@@ -144,13 +173,13 @@ install_import_hook(["foo", "bar.baz"], ...)
```python
### entry_point.py
from jaxtyping import install_import_hook
install_import_hook("do_stuff", ("typeguard", "typechecked"))
import do_stuff
with install_import_hook("do_stuff", ("typeguard", "typechecked")):
import do_stuff
### do_stuff.py
from jaxtyping import f32
from jaxtyping import Array, Float32
def g(x: f32["..."]):
def g(x: Float32[Array, "..."]):
...
```
@@ -159,11 +188,9 @@ def g(x: f32["..."]):
```python
### __init__.py
from jaxtyping import install_import_hook
hook = install_import_hook("my_library_name", ("beartype", "beartype"))
from .subpackage import foo # full name is my_library_name.subpackage so will be hook'd
from .another_subpackage import bar # full name is my_library_name.another_subpackage so will be hook'd.
hook.uninstall()
del hook, install_import_hook, jaxtyping # keep interface tidy
with install_import_hook("my_library_name", ("beartype", "beartype")):
from .subpackage import foo # full name is my_library_name.subpackage so will be hook'd
from .another_subpackage import bar # full name is my_library_name.another_subpackage so will be hook'd.
```
#### pytest hook
@@ -174,23 +201,29 @@ pytest --jaxtyping-packages=foo,bar.baz,beartype.beartype
```
which will apply the import hook to all modules whose names start with either `foo` or `bar.baz`. The typechecker used in this example is `beartype.beartype`.
## Static type checking
jaxtyping should be compatible with static type checkers (the big three are `mypy`, `pyright`, `pytype`) out of the box.
Due to limitations of static type checkers, only the array type (JAX array vs NumPy array vs PyTorch tensor vs TensorFlow tensor) is checked. Shape and dtype are not checked. [See the FAQ](./FAQ.md) for more details.
## Abstract base classes
### `jaxtyping.AbstractDtype`
The base class of all dtypes. This can be used to create your own custom collection of dtypes (analogous to `n`, `x` etc.) For example:
The base class of all dtypes. This can be used to create your own custom collection of dtypes (analogous to `Float`, `Inexact` etc.) For example:
```python
class u8_or_u16(AbstractDtype):
class UInt8or16(AbstractDtype):
dtypes = ["uint8", "uint16"]
u8_or_u16["shape"]
UInt8or16[Array, "shape"]
```
which is functionally equivalent to
```python
Union[u8["shape"], u16["shape"]]
Union[UInt8[Array, "shape"], UInt16[Array, "shape"]]
```
### `jaxtyping.AbstractArray`
The base class of all shape-and-dtype-specified arrays, e.g. it's a base class
for `f32["foo"]`.
for `Float32[Array, "foo"]`.
+29 -24
View File
@@ -1,43 +1,48 @@
# FAQ
## Does jaxtyping work with static type checkers like `mypy`/`pyright`/`pytype`?
There is partial support for these. An annotation of the form `dtype[array, shape]` should be treated as just `array` by a static type checker. Unfortunately full dtype/shape checking is beyond the scope of what static type checking is currently capable of.
(Note that at time of writing, `pytype` has a bug in that `dtype[array, shape]` is sometimes treated as `Any` rather than `array`. `mypy` and `pyright` both work fine.)
## How does jaxtyping interact with `jax.jit`?
jaxtyping and `jax.jit` synergise beautifully.
When calling JAX operations wrapped in a `jax.jit`, then the dtype/shape-checking will happen at trace time. (When JAX traces your function prior to compiling it.) The actual compiled code does not have any dtype/shape-checking, and will therefore still be just as fast as before!
## `flake8` is throwing an error.
In type annotations, strings are used for two different things. Sometimes they're strings. Sometimes they're "forward references", used to refer to a type that will be defined later.
Some tooling in the Python ecosystem assumes that only the latter is true, and will throw spurious errors if you try to use a string just as a string (like we do).
In the case of `flake8`, at least, this is easily resolved. Multi-dimensional arrays (e.g. `f32["b c"]`) will throw a very unusual error (F722, syntax error in forward annotation), so you can safely just disable this particular error globally. Uni-dimensional arrays (e.g. `f32["x"]`) will throw an error that's actually useful (F821, undefined name), so instead of disabling this globally, you should instead prepend a space to the start of your shape, e.g. `f32[" x"]`. `jaxtyping` will treat this in the same way, whilst `flake8` will now throw an F722 error that you can disable as before.
In the case of `flake8`, at least, this is easily resolved. Multi-dimensional arrays (e.g. `Float32[Array, "b c"]`) will throw a very unusual error (F722, syntax error in forward annotation), so you can safely just disable this particular error globally. Uni-dimensional arrays (e.g. `Float32[Array, "x"]`) will throw an error that's actually useful (F821, undefined name), so instead of disabling this globally, you should instead prepend a space to the start of your shape, e.g. `Float32[Array, " x"]`. `jaxtyping` will treat this in the same way, whilst `flake8` will now throw an F722 error that you can disable as before.
## What about support for static type checkers, like `mypy`, `pyright`, etc.?
## Does jaxtyping use [PEP 646](https://www.python.org/dev/peps/pep-0646/) (variadic generics)?
Nope.
The intention of PEP 646 was to make it possible for static type checkers to perform shape checks of arrays. Unfortunately, this still isn't yet practical, so jaxtyping deliberately does not use this. (Yet?)
Python's static typing ecosystem is a complicated collection of edge cases. Many of them block ML/scientific computing in particular. A few examples:
The real problem is that Python's static typing ecosystem is a complicated collection of edge cases. Many of them block ML/scientific computing in particular. For example:
1. The static type system is intrinsically not expressive enough to describe operations like concatenation, stacking, or broadcasting.
2. Axes have to be lifted to type-level variables. Meanwhile the approach taken in libraries like `jaxtyping` and [TorchTyping](https://github.com/patrick-kidger/torchtyping) is to use value-level variables for types: because that's what the underlying JAX, PyTorch etc. libraries use! As such, making a static type checker work with these libraries would require either fundamentally rewriting these libraries, or exhaustively maintaining type stubs for them, and would *still* require a `typing.cast` any time you use anything unstubbed (e.g. any third party library, or part of your codebase you haven't typed yet). This is a huge maintenance burden for anyone.
2. Axes have to be lifted to type-level variables. Meanwhile the approach taken in libraries like `jaxtyping` and [TorchTyping](https://github.com/patrick-kidger/torchtyping) is to use value-level variables for types: because that's what the underlying JAX, PyTorch etc. libraries use! As such, making a static type checker work with these libraries would require either fundamentally rewriting these libraries, or exhaustively maintaining type stubs for them, and would *still* require a `typing.cast` any time you use anything unstubbed (e.g. any third party library, or part of your codebase you haven't typed yet). This is a huge maintenance burden.
3. Static type checkers have a variety of bugs that affect this use case. `mypy` doesn't support `Protocol`s correctly. `pyright` doesn't support genericised subprotocols. etc.
4. Variadic generics exist. Variadic protocols do not. (It's not clear that these have been contemplated.)
4. Variadic generics exist. Variadic protocols do not. (It's not clear that these were contemplated.)
5. The syntax for static typing is verbose. You have to write things like `Array[Unpack[AnyShape], Literal[3], Height, Width]` instead of `Array["... 3 height width"]`.
5. The syntax for static typing is a little verbose. You have to write things like `Array[Float32, Unpack[AnyShape], Literal[3], Height, Width]` instead of `Float32[Array, "... 3 height width"]`.
6. [The underlying type system has flaws](https://github.com/patrick-kidger/torchtyping/issues/37#issuecomment-1153294196). [The numeric tower is broken](https://stackoverflow.com/a/69383462); [int is not a number](https://github.com/python/mypy/issues/3186#issuecomment-885718629); [virtual base classes don't work](https://github.com/python/mypy/issues/2922); [complex lies about having comparison operations, so type checkers have to lie about that lie in order to remove them again](https://posita.github.io/numerary/0.4/whytho/); `typing.*` don't work with `isinstance`; co/contra-variance are baked into containers (not specified at use-time); `dict` is variadic despite... not being variadic; bool is a subclass of int (!); ... etc. etc.
## What about [PEP 646](https://www.python.org/dev/peps/pep-0646/) and variadic generics?
[Doesn't change the previous issues, unfortunately.](https://github.com/patrick-kidger/torchtyping/issues/37) All the problems of the previous heading still hold true. They're just also true for types like `AnyDimensionalArray[Batch, Channels, AsManyArgumentsAsWePlease]` as well as types like `TwoDimensionalArray[Batch, Channels]`.
## Is the lack of interaction with static typing a problem?
At least for any software that is mostly just running JAX code, no!
The correct way to use JAX is to put together all your operations, and then put a single `jax.jit` right at the very top. This gives you optimal speed; anything else will be unnecessarily (and substantially) slower.
This means that all the type checking only gets resolved once: at trace time. Afterwards JAX still lowers everything down to the same optimised code.
In some sense, `python myprogram.py` just ends up doing the same as `mypy myprogram.py`. Except instead of throwing away all the work used to parse your code, build the abstract syntax tree, etc. (and requiring you to then run `python myprogram.py` afterwards to actually use it), it can keep it around and just run your code immediately.
TL;DR: `jax.jit` is amazing.
6. [The underlying type system has flaws](https://github.com/patrick-kidger/torchtyping/issues/37#issuecomment-1153294196).
[The numeric tower is broken](https://stackoverflow.com/a/69383462);
[int is not a number](https://github.com/python/mypy/issues/3186#issuecomment-885718629);
[virtual base classes don't work](https://github.com/python/mypy/issues/2922);
[complex lies about having comparison operations, so type checkers have to lie about that lie in order to remove them again](https://posita.github.io/numerary/0.4/whytho/);
`typing.*` don't work with `isinstance`;
co/contra-variance are baked into containers (not specified at use-time);
`dict` is variadic despite... not being variadic;
bool is a subclass of int (!);
... etc. etc.
+2 -2
View File
@@ -1,2 +1,2 @@
include LICENSE
prune tests
include jaxtyping/py.typed
prune test
+7 -4
View File
@@ -7,15 +7,18 @@ Type annotations **and runtime checking** for:
**For example:**
```python
from jaxtyping import f32, PyTree
from jaxtyping import Array, Float, PyTree
def matrix_multiply(x: f32["dim1 dim2"], y: f32["dim2 dim3"]) -> f32["dim1 dim3"]:
# Accepts floating-point 2D arrays with matching dimensions
def matrix_multiply(x: Float[Array, "dim1 dim2"],
y: Float[Array, "dim2 dim3"]
) -> Float[Array, "dim1 dim3"]:
...
def accepts_pytree_of_ints(x: PyTree[int]):
...
def accepts_pytree_of_arrays(x: PyTree[f32["batch c1 c2"]]):
def accepts_pytree_of_arrays(x: PyTree[Float[Array, "batch c1 c2"]]):
...
```
@@ -49,7 +52,7 @@ SymPy<->JAX conversion; train symbolic expressions via gradient descent: [sympy2
Shape annotations + runtime type checking is inspired by [TorchTyping](https://github.com/patrick-kidger/torchtyping).
The concise syntax is inspired by [etils.array_types](https://github.com/google/etils/tree/main/etils/array_types).
The concise syntax is partially inspired by [etils.array_types](https://github.com/google/etils/tree/main/etils/array_types).
### Disclaimer
+40 -24
View File
@@ -17,38 +17,54 @@
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import typing
if typing.TYPE_CHECKING:
# type checkers don't know which branch below will be executed
from jax.numpy import ndarray as Array
else:
if getattr(typing, "GENERATING_DOCUMENTATION", False):
class Array:
pass
Array.__module__ = "builtins"
else:
from jax.numpy import ndarray as Array
from .array_types import (
AbstractArray,
AbstractDtype,
Array,
b,
bf16,
c,
c64,
c128,
f,
f16,
f32,
f64,
BFloat16,
Bool,
Complex,
Complex64,
Complex128,
Float,
Float16,
Float32,
Float64,
get_array_name_format,
i,
i8,
i16,
i32,
i64,
n,
Inexact,
Int,
Int8,
Int16,
Int32,
Int64,
Integer,
Num,
set_array_name_format,
t,
u,
u8,
u16,
u32,
u64,
x,
Shaped,
UInt,
UInt8,
UInt16,
UInt32,
UInt64,
)
from .decorator import jaxtyped
from .import_hook import install_import_hook
from .pytree_type import PyTree
__version__ = "0.0.2"
__version__ = "0.2.6"
+334 -129
View File
@@ -17,11 +17,13 @@
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import enum
import functools as ft
from typing import Any, Dict, List, NoReturn, Optional, Tuple, Union
import typing
from typing import Any, Dict, List, NoReturn, Optional, Tuple, TYPE_CHECKING, Union
from typing_extensions import Literal
import jax.numpy as jnp
import numpy as np
from .decorator import storage
@@ -40,10 +42,17 @@ def set_array_name_format(value):
_any_dtype = object()
_anonymous_dim = object()
_anonymous_variadic_dim = object()
class _DimType(enum.Enum):
named = enum.auto()
fixed = enum.auto()
symbolic = enum.auto()
class _NamedDim:
def __init__(self, name, broadcastable):
self.name = name
@@ -62,21 +71,28 @@ class _FixedDim:
self.broadcastable = broadcastable
class _SymbolicDim:
def __init__(self, expr, broadcastable):
self.expr = expr
self.broadcastable = broadcastable
_AbstractDimOrVariadicDim = Union[
Literal[_anonymous_dim],
Literal[_anonymous_variadic_dim],
_NamedDim,
_NamedVariadicDim,
_FixedDim,
_SymbolicDim,
]
_AbstractDim = Union[Literal[_anonymous_dim], _NamedDim, _FixedDim]
_AbstractDim = Union[Literal[_anonymous_dim], _NamedDim, _FixedDim, _SymbolicDim]
def _check_dims(
cls_dims: List[_AbstractDim],
obj_shape: Tuple[int],
memo: Dict[str, Union[int, Tuple[int]]],
):
single_memo: Dict[str, int],
) -> bool:
assert len(cls_dims) == len(obj_shape)
for cls_dim, obj_size in zip(cls_dims, obj_shape):
if cls_dim is _anonymous_dim:
@@ -86,12 +102,24 @@ def _check_dims(
elif type(cls_dim) is _FixedDim:
if cls_dim.size != obj_size:
return False
elif type(cls_dim) is _SymbolicDim:
try:
eval_size = eval(cls_dim.expr, single_memo)
except NameError as e:
raise NameError(
f"Cannot process symbolic dimension '{cls_dim.expr}' as some "
"dimension names have not been processed. In practice you should "
"usually only use symbolic dimensions in annotations for return "
"types, referring only to dimensions annotated for arguments."
) from e
if eval_size != obj_size:
return False
else:
assert type(cls_dim) is _NamedDim
try:
cls_size = memo[cls_dim.name]
cls_size = single_memo[cls_dim.name]
except KeyError:
memo[cls_dim.name] = obj_size
single_memo[cls_dim.name] = obj_size
else:
if cls_size != obj_size:
return False
@@ -100,36 +128,67 @@ def _check_dims(
class _MetaAbstractArray(type):
def __instancecheck__(cls, obj):
if not isinstance(obj, jnp.ndarray):
if not isinstance(obj, cls.array_type):
return False
if cls.dtypes is not _any_dtype and obj.dtype not in cls.dtypes:
if hasattr(obj.dtype, "type") and hasattr(obj.dtype.type, "__name__"):
# JAX, numpy
dtype = obj.dtype.type.__name__
elif hasattr(obj.dtype, "as_numpy_dtype"):
# TensorFlow
dtype = obj.dtype.as_numpy_dtype.__name__
else:
# PyTorch
repr_dtype = repr(obj.dtype).split(".")
if len(repr_dtype) == 2 and repr_dtype[0] == "torch":
dtype = repr_dtype[1]
else:
raise RuntimeError(
"Unrecognised array/tensor type to extract dtype from"
)
if cls.dtypes is not _any_dtype and dtype not in cls.dtypes:
return False
if len(storage.memo_stack) == 0:
no_temp_memo = hasattr(storage, "memo_stack") and len(storage.memo_stack) != 0
if no_temp_memo:
single_memo, variadic_memo, variadic_broadcast_memo = storage.memo_stack[-1]
# Make a copy so we don't mutate the original memo during the shape check.
single_memo = single_memo.copy()
variadic_memo = variadic_memo.copy()
variadic_broadcast_memo = variadic_broadcast_memo.copy()
else:
# `isinstance` happening outside any @jaxtyped decorators, e.g. at the
# global scope. In this case just create a temporary memo, since we're not
# going to be comparing against any stored values anyway.
memo = {}
temp_memo = True
else:
# Make a copy so we don't mutate the original memo during the shape check.
memo = storage.memo_stack[-1].copy()
temp_memo = False
single_memo = {}
variadic_memo = {}
variadic_broadcast_memo = {}
if cls._check_shape(obj, memo):
if cls._check_shape(obj, single_memo, variadic_memo, variadic_broadcast_memo):
# We update the memo every time we successfully pass a shape check
if not temp_memo:
storage.memo_stack[-1] = memo
if no_temp_memo:
storage.memo_stack[-1] = (
single_memo,
variadic_memo,
variadic_broadcast_memo,
)
return True
else:
return False
def _check_shape(cls, obj, memo):
def _check_shape(
cls,
obj,
single_memo: Dict[str, int],
variadic_memo: Dict[str, Tuple[int, ...]],
variadic_broadcast_memo: Dict[str, List[Tuple[int, ...]]],
):
if cls.index_variadic is None:
if obj.ndim != len(cls.dims):
return False
return _check_dims(cls.dims, obj.shape, memo)
return _check_dims(cls.dims, obj.shape, single_memo)
else:
if obj.ndim < len(cls.dims) - 1:
return False
@@ -137,38 +196,47 @@ class _MetaAbstractArray(type):
j = -(len(cls.dims) - i - 1)
if j == 0:
j = None
if not _check_dims(cls.dims[:i], obj.shape[:i], memo):
if not _check_dims(cls.dims[:i], obj.shape[:i], single_memo):
return False
if j is not None and not _check_dims(cls.dims[j:], obj.shape[j:], memo):
if j is not None and not _check_dims(
cls.dims[j:], obj.shape[j:], single_memo
):
return False
variadic_dim = cls.dims[i]
if variadic_dim is not _anonymous_variadic_dim:
if variadic_dim is _anonymous_variadic_dim:
return True
else:
assert type(variadic_dim) is _NamedVariadicDim
variadic_name = variadic_dim.name
try:
variadic_shape = memo[variadic_name]
if variadic_dim.broadcastable:
variadic_shapes = variadic_broadcast_memo[variadic_name]
else:
variadic_shape = variadic_memo[variadic_name]
except KeyError:
memo[variadic_name] = obj.shape[i:j]
if variadic_dim.broadcastable:
variadic_broadcast_memo[variadic_name] = [obj.shape[i:j]]
else:
variadic_memo[variadic_name] = obj.shape[i:j]
return True
else:
if variadic_dim.broadcastable:
new_variadic_shape = []
obj_shape = obj.shape[i:j]
if len(variadic_shape) != len(obj_shape):
return False
for old_size, new_size in zip(variadic_shape, obj_shape):
if old_size == 1:
new_variadic_shape.append(new_size)
else:
if new_size != 1 and old_size != new_size:
return False
new_variadic_shape.append(old_size)
memo[variadic_name] = tuple(new_variadic_shape)
new_shape = obj.shape[i:j]
for existing_shape in variadic_shapes:
try:
np.broadcast_shapes(new_shape, existing_shape)
except ValueError:
return False
variadic_shapes.append(new_shape)
return True
else:
return variadic_shape == obj.shape[i:j]
return True
assert False
class AbstractArray(metaclass=_MetaAbstractArray):
dtypes: List[jnp.dtype]
array_type: Any
dtypes: List[str]
dims: List[_AbstractDimOrVariadicDim]
index_variadic: Optional[int]
@@ -176,16 +244,24 @@ class AbstractArray(metaclass=_MetaAbstractArray):
class _MetaAbstractDtype(type):
def __instancecheck__(cls, obj: Any) -> NoReturn:
raise RuntimeError(
f"Do not use `isinstance(x, jaxtyping.{cls.__name__}`. If you want to "
f"Do not use `isinstance(x, jaxtyping.{cls.__name__})`. If you want to "
"check just the dtype of an array, then use "
f'`jaxtyping.{cls.__name__}["..."]`.'
f'`jaxtyping.{cls.__name__}[jnp.ndarray, "..."]`.'
)
@ft.lru_cache(maxsize=None)
def __getitem__(cls, dim_str: str) -> _MetaAbstractArray:
def __getitem__(cls, item: Tuple[Any, str]) -> _MetaAbstractArray:
if not isinstance(item, tuple) or len(item) != 2:
raise ValueError(
"As of jaxtyping v0.2.0, type annotations must now include an explicit "
"array type. For example `jaxtyping.Float32[jnp.ndarray, 'foo bar']`."
)
array_type, dim_str = item
del item
if not isinstance(dim_str, str):
raise ValueError(
"Shape specification must be a string. Axes should be separated with spaces."
"Shape specification must be a string. Axes should be separated with "
"spaces."
)
dims = []
index_variadic = None
@@ -195,121 +271,250 @@ class _MetaAbstractDtype(type):
raise ValueError(
"Dimensions should be separated with spaces, not commas"
)
broadcastable = False
if elem.endswith("#"):
broadcastable = True
elem = elem[:-1]
try:
elem = int(elem)
except ValueError:
if elem == "_":
elem = _anonymous_dim
elif elem == "...":
if index_variadic is not None:
raise ValueError("Cannot have multiple variadic dimensions")
index_variadic = index
elem = _anonymous_variadic_dim
elif elem[0] == "*":
if index_variadic is not None:
raise ValueError("Cannot have multiple variadic dimensions")
index_variadic = index
elem = _NamedVariadicDim(elem[1:], broadcastable)
else:
elem = _NamedDim(elem, broadcastable)
raise ValueError(
"As of jaxtyping v0.1.0, broadcastable dimensions are now denoted "
"with a # at the start, rather than at the end"
)
if "..." in elem:
if elem != "...":
raise ValueError(
"Anonymous multiple dimension '...' must be used on its own; "
f"got {elem}"
)
broadcastable = False
variadic = True
anonymous = True
dim_type = _DimType.named
else:
broadcastable = False
variadic = False
anonymous = False
while True:
if len(elem) == 0:
# This branch needed as just `_` is valid
break
first_char = elem[0]
if first_char == "#":
if broadcastable:
raise ValueError(
"Do not use # twice to denote broadcastability, e.g. "
"`##foo` is not allowed"
)
broadcastable = True
elem = elem[1:]
elif first_char == "*":
if variadic:
raise ValueError(
"Do not use * twice to denote accepting multiple "
"dimensions, e.g. `**foo` is not allowed"
)
variadic = True
elem = elem[1:]
elif first_char == "_":
if anonymous:
raise ValueError(
"Do not use _ twice to denote anonymity, e.g. `__foo` "
"is not allowed"
)
anonymous = True
elem = elem[1:]
else:
break
try:
elem = int(elem)
except ValueError:
if len(elem) == 0 or elem.isidentifier():
dim_type = _DimType.named
else:
dim_type = _DimType.symbolic
else:
dim_type = _DimType.fixed
if variadic:
if index_variadic is not None:
raise ValueError(
"Cannot use multiple-dimension specifiers (`*name` or `...`) "
"more than once"
)
index_variadic = index
if dim_type is _DimType.fixed:
if variadic:
raise ValueError(
"Cannot have a fixed axis bind to multiple dimensions, e.g. "
"`*4` is not allowed"
)
if anonymous:
raise ValueError(
"Cannot have a fixed axis be anonymous, e.g. `_4` is not "
"allowed"
)
elem = _FixedDim(elem, broadcastable)
elif dim_type is _DimType.named:
if anonymous:
if broadcastable:
raise ValueError(
"Cannot have a dimension be both anonymous and "
"broadcastable, e.g. `#_` is not allowed"
)
if variadic:
elem = _anonymous_variadic_dim
else:
elem = _anonymous_dim
else:
if variadic:
elem = _NamedVariadicDim(elem, broadcastable)
else:
elem = _NamedDim(elem, broadcastable)
else:
assert dim_type is _DimType.symbolic
if anonymous:
raise ValueError(
"Cannot have a symbolic dimension be anonymous, e.g. "
"`_foo+bar` is not allowed"
)
if variadic:
raise ValueError(
"Cannot have symbolic multiple-dimensions, e.g. "
"`*foo+bar` is not allowed"
)
elem = compile(elem, "<string>", "eval")
elem = _SymbolicDim(elem, broadcastable)
dims.append(elem)
if _array_name_format == "dtype_and_shape":
name = f"{cls.__name__}['{dim_str}']"
# In python 3.8, e.g., typing.Union lacks `__name__`.
try:
type_str = array_type.__name__
except AttributeError:
type_str = repr(array_type)
name = f"{cls.__name__}[{type_str}, '{dim_str}']"
elif _array_name_format == "array":
name = "Array"
name = array_type.__name__
else:
raise ValueError(f"array_name_format {_array_name_format} not recognised")
return _MetaAbstractArray(
out = _MetaAbstractArray(
name,
(AbstractArray,),
dict(dtypes=cls.dtypes, dims=dims, index_variadic=index_variadic),
dict(
array_type=array_type,
dtypes=cls.dtypes,
dims=dims,
index_variadic=index_variadic,
),
)
if getattr(typing, "GENERATING_DOCUMENTATION", False):
out.__module__ = "builtins"
else:
out.__module__ = "jaxtyping"
return out
class AbstractDtype(metaclass=_MetaAbstractDtype):
dtypes: Union[str, List[str], Literal[_any_dtype]]
dtypes: Union[Literal[_any_dtype], List[str]]
def __init__(self, *args, **kwargs):
raise RuntimeError(
"AbstractDtype cannot be instantiated. Perhaps you wrote e.g. "
'`f32("shape")` when you mean `f32["shape"]`?'
'`Float32("shape")` when you mean `Float32[jnp.ndarray, "shape"]`?'
)
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
dtypes = cls.dtypes
if dtypes is not _any_dtype:
if not isinstance(dtypes, list):
dtypes = [dtypes]
dtypes = [jnp.dtype(d) for d in dtypes]
dtypes: Union[Literal[_any_dtype], str, List[str]] = cls.dtypes
if isinstance(dtypes, str):
dtypes = [dtypes]
cls.dtypes = dtypes
_bool = "bool"
_uint8 = "uint8"
_uint16 = "uint16"
_uint32 = "uint32"
_uint64 = "uint64"
_int8 = "int8"
_int16 = "int16"
_int32 = "int32"
_int64 = "int64"
_bfloat16 = "bfloat16"
_float16 = "float16"
_float32 = "float32"
_float64 = "float64"
_complex64 = "complex64"
_complex128 = "complex128"
if TYPE_CHECKING:
# Note that `from typing_extensions import Annotated; ... = Annotated`
# does not work with static type checkers. `Annotated` is a typeform rather
# than a type, meaning it cannot be assigned.
from typing_extensions import Annotated as BFloat16
from typing_extensions import Annotated as Bool
from typing_extensions import Annotated as Complex
from typing_extensions import Annotated as Complex64
from typing_extensions import Annotated as Complex128
from typing_extensions import Annotated as Float
from typing_extensions import Annotated as Float16
from typing_extensions import Annotated as Float32
from typing_extensions import Annotated as Float64
from typing_extensions import Annotated as Inexact
from typing_extensions import Annotated as Int
from typing_extensions import Annotated as Int8
from typing_extensions import Annotated as Int16
from typing_extensions import Annotated as Int32
from typing_extensions import Annotated as Int64
from typing_extensions import Annotated as Integer
from typing_extensions import Annotated as Num
from typing_extensions import Annotated as Shaped
from typing_extensions import Annotated as UInt
from typing_extensions import Annotated as UInt8
from typing_extensions import Annotated as UInt16
from typing_extensions import Annotated as UInt32
from typing_extensions import Annotated as UInt64
else:
_bool = "bool_"
_uint8 = "uint8"
_uint16 = "uint16"
_uint32 = "uint32"
_uint64 = "uint64"
_int8 = "int8"
_int16 = "int16"
_int32 = "int32"
_int64 = "int64"
_bfloat16 = "bfloat16"
_float16 = "float16"
_float32 = "float32"
_float64 = "float64"
_complex64 = "complex64"
_complex128 = "complex128"
def _make_dtype(_dtypes, name):
class _Cls(AbstractDtype):
dtypes = _dtypes
def _make_dtype(_dtypes, name):
class _Cls(AbstractDtype):
dtypes = _dtypes
_Cls.__name__ = name
_Cls.__qualname__ = name
if getattr(typing, "GENERATING_DOCUMENTATION", False):
_Cls.__module__ = "builtins"
else:
_Cls.__module__ = "jaxtyping"
return _Cls
_Cls.__name__ = name
_Cls.__qualname__ = name
return _Cls
UInt8 = _make_dtype(_uint8, "UInt8")
UInt16 = _make_dtype(_uint16, "UInt16")
UInt32 = _make_dtype(_uint32, "UInt32")
UInt64 = _make_dtype(_uint64, "UInt64")
Int8 = _make_dtype(_int8, "Int8")
Int16 = _make_dtype(_int16, "Int16")
Int32 = _make_dtype(_int32, "Int32")
Int64 = _make_dtype(_int64, "Int64")
BFloat16 = _make_dtype(_bfloat16, "BFloat16")
Float16 = _make_dtype(_float16, "Float16")
Float32 = _make_dtype(_float32, "Float32")
Float64 = _make_dtype(_float64, "Float64")
Complex64 = _make_dtype(_complex64, "Complex64")
Complex128 = _make_dtype(_complex128, "Complex128")
uints = [_uint8, _uint16, _uint32, _uint64]
ints = [_int8, _int16, _int32, _int64]
floats = [_bfloat16, _float16, _float32, _float64]
complexes = [_complex64, _complex128]
b = _make_dtype(_bool, "b")
u8 = _make_dtype(_uint8, "u8")
u16 = _make_dtype(_uint16, "u16")
u32 = _make_dtype(_uint32, "u32")
u64 = _make_dtype(_uint64, "u64")
i8 = _make_dtype(_int8, "i8")
i16 = _make_dtype(_int16, "i16")
i32 = _make_dtype(_int32, "i32")
i64 = _make_dtype(_int64, "i64")
bf16 = _make_dtype(_bfloat16, "bf16")
f16 = _make_dtype(_float16, "f16")
f32 = _make_dtype(_float32, "f32")
f64 = _make_dtype(_float64, "f64")
c64 = _make_dtype(_complex64, "c64")
c128 = _make_dtype(_complex128, "c128")
# We match NumPy's type hierarachy in what types to provide. See the diagram at
# https://numpy.org/doc/stable/reference/arrays.scalars.html#scalars
uints = [_uint8, _uint16, _uint32, _uint64]
ints = [_int8, _int16, _int32, _int64]
floats = [_bfloat16, _float16, _float32, _float64]
complexes = [_complex64, _complex128]
Bool = _make_dtype(_bool, "Bool")
UInt = _make_dtype(uints, "UInt")
Int = _make_dtype(ints, "Int")
Integer = _make_dtype(uints + ints, "Integer")
Float = _make_dtype(floats, "Float")
Complex = _make_dtype(complexes, "Complex")
Inexact = _make_dtype(floats + complexes, "Inexact")
Num = _make_dtype(uints + ints + floats + complexes, "Num")
# We match NumPy's type hierarachy in what types to provide. See the diagram at
# https://numpy.org/doc/stable/reference/arrays.scalars.html#scalars
#
# No attempt is made to match up against their character codes: all of the below are
# abstract base classes without NumPy chararacter codes.
u = _make_dtype(uints, "u")
i = _make_dtype(ints, "i")
t = _make_dtype(uints + ints, "t") # integer
f = _make_dtype(floats, "f")
c = _make_dtype(complexes, "c")
x = _make_dtype(floats + complexes, "x") # inexact
n = _make_dtype(uints + ints + floats + complexes, "n") # number
Array = _make_dtype(_any_dtype, "Array")
Shaped = _make_dtype(_any_dtype, "Shaped")
+20 -11
View File
@@ -22,17 +22,26 @@ import threading
storage = threading.local()
storage.memo_stack = []
class _Jaxtyped:
def __init__(self, fn):
self.fn = fn
def __get__(self, instance, owner):
return ft.wraps(self.fn)(_Jaxtyped(self.fn.__get__(instance, owner)))
def __call__(self, *args, **kwargs):
try:
memo_stack = storage.memo_stack
except AttributeError:
memo_stack = storage.memo_stack = []
memo_stack.append(({}, {}, {}))
try:
return self.fn(*args, **kwargs)
finally:
memo_stack.pop()
def jaxtyped(fn):
@ft.wraps(fn)
def wrapper(*args, **kwargs):
memo = {}
storage.memo_stack.append(memo)
try:
return fn(*args, **kwargs)
finally:
storage.memo_stack.pop()
return wrapper
return ft.wraps(fn)(_Jaxtyped(fn))
+15 -12
View File
@@ -31,19 +31,20 @@
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this
# software and associated documentation files (the "Software"), to deal in the Software
# without restriction, including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
# to whom the Software is furnished to do so, subject to the following conditions:
# without restriction, including without limitation the rights to use, copy, modify,
# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or
# substantial portions of the Software.
# The above copyright notice and this permission notice shall be included in all copies
# or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
# FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
# OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# ---------
@@ -148,7 +149,8 @@ class _JaxtypingLoader(SourceFileLoader):
)
def exec_module(self, module):
# Use a custom optimization marker the import lock should make this monkey patch safe
# Use a custom optimization marker - the import lock should make this monkey
# patch safe
with patch(
"importlib._bootstrap_external.cache_from_source",
_optimized_cache_from_source,
@@ -216,7 +218,8 @@ class ImportHookManager:
def install_import_hook(
modules: Iterable[str], typechecker: Optional[Tuple[str, str]]
) -> ImportHookManager:
"""Automatically apply `@jaxtyped`, and optionally a type checker, to all classes and functions.
"""Automatically apply `@jaxtyped`, and optionally a type checker, to all classes
and functions.
It will only be applied to modules loaded **after** this hook has been installed.
+1
View File
@@ -0,0 +1 @@
+23 -4
View File
@@ -18,7 +18,9 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import functools as ft
from typing import Generic, TypeVar
import typing
from typing import Generic, TYPE_CHECKING, TypeVar
from typing_extensions import Protocol
import jax
import typeguard
@@ -33,6 +35,7 @@ class _FakePyTree(Generic[_T]):
_FakePyTree.__name__ = "PyTree"
_FakePyTree.__qualname__ = "PyTree"
_FakePyTree.__module__ = "builtins"
# Can't do type("PyTree", (Generic[_T],), {}) because dynamic subclassing of typeforms
# isn't allowed.
# Can't do types.new_class("PyTree", (Generic[_T],), {}) because that has __module__
@@ -49,7 +52,12 @@ class _MetaPyTree(type):
@ft.lru_cache(maxsize=None)
def __getitem__(cls, item):
name = str(_FakePyTree[item])
return _MetaSubscriptPyTree(name, (), {"leaftype": item})
out = _MetaSubscriptPyTree(name, (), {"leaftype": item})
if getattr(typing, "GENERATING_DOCUMENTATION", False):
out.__module__ = "builtins"
else:
out.__module__ = "jaxtyping"
return out
class _MetaSubscriptPyTree(type):
@@ -79,7 +87,18 @@ class _MetaSubscriptPyTree(type):
return all(map(is_leaftype, leaves))
PyTree = _MetaPyTree("PyTree", (), {})
if TYPE_CHECKING:
# Work around pytype bug #1288
# pytype: skip-file
class PyTree(Protocol[_T]):
pass
else:
PyTree = _MetaPyTree("PyTree", (), {})
if getattr(typing, "GENERATING_DOCUMENTATION", False):
PyTree.__module__ = "builtins"
else:
PyTree.__module__ = "jaxtyping"
# Can't do `class PyTree(Generic[_T]): ...` because we need to override the
# instancecheck for PyTree[foo], but we subclassing
# instancecheck for PyTree[foo], but subclassing
# `type(Generic[int])`, i.e. `typing._GenericAlias` is disallowed.
+13 -3
View File
@@ -28,7 +28,8 @@ _here = pathlib.Path(__file__).resolve().parent
name = "jaxtyping"
# for simplicity we actually store the version in the __version__ attribute in the source
# for simplicity we actually store the version in the __version__ attribute in the
# source
with open(_here / name / "__init__.py") as f:
meta_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", f.read(), re.M)
if meta_match:
@@ -40,7 +41,10 @@ author = "Patrick Kidger"
author_email = "contact@kidger.site"
description = "Type annotations and runtime checking for shape and dtype of JAX arrays, and PyTrees."
description = (
"Type annotations and runtime checking for shape and dtype of JAX "
"arrays, and PyTrees."
)
with open(_here / "README.md", "r") as f:
readme = f.read()
@@ -63,7 +67,12 @@ python_requires = "~=3.7"
# We use typeguard internally (in a fairly minimal way), but it's not required that
# end users make the same choice.
install_requires = ["jax>=0.3.4", "typeguard>=2.13.3", "typing_extensions>=4.2.0"]
install_requires = [
"jax>=0.3.4",
"numpy>=1.20.0",
"typeguard>=2.13.3",
"typing_extensions>=4.2.0",
]
entry_points = dict(pytest11=["jaxtyping = jaxtyping.pytest_plugin"])
@@ -85,4 +94,5 @@ setuptools.setup(
install_requires=install_requires,
entry_points=entry_points,
packages=[name],
include_package_data=True,
)
View File
+13 -5
View File
@@ -19,13 +19,24 @@
import random
import beartype
import jax.random as jr
import pytest
import typeguard
@pytest.fixture(params=[typeguard.typechecked, beartype.beartype])
try:
import beartype
except ImportError:
def skip(*args, **kwargs):
pytest.skip("Beartype not installed")
typecheck_params = [typeguard.typechecked, skip]
else:
typecheck_params = [typeguard.typechecked, beartype.beartype]
@pytest.fixture(params=typecheck_params)
def typecheck(request):
return request.param
@@ -37,6 +48,3 @@ def getkey():
return jr.PRNGKey(random.randint(0, 2**31 - 1))
return _getkey
ParamException = (TypeError, beartype.roar.BeartypeCallHintParamViolation)
+8 -3
View File
@@ -17,12 +17,17 @@
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import beartype
import equinox as eqx
ParamError = (TypeError, beartype.roar.BeartypeCallHintParamViolation)
ReturnError = (TypeError, beartype.roar.BeartypeCallHintReturnViolation)
try:
import beartype
except ImportError:
ParamError = TypeError
ReturnError = TypeError
else:
ParamError = (TypeError, beartype.roar.BeartypeCallHintParamViolation)
ReturnError = (TypeError, beartype.roar.BeartypeCallHintReturnViolation)
@eqx.filter_jit
+4 -3
View File
@@ -19,12 +19,13 @@
import jax.numpy as jnp
import pytest
from helpers import ParamError
from jaxtyping import f32
from jaxtyping import Float32
from .helpers import ParamError
def g(x: f32[" b"]):
def g(x: Float32[jnp.ndarray, " b"]):
pass
+4 -3
View File
@@ -19,12 +19,13 @@
import jax.numpy as jnp
import pytest
from helpers import ParamError
from jaxtyping import f32
from jaxtyping import Float32
from .helpers import ParamError
def g(x: f32[" b"]):
def g(x: Float32[jnp.ndarray, " b"]):
pass
@@ -19,12 +19,13 @@
import jax.numpy as jnp
import pytest
from helpers import ParamError
from jaxtyping import f32
from jaxtyping import Float32
from ..helpers import ParamError
def g(x: f32[" b"]):
def g(x: Float32[jnp.ndarray, " b"]):
pass
+4 -3
View File
@@ -19,12 +19,13 @@
import jax.numpy as jnp
import pytest
from helpers import ParamError
from jaxtyping import f32
from jaxtyping import Float32
from .helpers import ParamError
def g(x: f32[" b"]):
def g(x: Float32[jnp.ndarray, " b"]):
pass
+120 -40
View File
@@ -20,31 +20,64 @@
import jax.numpy as jnp
import jax.random as jr
import pytest
from helpers import ParamError, ReturnError
from jaxtyping import Array, f, f32, jaxtyped
from jaxtyping import AbstractDtype, Array, Float, Float32, jaxtyped, Shaped
from .helpers import ParamError, ReturnError
def test_basic(typecheck):
@jaxtyped
@typecheck
def g(x: Array["..."]):
def g(x: Shaped[Array, "..."]):
pass
g(jnp.array(1.0))
def test_dtypes():
from jaxtyping import ( # noqa: F401
Array,
BFloat16,
Bool,
Complex,
Complex64,
Complex128,
Float,
Float16,
Float32,
Float64,
Inexact,
Int,
Int8,
Int16,
Int32,
Int64,
Num,
Shaped,
UInt,
UInt8,
UInt16,
UInt32,
UInt64,
)
for key, val in locals().items():
if issubclass(val, AbstractDtype):
assert key == val.__name__
def test_return(typecheck, getkey):
@jaxtyped
@typecheck
def g(x: f["b c"]) -> f["c b"]:
def g(x: Float[Array, "b c"]) -> Float[Array, "c b"]:
return jnp.transpose(x)
g(jr.normal(getkey(), (3, 4)))
@jaxtyped
@typecheck
def h(x: f["b c"]) -> f["b c"]:
def h(x: Float[Array, "b c"]) -> Float[Array, "b c"]:
return jnp.transpose(x)
with pytest.raises(ReturnError):
@@ -54,7 +87,7 @@ def test_return(typecheck, getkey):
def test_two_args(typecheck, getkey):
@jaxtyped
@typecheck
def g(x: Array["b c"], y: Array["c d"]):
def g(x: Shaped[Array, "b c"], y: Shaped[Array, "c d"]):
return x @ y
g(jr.normal(getkey(), (3, 4)), jr.normal(getkey(), (4, 5)))
@@ -63,7 +96,7 @@ def test_two_args(typecheck, getkey):
@jaxtyped
@typecheck
def h(x: Array["b c"], y: Array["c d"]) -> Array["b d"]:
def h(x: Shaped[Array, "b c"], y: Shaped[Array, "c d"]) -> Shaped[Array, "b d"]:
return x @ y
h(jr.normal(getkey(), (3, 4)), jr.normal(getkey(), (4, 5)))
@@ -74,7 +107,7 @@ def test_two_args(typecheck, getkey):
def test_any_dtype(typecheck, getkey):
@jaxtyped
@typecheck
def g(x: Array["a b"]) -> Array["a b"]:
def g(x: Shaped[Array, "a b"]) -> Shaped[Array, "a b"]:
return x
g(jr.normal(getkey(), (3, 4)))
@@ -91,12 +124,12 @@ def test_any_dtype(typecheck, getkey):
def test_nested_jaxtyped(typecheck, getkey):
@jaxtyped
@typecheck
def g(x: f32["b c"], transpose: bool) -> f32["c b"]:
def g(x: Float32[Array, "b c"], transpose: bool) -> Float32[Array, "c b"]:
return h(x, transpose)
@jaxtyped
@typecheck
def h(x: f32["c b"], transpose: bool) -> f32["b c"]:
def h(x: Float32[Array, "c b"], transpose: bool) -> Float32[Array, "b c"]:
if transpose:
return jnp.transpose(x)
else:
@@ -112,11 +145,11 @@ def test_nested_jaxtyped(typecheck, getkey):
def test_nested_nojaxtyped(typecheck, getkey):
@jaxtyped
@typecheck
def g(x: f32["b c"]):
def g(x: Float32[Array, "b c"]):
return h(x)
@typecheck
def h(x: f32["c b"]):
def h(x: Float32[Array, "c b"]):
return x
with pytest.raises(ParamError):
@@ -126,14 +159,14 @@ def test_nested_nojaxtyped(typecheck, getkey):
def test_isinstance(typecheck, getkey):
@jaxtyped
@typecheck
def g(x: f32["b c"]) -> f32[" z"]:
def g(x: Float32[Array, "b c"]) -> Float32[Array, " z"]:
y = jnp.transpose(x)
assert isinstance(y, f32["c b"])
assert isinstance(y, Float32[Array, "c b"])
assert not isinstance(
y, f32["b z"]
y, Float32[Array, "b z"]
) # z left unbound as b!=c (unless x symmetric, which it isn't)
out = jr.normal(getkey(), (500,))
assert isinstance(out, f32["z"]) # z now bound
assert isinstance(out, Float32[Array, "z"]) # z now bound
return out
g(jr.normal(getkey(), (2, 3)))
@@ -142,7 +175,9 @@ def test_isinstance(typecheck, getkey):
def test_fixed(typecheck, getkey):
@jaxtyped
@typecheck
def g(x: f32["4 5 foo"], y: f32[" foo"]) -> f32["4 5"]:
def g(
x: Float32[Array, "4 5 foo"], y: Float32[Array, " foo"]
) -> Float32[Array, "4 5"]:
return x @ y
a = jr.normal(getkey(), (4, 5, 2))
@@ -157,7 +192,7 @@ def test_fixed(typecheck, getkey):
def test_anonymous(typecheck, getkey):
@jaxtyped
@typecheck
def g(x: f32["foo _"], y: f32[" _"]):
def g(x: Float32[Array, "foo _"], y: Float32[Array, " _"]):
pass
a = jr.normal(getkey(), (3, 4))
@@ -168,7 +203,11 @@ def test_anonymous(typecheck, getkey):
def test_named_variadic(typecheck, getkey):
@jaxtyped
@typecheck
def g(x: f32["*batch foo"], y: f32[" *batch"], z: f32[" foo"]):
def g(
x: Float32[Array, "*batch foo"],
y: Float32[Array, " *batch"],
z: Float32[Array, " foo"],
):
pass
c = jr.normal(getkey(), (5,))
@@ -188,7 +227,7 @@ def test_named_variadic(typecheck, getkey):
@jaxtyped
@typecheck
def h(x: f32[" foo *batch"], y: f32[" foo *batch bar"]):
def h(x: Float32[Array, " foo *batch"], y: Float32[Array, " foo *batch bar"]):
pass
a = jr.normal(getkey(), (4,))
@@ -204,7 +243,7 @@ def test_named_variadic(typecheck, getkey):
def test_anonymous_variadic(typecheck, getkey):
@jaxtyped
@typecheck
def g(x: f32["... foo"], y: f32[" foo"]):
def g(x: Float32[Array, "... foo"], y: Float32[Array, " foo"]):
pass
a1 = jr.normal(getkey(), (5,))
@@ -226,7 +265,7 @@ def test_anonymous_variadic(typecheck, getkey):
def test_broadcast_fixed(typecheck, getkey):
@jaxtyped
@typecheck
def g(x: f32["4#"]):
def g(x: Float32[Array, "#4"]):
pass
g(jr.normal(getkey(), (4,)))
@@ -239,7 +278,7 @@ def test_broadcast_fixed(typecheck, getkey):
def test_broadcast_named(typecheck, getkey):
@jaxtyped
@typecheck
def g(x: f32[" foo#"], y: f32[" foo#"]):
def g(x: Float32[Array, " #foo"], y: Float32[Array, " #foo"]):
pass
a = jr.normal(getkey(), (3,))
@@ -263,7 +302,7 @@ def test_broadcast_named(typecheck, getkey):
def test_broadcast_variadic_named(typecheck, getkey):
@jaxtyped
@typecheck
def g(x: f32[" *foo#"], y: f32[" *foo#"]):
def g(x: Float32[Array, " *#foo"], y: Float32[Array, " *#foo"]):
pass
a = jr.normal(getkey(), (3,))
@@ -282,12 +321,11 @@ def test_broadcast_variadic_named(typecheck, getkey):
g(b, b)
g(c, c)
g(d, d)
g(b, c)
with pytest.raises(ParamError):
g(a, b)
with pytest.raises(ParamError):
g(a, c)
with pytest.raises(ParamError):
g(b, c)
with pytest.raises(ParamError):
g(a, b)
with pytest.raises(ParamError):
@@ -295,26 +333,20 @@ def test_broadcast_variadic_named(typecheck, getkey):
g(a, j)
g(b, j)
with pytest.raises(ParamError):
g(c, j)
with pytest.raises(ParamError):
g(d, j)
with pytest.raises(ParamError):
g(b, k)
g(c, j)
g(d, j)
g(b, k)
g(c, k)
with pytest.raises(ParamError):
g(d, k)
with pytest.raises(ParamError):
g(c, l)
g(d, l)
with pytest.raises(ParamError):
g(a, m)
g(a, m)
g(c, m)
g(d, m)
with pytest.raises(ParamError):
g(a, n)
with pytest.raises(ParamError):
g(b, n)
g(a, n)
g(b, n)
with pytest.raises(ParamError):
g(c, n)
with pytest.raises(ParamError):
@@ -326,6 +358,54 @@ def test_broadcast_variadic_named(typecheck, getkey):
g(o, a)
def test_no_commas(typecheck, getkey):
def test_no_commas():
with pytest.raises(ValueError):
f32["foo, bar"]
Float32[Array, "foo, bar"]
def test_symbolic(typecheck, getkey):
@jaxtyped
@typecheck
def make_slice(x: Float32[Array, " dim"]) -> Float32[Array, " dim-1"]:
return x[1:]
@jaxtyped
@typecheck
def cat(x: Float32[Array, " dim"]) -> Float32[Array, " 2*dim"]:
return jnp.concatenate([x, x])
@jaxtyped
@typecheck
def bad_make_slice(x: Float32[Array, " dim"]) -> Float32[Array, " dim-1"]:
return x
@jaxtyped
@typecheck
def bad_cat(x: Float32[Array, " dim"]) -> Float32[Array, " 2*dim"]:
return jnp.concatenate([x, x, x])
x = jr.normal(getkey(), (5,))
assert make_slice(x).shape == (4,)
assert cat(x).shape == (10,)
y = jr.normal(getkey(), (3, 4))
with pytest.raises(ParamError):
make_slice(y)
with pytest.raises(ParamError):
cat(y)
with pytest.raises(ReturnError):
bad_make_slice(x)
with pytest.raises(ReturnError):
bad_cat(x)
def test_incomplete_symbolic(typecheck, getkey):
@jaxtyped
@typecheck
def foo(x: Float32[Array, " 2*dim"]):
pass
x = jr.normal(getkey(), (4,))
with pytest.raises(NameError):
foo(x)
+13
View File
@@ -0,0 +1,13 @@
from jaxtyping import jaxtyped
class M:
@jaxtyped
@classmethod
def f(cls):
return 3
# Check that the @jaxtyped decorator doesn't blat the __get__ of @classmethod
def test_decorator():
assert M.f() == 3
+16 -9
View File
@@ -24,33 +24,40 @@ from jaxtyping import install_import_hook
def test_import_hook_typeguard():
hook = install_import_hook(
"import_hook_tester_typeguard", ("typeguard", "typechecked")
"test.import_hook_tester_typeguard", ("typeguard", "typechecked")
)
import import_hook_tester_typeguard # noqa: F401
from . import import_hook_tester_typeguard # noqa: F401
hook.uninstall()
def test_import_hook_beartype():
hook = install_import_hook("import_hook_tester_beartype", ("beartype", "beartype"))
import import_hook_tester_beartype # noqa: F401
try:
import beartype # noqa: F401
except ImportError:
pytest.skip("Beartype not installed")
else:
hook = install_import_hook(
"test.import_hook_tester_beartype", ("beartype", "beartype")
)
from . import import_hook_tester_beartype # noqa: F401
hook.uninstall()
hook.uninstall()
def test_import_hook_transitive():
hook = install_import_hook(
"import_hook_tester_transitive", ("typeguard", "typechecked")
"test.import_hook_tester_transitive", ("typeguard", "typechecked")
)
import import_hook_tester_transitive # noqa: F401
from . import import_hook_tester_transitive # noqa: F401
hook.uninstall()
def test_import_hook_broken_checker():
hook = install_import_hook(
"import_hook_tester_broken_checker", ("jaxtyping", "does_not_exist")
"test.import_hook_tester_broken_checker", ("jaxtyping", "does_not_exist")
)
with pytest.raises(AttributeError):
import import_hook_tester_broken_checker # noqa: F401
from . import import_hook_tester_broken_checker # noqa: F401
hook.uninstall()
+5 -4
View File
@@ -24,9 +24,10 @@ import jax
import jax.numpy as jnp
import jax.random as jr
import pytest
from helpers import make_mlp, ParamError
from jaxtyping import f, jaxtyped, PyTree
from jaxtyping import Float, jaxtyped, PyTree
from .helpers import make_mlp, ParamError
def test_direct(typecheck):
@@ -94,7 +95,7 @@ def test_nested_pytrees(getkey, typecheck):
def test_pytree_array(typecheck):
@jaxtyped
@typecheck
def g(x: PyTree[f["..."]]):
def g(x: PyTree[Float[jnp.ndarray, "..."]]):
pass
g(jnp.array(1.0))
@@ -108,7 +109,7 @@ def test_pytree_array(typecheck):
def test_pytree_shaped_array(typecheck, getkey):
@jaxtyped
@typecheck
def g(x: PyTree[f["b c"]]):
def g(x: PyTree[Float[jnp.ndarray, "b c"]]):
pass
g(jnp.array([[1.0]]))
+66
View File
@@ -0,0 +1,66 @@
# Copyright (c) 2022 Google LLC
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import threading
import jax.numpy as jnp
from typeguard import typechecked
from jaxtyping import Array, Float, jaxtyped
class _ErrorableThread(threading.Thread):
def run(self):
try:
super().run()
except Exception as e:
self.exc = e
finally:
del self._target, self._args, self._kwargs
def join(self, timeout=None):
super().join(timeout)
if hasattr(self, "exc"):
raise self.exc
def test_threading_jaxtyped():
@jaxtyped
@typechecked
def add(x: Float[Array, "a b"], y: Float[Array, "a b"]) -> Float[Array, "a b"]:
return x + y
def run():
a = jnp.array([[1.0, 2.0]])
b = jnp.array([[2.0, 3.0]])
add(a, b)
thread = _ErrorableThread(target=run)
thread.start()
thread.join()
def test_threading_nojaxtyped():
def run():
a = jnp.array([[1.0, 2.0]])
assert isinstance(a, Float[Array, "..."])
thread = _ErrorableThread(target=run)
thread.start()
thread.join()