array-api-shape-check 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- array_api_shape_check-0.1.0/LICENSE +22 -0
- array_api_shape_check-0.1.0/PKG-INFO +157 -0
- array_api_shape_check-0.1.0/README.md +129 -0
- array_api_shape_check-0.1.0/pyproject.toml +149 -0
- array_api_shape_check-0.1.0/setup.cfg +4 -0
- array_api_shape_check-0.1.0/src/array_api_shape_check/__init__.py +34 -0
- array_api_shape_check-0.1.0/src/array_api_shape_check/_main.py +446 -0
- array_api_shape_check-0.1.0/src/array_api_shape_check/py.typed +0 -0
- array_api_shape_check-0.1.0/src/array_api_shape_check.egg-info/PKG-INFO +157 -0
- array_api_shape_check-0.1.0/src/array_api_shape_check.egg-info/SOURCES.txt +12 -0
- array_api_shape_check-0.1.0/src/array_api_shape_check.egg-info/dependency_links.txt +1 -0
- array_api_shape_check-0.1.0/src/array_api_shape_check.egg-info/requires.txt +4 -0
- array_api_shape_check-0.1.0/src/array_api_shape_check.egg-info/top_level.txt +1 -0
- array_api_shape_check-0.1.0/tests/test_main.py +11 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
MIT License
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2025 34j
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: array-api-shape-check
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Check shapes of input arrays easily
|
|
5
|
+
Author-email: 34j <34j.95a2p@simplelogin.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/34j/array-api-shape-check/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/34j/array-api-shape-check/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: documentation, https://array-api-shape-check.readthedocs.io
|
|
10
|
+
Project-URL: repository, https://github.com/34j/array-api-shape-check
|
|
11
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Natural Language :: English
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: attrs>=26.1
|
|
24
|
+
Requires-Dist: more-itertools>=11.0.2
|
|
25
|
+
Requires-Dist: numpy>=2.4.4
|
|
26
|
+
Requires-Dist: types-array-api>=1.1.4
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# array-api-shape-check
|
|
30
|
+
|
|
31
|
+
<p align="center">
|
|
32
|
+
<a href="https://github.com/34j/array-api-shape-check/actions/workflows/ci.yml?query=branch%3Amain">
|
|
33
|
+
<img src="https://img.shields.io/github/actions/workflow/status/34j/array-api-shape-check/ci.yml?branch=main&label=CI&logo=github&style=flat-square" alt="CI Status" >
|
|
34
|
+
</a>
|
|
35
|
+
<a href="https://array-api-shape-check.readthedocs.io">
|
|
36
|
+
<img src="https://img.shields.io/readthedocs/array-api-shape-check.svg?logo=read-the-docs&logoColor=fff&style=flat-square" alt="Documentation Status">
|
|
37
|
+
</a>
|
|
38
|
+
<a href="https://codecov.io/gh/34j/array-api-shape-check">
|
|
39
|
+
<img src="https://img.shields.io/codecov/c/github/34j/array-api-shape-check.svg?logo=codecov&logoColor=fff&style=flat-square" alt="Test coverage percentage">
|
|
40
|
+
</a>
|
|
41
|
+
</p>
|
|
42
|
+
<p align="center">
|
|
43
|
+
<a href="https://github.com/astral-sh/uv">
|
|
44
|
+
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json" alt="uv">
|
|
45
|
+
</a>
|
|
46
|
+
<a href="https://github.com/astral-sh/ruff">
|
|
47
|
+
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff">
|
|
48
|
+
</a>
|
|
49
|
+
<a href="https://github.com/j178/prek">
|
|
50
|
+
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/j178/prek/master/docs/assets/badge-v0.json" alt="prek">
|
|
51
|
+
</a>
|
|
52
|
+
</p>
|
|
53
|
+
<p align="center">
|
|
54
|
+
<a href="https://pypi.org/project/array-api-shape-check/">
|
|
55
|
+
<img src="https://img.shields.io/pypi/v/array-api-shape-check.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">
|
|
56
|
+
</a>
|
|
57
|
+
<img src="https://img.shields.io/pypi/pyversions/array-api-shape-check.svg?style=flat-square&logo=python&logoColor=fff" alt="Supported Python versions">
|
|
58
|
+
<img src="https://img.shields.io/pypi/l/array-api-shape-check.svg?style=flat-square" alt="License">
|
|
59
|
+
</p>
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
**Documentation**: <a href="https://array-api-shape-check.readthedocs.io" target="_blank">https://array-api-shape-check.readthedocs.io </a>
|
|
64
|
+
|
|
65
|
+
**Source Code**: <a href="https://github.com/34j/array-api-shape-check" target="_blank">https://github.com/34j/array-api-shape-check </a>
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
Check shapes of input arrays easily.
|
|
70
|
+
|
|
71
|
+
## Installation
|
|
72
|
+
|
|
73
|
+
Install this via pip (or your favourite package manager):
|
|
74
|
+
|
|
75
|
+
`pip install array-api-shape-check`
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
>>> from array_api_shape_check import check_shapes
|
|
81
|
+
>>> info = check_shapes("ij,*k*l,*li", (1, 4), (5, 6, 7), (1, 7, 3))
|
|
82
|
+
>>> info.all
|
|
83
|
+
((i:1->3, j:4), (*k:(5,), *l:(6, 7)), (*l:(1, 7)->(6, 7), i:3))
|
|
84
|
+
>>> info.unique
|
|
85
|
+
(i:3, j:4, *k:(5,), *l:(6, 7))
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Internally `check_shapes()` calls `parse_variable_ndim()`, which determines the number of dimensions for variable subscripts by least squares. If this is successful, checks if each subscript is consistent, then finnaly raises error for all inconsistencies at once.
|
|
89
|
+
|
|
90
|
+
Diving into the details of the first item:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
>>> item = info.all[0][0]
|
|
94
|
+
>>> item.name # the name of the subscript
|
|
95
|
+
'i'
|
|
96
|
+
>>> item.is_variable # whether the subscript is variable (starts with "*")
|
|
97
|
+
False
|
|
98
|
+
>>> item.shape_current # the current shape of the subscript
|
|
99
|
+
(1,)
|
|
100
|
+
>>> item.shape_broadcasted # the broadcasted shape of the subscript
|
|
101
|
+
(3,)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Not enough information to determine variable subscript ndims:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
>>> import pytest
|
|
108
|
+
>>> from array_api_shape_check import InconsistentNdimErrorMultipleSolutions, InconsistentNdimErrorNoSolutions, InconsistentShapeError
|
|
109
|
+
>>> with pytest.raises(InconsistentNdimErrorMultipleSolutions, match="number of variables"):
|
|
110
|
+
... check_shapes(
|
|
111
|
+
... "*i*j",
|
|
112
|
+
... (
|
|
113
|
+
... 1,
|
|
114
|
+
... 1,
|
|
115
|
+
... ),
|
|
116
|
+
... )
|
|
117
|
+
>>> with pytest.raises(InconsistentNdimErrorMultipleSolutions, match="rank"):
|
|
118
|
+
... check_shapes("*i*j,*i*j", (1, 1), (1, 1))
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
No solution to determine variable subscript ndims:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
>>> with pytest.raises(InconsistentNdimErrorNoSolutions, match="residuals"):
|
|
125
|
+
... check_shapes("*i,*i", (1, 1), (1, 1, 1))
|
|
126
|
+
>>> with pytest.raises(InconsistentNdimErrorNoSolutions, match="negative"):
|
|
127
|
+
... check_shapes("*ij", ())
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Does not match:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
>>> with pytest.raises(InconsistentShapeError):
|
|
134
|
+
... check_shapes("ij,*k*l,*li", (3, 4), (5, 6), (1, 7, 3))
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Contributors ✨
|
|
138
|
+
|
|
139
|
+
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
|
140
|
+
|
|
141
|
+
<!-- prettier-ignore-start -->
|
|
142
|
+
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
|
143
|
+
<!-- markdownlint-disable -->
|
|
144
|
+
<!-- markdownlint-enable -->
|
|
145
|
+
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
|
146
|
+
<!-- prettier-ignore-end -->
|
|
147
|
+
|
|
148
|
+
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
|
149
|
+
|
|
150
|
+
## Credits
|
|
151
|
+
|
|
152
|
+
[](https://github.com/copier-org/copier)
|
|
153
|
+
|
|
154
|
+
This package was created with
|
|
155
|
+
[Copier](https://copier.readthedocs.io/) and the
|
|
156
|
+
[browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template)
|
|
157
|
+
project template.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# array-api-shape-check
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://github.com/34j/array-api-shape-check/actions/workflows/ci.yml?query=branch%3Amain">
|
|
5
|
+
<img src="https://img.shields.io/github/actions/workflow/status/34j/array-api-shape-check/ci.yml?branch=main&label=CI&logo=github&style=flat-square" alt="CI Status" >
|
|
6
|
+
</a>
|
|
7
|
+
<a href="https://array-api-shape-check.readthedocs.io">
|
|
8
|
+
<img src="https://img.shields.io/readthedocs/array-api-shape-check.svg?logo=read-the-docs&logoColor=fff&style=flat-square" alt="Documentation Status">
|
|
9
|
+
</a>
|
|
10
|
+
<a href="https://codecov.io/gh/34j/array-api-shape-check">
|
|
11
|
+
<img src="https://img.shields.io/codecov/c/github/34j/array-api-shape-check.svg?logo=codecov&logoColor=fff&style=flat-square" alt="Test coverage percentage">
|
|
12
|
+
</a>
|
|
13
|
+
</p>
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://github.com/astral-sh/uv">
|
|
16
|
+
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json" alt="uv">
|
|
17
|
+
</a>
|
|
18
|
+
<a href="https://github.com/astral-sh/ruff">
|
|
19
|
+
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff">
|
|
20
|
+
</a>
|
|
21
|
+
<a href="https://github.com/j178/prek">
|
|
22
|
+
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/j178/prek/master/docs/assets/badge-v0.json" alt="prek">
|
|
23
|
+
</a>
|
|
24
|
+
</p>
|
|
25
|
+
<p align="center">
|
|
26
|
+
<a href="https://pypi.org/project/array-api-shape-check/">
|
|
27
|
+
<img src="https://img.shields.io/pypi/v/array-api-shape-check.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">
|
|
28
|
+
</a>
|
|
29
|
+
<img src="https://img.shields.io/pypi/pyversions/array-api-shape-check.svg?style=flat-square&logo=python&logoColor=fff" alt="Supported Python versions">
|
|
30
|
+
<img src="https://img.shields.io/pypi/l/array-api-shape-check.svg?style=flat-square" alt="License">
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
**Documentation**: <a href="https://array-api-shape-check.readthedocs.io" target="_blank">https://array-api-shape-check.readthedocs.io </a>
|
|
36
|
+
|
|
37
|
+
**Source Code**: <a href="https://github.com/34j/array-api-shape-check" target="_blank">https://github.com/34j/array-api-shape-check </a>
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
Check shapes of input arrays easily.
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
Install this via pip (or your favourite package manager):
|
|
46
|
+
|
|
47
|
+
`pip install array-api-shape-check`
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
>>> from array_api_shape_check import check_shapes
|
|
53
|
+
>>> info = check_shapes("ij,*k*l,*li", (1, 4), (5, 6, 7), (1, 7, 3))
|
|
54
|
+
>>> info.all
|
|
55
|
+
((i:1->3, j:4), (*k:(5,), *l:(6, 7)), (*l:(1, 7)->(6, 7), i:3))
|
|
56
|
+
>>> info.unique
|
|
57
|
+
(i:3, j:4, *k:(5,), *l:(6, 7))
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Internally `check_shapes()` calls `parse_variable_ndim()`, which determines the number of dimensions for variable subscripts by least squares. If this is successful, checks if each subscript is consistent, then finnaly raises error for all inconsistencies at once.
|
|
61
|
+
|
|
62
|
+
Diving into the details of the first item:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
>>> item = info.all[0][0]
|
|
66
|
+
>>> item.name # the name of the subscript
|
|
67
|
+
'i'
|
|
68
|
+
>>> item.is_variable # whether the subscript is variable (starts with "*")
|
|
69
|
+
False
|
|
70
|
+
>>> item.shape_current # the current shape of the subscript
|
|
71
|
+
(1,)
|
|
72
|
+
>>> item.shape_broadcasted # the broadcasted shape of the subscript
|
|
73
|
+
(3,)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Not enough information to determine variable subscript ndims:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
>>> import pytest
|
|
80
|
+
>>> from array_api_shape_check import InconsistentNdimErrorMultipleSolutions, InconsistentNdimErrorNoSolutions, InconsistentShapeError
|
|
81
|
+
>>> with pytest.raises(InconsistentNdimErrorMultipleSolutions, match="number of variables"):
|
|
82
|
+
... check_shapes(
|
|
83
|
+
... "*i*j",
|
|
84
|
+
... (
|
|
85
|
+
... 1,
|
|
86
|
+
... 1,
|
|
87
|
+
... ),
|
|
88
|
+
... )
|
|
89
|
+
>>> with pytest.raises(InconsistentNdimErrorMultipleSolutions, match="rank"):
|
|
90
|
+
... check_shapes("*i*j,*i*j", (1, 1), (1, 1))
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
No solution to determine variable subscript ndims:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
>>> with pytest.raises(InconsistentNdimErrorNoSolutions, match="residuals"):
|
|
97
|
+
... check_shapes("*i,*i", (1, 1), (1, 1, 1))
|
|
98
|
+
>>> with pytest.raises(InconsistentNdimErrorNoSolutions, match="negative"):
|
|
99
|
+
... check_shapes("*ij", ())
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Does not match:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
>>> with pytest.raises(InconsistentShapeError):
|
|
106
|
+
... check_shapes("ij,*k*l,*li", (3, 4), (5, 6), (1, 7, 3))
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Contributors ✨
|
|
110
|
+
|
|
111
|
+
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
|
112
|
+
|
|
113
|
+
<!-- prettier-ignore-start -->
|
|
114
|
+
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
|
115
|
+
<!-- markdownlint-disable -->
|
|
116
|
+
<!-- markdownlint-enable -->
|
|
117
|
+
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
|
118
|
+
<!-- prettier-ignore-end -->
|
|
119
|
+
|
|
120
|
+
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
|
121
|
+
|
|
122
|
+
## Credits
|
|
123
|
+
|
|
124
|
+
[](https://github.com/copier-org/copier)
|
|
125
|
+
|
|
126
|
+
This package was created with
|
|
127
|
+
[Copier](https://copier.readthedocs.io/) and the
|
|
128
|
+
[browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template)
|
|
129
|
+
project template.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "setuptools.build_meta"
|
|
3
|
+
requires = [ "setuptools>=77.0.3" ]
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "array-api-shape-check"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Check shapes of input arrays easily"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "34j", email = "34j.95a2p@simplelogin.com" },
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.12"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Natural Language :: English",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Topic :: Software Development :: Libraries",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"attrs>=26.1",
|
|
28
|
+
"more-itertools>=11.0.2",
|
|
29
|
+
"numpy>=2.4.4",
|
|
30
|
+
"types-array-api>=1.1.4",
|
|
31
|
+
]
|
|
32
|
+
urls."Bug Tracker" = "https://github.com/34j/array-api-shape-check/issues"
|
|
33
|
+
urls.Changelog = "https://github.com/34j/array-api-shape-check/blob/main/CHANGELOG.md"
|
|
34
|
+
urls.documentation = "https://array-api-shape-check.readthedocs.io"
|
|
35
|
+
urls.repository = "https://github.com/34j/array-api-shape-check"
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = [
|
|
39
|
+
"coverage",
|
|
40
|
+
"numpy>=2.4.4",
|
|
41
|
+
"pytest==9.*",
|
|
42
|
+
"pytest-cov==7.*",
|
|
43
|
+
"sybil[pytest]",
|
|
44
|
+
"ty",
|
|
45
|
+
]
|
|
46
|
+
docs = [
|
|
47
|
+
"furo; python_version>='3.12'",
|
|
48
|
+
"myst-parser; python_version>='3.12'",
|
|
49
|
+
"sphinx==9.*; python_version>='3.12'",
|
|
50
|
+
"sphinx-autobuild; python_version>='3.12'",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
line-length = 100
|
|
55
|
+
preview = true
|
|
56
|
+
format.docstring-code-format = true
|
|
57
|
+
lint.select = [
|
|
58
|
+
"B", # flake8-bugbear
|
|
59
|
+
"C4", # flake8-comprehensions
|
|
60
|
+
"D", # flake8-docstrings
|
|
61
|
+
"E", # pycodestyle
|
|
62
|
+
"F", # pyflake
|
|
63
|
+
"I", # isort
|
|
64
|
+
"RUF", # ruff specific
|
|
65
|
+
"S", # flake8-bandit
|
|
66
|
+
"UP", # pyupgrade
|
|
67
|
+
"W", # pycodestyle
|
|
68
|
+
]
|
|
69
|
+
lint.ignore = [
|
|
70
|
+
"D100", # Missing docstring in public module
|
|
71
|
+
"D104", # Missing docstring in public package
|
|
72
|
+
"D107", # Missing docstring in `__init__`
|
|
73
|
+
"D203", # 1 blank line required before class docstring
|
|
74
|
+
"D212", # Multi-line docstring summary should start at the first line
|
|
75
|
+
"D401", # First line of docstring should be in imperative mood
|
|
76
|
+
]
|
|
77
|
+
lint.per-file-ignores."conftest.py" = [ "D100" ]
|
|
78
|
+
lint.per-file-ignores."docs/conf.py" = [ "D100" ]
|
|
79
|
+
lint.per-file-ignores."setup.py" = [ "D100" ]
|
|
80
|
+
lint.per-file-ignores."tests/**/*" = [
|
|
81
|
+
"D100",
|
|
82
|
+
"D101",
|
|
83
|
+
"D102",
|
|
84
|
+
"D103",
|
|
85
|
+
"D104",
|
|
86
|
+
"S101",
|
|
87
|
+
]
|
|
88
|
+
lint.isort.known-first-party = [ "array_api_shape_check", "tests" ]
|
|
89
|
+
|
|
90
|
+
[tool.pyproject-fmt]
|
|
91
|
+
max_supported_python = "3.14"
|
|
92
|
+
|
|
93
|
+
[tool.ty]
|
|
94
|
+
terminal.error-on-warning = true
|
|
95
|
+
src.exclude = [ "docs/" ]
|
|
96
|
+
|
|
97
|
+
[tool.pytest]
|
|
98
|
+
minversion = "9.0"
|
|
99
|
+
addopts = [
|
|
100
|
+
"-v",
|
|
101
|
+
"-Wonce",
|
|
102
|
+
"-p no:doctest",
|
|
103
|
+
"--cov=array_api_shape_check",
|
|
104
|
+
"--cov-report=term",
|
|
105
|
+
"--cov-report=xml",
|
|
106
|
+
]
|
|
107
|
+
pythonpath = [ "src" ]
|
|
108
|
+
|
|
109
|
+
[tool.coverage]
|
|
110
|
+
run.branch = true
|
|
111
|
+
paths.source = [
|
|
112
|
+
"src",
|
|
113
|
+
".tox/**/site-packages",
|
|
114
|
+
]
|
|
115
|
+
report.exclude_lines = [
|
|
116
|
+
"@overload",
|
|
117
|
+
'if __name__ == "__main__":',
|
|
118
|
+
"if TYPE_CHECKING",
|
|
119
|
+
"pragma: no cover",
|
|
120
|
+
"raise NotImplementedError",
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
[tool.semantic_release]
|
|
124
|
+
allow_zero_version = true
|
|
125
|
+
version_toml = [ "pyproject.toml:project.version" ]
|
|
126
|
+
version_variables = [
|
|
127
|
+
"src/array_api_shape_check/__init__.py:__version__",
|
|
128
|
+
"docs/conf.py:release",
|
|
129
|
+
]
|
|
130
|
+
build_command = """
|
|
131
|
+
pip install uv
|
|
132
|
+
uv lock
|
|
133
|
+
git add uv.lock
|
|
134
|
+
uv build
|
|
135
|
+
"""
|
|
136
|
+
branches.main.match = "main"
|
|
137
|
+
branches.noop.match = "(?!main$)"
|
|
138
|
+
branches.noop.prerelease = true
|
|
139
|
+
changelog.exclude_commit_patterns = [
|
|
140
|
+
"chore(?:\\([^)]*?\\))?: .+",
|
|
141
|
+
"ci(?:\\([^)]*?\\))?: .+",
|
|
142
|
+
"refactor(?:\\([^)]*?\\))?: .+",
|
|
143
|
+
"style(?:\\([^)]*?\\))?: .+",
|
|
144
|
+
"test(?:\\([^)]*?\\))?: .+",
|
|
145
|
+
"build\\((?!deps\\): .+)",
|
|
146
|
+
"Merged? .*",
|
|
147
|
+
"Initial [Cc]ommit.*", # codespell:ignore
|
|
148
|
+
]
|
|
149
|
+
changelog.environment.keep_trailing_newline = true
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
2
|
+
from ._main import (
|
|
3
|
+
InconsistentNdimError,
|
|
4
|
+
InconsistentNdimErrorMultipleSolutions,
|
|
5
|
+
InconsistentNdimErrorNoSolutions,
|
|
6
|
+
InconsistentShapeError,
|
|
7
|
+
SubscriptInfoFromShape,
|
|
8
|
+
SubscriptInfoFromShapeItem,
|
|
9
|
+
SubscriptInfoFromShapeItemUnchecked,
|
|
10
|
+
SubscriptInfoFromShapeItemUnique,
|
|
11
|
+
SubscriptInfoFromSubcript,
|
|
12
|
+
SubscriptInfoFromSubcriptItem,
|
|
13
|
+
_parse_shapes,
|
|
14
|
+
check_shapes,
|
|
15
|
+
parse_subscripts,
|
|
16
|
+
parse_variable_ndim,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"InconsistentNdimError",
|
|
21
|
+
"InconsistentNdimErrorMultipleSolutions",
|
|
22
|
+
"InconsistentNdimErrorNoSolutions",
|
|
23
|
+
"InconsistentShapeError",
|
|
24
|
+
"SubscriptInfoFromShape",
|
|
25
|
+
"SubscriptInfoFromShapeItem",
|
|
26
|
+
"SubscriptInfoFromShapeItemUnchecked",
|
|
27
|
+
"SubscriptInfoFromShapeItemUnique",
|
|
28
|
+
"SubscriptInfoFromSubcript",
|
|
29
|
+
"SubscriptInfoFromSubcriptItem",
|
|
30
|
+
"_parse_shapes",
|
|
31
|
+
"check_shapes",
|
|
32
|
+
"parse_subscripts",
|
|
33
|
+
"parse_variable_ndim",
|
|
34
|
+
]
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from itertools import groupby
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import attrs
|
|
8
|
+
import numpy as np
|
|
9
|
+
from array_api.latest import Array
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@attrs.frozen(kw_only=True)
|
|
13
|
+
class SubscriptInfoFromSubcriptItem:
|
|
14
|
+
name: str
|
|
15
|
+
"""The name of the subscript, must be of length 1 and must not be '*' or '.'"""
|
|
16
|
+
is_variable: bool
|
|
17
|
+
"""Whether the subscript is variable (* prefix or ...)"""
|
|
18
|
+
|
|
19
|
+
def __repr__(self) -> str:
|
|
20
|
+
return f"{'*' if self.is_variable else ''}{self.name}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@attrs.frozen(kw_only=True)
|
|
24
|
+
class SubscriptInfoFromSubcript:
|
|
25
|
+
all: tuple[tuple[SubscriptInfoFromSubcriptItem, ...], ...]
|
|
26
|
+
unique: tuple[SubscriptInfoFromSubcriptItem, ...]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@attrs.frozen(kw_only=True)
|
|
30
|
+
class SubscriptInfoFromShapeItemUnchecked(SubscriptInfoFromSubcriptItem):
|
|
31
|
+
shape_current: tuple[int, ...]
|
|
32
|
+
"""The shape of the operand corresponding to this subscript"""
|
|
33
|
+
|
|
34
|
+
def __repr__(self) -> str:
|
|
35
|
+
if self.is_variable:
|
|
36
|
+
return f"*{self.name}:{self.shape_current}"
|
|
37
|
+
else:
|
|
38
|
+
return f"{self.name}:{self.shape_current[0]}"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@attrs.frozen(kw_only=True)
|
|
42
|
+
class SubscriptInfoFromShapeItemUnique(SubscriptInfoFromSubcriptItem):
|
|
43
|
+
shape_broadcasted: tuple[int, ...]
|
|
44
|
+
"""The shape after broadcasting with other subscripts with the same name"""
|
|
45
|
+
|
|
46
|
+
def __repr__(self) -> str:
|
|
47
|
+
if self.is_variable:
|
|
48
|
+
return f"*{self.name}:{self.shape_broadcasted}"
|
|
49
|
+
else:
|
|
50
|
+
return f"{self.name}:{self.shape_broadcasted[0]}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@attrs.frozen(kw_only=True)
|
|
54
|
+
class SubscriptInfoFromShapeItem(SubscriptInfoFromShapeItemUnchecked):
|
|
55
|
+
shape_broadcasted: tuple[int, ...]
|
|
56
|
+
"""The shape after broadcasting with other subscripts with the same name"""
|
|
57
|
+
|
|
58
|
+
def __repr__(self) -> str:
|
|
59
|
+
if self.is_variable:
|
|
60
|
+
if self.shape_current != self.shape_broadcasted:
|
|
61
|
+
return f"*{self.name}:{self.shape_current}->{self.shape_broadcasted}"
|
|
62
|
+
return f"*{self.name}:{self.shape_current}"
|
|
63
|
+
else:
|
|
64
|
+
if self.shape_current != self.shape_broadcasted:
|
|
65
|
+
return f"{self.name}:{self.shape_current[0]}->{self.shape_broadcasted[0]}"
|
|
66
|
+
return f"{self.name}:{self.shape_current[0]}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@attrs.frozen(kw_only=True)
|
|
70
|
+
class SubscriptInfoFromShape:
|
|
71
|
+
all: tuple[tuple[SubscriptInfoFromShapeItem, ...], ...]
|
|
72
|
+
"""The subscript info grouped by order, then by operand"""
|
|
73
|
+
unique: tuple[SubscriptInfoFromShapeItemUnique, ...]
|
|
74
|
+
"""The unique subscripts,
|
|
75
|
+
ignoring order and shapes not broadcasted,
|
|
76
|
+
sorted lexicographically by name"""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class InconsistentNdimError(ValueError):
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class InconsistentNdimErrorMultipleSolutions(InconsistentNdimError):
|
|
84
|
+
def __init__(self, reason: str, *args: object) -> None:
|
|
85
|
+
super().__init__(
|
|
86
|
+
f"Inconsistent ndims: there are multiple possible solutions "
|
|
87
|
+
"to determine the number of dimensions "
|
|
88
|
+
f"for variable subscripts [{reason}]",
|
|
89
|
+
*args,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class InconsistentNdimErrorNoSolutions(InconsistentNdimError):
|
|
94
|
+
def __init__(self, reason: str, *args: object) -> None:
|
|
95
|
+
super().__init__(
|
|
96
|
+
f"Inconsistent ndims: there are no solution "
|
|
97
|
+
f"to determine the number of dimensions "
|
|
98
|
+
f"for variable subscripts [{reason}]",
|
|
99
|
+
*args,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class InconsistentShapeError(ValueError):
|
|
104
|
+
subscript_info: SubscriptInfoFromShape
|
|
105
|
+
|
|
106
|
+
def __init__(self, subscript_info: SubscriptInfoFromShape, reason: str, *args: object) -> None:
|
|
107
|
+
super().__init__("Inconsistent shapes: " + str(subscript_info.all) + f"\n {reason}", *args)
|
|
108
|
+
self.subscript_info = subscript_info
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _get_operand_names(*operands: Any) -> list[str | None]:
|
|
112
|
+
import inspect
|
|
113
|
+
|
|
114
|
+
frame = inspect.currentframe()
|
|
115
|
+
if frame is None:
|
|
116
|
+
return [None] * len(operands)
|
|
117
|
+
frame = frame.f_back
|
|
118
|
+
if frame is None:
|
|
119
|
+
return [None] * len(operands)
|
|
120
|
+
frame = frame.f_back
|
|
121
|
+
if frame is None:
|
|
122
|
+
return [None] * len(operands)
|
|
123
|
+
names = []
|
|
124
|
+
for operand in operands:
|
|
125
|
+
for var_name, var_value in frame.f_locals.items():
|
|
126
|
+
if var_value is operand:
|
|
127
|
+
names.append(var_name)
|
|
128
|
+
break
|
|
129
|
+
else:
|
|
130
|
+
names.append(None)
|
|
131
|
+
return names
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def parse_subscripts(subscripts: str, /) -> SubscriptInfoFromSubcript:
|
|
135
|
+
"""
|
|
136
|
+
Parse subscripts str.
|
|
137
|
+
|
|
138
|
+
Parameters
|
|
139
|
+
----------
|
|
140
|
+
subscripts : str
|
|
141
|
+
Subscripts separated by "," per operand.
|
|
142
|
+
|
|
143
|
+
1. Subscripts must be of length 1
|
|
144
|
+
2. Subscripts must not be "*" or ".".
|
|
145
|
+
3. If start with "*", the subscript is treated as variable.
|
|
146
|
+
4. "..." is replaced with "*.".
|
|
147
|
+
|
|
148
|
+
Returns
|
|
149
|
+
-------
|
|
150
|
+
SubscriptInfoFromSubcript
|
|
151
|
+
The parsed subscript info.
|
|
152
|
+
|
|
153
|
+
Raises
|
|
154
|
+
------
|
|
155
|
+
ValueError
|
|
156
|
+
If the subscript is invalid.
|
|
157
|
+
|
|
158
|
+
Examples
|
|
159
|
+
--------
|
|
160
|
+
>>> parse_subscripts("ij,*k*l,*li")
|
|
161
|
+
SubscriptInfoFromSubcript(all=((i, j), (*k, *l), (*l, i)), unique=(i, j, *k, *l))
|
|
162
|
+
|
|
163
|
+
"""
|
|
164
|
+
# If . other than ...
|
|
165
|
+
if re.search(r"(?<!\.)\.(?!\.)", subscripts):
|
|
166
|
+
raise ValueError("Invalid subscript: '.' is not allowed except for '...'")
|
|
167
|
+
# Replace ... with *.
|
|
168
|
+
subscripts = re.sub(r"\.\.\.", "*.", subscripts)
|
|
169
|
+
subscripts = subscripts.rstrip()
|
|
170
|
+
info_all: tuple[tuple[SubscriptInfoFromSubcriptItem, ...], ...] = ()
|
|
171
|
+
for name_ in subscripts.split(","):
|
|
172
|
+
info_array: tuple[SubscriptInfoFromSubcriptItem, ...] = ()
|
|
173
|
+
is_variable = False
|
|
174
|
+
for name in name_:
|
|
175
|
+
if name == "*":
|
|
176
|
+
if is_variable:
|
|
177
|
+
raise ValueError("Invalid subscript: '*' cannot be repeated")
|
|
178
|
+
is_variable = True
|
|
179
|
+
else:
|
|
180
|
+
info_array += (SubscriptInfoFromSubcriptItem(name=name, is_variable=is_variable),)
|
|
181
|
+
is_variable = False
|
|
182
|
+
info_all += (info_array,)
|
|
183
|
+
|
|
184
|
+
info_unique: set[SubscriptInfoFromSubcriptItem] = {
|
|
185
|
+
x for info_array in info_all for x in info_array
|
|
186
|
+
}
|
|
187
|
+
# raise if there are subscripts with same name but different is_variable
|
|
188
|
+
names = set()
|
|
189
|
+
for info in info_unique:
|
|
190
|
+
if info.name in names:
|
|
191
|
+
raise ValueError(
|
|
192
|
+
f"Subscript '{info.name}' is duplicated with different variable status"
|
|
193
|
+
)
|
|
194
|
+
names.add(info.name)
|
|
195
|
+
|
|
196
|
+
return SubscriptInfoFromSubcript(
|
|
197
|
+
all=info_all, unique=tuple(sorted(info_unique, key=lambda x: x.name))
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def parse_variable_ndim(subscripts: str, ndims: Sequence[int], /) -> dict[str, int]:
|
|
202
|
+
"""
|
|
203
|
+
Parse variable subscript ndims by solving linear equations.
|
|
204
|
+
|
|
205
|
+
Parameters
|
|
206
|
+
----------
|
|
207
|
+
subscripts : str
|
|
208
|
+
Subscripts separated by "," per operand.
|
|
209
|
+
|
|
210
|
+
1. Subscripts must be of length 1
|
|
211
|
+
2. Subscripts must not be "*" or ".".
|
|
212
|
+
3. If start with "*", the subscript is treated as variable.
|
|
213
|
+
4. "..." is replaced with "*.".]
|
|
214
|
+
ndims : Sequence[int]
|
|
215
|
+
The number of dimensions for each operand.
|
|
216
|
+
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
SubscriptInfoFromSubcript
|
|
220
|
+
The parsed subscript info.
|
|
221
|
+
|
|
222
|
+
Raises
|
|
223
|
+
------
|
|
224
|
+
ValueError
|
|
225
|
+
If the subscript is invalid.
|
|
226
|
+
|
|
227
|
+
Examples
|
|
228
|
+
--------
|
|
229
|
+
>>> parse_variable_ndim("ij,*k*l,*li", (2, 3, 3))
|
|
230
|
+
{'k': 1, 'l': 2}
|
|
231
|
+
|
|
232
|
+
Not enough information to determine variable subscript ndims:
|
|
233
|
+
|
|
234
|
+
>>> import pytest
|
|
235
|
+
>>> with pytest.raises(InconsistentNdimErrorMultipleSolutions, match="number of variables"):
|
|
236
|
+
... parse_variable_ndim("*i*j", (2,))
|
|
237
|
+
>>> with pytest.raises(InconsistentNdimErrorMultipleSolutions, match="rank"):
|
|
238
|
+
... parse_variable_ndim("*i*j,*i*j", (2, 2))
|
|
239
|
+
|
|
240
|
+
No solution to determine variable subscript ndims:
|
|
241
|
+
|
|
242
|
+
>>> with pytest.raises(InconsistentNdimErrorNoSolutions, match="residuals"):
|
|
243
|
+
... parse_variable_ndim("*i,*i", (2, 3))
|
|
244
|
+
>>> with pytest.raises(InconsistentNdimErrorNoSolutions, match="negative"):
|
|
245
|
+
... parse_variable_ndim("*ij", (0,))
|
|
246
|
+
|
|
247
|
+
"""
|
|
248
|
+
info = parse_subscripts(subscripts)
|
|
249
|
+
del subscripts
|
|
250
|
+
if len(info.all) != len(ndims):
|
|
251
|
+
raise ValueError(
|
|
252
|
+
f"Number of subscripts ({len(info.all)}) does not match number of ndims ({len(ndims)})"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# decide dimensions by solving linear equations
|
|
256
|
+
info_variable_unique = [subscript for subscript in info.unique if subscript.is_variable]
|
|
257
|
+
if len(info_variable_unique) > len(info.all):
|
|
258
|
+
raise InconsistentNdimErrorMultipleSolutions("number of variables")
|
|
259
|
+
rhs = np.asarray(ndims, dtype=int)
|
|
260
|
+
del ndims
|
|
261
|
+
mat = np.zeros((len(info.all), len(info_variable_unique)), dtype=int)
|
|
262
|
+
for i, info_array in enumerate(info.all):
|
|
263
|
+
for subscript in info_array:
|
|
264
|
+
if subscript.is_variable:
|
|
265
|
+
j = info_variable_unique.index(subscript)
|
|
266
|
+
mat[i, j] += 1
|
|
267
|
+
else:
|
|
268
|
+
rhs[i] -= 1
|
|
269
|
+
|
|
270
|
+
# solve overdetermined linear equations using least squares method
|
|
271
|
+
variable_dims, residuals, rank, _singular_values = np.linalg.lstsq(mat, rhs, rcond=None)
|
|
272
|
+
if rank != len(info_variable_unique):
|
|
273
|
+
raise InconsistentNdimErrorMultipleSolutions("rank")
|
|
274
|
+
if residuals.size > 0 and not np.isclose(residuals[0], 0):
|
|
275
|
+
raise InconsistentNdimErrorNoSolutions("residuals")
|
|
276
|
+
variable_dims = np.round(variable_dims).astype(int)
|
|
277
|
+
if np.any(variable_dims < 0):
|
|
278
|
+
raise InconsistentNdimErrorNoSolutions("negative")
|
|
279
|
+
return {
|
|
280
|
+
subscript.name: int(variable_dims[j]) for j, subscript in enumerate(info_variable_unique)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _parse_shapes(
|
|
285
|
+
subscripts: str, /, *operands: Array | tuple[int, ...]
|
|
286
|
+
) -> tuple[tuple[SubscriptInfoFromShapeItemUnchecked, ...], ...]:
|
|
287
|
+
info = parse_subscripts(subscripts)
|
|
288
|
+
if len(info.all) != len(operands):
|
|
289
|
+
raise ValueError(
|
|
290
|
+
f"Number of subscripts ({len(info.all)}) "
|
|
291
|
+
f"does not match number of operands ({len(operands)})"
|
|
292
|
+
)
|
|
293
|
+
shapes: tuple[tuple[int, ...], ...] = ()
|
|
294
|
+
for operand in operands:
|
|
295
|
+
shape: tuple[int, ...] # type: ignore
|
|
296
|
+
if isinstance(operand, tuple):
|
|
297
|
+
shape = operand
|
|
298
|
+
elif hasattr(operand, "shape"):
|
|
299
|
+
if None in operand.shape:
|
|
300
|
+
raise ValueError("Operand shape cannot contain None")
|
|
301
|
+
shape = operand.shape
|
|
302
|
+
else:
|
|
303
|
+
raise TypeError(
|
|
304
|
+
f"Invalid operand: expected an array or a shape tuple, but got {type(operand)}"
|
|
305
|
+
)
|
|
306
|
+
shapes += (shape,) # type: ignore
|
|
307
|
+
del operands
|
|
308
|
+
ndims = [len(shape) for shape in shapes]
|
|
309
|
+
name_to_ndim = defaultdict(lambda: 1, parse_variable_ndim(subscripts, ndims))
|
|
310
|
+
del subscripts, ndims
|
|
311
|
+
info_all: tuple[tuple[SubscriptInfoFromShapeItemUnchecked, ...], ...] = ()
|
|
312
|
+
for info_array, shape in zip(info.all, shapes, strict=False):
|
|
313
|
+
info_array_new = ()
|
|
314
|
+
for item in info_array:
|
|
315
|
+
info_array_new += (
|
|
316
|
+
SubscriptInfoFromShapeItemUnchecked(
|
|
317
|
+
name=item.name,
|
|
318
|
+
is_variable=item.is_variable,
|
|
319
|
+
shape_current=shape[: name_to_ndim[item.name]],
|
|
320
|
+
),
|
|
321
|
+
)
|
|
322
|
+
shape = shape[name_to_ndim[item.name] :]
|
|
323
|
+
info_all += (info_array_new,)
|
|
324
|
+
return info_all
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def check_shapes(
|
|
328
|
+
subscripts: str, /, *operands: Array | tuple[int, ...], names: str | None = None
|
|
329
|
+
) -> SubscriptInfoFromShape:
|
|
330
|
+
"""
|
|
331
|
+
Parse variable subscript ndims by solving linear equations.
|
|
332
|
+
|
|
333
|
+
Parameters
|
|
334
|
+
----------
|
|
335
|
+
subscripts : str
|
|
336
|
+
Subscripts separated by "," per operand.
|
|
337
|
+
|
|
338
|
+
1. Subscripts must be of length 1
|
|
339
|
+
2. Subscripts must not be "*" or ".".
|
|
340
|
+
3. If start with "*", the subscript is treated as variable.
|
|
341
|
+
4. "..." is replaced with "*.".]
|
|
342
|
+
operands : Array or tuple[int, ...]
|
|
343
|
+
Arrays or shape tuples corresponding to check.
|
|
344
|
+
ndims : Sequence[int]
|
|
345
|
+
The number of dimensions for each operand.
|
|
346
|
+
names : str | None
|
|
347
|
+
The names of operands separated by ",",
|
|
348
|
+
used for error messages. If None, operand indices are used instead.
|
|
349
|
+
|
|
350
|
+
Returns
|
|
351
|
+
-------
|
|
352
|
+
SubscriptInfoFromSubcript
|
|
353
|
+
The parsed subscript info.
|
|
354
|
+
|
|
355
|
+
Raises
|
|
356
|
+
------
|
|
357
|
+
ValueError
|
|
358
|
+
If the subscript is invalid.
|
|
359
|
+
|
|
360
|
+
Examples
|
|
361
|
+
--------
|
|
362
|
+
>>> info = check_shapes("ij,*k*l,*li", (1, 4), (5, 6, 7), (1, 7, 3))
|
|
363
|
+
>>> info.all
|
|
364
|
+
((i:1->3, j:4), (*k:(5,), *l:(6, 7)), (*l:(1, 7)->(6, 7), i:3))
|
|
365
|
+
>>> info.unique
|
|
366
|
+
(i:3, j:4, *k:(5,), *l:(6, 7))
|
|
367
|
+
|
|
368
|
+
Not enough information to determine variable subscript ndims:
|
|
369
|
+
|
|
370
|
+
>>> import pytest
|
|
371
|
+
>>> with pytest.raises(InconsistentNdimErrorMultipleSolutions, match="number of variables"):
|
|
372
|
+
... check_shapes(
|
|
373
|
+
... "*i*j",
|
|
374
|
+
... (
|
|
375
|
+
... 1,
|
|
376
|
+
... 1,
|
|
377
|
+
... ),
|
|
378
|
+
... )
|
|
379
|
+
>>> with pytest.raises(InconsistentNdimErrorMultipleSolutions, match="rank"):
|
|
380
|
+
... check_shapes("*i*j,*i*j", (1, 1), (1, 1))
|
|
381
|
+
|
|
382
|
+
No solution to determine variable subscript ndims:
|
|
383
|
+
|
|
384
|
+
>>> with pytest.raises(InconsistentNdimErrorNoSolutions, match="residuals"):
|
|
385
|
+
... check_shapes("*i,*i", (1, 1), (1, 1, 1))
|
|
386
|
+
>>> with pytest.raises(InconsistentNdimErrorNoSolutions, match="negative"):
|
|
387
|
+
... check_shapes("*ij", ())
|
|
388
|
+
|
|
389
|
+
Does not match:
|
|
390
|
+
>>> with pytest.raises(InconsistentShapeError):
|
|
391
|
+
... check_shapes("ij,*k*l,*li", (3, 4), (5, 6), (1, 7, 3))
|
|
392
|
+
|
|
393
|
+
"""
|
|
394
|
+
info_all = _parse_shapes(subscripts, *operands)
|
|
395
|
+
if names is not None:
|
|
396
|
+
names_ = [str(name) for name in names.split(",")]
|
|
397
|
+
if len(names_) != len(info_all):
|
|
398
|
+
raise ValueError(
|
|
399
|
+
f"Number of names ({len(names_)}) "
|
|
400
|
+
f"does not match number of operands ({len(info_all)})"
|
|
401
|
+
)
|
|
402
|
+
else:
|
|
403
|
+
names_ = [str(i) for i in range(len(info_all))]
|
|
404
|
+
info_flatten_keyed = [(i, item) for i, info_array in enumerate(info_all) for item in info_array]
|
|
405
|
+
errors = []
|
|
406
|
+
shape_broadcasted = {}
|
|
407
|
+
for key, group in groupby(info_flatten_keyed, key=lambda x: x[1].name):
|
|
408
|
+
group_list = list(group)
|
|
409
|
+
shapes = [item.shape_current for _, item in group_list]
|
|
410
|
+
try:
|
|
411
|
+
shape_broadcasted[key] = np.broadcast_shapes(*shapes)
|
|
412
|
+
except ValueError:
|
|
413
|
+
inner_text = ", ".join([f"{shape} ({names_[i]})" for i, shape in group_list])
|
|
414
|
+
errors.append(f"Key {key}: shapes {inner_text} are not broadcastable")
|
|
415
|
+
|
|
416
|
+
info_all_new = ()
|
|
417
|
+
for info_array in info_all:
|
|
418
|
+
info_array_new = ()
|
|
419
|
+
for item in info_array:
|
|
420
|
+
info_array_new += (
|
|
421
|
+
SubscriptInfoFromShapeItem(
|
|
422
|
+
name=item.name,
|
|
423
|
+
is_variable=item.is_variable,
|
|
424
|
+
shape_current=item.shape_current,
|
|
425
|
+
shape_broadcasted=shape_broadcasted.get(item.name), # type: ignore
|
|
426
|
+
),
|
|
427
|
+
)
|
|
428
|
+
info_all_new += (info_array_new,)
|
|
429
|
+
|
|
430
|
+
info_unique = {
|
|
431
|
+
SubscriptInfoFromShapeItemUnique(
|
|
432
|
+
name=item.name,
|
|
433
|
+
is_variable=item.is_variable,
|
|
434
|
+
shape_broadcasted=shape_broadcasted.get(item.name), # type: ignore
|
|
435
|
+
)
|
|
436
|
+
for info_array in info_all_new
|
|
437
|
+
for item in info_array
|
|
438
|
+
}
|
|
439
|
+
result = SubscriptInfoFromShape(
|
|
440
|
+
all=info_all_new, unique=tuple(sorted(info_unique, key=lambda x: x.name))
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
if errors:
|
|
444
|
+
raise InconsistentShapeError(result, "\n".join(errors))
|
|
445
|
+
|
|
446
|
+
return result
|
|
File without changes
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: array-api-shape-check
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Check shapes of input arrays easily
|
|
5
|
+
Author-email: 34j <34j.95a2p@simplelogin.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/34j/array-api-shape-check/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/34j/array-api-shape-check/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: documentation, https://array-api-shape-check.readthedocs.io
|
|
10
|
+
Project-URL: repository, https://github.com/34j/array-api-shape-check
|
|
11
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Natural Language :: English
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: attrs>=26.1
|
|
24
|
+
Requires-Dist: more-itertools>=11.0.2
|
|
25
|
+
Requires-Dist: numpy>=2.4.4
|
|
26
|
+
Requires-Dist: types-array-api>=1.1.4
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# array-api-shape-check
|
|
30
|
+
|
|
31
|
+
<p align="center">
|
|
32
|
+
<a href="https://github.com/34j/array-api-shape-check/actions/workflows/ci.yml?query=branch%3Amain">
|
|
33
|
+
<img src="https://img.shields.io/github/actions/workflow/status/34j/array-api-shape-check/ci.yml?branch=main&label=CI&logo=github&style=flat-square" alt="CI Status" >
|
|
34
|
+
</a>
|
|
35
|
+
<a href="https://array-api-shape-check.readthedocs.io">
|
|
36
|
+
<img src="https://img.shields.io/readthedocs/array-api-shape-check.svg?logo=read-the-docs&logoColor=fff&style=flat-square" alt="Documentation Status">
|
|
37
|
+
</a>
|
|
38
|
+
<a href="https://codecov.io/gh/34j/array-api-shape-check">
|
|
39
|
+
<img src="https://img.shields.io/codecov/c/github/34j/array-api-shape-check.svg?logo=codecov&logoColor=fff&style=flat-square" alt="Test coverage percentage">
|
|
40
|
+
</a>
|
|
41
|
+
</p>
|
|
42
|
+
<p align="center">
|
|
43
|
+
<a href="https://github.com/astral-sh/uv">
|
|
44
|
+
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json" alt="uv">
|
|
45
|
+
</a>
|
|
46
|
+
<a href="https://github.com/astral-sh/ruff">
|
|
47
|
+
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff">
|
|
48
|
+
</a>
|
|
49
|
+
<a href="https://github.com/j178/prek">
|
|
50
|
+
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/j178/prek/master/docs/assets/badge-v0.json" alt="prek">
|
|
51
|
+
</a>
|
|
52
|
+
</p>
|
|
53
|
+
<p align="center">
|
|
54
|
+
<a href="https://pypi.org/project/array-api-shape-check/">
|
|
55
|
+
<img src="https://img.shields.io/pypi/v/array-api-shape-check.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">
|
|
56
|
+
</a>
|
|
57
|
+
<img src="https://img.shields.io/pypi/pyversions/array-api-shape-check.svg?style=flat-square&logo=python&logoColor=fff" alt="Supported Python versions">
|
|
58
|
+
<img src="https://img.shields.io/pypi/l/array-api-shape-check.svg?style=flat-square" alt="License">
|
|
59
|
+
</p>
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
**Documentation**: <a href="https://array-api-shape-check.readthedocs.io" target="_blank">https://array-api-shape-check.readthedocs.io </a>
|
|
64
|
+
|
|
65
|
+
**Source Code**: <a href="https://github.com/34j/array-api-shape-check" target="_blank">https://github.com/34j/array-api-shape-check </a>
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
Check shapes of input arrays easily.
|
|
70
|
+
|
|
71
|
+
## Installation
|
|
72
|
+
|
|
73
|
+
Install this via pip (or your favourite package manager):
|
|
74
|
+
|
|
75
|
+
`pip install array-api-shape-check`
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
>>> from array_api_shape_check import check_shapes
|
|
81
|
+
>>> info = check_shapes("ij,*k*l,*li", (1, 4), (5, 6, 7), (1, 7, 3))
|
|
82
|
+
>>> info.all
|
|
83
|
+
((i:1->3, j:4), (*k:(5,), *l:(6, 7)), (*l:(1, 7)->(6, 7), i:3))
|
|
84
|
+
>>> info.unique
|
|
85
|
+
(i:3, j:4, *k:(5,), *l:(6, 7))
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Internally `check_shapes()` calls `parse_variable_ndim()`, which determines the number of dimensions for variable subscripts by least squares. If this is successful, checks if each subscript is consistent, then finnaly raises error for all inconsistencies at once.
|
|
89
|
+
|
|
90
|
+
Diving into the details of the first item:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
>>> item = info.all[0][0]
|
|
94
|
+
>>> item.name # the name of the subscript
|
|
95
|
+
'i'
|
|
96
|
+
>>> item.is_variable # whether the subscript is variable (starts with "*")
|
|
97
|
+
False
|
|
98
|
+
>>> item.shape_current # the current shape of the subscript
|
|
99
|
+
(1,)
|
|
100
|
+
>>> item.shape_broadcasted # the broadcasted shape of the subscript
|
|
101
|
+
(3,)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Not enough information to determine variable subscript ndims:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
>>> import pytest
|
|
108
|
+
>>> from array_api_shape_check import InconsistentNdimErrorMultipleSolutions, InconsistentNdimErrorNoSolutions, InconsistentShapeError
|
|
109
|
+
>>> with pytest.raises(InconsistentNdimErrorMultipleSolutions, match="number of variables"):
|
|
110
|
+
... check_shapes(
|
|
111
|
+
... "*i*j",
|
|
112
|
+
... (
|
|
113
|
+
... 1,
|
|
114
|
+
... 1,
|
|
115
|
+
... ),
|
|
116
|
+
... )
|
|
117
|
+
>>> with pytest.raises(InconsistentNdimErrorMultipleSolutions, match="rank"):
|
|
118
|
+
... check_shapes("*i*j,*i*j", (1, 1), (1, 1))
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
No solution to determine variable subscript ndims:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
>>> with pytest.raises(InconsistentNdimErrorNoSolutions, match="residuals"):
|
|
125
|
+
... check_shapes("*i,*i", (1, 1), (1, 1, 1))
|
|
126
|
+
>>> with pytest.raises(InconsistentNdimErrorNoSolutions, match="negative"):
|
|
127
|
+
... check_shapes("*ij", ())
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Does not match:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
>>> with pytest.raises(InconsistentShapeError):
|
|
134
|
+
... check_shapes("ij,*k*l,*li", (3, 4), (5, 6), (1, 7, 3))
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Contributors ✨
|
|
138
|
+
|
|
139
|
+
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
|
140
|
+
|
|
141
|
+
<!-- prettier-ignore-start -->
|
|
142
|
+
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
|
143
|
+
<!-- markdownlint-disable -->
|
|
144
|
+
<!-- markdownlint-enable -->
|
|
145
|
+
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
|
146
|
+
<!-- prettier-ignore-end -->
|
|
147
|
+
|
|
148
|
+
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
|
149
|
+
|
|
150
|
+
## Credits
|
|
151
|
+
|
|
152
|
+
[](https://github.com/copier-org/copier)
|
|
153
|
+
|
|
154
|
+
This package was created with
|
|
155
|
+
[Copier](https://copier.readthedocs.io/) and the
|
|
156
|
+
[browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template)
|
|
157
|
+
project template.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/array_api_shape_check/__init__.py
|
|
5
|
+
src/array_api_shape_check/_main.py
|
|
6
|
+
src/array_api_shape_check/py.typed
|
|
7
|
+
src/array_api_shape_check.egg-info/PKG-INFO
|
|
8
|
+
src/array_api_shape_check.egg-info/SOURCES.txt
|
|
9
|
+
src/array_api_shape_check.egg-info/dependency_links.txt
|
|
10
|
+
src/array_api_shape_check.egg-info/requires.txt
|
|
11
|
+
src/array_api_shape_check.egg-info/top_level.txt
|
|
12
|
+
tests/test_main.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
array_api_shape_check
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from array_api_shape_check._main import _get_operand_names
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_get_operand_names():
|
|
5
|
+
def inner(a, b) -> list[str | None]:
|
|
6
|
+
return _get_operand_names(a, b)
|
|
7
|
+
|
|
8
|
+
c = 1
|
|
9
|
+
assert inner(c, 4 * 4)[0] == "c"
|
|
10
|
+
# assert inner(c, 4 * 4)[1] is not None
|
|
11
|
+
# assert "@py_assert" in repr(inner(c, 4 * 4)[1])
|