valarray 0.4__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.
- valarray-0.4/.coveragerc +6 -0
- valarray-0.4/.gitignore +8 -0
- valarray-0.4/PKG-INFO +698 -0
- valarray-0.4/README.md +682 -0
- valarray-0.4/assets/images/valarray_logo.svg +915 -0
- valarray-0.4/docs/example_code/introduction/issue1_problem.py +24 -0
- valarray-0.4/docs/example_code/introduction/issue1_solution.py +44 -0
- valarray-0.4/docs/example_code/introduction/issue2_problem.py +31 -0
- valarray-0.4/docs/example_code/introduction/issue2_solution.py +66 -0
- valarray-0.4/docs/example_code/introduction/issue_3_problem.py +25 -0
- valarray-0.4/docs/example_code/introduction/issue_3_solution.py +41 -0
- valarray-0.4/pyproject.toml +29 -0
- valarray-0.4/requirements.txt +1 -0
- valarray-0.4/requirements_dev.txt +5 -0
- valarray-0.4/tests/core/classes_for_testing/__init__.py +17 -0
- valarray-0.4/tests/core/classes_for_testing/array_and_dtype.py +43 -0
- valarray-0.4/tests/core/classes_for_testing/array_type_adapter.py +85 -0
- valarray-0.4/tests/core/classes_for_testing/comparisons.py +22 -0
- valarray-0.4/tests/core/classes_for_testing/errors_exceptions.py +36 -0
- valarray-0.4/tests/core/classes_for_testing/validated_array.py +16 -0
- valarray-0.4/tests/core/classes_for_testing/validators.py +47 -0
- valarray-0.4/tests/core/errors_exceptions/test_axes_errors.py +26 -0
- valarray-0.4/tests/core/errors_exceptions/test_dtype_errors.py +33 -0
- valarray-0.4/tests/core/errors_exceptions/test_error_list.py +110 -0
- valarray-0.4/tests/core/errors_exceptions/test_exceptions.py +45 -0
- valarray-0.4/tests/core/errors_exceptions/test_values_errors.py +75 -0
- valarray-0.4/tests/core/test_array.py +65 -0
- valarray-0.4/tests/core/test_axes_and_fields.py +22 -0
- valarray-0.4/tests/core/test_misc.py +49 -0
- valarray-0.4/tests/core/test_utils.py +58 -0
- valarray-0.4/tests/core/test_validators.py +105 -0
- valarray-0.4/tests/core/validation_functions/field_values/test_ax_and_field_strings.py +85 -0
- valarray-0.4/tests/core/validation_functions/field_values/test_val_field_values_core.py +117 -0
- valarray-0.4/tests/core/validation_functions/field_values/test_val_field_values_utils.py +154 -0
- valarray-0.4/tests/core/validation_functions/test_val_array.py +81 -0
- valarray-0.4/tests/core/validation_functions/test_val_array_values.py +51 -0
- valarray-0.4/tests/core/validation_functions/test_val_dtype.py +39 -0
- valarray-0.4/tests/core/validation_functions/test_val_shape.py +43 -0
- valarray-0.4/tests/numpy/common.py +37 -0
- valarray-0.4/tests/numpy/test_adapter.py +277 -0
- valarray-0.4/tests/numpy/test_numpy_array.py +84 -0
- valarray-0.4/tests/numpy/test_numpy_comparisons.py +132 -0
- valarray-0.4/tests/numpy/test_numpy_errors_and_exceptions.py +81 -0
- valarray-0.4/tests/numpy/test_validation.py +244 -0
- valarray-0.4/tests/test_imports.py +101 -0
- valarray-0.4/valarray/__init__.py +6 -0
- valarray-0.4/valarray/core/__init__.py +27 -0
- valarray-0.4/valarray/core/array.py +121 -0
- valarray-0.4/valarray/core/array_type_adapter.py +109 -0
- valarray-0.4/valarray/core/axes_and_fields.py +127 -0
- valarray-0.4/valarray/core/comparisons.py +71 -0
- valarray-0.4/valarray/core/errors_exceptions/__init__.py +41 -0
- valarray-0.4/valarray/core/errors_exceptions/error_list.py +87 -0
- valarray-0.4/valarray/core/errors_exceptions/exceptions.py +75 -0
- valarray-0.4/valarray/core/errors_exceptions/generic.py +57 -0
- valarray-0.4/valarray/core/errors_exceptions/validation_errors/__init__.py +0 -0
- valarray-0.4/valarray/core/errors_exceptions/validation_errors/array_creation.py +14 -0
- valarray-0.4/valarray/core/errors_exceptions/validation_errors/axes.py +68 -0
- valarray-0.4/valarray/core/errors_exceptions/validation_errors/base.py +26 -0
- valarray-0.4/valarray/core/errors_exceptions/validation_errors/dtype.py +60 -0
- valarray-0.4/valarray/core/errors_exceptions/validation_errors/values.py +99 -0
- valarray-0.4/valarray/core/types/__init__.py +24 -0
- valarray-0.4/valarray/core/types/generics.py +40 -0
- valarray-0.4/valarray/core/types/other.py +23 -0
- valarray-0.4/valarray/core/utils.py +89 -0
- valarray-0.4/valarray/core/validation_functions/__init__.py +20 -0
- valarray-0.4/valarray/core/validation_functions/array.py +62 -0
- valarray-0.4/valarray/core/validation_functions/array_values.py +47 -0
- valarray-0.4/valarray/core/validation_functions/dtype.py +44 -0
- valarray-0.4/valarray/core/validation_functions/field_values/__init__.py +0 -0
- valarray-0.4/valarray/core/validation_functions/field_values/core.py +106 -0
- valarray-0.4/valarray/core/validation_functions/field_values/types_and_data_structures.py +90 -0
- valarray-0.4/valarray/core/validation_functions/field_values/utils.py +143 -0
- valarray-0.4/valarray/core/validation_functions/shape.py +67 -0
- valarray-0.4/valarray/core/validation_functions/utils.py +24 -0
- valarray-0.4/valarray/core/validators/__init__.py +13 -0
- valarray-0.4/valarray/core/validators/base.py +72 -0
- valarray-0.4/valarray/core/validators/value_comparisons.py +107 -0
- valarray-0.4/valarray/numpy/__init__.py +24 -0
- valarray-0.4/valarray/numpy/array.py +63 -0
- valarray-0.4/valarray/numpy/array_type_adapter.py +133 -0
- valarray-0.4/valarray/numpy/axes_and_fields.py +83 -0
- valarray-0.4/valarray/numpy/comparisons.py +135 -0
- valarray-0.4/valarray/numpy/errors_exceptions.py +172 -0
- valarray-0.4/valarray/numpy/types.py +32 -0
- valarray-0.4/valarray/numpy/validation_functions.py +170 -0
- valarray-0.4/valarray/numpy/validators.py +35 -0
valarray-0.4/.coveragerc
ADDED
valarray-0.4/.gitignore
ADDED
valarray-0.4/PKG-INFO
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: valarray
|
|
3
|
+
Version: 0.4
|
|
4
|
+
Summary: Library for validating numpy arrays.
|
|
5
|
+
Project-URL: Homepage, https://codeberg.org/jfranek/valarray
|
|
6
|
+
Author-email: "J. Franek" <franek.j27@email.cz>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: numpy, validation, array, typing
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Requires-Python: >=3.7
|
|
14
|
+
Requires-Dist: numpy>=2
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
In short, library for validating numpy arrays that also helps with static analysis and documentation. In long, see [Library rationale](#library-rationale).
|
|
20
|
+
|
|
21
|
+
Currently intended primarily as a personal/hobby project (see [Caveats](#caveats))
|
|
22
|
+
|
|
23
|
+
I have gotten away with using it in a professional setting, but YMMV.
|
|
24
|
+
|
|
25
|
+
# Quick start <!-- omit from toc -->
|
|
26
|
+
Install ***valarray*** via pip:
|
|
27
|
+
```shell
|
|
28
|
+
pip install valarray
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Define a validate array class:
|
|
32
|
+
```python
|
|
33
|
+
import numpy as np
|
|
34
|
+
from valarray.numpy import ValidatedNumpyArray
|
|
35
|
+
from valarray.core.errors_exceptions import ValidationException
|
|
36
|
+
|
|
37
|
+
class ExampleValidatedNumpyArray(ValidatedNumpyArray[np.float32]):
|
|
38
|
+
dtype = "float32"
|
|
39
|
+
schema = ('n', 3)
|
|
40
|
+
|
|
41
|
+
ge=0
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Validate a numpy array:
|
|
45
|
+
```python
|
|
46
|
+
try:
|
|
47
|
+
v_arr = ExampleValidatedNumpyArray(np.array([[1,-2,3], [4,5,-6]]))
|
|
48
|
+
except ValidationException as v_exc:
|
|
49
|
+
print(v_exc)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
>>> 'ExampleValidatedNumpyArray' validation failed:
|
|
53
|
+
>>> Incorrect axis sizes: (2, *4*), expected (any, 3).
|
|
54
|
+
>>> Invalid Array Values (>= 0):
|
|
55
|
+
>>> [-2.0, -6.0]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
# Table of contents
|
|
59
|
+
- [Table of contents](#table-of-contents)
|
|
60
|
+
- [Library rationale](#library-rationale)
|
|
61
|
+
- [1) Invalid values causing unintended behaviour](#1-invalid-values-causing-unintended-behaviour)
|
|
62
|
+
- [Problem](#problem)
|
|
63
|
+
- [Solution](#solution)
|
|
64
|
+
- [2) Limited support for static analysis](#2-limited-support-for-static-analysis)
|
|
65
|
+
- [Problem](#problem-1)
|
|
66
|
+
- [Solution](#solution-1)
|
|
67
|
+
- [3) Need for explicit documentation](#3-need-for-explicit-documentation)
|
|
68
|
+
- [Problem](#problem-2)
|
|
69
|
+
- [Solution](#solution-2)
|
|
70
|
+
- [Validated Array](#validated-array)
|
|
71
|
+
- [Defining a validated array](#defining-a-validated-array)
|
|
72
|
+
- [Creating a validated array instance](#creating-a-validated-array-instance)
|
|
73
|
+
- [Accessing array values](#accessing-array-values)
|
|
74
|
+
- [Validation functions](#validation-functions)
|
|
75
|
+
- [Array schema](#array-schema)
|
|
76
|
+
- [Field](#field)
|
|
77
|
+
- [Array schema examples](#array-schema-examples)
|
|
78
|
+
- [rectangles](#rectangles)
|
|
79
|
+
- [Validators](#validators)
|
|
80
|
+
- [Defining a validator](#defining-a-validator)
|
|
81
|
+
- [ValidationResult](#validationresult)
|
|
82
|
+
- [Example Validator](#example-validator)
|
|
83
|
+
- [Catching exceptions](#catching-exceptions)
|
|
84
|
+
- [Special exceptions and errors](#special-exceptions-and-errors)
|
|
85
|
+
- [Generic Errors](#generic-errors)
|
|
86
|
+
- [Caveats](#caveats)
|
|
87
|
+
|
|
88
|
+
# Library rationale
|
|
89
|
+
This library aims to help with 3 issues encountered when working with numpy arrays:
|
|
90
|
+
|
|
91
|
+
## 1) Invalid values causing unintended behaviour
|
|
92
|
+
### Problem
|
|
93
|
+
Invalid values can cause crashes, or worse, cause silent failures.
|
|
94
|
+
|
|
95
|
+
For example the following code fails silently when attempting to cut patches from image using bounding boxes with invalid coordinates.
|
|
96
|
+
```python
|
|
97
|
+
import numpy as np
|
|
98
|
+
import numpy.typing as npt
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def cut_patches(
|
|
102
|
+
img: npt.NDArray[np.uint8], boxes: npt.NDArray[np.int64]
|
|
103
|
+
) -> list[npt.NDArray[np.uint8]]:
|
|
104
|
+
patches = []
|
|
105
|
+
for box in boxes:
|
|
106
|
+
patch = img[box[0] : box[2], box[1] : box[3], :]
|
|
107
|
+
patches.append(patch)
|
|
108
|
+
|
|
109
|
+
return patches
|
|
110
|
+
|
|
111
|
+
img_random = np.random.random((400, 400, 3)).astype(np.uint8) * 255
|
|
112
|
+
|
|
113
|
+
boxes_xyxy_invalid = np.array(
|
|
114
|
+
[[-10, 100, 200, 200], [150, 50, 200, 250]], dtype=int
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
patches = cut_patches(img_random, boxes_xyxy_invalid)
|
|
118
|
+
|
|
119
|
+
for patch in patches:
|
|
120
|
+
print(patch.shape)
|
|
121
|
+
|
|
122
|
+
>>> (0, 100, 3) # empty image patch
|
|
123
|
+
>>> (50, 200, 3)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Solution
|
|
127
|
+
Validate boxes array first. If errors are encountered, print descriptive error message(s).
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
import numpy as np
|
|
131
|
+
import numpy.typing as npt
|
|
132
|
+
|
|
133
|
+
from valarray.core.errors_exceptions import ValidationException
|
|
134
|
+
from valarray.numpy import Field
|
|
135
|
+
from valarray.numpy.array import ValidatedNumpyArray
|
|
136
|
+
|
|
137
|
+
class BoxesXYXY(ValidatedNumpyArray[np.int64]):
|
|
138
|
+
dtype = int
|
|
139
|
+
schema = (
|
|
140
|
+
"n",
|
|
141
|
+
(
|
|
142
|
+
Field(ge=0),
|
|
143
|
+
Field(ge=0),
|
|
144
|
+
Field(ge=0),
|
|
145
|
+
Field(ge=0),
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def cut_patches(
|
|
150
|
+
img: npt.NDArray[np.uint8], boxes: npt.NDArray[np.int64]
|
|
151
|
+
) -> list[npt.NDArray[np.uint8]]:
|
|
152
|
+
patches = []
|
|
153
|
+
for box in boxes:
|
|
154
|
+
patch = img[box[0] : box[2], box[1] : box[3], :]
|
|
155
|
+
patches.append(patch)
|
|
156
|
+
|
|
157
|
+
return patches
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
img_random = np.random.random((400, 400, 3)).astype(np.uint8) * 255
|
|
161
|
+
|
|
162
|
+
boxes_xyxy_invalid = BoxesXYXY.validate(
|
|
163
|
+
np.array([[-10, 100, 200, 200], [150, 50, 200, 250]], dtype=int)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
patches = cut_patches(img_random, boxes_xyxy_invalid.array)
|
|
167
|
+
|
|
168
|
+
for patch in patches:
|
|
169
|
+
print(patch.shape)
|
|
170
|
+
|
|
171
|
+
except ValidationException as exc:
|
|
172
|
+
for err in exc.errs:
|
|
173
|
+
print(err.msg)
|
|
174
|
+
|
|
175
|
+
>>> Invalid Field Values (< 0):
|
|
176
|
+
>>> Axis < 1 >: '_sized_4'
|
|
177
|
+
>>> Field < 0 >: [-10]
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## 2) Limited support for static analysis
|
|
181
|
+
### Problem
|
|
182
|
+
Support for static analysis is limited. Tools can only check whether the datatype is correct, but not shape, values or what those values actually represent.
|
|
183
|
+
|
|
184
|
+
For example, the function to crop patches needs the boxes to be defined by `xmin, ymin, xmax, ymax` but doesn't throw an error if input boxes are defined by `x_center, y_center, width, height` and static analysis tools cannot detect this error using bulit-in numpy types.
|
|
185
|
+
```python
|
|
186
|
+
import numpy as np
|
|
187
|
+
import numpy.typing as npt
|
|
188
|
+
|
|
189
|
+
def cut_patches(
|
|
190
|
+
img: npt.NDArray[np.uint8], boxes: npt.NDArray[np.int64]
|
|
191
|
+
) -> list[npt.NDArray[np.uint8]]:
|
|
192
|
+
patches = []
|
|
193
|
+
for box in boxes:
|
|
194
|
+
patch = img[box[0] : box[2], box[1] : box[3], :]
|
|
195
|
+
patches.append(patch)
|
|
196
|
+
|
|
197
|
+
return patches
|
|
198
|
+
|
|
199
|
+
img_random = np.random.random((400, 400, 3)).astype(np.uint8) * 255
|
|
200
|
+
|
|
201
|
+
boxes_xyxy = np.array([[0, 100, 200, 200], [150, 50, 200, 250]], dtype=int)
|
|
202
|
+
|
|
203
|
+
boxes_xywh = np.array([[100, 150, 200, 100], [175, 50, 50, 250]], dtype=int)
|
|
204
|
+
|
|
205
|
+
patches = cut_patches(img_random, boxes_xyxy) # type checker does not complain
|
|
206
|
+
|
|
207
|
+
print("Valid")
|
|
208
|
+
for patch in patches:
|
|
209
|
+
print(patch.shape)
|
|
210
|
+
|
|
211
|
+
patches_inv = cut_patches(img_random, boxes_xywh) # type checker still does not complain
|
|
212
|
+
|
|
213
|
+
print("Invalid")
|
|
214
|
+
for patch in patches_inv:
|
|
215
|
+
print(patch.shape)
|
|
216
|
+
|
|
217
|
+
>>> Valid
|
|
218
|
+
>>> (200, 100, 3)
|
|
219
|
+
>>> (50, 200, 3)
|
|
220
|
+
>>> Invalid
|
|
221
|
+
>>> (100, 0, 3)
|
|
222
|
+
>>> (0, 200, 3)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Solution
|
|
226
|
+
`ValidatedNumpyArray` subclasses can represent these two types of boxes arrays, and can be used instead of bare numpy arrays in function/method signatures and such.
|
|
227
|
+
```python
|
|
228
|
+
import numpy as np
|
|
229
|
+
import numpy.typing as npt
|
|
230
|
+
|
|
231
|
+
from valarray.numpy import Field
|
|
232
|
+
from valarray.numpy.array import ValidatedNumpyArray
|
|
233
|
+
|
|
234
|
+
class BoxesXYXY(ValidatedNumpyArray[np.int64]):
|
|
235
|
+
dtype = int
|
|
236
|
+
schema = (
|
|
237
|
+
"n",
|
|
238
|
+
(
|
|
239
|
+
Field(ge=0),
|
|
240
|
+
Field(ge=0),
|
|
241
|
+
Field(ge=0),
|
|
242
|
+
Field(ge=0),
|
|
243
|
+
),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
class BoxesXYWH(ValidatedNumpyArray[np.int64]):
|
|
247
|
+
dtype = int
|
|
248
|
+
schema = (
|
|
249
|
+
"n",
|
|
250
|
+
(
|
|
251
|
+
Field(ge=0),
|
|
252
|
+
Field(ge=0),
|
|
253
|
+
Field(gt=0),
|
|
254
|
+
Field(gt=0),
|
|
255
|
+
),
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def cut_patches(
|
|
259
|
+
img: npt.NDArray[np.uint8], boxes: BoxesXYXY
|
|
260
|
+
) -> list[npt.NDArray[np.uint8]]:
|
|
261
|
+
patches = []
|
|
262
|
+
for box in boxes.array:
|
|
263
|
+
patch = img[box[0] : box[2], box[1] : box[3], :]
|
|
264
|
+
patches.append(patch)
|
|
265
|
+
|
|
266
|
+
return patches
|
|
267
|
+
|
|
268
|
+
img_random = np.random.random((400, 400, 3)).astype(np.uint8) * 255
|
|
269
|
+
|
|
270
|
+
boxes_xyxy = BoxesXYXY.wrap(
|
|
271
|
+
np.array([[0, 100, 200, 200], [150, 50, 200, 250]], dtype=int)
|
|
272
|
+
)
|
|
273
|
+
boxes_xywh = BoxesXYWH.wrap(
|
|
274
|
+
np.array([[100, 150, 200, 100], [175, 50, 50, 250]], dtype=int)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
patches = cut_patches(img_random, boxes_xyxy) # type checker does not complain
|
|
278
|
+
|
|
279
|
+
print("Valid")
|
|
280
|
+
for patch in patches:
|
|
281
|
+
print(patch.shape)
|
|
282
|
+
|
|
283
|
+
patches_inv = cut_patches(
|
|
284
|
+
img_random, boxes_xywh # type checker reports wrong argument type
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
print("Invalid")
|
|
288
|
+
for patch in patches_inv:
|
|
289
|
+
print(patch.shape)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## 3) Need for explicit documentation
|
|
293
|
+
### Problem
|
|
294
|
+
Using built-in numpy types provides only documentation for data types. Shape, values, constraints and what the array represents need to be explicitly documented either in comments or docstrings.
|
|
295
|
+
|
|
296
|
+
If this type of array is used in multiple places / functions, this can cause duplicated documentation.
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
import numpy as np
|
|
300
|
+
import numpy.typing as npt
|
|
301
|
+
|
|
302
|
+
def cut_patches(
|
|
303
|
+
img: npt.NDArray[np.uint8], boxes: npt.NDArray[np.int64]
|
|
304
|
+
) -> list[npt.NDArray[np.uint8]]:
|
|
305
|
+
"""Cuts patches from an image.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
img (npt.NDArray[np.uint8]): Source image
|
|
309
|
+
boxes (npt.NDArray[np.int64]): Array of N boxes `xmin, ymin, xmax, ymax` in pixels.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
list[npt.NDArray[np.uint8]]: List of patches.
|
|
313
|
+
"""
|
|
314
|
+
patches = []
|
|
315
|
+
for box in boxes:
|
|
316
|
+
patch = img[box[0] : box[2], box[1] : box[3], :]
|
|
317
|
+
patches.append(patch)
|
|
318
|
+
|
|
319
|
+
return patches
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Solution
|
|
323
|
+
Defining data type, schema and constraints on a `ValidatedNumpyArray` subclass already implicitly documents them.
|
|
324
|
+
|
|
325
|
+
This can be complemented by adding additional (or summary) documentation in the class docstring.
|
|
326
|
+
|
|
327
|
+
This implicit/explicit documentation can be then accessed from multiple functions via parameter type.
|
|
328
|
+
```python
|
|
329
|
+
import numpy as np
|
|
330
|
+
import numpy.typing as npt
|
|
331
|
+
|
|
332
|
+
from valarray.numpy import Field
|
|
333
|
+
from valarray.numpy.array import ValidatedNumpyArray
|
|
334
|
+
|
|
335
|
+
class BoxesXYXY(ValidatedNumpyArray[np.int64]):
|
|
336
|
+
"""Array of N `xyxy` boxes in pixels."""
|
|
337
|
+
|
|
338
|
+
dtype = int
|
|
339
|
+
schema = (
|
|
340
|
+
"n",
|
|
341
|
+
(
|
|
342
|
+
Field("xmin_px", ge=0),
|
|
343
|
+
Field("ymin_px", ge=0),
|
|
344
|
+
Field("xmax_px", ge=0),
|
|
345
|
+
Field("ymax_px", ge=0),
|
|
346
|
+
),
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def cut_patches(
|
|
350
|
+
img: npt.NDArray[np.uint8], boxes: BoxesXYXY
|
|
351
|
+
) -> list[npt.NDArray[np.uint8]]:
|
|
352
|
+
"""Cuts patches from an image.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
img (npt.NDArray[np.uint8]): Source image
|
|
356
|
+
boxes (BoxesXYXY): Boxes to cut patches with.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
list[npt.NDArray[np.uint8]]: List of patches.
|
|
360
|
+
"""
|
|
361
|
+
patches = []
|
|
362
|
+
for box in boxes.array:
|
|
363
|
+
patch = img[box[0] : box[2], box[1] : box[3], :]
|
|
364
|
+
patches.append(patch)
|
|
365
|
+
|
|
366
|
+
return patches
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
# Validated Array
|
|
371
|
+
## Defining a validated array
|
|
372
|
+
Subclass `ValidatedNumpyArray` and define:
|
|
373
|
+
- `dtype` - expected data type specification (such as `float`, `"float64"`, `np.float64`).
|
|
374
|
+
If not specified, data type is not validated.
|
|
375
|
+
For full list of accepted values, see:
|
|
376
|
+
https://numpy.org/doc/stable/reference/arrays.dtypes.html#specifying-and-constructing-data-types
|
|
377
|
+
- `schema` - expected shape specification (of type `valarray.numpy.axes_and_fields.AxesTuple`). For details, see [Array Schema](#array-schema).
|
|
378
|
+
If not specified, shape is not validated (and no field validators are applied).
|
|
379
|
+
- `lt`/`le`/`ge`/`gt`/`eq` - basic array value constraints -> less (or equal) than, greater (or equal) than, equal to
|
|
380
|
+
- `validators` - optional list of validators applied to the whole array. For details, see [Validators](##validators).
|
|
381
|
+
|
|
382
|
+
```python
|
|
383
|
+
import numpy as np
|
|
384
|
+
from valarray.numpy import ValidatedNumpyArray
|
|
385
|
+
|
|
386
|
+
class ExampleValidatedNumpyArray(ValidatedNumpyArray):
|
|
387
|
+
dtype = np.float32
|
|
388
|
+
schema = ('batch_size', 3, 5)
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
## Creating a validated array instance
|
|
392
|
+
There are 4 ways to create a validated array instance:
|
|
393
|
+
- validate an existing array
|
|
394
|
+
```python
|
|
395
|
+
# using .validate()
|
|
396
|
+
v_arr = ExampleValidatedNumpyArray.validate(np.array([1,2],dtype=np.float32))
|
|
397
|
+
# using .__init__()
|
|
398
|
+
v_arr = ExampleValidatedNumpyArray(np.array([1,2],dtype=np.float32), validate=True)
|
|
399
|
+
```
|
|
400
|
+
- from an existing array without validation (to be used as a type hint)
|
|
401
|
+
```python
|
|
402
|
+
# using .wrap()
|
|
403
|
+
v_arr = ExampleValidatedNumpyArray.wrap(np.array([1,2],dtype=np.float32))
|
|
404
|
+
# using .__init__()
|
|
405
|
+
v_arr = ExampleValidatedNumpyArray(np.array([1,2],dtype=np.float32), validate=False)
|
|
406
|
+
|
|
407
|
+
# NOTE: validation can be performed at a later stage using:
|
|
408
|
+
v_arr.validate_array()
|
|
409
|
+
```
|
|
410
|
+
- from an arbitrary object pased to `np.array` constructor
|
|
411
|
+
(data type of the resulting array is taken from the validated array class definition,
|
|
412
|
+
if no data type is defined, the most appropriate type is chosen by using `np.asarray()`)
|
|
413
|
+
```python
|
|
414
|
+
v_arr = ExampleValidatedNumpyArray([1,2])
|
|
415
|
+
```
|
|
416
|
+
- create an empty array
|
|
417
|
+
- ***shape*** inferred from schema or empty if not defined
|
|
418
|
+
- ***dtype*** from validated array class definition or default if not defined
|
|
419
|
+
```python
|
|
420
|
+
# using .empty()
|
|
421
|
+
v_arr = ExampleValidatedNumpyArray.empty()
|
|
422
|
+
# or __init__
|
|
423
|
+
v_arr = ExampleValidatedNumpyArray(None)
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
If created from an existing array, there is an option to try to coerce array to the expected data type.
|
|
427
|
+
`CoerceDTypeException` is raised if this fails.
|
|
428
|
+
```python
|
|
429
|
+
# (only) using .__init__()
|
|
430
|
+
v_arr = ExampleValidatedNumpyArray(np.array([1,2],dtype=np.float32), coerce_dtype=True, validate=True)
|
|
431
|
+
v_arr = ExampleValidatedNumpyArray(np.array([1,2],dtype=np.float32), coerce_dtype=True, validate=False)
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## Accessing array values
|
|
435
|
+
You can access the underlying array using the `.array`/`.a_` property:
|
|
436
|
+
```python
|
|
437
|
+
arr = v_arr.array
|
|
438
|
+
# or
|
|
439
|
+
arr = v_arr.a_
|
|
440
|
+
```
|
|
441
|
+
It is recommended to make a copy before performing operations that could invalidate the array:
|
|
442
|
+
```python
|
|
443
|
+
arr = v_arr.array.copy()
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
It is also recommended to specify array data type when subclassing `ValidatedNumpyArray` to ensure correct type hint:
|
|
447
|
+
```python
|
|
448
|
+
class UnspecifiedDataTypeArray(ValidatedNumpyArray): ...
|
|
449
|
+
|
|
450
|
+
arr = UnspecifiedDataTypeArray(...).array # np.typing.NDArray[Unknown]
|
|
451
|
+
|
|
452
|
+
class SpecifiedDataTypeArray(ValidatedNumpyArray[np.float32]): ...
|
|
453
|
+
|
|
454
|
+
arr = SpecifiedDataTypeArray(...).array # np.typing.NDArray[np.float32]
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
# Validation functions
|
|
458
|
+
Array validation is designed to be modular and composable and validation functions can be used on they own if only runtime validation is required.
|
|
459
|
+
Each validation function returns a list of errors, from which a `ValidationException` can be raised. For details see [Catching exceptions](#catching-exceptions).
|
|
460
|
+
```python
|
|
461
|
+
from valarray.numpy.validation_functions import validate_*
|
|
462
|
+
|
|
463
|
+
# validate array and get a list of errors (empty if no errors)
|
|
464
|
+
errs = validate_*(arr, ...)
|
|
465
|
+
|
|
466
|
+
# validate array and raise exception if errors are returned
|
|
467
|
+
validate_*(arr, ...).raise_()
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
There are 4 validation functions:
|
|
471
|
+
- **validate_dtype**
|
|
472
|
+
- Checks that array has the expected datatype.
|
|
473
|
+
- *returns* `NumpyIncorrectDTypeError`
|
|
474
|
+
- **validate_shape**
|
|
475
|
+
- Checks that array has the right number of axes, and that the axes have expected sizes.
|
|
476
|
+
- *returns* `IncorrectAxNumberError` and or `IncorrectAxSizesError`
|
|
477
|
+
- **validate_array_values**
|
|
478
|
+
- Performs an arbitrary check on the values of the whole array using a [Validator](#validators).
|
|
479
|
+
- *returns* `NumpyInvalidArrayValuesError`
|
|
480
|
+
- **validate_field_values**
|
|
481
|
+
- Performs an arbitrary check on the values of selected fields using a [Validator](#validators) defined in [Array Schema](#array-schema).
|
|
482
|
+
- By default expects array to be in the correct shape. If this is not guaranteed, set parameter `check_shape=True`.
|
|
483
|
+
- *returns* `NumpyInvalidFieldValuesError` (and possibly `IncorrectAxNumberError`/`IncorrectAxSizesError` if `check_shape=True`)
|
|
484
|
+
|
|
485
|
+
and a "composite" validation function:
|
|
486
|
+
- **validate_array**
|
|
487
|
+
- performs validation in the following order:
|
|
488
|
+
- `validate_dtype()`
|
|
489
|
+
- `validate_shape()`
|
|
490
|
+
- *returns* `NumpyIncorrectDTypeError`/`IncorrectAxNumberError`/`IncorrectAxSizesError` if any.
|
|
491
|
+
- `validate_array_values()`
|
|
492
|
+
- `validate_field_values()`
|
|
493
|
+
- *returns* `NumpyInvalidArrayValuesError`/`NumpyInvalidFieldValuesError` or no errors.
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
# Array schema
|
|
497
|
+
Schema defines expected axes, and for each axis its' fields and optionally constraints on the field values.
|
|
498
|
+
|
|
499
|
+
Axes can be defined with:
|
|
500
|
+
- integer size (`6`)
|
|
501
|
+
- name string ('axis_name`)
|
|
502
|
+
- tuple of fields
|
|
503
|
+
|
|
504
|
+
Fields can be defined with:
|
|
505
|
+
- name string ('field_name')
|
|
506
|
+
- instance of `valarray.numpy.Field`
|
|
507
|
+
|
|
508
|
+
``` python
|
|
509
|
+
from valarray.numpy import Field
|
|
510
|
+
|
|
511
|
+
schema = (
|
|
512
|
+
"axis_0",
|
|
513
|
+
3,
|
|
514
|
+
("field_a", Field())
|
|
515
|
+
)
|
|
516
|
+
```
|
|
517
|
+
## Field
|
|
518
|
+
Defines (optional) name and value constrints for array field. More specifically:
|
|
519
|
+
- `name` - descriptive name used in error messages (if missing, field index is used instead)
|
|
520
|
+
- `lt`/`le`/`ge`/`gt`/`eq` - basic array value constraints -> less (or equal) than, greater (or equal) than, equal to
|
|
521
|
+
- `validators` - other validators of fields values. For details, see [Validators](##validators).
|
|
522
|
+
|
|
523
|
+
```python
|
|
524
|
+
from typing import Any
|
|
525
|
+
|
|
526
|
+
from valarray.numpy import Field, NumpyValidator
|
|
527
|
+
|
|
528
|
+
class ExampleNumpyValidator(NumpyValidator[Any]):
|
|
529
|
+
def validate(self, arr):
|
|
530
|
+
return True
|
|
531
|
+
|
|
532
|
+
f1 = Field("example_named_field", ge=0)
|
|
533
|
+
f2 = Field(gt=10, validators=(ExampleNumpyValidator(),))
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
## Array schema examples
|
|
537
|
+
### rectangles
|
|
538
|
+
An array of arbitrary number of rectangles defined by min and max coordinates which has two axes: *n_rects* and *rect*.
|
|
539
|
+
Axis *rect* is has 4 fields: *x_min*,*y_min*,*x_max*,*y_max*, where values must be greater or equal to zero.
|
|
540
|
+
|
|
541
|
+
``` python
|
|
542
|
+
import numpy as np
|
|
543
|
+
|
|
544
|
+
from valarray.numpy import ValidatedNumpyArray
|
|
545
|
+
from valarray.numpy.axes_and_fields import Field
|
|
546
|
+
|
|
547
|
+
# validated array with schema
|
|
548
|
+
class Rect(ValidatedNumpyArray):
|
|
549
|
+
schema = (
|
|
550
|
+
"n_rects",
|
|
551
|
+
(
|
|
552
|
+
Field("x_min", ge=0),
|
|
553
|
+
Field("y_min", ge=0),
|
|
554
|
+
Field("x_max", ge=0),
|
|
555
|
+
Field("y_max", ge=0),
|
|
556
|
+
),
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
# example array
|
|
560
|
+
arr = np.array(
|
|
561
|
+
[
|
|
562
|
+
[10, 20, 30, 40],
|
|
563
|
+
[15, 25, 35, 45],
|
|
564
|
+
],
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
Rect.validate(arr)
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
# Validators
|
|
571
|
+
Validators are objects that perform arbitrary validation of array or field values defined by user.
|
|
572
|
+
|
|
573
|
+
## Defining a validator
|
|
574
|
+
Validators must subclass `valarray.numpy.NumpyValidator` Abstract Base Class
|
|
575
|
+
and implement the `.validate()` method that takes an array as an input and results in success/failure of validation using these options:
|
|
576
|
+
|
|
577
|
+
- **on success**:
|
|
578
|
+
- *returns* `valarray.core.ValidationResult(status="OK")`
|
|
579
|
+
- *returns* `True`
|
|
580
|
+
- *returns* `None`
|
|
581
|
+
- **on failure**:
|
|
582
|
+
- *returns* `valarray.core.ValidationResult(status="FAIL")`
|
|
583
|
+
- *returns* `False`
|
|
584
|
+
- *raises* `ValueError`
|
|
585
|
+
|
|
586
|
+
### ValidationResult
|
|
587
|
+
Contains result status of validation `status="OK"`/`status="FAIL"`
|
|
588
|
+
|
|
589
|
+
Can also optionally contain:
|
|
590
|
+
- message to be added to validation error
|
|
591
|
+
- indices of invalid values
|
|
592
|
+
|
|
593
|
+
Indices use [advanced numpy indexing](https://numpy.org/doc/stable/user/basics.indexing.html#advanced-indexing) and can be either:
|
|
594
|
+
- a boolean array
|
|
595
|
+
- a tuple of integer arrays with length equal to the number of array axes
|
|
596
|
+
|
|
597
|
+
**!** If used for validating field values, it is recommended that validators return ValidationResult with indices.
|
|
598
|
+
Error messages can then properly show which values of which fields caused the validation to fail.
|
|
599
|
+
|
|
600
|
+
```python
|
|
601
|
+
import numpy as np
|
|
602
|
+
from valarray.core import ValidationResult
|
|
603
|
+
|
|
604
|
+
# 2D array of shape (3,3)
|
|
605
|
+
indices = np.array(
|
|
606
|
+
[
|
|
607
|
+
[False, False, False],
|
|
608
|
+
[True, False, False],
|
|
609
|
+
[False, False, True],
|
|
610
|
+
]
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
indices = (np.array([0, 1, 1]), np.array([1, 0, 1]))
|
|
614
|
+
|
|
615
|
+
res = ValidationResult(status="FAIL", indices_invalid=indices, msg="Optional error message.")
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
### Example Validator
|
|
619
|
+
```python
|
|
620
|
+
from dataclasses import dataclass
|
|
621
|
+
from typing import Literal
|
|
622
|
+
|
|
623
|
+
import numpy as np
|
|
624
|
+
|
|
625
|
+
from valarray.core import ValidationResult
|
|
626
|
+
from valarray.numpy import NumpyValidator
|
|
627
|
+
|
|
628
|
+
@dataclass
|
|
629
|
+
class ExampleIsEvenValidator(NumpyValidator[np.uint8]):
|
|
630
|
+
method: Literal["boolean", "raise", "result"] = "boolean"
|
|
631
|
+
|
|
632
|
+
def validate(self, arr):
|
|
633
|
+
even = arr % 2 == 0
|
|
634
|
+
|
|
635
|
+
all_even = np.all(even)
|
|
636
|
+
|
|
637
|
+
match self.method:
|
|
638
|
+
case "boolean":
|
|
639
|
+
if all_even:
|
|
640
|
+
return True
|
|
641
|
+
|
|
642
|
+
return False
|
|
643
|
+
case "raise":
|
|
644
|
+
if all_even:
|
|
645
|
+
return None
|
|
646
|
+
|
|
647
|
+
raise ValueError()
|
|
648
|
+
case "result":
|
|
649
|
+
if all_even:
|
|
650
|
+
return ValidationResult("OK")
|
|
651
|
+
|
|
652
|
+
return ValidationResult("FAIL", indices_invalid=~even)
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
# Catching exceptions
|
|
656
|
+
Failed validation results in `valarray.core.errors_exceptions.ValidationException` being raised containing list of errors responsible (and name of array class if available).
|
|
657
|
+
|
|
658
|
+
Main error types are:
|
|
659
|
+
- `IncorrectDTypeError` - Wrong data type. **\***
|
|
660
|
+
- `IncorrectAxNumberError` - Wrong number of axes
|
|
661
|
+
- `IncorrectAxSizesError` - Ax or axes have wrong size(s)
|
|
662
|
+
- `InvalidArrayValuesError` - Validator applied to the whole array failed. **\***
|
|
663
|
+
- `InvalidFieldValuesError` - Validator applied field(s) failed. **\***
|
|
664
|
+
|
|
665
|
+
**\*** These errors have special variants that ensure proper type hints. See [Generic Errors](#generic-errors).
|
|
666
|
+
|
|
667
|
+
Error list is a special list type that in addition to integer index and slice can be filtered by error type(s).
|
|
668
|
+
```python
|
|
669
|
+
try:
|
|
670
|
+
...
|
|
671
|
+
except ValidationException as exc:
|
|
672
|
+
err = exc.errs[0]
|
|
673
|
+
|
|
674
|
+
sliced_errs = exc.errs[:2]
|
|
675
|
+
|
|
676
|
+
dtype_errs = exc.errs[NumpyIncorrectDTypeError]
|
|
677
|
+
|
|
678
|
+
axis_errs = exc.errs[(IncorrectAxSizesError, IncorrectAxNumberError)]
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
## Special exceptions and errors
|
|
682
|
+
There are two special subclasses of `ValidationException` with associated validation errors raised during [instantiation](#creating-a-validated-array-instance):
|
|
683
|
+
- `CreateArrayException` -> `CannotCreateArrayError` - Array cannot be created from supplied object.
|
|
684
|
+
- `CoerceDTypeException` -> `CannotCoerceDTypeError` - If array data type cannot be coerced when creating array with `ValidatedArray(coerce_dtype=True)`
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
## Generic Errors
|
|
688
|
+
These error types have subclasses ensuring proper type hints:
|
|
689
|
+
- `IncorrectDTypeError` -> `NumpyIncorrectDTypeError`
|
|
690
|
+
- `CannotCoerceDTypeError` -> `NumpyCannotCoerceDTypeError`
|
|
691
|
+
- `InvalidArrayValuesError` -> `NumpyInvalidArrayValuesError`
|
|
692
|
+
- `InvalidFieldValuesError` -> `NumpyInvalidFieldValuesError`
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
# Caveats
|
|
696
|
+
- I cannot guarantee that the test suite is foolproof ATM as I'm currently the only one testing this library.
|
|
697
|
+
- Library has so far only been tested with `python==3.12` and `numpy==2.4.0`
|
|
698
|
+
- Library isn't tested for performance, use in production only if the primary bottleneck is brain and not hardware.
|