numguard-ci 1.0.0__py3-none-any.whl
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.
- numguard/__init__.py +70 -0
- numguard/__main__.py +6 -0
- numguard/_corpus/__init__.py +1 -0
- numguard/_corpus/cosine_similarity/__init__.py +1 -0
- numguard/_corpus/cosine_similarity/buggy.py +7 -0
- numguard/_corpus/cosine_similarity/reference.py +27 -0
- numguard/_corpus/cosine_similarity/spec.py +21 -0
- numguard/_corpus/cosine_similarity/task.md +17 -0
- numguard/_corpus/hypot/__init__.py +1 -0
- numguard/_corpus/hypot/buggy.py +7 -0
- numguard/_corpus/hypot/reference.py +18 -0
- numguard/_corpus/hypot/spec.py +21 -0
- numguard/_corpus/hypot/task.md +15 -0
- numguard/_corpus/log1p/__init__.py +1 -0
- numguard/_corpus/log1p/buggy.py +7 -0
- numguard/_corpus/log1p/reference.py +15 -0
- numguard/_corpus/log1p/spec.py +30 -0
- numguard/_corpus/log1p/task.md +15 -0
- numguard/_corpus/logsumexp/__init__.py +1 -0
- numguard/_corpus/logsumexp/buggy.py +7 -0
- numguard/_corpus/logsumexp/reference.py +16 -0
- numguard/_corpus/logsumexp/spec.py +20 -0
- numguard/_corpus/logsumexp/task.md +15 -0
- numguard/_corpus/loo_mean/__init__.py +1 -0
- numguard/_corpus/loo_mean/buggy.py +9 -0
- numguard/_corpus/loo_mean/reference.py +22 -0
- numguard/_corpus/loo_mean/spec.py +25 -0
- numguard/_corpus/loo_mean/task.md +18 -0
- numguard/_corpus/mean_float16/__init__.py +1 -0
- numguard/_corpus/mean_float16/buggy.py +9 -0
- numguard/_corpus/mean_float16/reference.py +14 -0
- numguard/_corpus/mean_float16/spec.py +20 -0
- numguard/_corpus/mean_float16/task.md +15 -0
- numguard/_corpus/normalize/__init__.py +1 -0
- numguard/_corpus/normalize/buggy.py +7 -0
- numguard/_corpus/normalize/reference.py +17 -0
- numguard/_corpus/normalize/spec.py +20 -0
- numguard/_corpus/normalize/task.md +15 -0
- numguard/_corpus/quantize_dynamic/__init__.py +1 -0
- numguard/_corpus/quantize_dynamic/buggy.py +13 -0
- numguard/_corpus/quantize_dynamic/reference.py +18 -0
- numguard/_corpus/quantize_dynamic/spec.py +34 -0
- numguard/_corpus/quantize_dynamic/task.md +20 -0
- numguard/_corpus/sample_std/__init__.py +1 -0
- numguard/_corpus/sample_std/buggy.py +10 -0
- numguard/_corpus/sample_std/reference.py +18 -0
- numguard/_corpus/sample_std/spec.py +19 -0
- numguard/_corpus/sample_std/task.md +16 -0
- numguard/_corpus/sigmoid/__init__.py +1 -0
- numguard/_corpus/sigmoid/buggy.py +7 -0
- numguard/_corpus/sigmoid/reference.py +10 -0
- numguard/_corpus/sigmoid/spec.py +29 -0
- numguard/_corpus/sigmoid/task.md +17 -0
- numguard/_corpus/softmax/__init__.py +1 -0
- numguard/_corpus/softmax/buggy.py +8 -0
- numguard/_corpus/softmax/reference.py +13 -0
- numguard/_corpus/softmax/spec.py +21 -0
- numguard/_corpus/softmax/task.md +16 -0
- numguard/_corpus/variance_onepass/__init__.py +1 -0
- numguard/_corpus/variance_onepass/buggy.py +11 -0
- numguard/_corpus/variance_onepass/reference.py +18 -0
- numguard/_corpus/variance_onepass/spec.py +22 -0
- numguard/_corpus/variance_onepass/task.md +15 -0
- numguard/agent/__init__.py +9 -0
- numguard/agent/bench.py +154 -0
- numguard/agent/ci.py +258 -0
- numguard/agent/common.py +90 -0
- numguard/agent/fix.py +288 -0
- numguard/agent/llm.py +420 -0
- numguard/agent/prompts.py +166 -0
- numguard/agent/sandbox.py +244 -0
- numguard/agent/session.py +164 -0
- numguard/agent/suggest.py +170 -0
- numguard/agent/tools.py +185 -0
- numguard/agent/worker.py +188 -0
- numguard/api.py +365 -0
- numguard/backends.py +256 -0
- numguard/card.py +241 -0
- numguard/carddata.py +119 -0
- numguard/cases.py +738 -0
- numguard/checks.py +701 -0
- numguard/ci.py +44 -0
- numguard/cli.py +772 -0
- numguard/config.py +191 -0
- numguard/corpus.py +305 -0
- numguard/dtypes.py +140 -0
- numguard/github_api.py +295 -0
- numguard/httpclient.py +44 -0
- numguard/init_cmd.py +287 -0
- numguard/properties.py +275 -0
- numguard/py.typed +0 -0
- numguard/pytest_plugin.py +216 -0
- numguard/reference.py +247 -0
- numguard/report.py +235 -0
- numguard/resolve.py +170 -0
- numguard/result.py +260 -0
- numguard/runconfig.py +153 -0
- numguard/specdoc.py +681 -0
- numguard/strategies.py +176 -0
- numguard_ci-1.0.0.dist-info/METADATA +911 -0
- numguard_ci-1.0.0.dist-info/RECORD +105 -0
- numguard_ci-1.0.0.dist-info/WHEEL +5 -0
- numguard_ci-1.0.0.dist-info/entry_points.txt +5 -0
- numguard_ci-1.0.0.dist-info/licenses/LICENSE +21 -0
- numguard_ci-1.0.0.dist-info/top_level.txt +1 -0
numguard/__init__.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""numguard - numerical edge-case testing for array code.
|
|
2
|
+
|
|
3
|
+
Generate the inputs that numerical code gets wrong (zeros, empty arrays, NaN,
|
|
4
|
+
infinities, huge and subnormal magnitudes, signed zero, cancellation-prone
|
|
5
|
+
data, float16 precision, ...) and check a function against them::
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import numguard
|
|
9
|
+
|
|
10
|
+
def logsumexp(x):
|
|
11
|
+
return np.log(np.sum(np.exp(x)))
|
|
12
|
+
|
|
13
|
+
report = numguard.check(logsumexp, reference=scipy.special.logsumexp,
|
|
14
|
+
properties=["translation_equivariant"])
|
|
15
|
+
print(report.summary())
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from . import properties
|
|
21
|
+
from .api import Spec, check, check_spec, get_spec, guard
|
|
22
|
+
from .cases import (
|
|
23
|
+
ANY,
|
|
24
|
+
DEFAULT_DTYPES,
|
|
25
|
+
FLOAT_DTYPES,
|
|
26
|
+
KINDS,
|
|
27
|
+
Array,
|
|
28
|
+
ArraySpec,
|
|
29
|
+
Case,
|
|
30
|
+
Domain,
|
|
31
|
+
generate_cases,
|
|
32
|
+
integers,
|
|
33
|
+
interval,
|
|
34
|
+
)
|
|
35
|
+
from .checks import compare
|
|
36
|
+
from .properties import Property
|
|
37
|
+
from .reference import highprec, longdouble, mp_scalar, ulp_distance
|
|
38
|
+
from .report import CheckResult, NumGuardError, Report
|
|
39
|
+
|
|
40
|
+
__version__ = "1.0.0"
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"ANY",
|
|
44
|
+
"Array",
|
|
45
|
+
"ArraySpec",
|
|
46
|
+
"Case",
|
|
47
|
+
"CheckResult",
|
|
48
|
+
"DEFAULT_DTYPES",
|
|
49
|
+
"Domain",
|
|
50
|
+
"FLOAT_DTYPES",
|
|
51
|
+
"KINDS",
|
|
52
|
+
"NumGuardError",
|
|
53
|
+
"Property",
|
|
54
|
+
"Report",
|
|
55
|
+
"Spec",
|
|
56
|
+
"__version__",
|
|
57
|
+
"check",
|
|
58
|
+
"check_spec",
|
|
59
|
+
"compare",
|
|
60
|
+
"generate_cases",
|
|
61
|
+
"get_spec",
|
|
62
|
+
"guard",
|
|
63
|
+
"highprec",
|
|
64
|
+
"integers",
|
|
65
|
+
"interval",
|
|
66
|
+
"longdouble",
|
|
67
|
+
"mp_scalar",
|
|
68
|
+
"properties",
|
|
69
|
+
"ulp_distance",
|
|
70
|
+
]
|
numguard/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Bundled numguard bug corpus (task folders; installed as ``numguard._corpus``)."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Corpus task folder (data; graded by numguard.corpus)."""
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Scaled cosine similarity with an explicit zero-vector convention."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def cosine_similarity(a, b):
|
|
7
|
+
a = np.asarray(a)
|
|
8
|
+
b = np.asarray(b)
|
|
9
|
+
dt = np.result_type(a, b)
|
|
10
|
+
if np.isnan(a).any() or np.isnan(b).any():
|
|
11
|
+
return dt.type(np.nan)
|
|
12
|
+
sa = np.max(np.abs(a), initial=0.0)
|
|
13
|
+
sb = np.max(np.abs(b), initial=0.0)
|
|
14
|
+
if sa == 0 or sb == 0:
|
|
15
|
+
return dt.type(0.0)
|
|
16
|
+
acc = np.float64 if dt.itemsize < 8 else dt.type
|
|
17
|
+
ua = a.astype(acc) / acc(sa)
|
|
18
|
+
ub = b.astype(acc) / acc(sb)
|
|
19
|
+
if not (np.all(np.isfinite(ua)) and np.all(np.isfinite(ub))):
|
|
20
|
+
# infinite components: only their directions matter
|
|
21
|
+
ua = np.where(np.isinf(a), np.sign(a), 0).astype(acc)
|
|
22
|
+
ub = np.where(np.isinf(b), np.sign(b), 0).astype(acc)
|
|
23
|
+
na, nb = np.linalg.norm(ua), np.linalg.norm(ub)
|
|
24
|
+
if na == 0 or nb == 0:
|
|
25
|
+
return dt.type(0.0)
|
|
26
|
+
c = np.dot(ua / na, ub / nb)
|
|
27
|
+
return dt.type(np.clip(c, -1.0, 1.0))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""numguard spec for cosine_similarity."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
FUNCTION = "cosine_similarity"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def options(reference):
|
|
9
|
+
return dict(
|
|
10
|
+
inputs={"a": "any", "b": "any"},
|
|
11
|
+
dtypes=["float16", "float32", "float64"],
|
|
12
|
+
reference=reference,
|
|
13
|
+
properties=["symmetric", "scale_invariant", "permutation_invariant"],
|
|
14
|
+
bounds=(-1.0, 1.0),
|
|
15
|
+
# infinite components have no meaningful cosine; only require no crash
|
|
16
|
+
ignore=["reference:inf_input"],
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def happy_path(fn):
|
|
21
|
+
assert np.isclose(fn(np.array([1.0, 0.0]), np.array([1.0, 1.0])), np.sqrt(0.5))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Task: cosine similarity
|
|
2
|
+
|
|
3
|
+
Implement `cosine_similarity(a, b)` in `solution.py`.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> np.floating:
|
|
7
|
+
"""Cosine of the angle between two 1-D float vectors of equal length."""
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Requirements:
|
|
11
|
+
|
|
12
|
+
* result in `[-1, 1]`, same dtype as the inputs;
|
|
13
|
+
* if either vector is all zeros (or empty) the similarity is defined as `0.0`
|
|
14
|
+
(this is the convention of the retrieval system that consumes it) - never NaN;
|
|
15
|
+
* must not overflow or underflow for vectors with very large or very small
|
|
16
|
+
components (embeddings stored in float16/float32 can have either);
|
|
17
|
+
* NaN in either input gives NaN.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Corpus task folder (data; graded by numguard.corpus)."""
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Scaled hypot."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def hypot(x, y):
|
|
7
|
+
x = np.abs(np.asarray(x))
|
|
8
|
+
y = np.abs(np.asarray(y))
|
|
9
|
+
dt = np.result_type(x, y)
|
|
10
|
+
with np.errstate(invalid="ignore", divide="ignore"):
|
|
11
|
+
big = np.maximum(x, y)
|
|
12
|
+
small = np.minimum(x, y)
|
|
13
|
+
safe = np.where((big > 0) & np.isfinite(big), big, 1).astype(dt)
|
|
14
|
+
r = small / safe
|
|
15
|
+
out = big * np.sqrt(1 + r * r)
|
|
16
|
+
out = np.where(big == 0, 0, out)
|
|
17
|
+
out = np.where(np.isinf(x) | np.isinf(y), np.inf, out)
|
|
18
|
+
return out.astype(dt)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""numguard spec for hypot."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
FUNCTION = "hypot"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def options(reference):
|
|
9
|
+
from numguard import longdouble
|
|
10
|
+
|
|
11
|
+
return dict(
|
|
12
|
+
inputs={"x": "any", "y": "any"},
|
|
13
|
+
dtypes=["float32", "float64"],
|
|
14
|
+
reference=longdouble(np.hypot),
|
|
15
|
+
properties=["symmetric", "scale_equivariant:1", "even", "permutation_equivariant"],
|
|
16
|
+
nan="ignore", # hypot(inf, nan) == inf by IEEE 754; the reference covers NaN
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def happy_path(fn):
|
|
21
|
+
assert np.allclose(fn(np.array([3.0, 5.0]), np.array([4.0, 12.0])), [5.0, 13.0])
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Task: hypot
|
|
2
|
+
|
|
3
|
+
Implement `hypot(x, y)` in `solution.py` **without calling `np.hypot`**.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
def hypot(x: np.ndarray, y: np.ndarray) -> np.ndarray:
|
|
7
|
+
"""Elementwise sqrt(x**2 + y**2) for same-shape float arrays; same dtype."""
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Requirements:
|
|
11
|
+
|
|
12
|
+
* no intermediate overflow or underflow: `hypot(3e200, 4e200) == 5e200` and
|
|
13
|
+
`hypot(3e-200, 4e-200) == 5e-200`;
|
|
14
|
+
* `hypot(inf, nan)` is `inf` (IEEE 754); otherwise NaN propagates;
|
|
15
|
+
* symmetric in its arguments and independent of their signs.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Corpus task folder (data; graded by numguard.corpus)."""
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""log1p via the classic correction trick (Goldberg / HP-15C), no np.log1p."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def log1p(x):
|
|
7
|
+
x = np.asarray(x)
|
|
8
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
9
|
+
u = x.dtype.type(1) + x
|
|
10
|
+
d = u - x.dtype.type(1)
|
|
11
|
+
# where 1 + x rounds to 1, log(1 + x) ~= x (exact to working precision);
|
|
12
|
+
# otherwise log(u) * x / (u - 1) cancels the rounding error made in u
|
|
13
|
+
out = np.where(d == 0, x, np.log(u) * (x / np.where(d == 0, 1, d)))
|
|
14
|
+
out = np.where(np.isinf(u), u, out)
|
|
15
|
+
return out.astype(x.dtype)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""numguard spec for log1p."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from numguard import highprec
|
|
6
|
+
|
|
7
|
+
FUNCTION = "log1p"
|
|
8
|
+
DOMAIN = "(-1, inf)"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _mp_log1p(x):
|
|
12
|
+
import mpmath # only called when mpmath is installed
|
|
13
|
+
|
|
14
|
+
return mpmath.log1p(x)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def options(reference):
|
|
18
|
+
return dict(
|
|
19
|
+
inputs=DOMAIN,
|
|
20
|
+
dtypes=["float32", "float64"],
|
|
21
|
+
# an independent high-precision oracle (mpmath if installed, else
|
|
22
|
+
# np.log1p evaluated in long double) rather than the bundled reference
|
|
23
|
+
reference=highprec(numpy_fn=np.log1p, mp_fn=_mp_log1p),
|
|
24
|
+
monotonic="increasing",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def happy_path(fn):
|
|
29
|
+
x = np.array([0.5, 1.0, 3.0])
|
|
30
|
+
assert np.allclose(fn(x), np.log(1 + x))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Task: log(1 + x)
|
|
2
|
+
|
|
3
|
+
Implement `log1p(x)` in `solution.py` **without calling `np.log1p`** (the
|
|
4
|
+
point of the exercise is the algorithm; imagine a backend that lacks it).
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
def log1p(x: np.ndarray) -> np.ndarray:
|
|
8
|
+
"""Elementwise log(1 + x) for x > -1; same shape and dtype as x."""
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requirements:
|
|
12
|
+
|
|
13
|
+
* full relative accuracy for tiny `|x|` (e.g. `log1p(1e-10) == 9.9999999995e-11`);
|
|
14
|
+
* `log1p(-0.0)` is `-0.0`, `log1p(inf)` is `inf`, NaN propagates;
|
|
15
|
+
* elementwise: works for any shape, including empty arrays.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Corpus task folder (data; graded by numguard.corpus)."""
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Max-shifted logsumexp."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def logsumexp(x):
|
|
7
|
+
x = np.asarray(x)
|
|
8
|
+
if x.size == 0:
|
|
9
|
+
return x.dtype.type(-np.inf)
|
|
10
|
+
m = np.max(x)
|
|
11
|
+
if np.isnan(m):
|
|
12
|
+
return m
|
|
13
|
+
if not np.isfinite(m):
|
|
14
|
+
# +inf dominates; if every element is -inf the result is -inf
|
|
15
|
+
return m
|
|
16
|
+
return (m + np.log(np.sum(np.exp(x - m)))).astype(x.dtype)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""numguard spec for logsumexp."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
FUNCTION = "logsumexp"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def options(reference):
|
|
9
|
+
return dict(
|
|
10
|
+
inputs="any",
|
|
11
|
+
dtypes=["float32", "float64"],
|
|
12
|
+
reference=reference,
|
|
13
|
+
properties=["translation_equivariant"],
|
|
14
|
+
nan="propagate",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def happy_path(fn):
|
|
19
|
+
x = np.array([1.0, 2.0, 3.0])
|
|
20
|
+
assert np.isclose(fn(x), np.log(np.exp(1) + np.exp(2) + np.exp(3)))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Task: logsumexp
|
|
2
|
+
|
|
3
|
+
Implement `logsumexp(x)` in `solution.py`.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
def logsumexp(x: np.ndarray) -> np.floating:
|
|
7
|
+
"""Return log(sum(exp(x))) over all elements of the 1-D float array `x`."""
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Requirements:
|
|
11
|
+
|
|
12
|
+
* return a scalar of the same floating dtype as `x` (float32 in, float32 out);
|
|
13
|
+
* the result must be accurate for any finite input, including very large or
|
|
14
|
+
very negative values (e.g. `[1000.0, 1000.0]` -> `1000.6931...`);
|
|
15
|
+
* `logsumexp([])` is `-inf`; NaN in the input gives NaN; `+inf` gives `+inf`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Corpus task folder (data; graded by numguard.corpus)."""
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Leave-one-out means from prefix and suffix sums: no subtraction of x[i]."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def loo_mean(x):
|
|
7
|
+
x = np.asarray(x)
|
|
8
|
+
n = x.size
|
|
9
|
+
if n == 1:
|
|
10
|
+
raise ValueError("leave-one-out mean needs at least 2 values")
|
|
11
|
+
if n == 0:
|
|
12
|
+
return x.copy()
|
|
13
|
+
# sum of the others = (sum of elements before i) + (sum of elements after i).
|
|
14
|
+
# Subtracting x[i] from the full sum instead would cancel catastrophically
|
|
15
|
+
# when x[i] dominates the total (e.g. [1e16, 1.0, 2.0]). Accumulate in the
|
|
16
|
+
# widest available float type (long double where the platform has one).
|
|
17
|
+
acc = np.longdouble if np.finfo(np.longdouble).bits > 64 else np.float64
|
|
18
|
+
xl = x.astype(acc)
|
|
19
|
+
zero = np.zeros(1, dtype=acc)
|
|
20
|
+
prefix = np.concatenate([zero, np.cumsum(xl)[:-1]])
|
|
21
|
+
suffix = np.concatenate([np.cumsum(xl[::-1])[::-1][1:], zero])
|
|
22
|
+
return ((prefix + suffix) / (n - 1)).astype(x.dtype)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""numguard spec for loo_mean."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
FUNCTION = "loo_mean"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def options(reference):
|
|
9
|
+
return dict(
|
|
10
|
+
inputs="any",
|
|
11
|
+
dtypes=["float32", "float64"],
|
|
12
|
+
reference=reference,
|
|
13
|
+
properties=["translation_equivariant", "scale_equivariant:1", "permutation_equivariant"],
|
|
14
|
+
# with NaN/inf in x, whether the *other* leave-one-out means should
|
|
15
|
+
# stay finite is a design choice; do not grade it
|
|
16
|
+
ignore=["reference:nonfinite_input"],
|
|
17
|
+
extra_cases={
|
|
18
|
+
"dominant_value": [1e16, 1.0, 2.0],
|
|
19
|
+
"pair": [3.0, -7.0],
|
|
20
|
+
},
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def happy_path(fn):
|
|
25
|
+
assert np.allclose(fn(np.array([1.0, 2.0, 3.0, 6.0])), [11 / 3, 10 / 3, 3.0, 2.0])
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Task: leave-one-out means
|
|
2
|
+
|
|
3
|
+
Implement `loo_mean(x)` in `solution.py`.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
def loo_mean(x: np.ndarray) -> np.ndarray:
|
|
7
|
+
"""out[i] = mean of x with element i removed; same shape and dtype as x."""
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Requirements (this is a building block for jackknife estimates and
|
|
11
|
+
leave-one-out influence diagnostics):
|
|
12
|
+
|
|
13
|
+
* O(n): use the closed form `(sum(x) - x[i]) / (n - 1)` or equivalent;
|
|
14
|
+
* accurate when the data contain values of very different magnitude
|
|
15
|
+
(e.g. `[1e16, 1.0, 2.0]` -> `[1.5, 5e15 + 1, 5e15 + 0.5]`) and when the data
|
|
16
|
+
have a large common offset;
|
|
17
|
+
* `len(x) == 1` (no remaining elements) must raise `ValueError`;
|
|
18
|
+
`len(x) == 2` returns the swapped pair.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Corpus task folder (data; graded by numguard.corpus)."""
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Mean with a wide accumulator and overflow-safe scaling."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def mean(x):
|
|
7
|
+
x = np.asarray(x)
|
|
8
|
+
if x.size == 0:
|
|
9
|
+
raise ValueError("mean of an empty array is undefined")
|
|
10
|
+
acc = np.float64 if x.dtype.itemsize <= 8 else x.dtype
|
|
11
|
+
xa = x.astype(acc)
|
|
12
|
+
# divide first so that a sum of values near float64 max cannot overflow
|
|
13
|
+
m = np.sum(xa / x.size)
|
|
14
|
+
return np.asarray(m).astype(x.dtype)[()]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""numguard spec for mean (float16 focus)."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
FUNCTION = "mean"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def options(reference):
|
|
9
|
+
return dict(
|
|
10
|
+
inputs="any",
|
|
11
|
+
dtypes=["float16", "float32", "float64"],
|
|
12
|
+
reference=reference,
|
|
13
|
+
properties=["permutation_invariant", "translation_equivariant", "scale_equivariant:1"],
|
|
14
|
+
may_raise=["empty"],
|
|
15
|
+
large_n=100_000,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def happy_path(fn):
|
|
20
|
+
assert np.isclose(fn(np.array([1.0, 2.0, 3.0], dtype=np.float16)), 2.0)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Task: mean of a half-precision array
|
|
2
|
+
|
|
3
|
+
Implement `mean(x)` in `solution.py`.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
def mean(x: np.ndarray) -> np.floating:
|
|
7
|
+
"""Arithmetic mean of a non-empty 1-D float array; returns a scalar of x's dtype."""
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Requirements:
|
|
11
|
+
|
|
12
|
+
* the result has the same dtype as `x` (float16 in, float16 out);
|
|
13
|
+
* must be accurate for long float16 arrays (e.g. 100,000 values between 1 and 2,
|
|
14
|
+
sensor data, activations) and for values close to the dtype's maximum;
|
|
15
|
+
* NaN in the input gives NaN.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Corpus task folder (data; graded by numguard.corpus)."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Row-wise, scaled L2 normalization with a zero-row guard."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def normalize(x):
|
|
7
|
+
x = np.asarray(x)
|
|
8
|
+
if x.shape[-1] == 0:
|
|
9
|
+
return x.copy()
|
|
10
|
+
scale = np.max(np.abs(x), axis=-1, keepdims=True)
|
|
11
|
+
safe = np.where((scale > 0) & np.isfinite(scale), scale, 1).astype(x.dtype)
|
|
12
|
+
acc = np.float64 if x.dtype.itemsize < 8 else x.dtype
|
|
13
|
+
y = x.astype(acc) / safe.astype(acc)
|
|
14
|
+
norm = np.sqrt(np.sum(y * y, axis=-1, keepdims=True))
|
|
15
|
+
# zero rows stay zero; a NaN anywhere in a row makes the whole row NaN
|
|
16
|
+
out = np.where(norm == 0, y, y / np.where(norm == 0, 1, norm))
|
|
17
|
+
return out.astype(x.dtype)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""numguard spec for normalize."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
FUNCTION = "normalize"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def options(reference):
|
|
9
|
+
return dict(
|
|
10
|
+
inputs="any",
|
|
11
|
+
dtypes=["float16", "float32", "float64"],
|
|
12
|
+
reference=reference,
|
|
13
|
+
properties=["scale_invariant", "permutation_equivariant", "odd"],
|
|
14
|
+
batched=True,
|
|
15
|
+
bounds=(-1.0, 1.0),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def happy_path(fn):
|
|
20
|
+
assert np.allclose(fn(np.array([3.0, 4.0])), [0.6, 0.8])
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Task: L2 normalization
|
|
2
|
+
|
|
3
|
+
Implement `normalize(x)` in `solution.py`.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
def normalize(x: np.ndarray) -> np.ndarray:
|
|
7
|
+
"""Scale each row (last axis) of x to unit L2 norm; same shape and dtype."""
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Requirements:
|
|
11
|
+
|
|
12
|
+
* rows that are entirely zero are returned unchanged (all zeros) - never NaN;
|
|
13
|
+
* works for rows with very large or very small components without overflow or
|
|
14
|
+
underflow (the result of a non-zero row always has norm 1);
|
|
15
|
+
* rows are independent: batching along leading axes must not change results.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Corpus task folder (data; graded by numguard.corpus)."""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Naive dynamic quantization: the textbook formula, no guard for a zero range."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def quantize_dynamic(x):
|
|
7
|
+
x = np.asarray(x, dtype=np.float32)
|
|
8
|
+
x_min = np.minimum(x.min(), np.float32(0))
|
|
9
|
+
x_max = np.maximum(x.max(), np.float32(0))
|
|
10
|
+
scale = np.float32((x_max - x_min) / np.float32(255))
|
|
11
|
+
zero_point = np.uint8(np.clip(np.rint(-x_min / scale), 0, 255))
|
|
12
|
+
y = np.clip(np.rint(x / scale) + zero_point, 0, 255).astype(np.uint8)
|
|
13
|
+
return y, scale, zero_point
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Dynamic quantization that never produces a zero (or overflowing) scale."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def quantize_dynamic(x):
|
|
7
|
+
x = np.asarray(x, dtype=np.float32)
|
|
8
|
+
x_min = np.minimum(x.min(), np.float32(0))
|
|
9
|
+
x_max = np.maximum(x.max(), np.float32(0))
|
|
10
|
+
# divide before subtracting so that the range cannot overflow float32
|
|
11
|
+
scale = np.float32(x_max / np.float32(255) - x_min / np.float32(255))
|
|
12
|
+
if not scale > 0:
|
|
13
|
+
# all-zero input (or a range that underflows): any positive scale
|
|
14
|
+
# represents the data exactly; 1.0 keeps downstream divisions finite
|
|
15
|
+
scale = np.float32(1.0)
|
|
16
|
+
zero_point = np.uint8(np.clip(np.rint(-x_min / scale), 0, 255))
|
|
17
|
+
y = np.clip(np.rint(x / scale) + np.float32(zero_point), 0, 255).astype(np.uint8)
|
|
18
|
+
return y, scale, zero_point
|