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.
Files changed (105) hide show
  1. numguard/__init__.py +70 -0
  2. numguard/__main__.py +6 -0
  3. numguard/_corpus/__init__.py +1 -0
  4. numguard/_corpus/cosine_similarity/__init__.py +1 -0
  5. numguard/_corpus/cosine_similarity/buggy.py +7 -0
  6. numguard/_corpus/cosine_similarity/reference.py +27 -0
  7. numguard/_corpus/cosine_similarity/spec.py +21 -0
  8. numguard/_corpus/cosine_similarity/task.md +17 -0
  9. numguard/_corpus/hypot/__init__.py +1 -0
  10. numguard/_corpus/hypot/buggy.py +7 -0
  11. numguard/_corpus/hypot/reference.py +18 -0
  12. numguard/_corpus/hypot/spec.py +21 -0
  13. numguard/_corpus/hypot/task.md +15 -0
  14. numguard/_corpus/log1p/__init__.py +1 -0
  15. numguard/_corpus/log1p/buggy.py +7 -0
  16. numguard/_corpus/log1p/reference.py +15 -0
  17. numguard/_corpus/log1p/spec.py +30 -0
  18. numguard/_corpus/log1p/task.md +15 -0
  19. numguard/_corpus/logsumexp/__init__.py +1 -0
  20. numguard/_corpus/logsumexp/buggy.py +7 -0
  21. numguard/_corpus/logsumexp/reference.py +16 -0
  22. numguard/_corpus/logsumexp/spec.py +20 -0
  23. numguard/_corpus/logsumexp/task.md +15 -0
  24. numguard/_corpus/loo_mean/__init__.py +1 -0
  25. numguard/_corpus/loo_mean/buggy.py +9 -0
  26. numguard/_corpus/loo_mean/reference.py +22 -0
  27. numguard/_corpus/loo_mean/spec.py +25 -0
  28. numguard/_corpus/loo_mean/task.md +18 -0
  29. numguard/_corpus/mean_float16/__init__.py +1 -0
  30. numguard/_corpus/mean_float16/buggy.py +9 -0
  31. numguard/_corpus/mean_float16/reference.py +14 -0
  32. numguard/_corpus/mean_float16/spec.py +20 -0
  33. numguard/_corpus/mean_float16/task.md +15 -0
  34. numguard/_corpus/normalize/__init__.py +1 -0
  35. numguard/_corpus/normalize/buggy.py +7 -0
  36. numguard/_corpus/normalize/reference.py +17 -0
  37. numguard/_corpus/normalize/spec.py +20 -0
  38. numguard/_corpus/normalize/task.md +15 -0
  39. numguard/_corpus/quantize_dynamic/__init__.py +1 -0
  40. numguard/_corpus/quantize_dynamic/buggy.py +13 -0
  41. numguard/_corpus/quantize_dynamic/reference.py +18 -0
  42. numguard/_corpus/quantize_dynamic/spec.py +34 -0
  43. numguard/_corpus/quantize_dynamic/task.md +20 -0
  44. numguard/_corpus/sample_std/__init__.py +1 -0
  45. numguard/_corpus/sample_std/buggy.py +10 -0
  46. numguard/_corpus/sample_std/reference.py +18 -0
  47. numguard/_corpus/sample_std/spec.py +19 -0
  48. numguard/_corpus/sample_std/task.md +16 -0
  49. numguard/_corpus/sigmoid/__init__.py +1 -0
  50. numguard/_corpus/sigmoid/buggy.py +7 -0
  51. numguard/_corpus/sigmoid/reference.py +10 -0
  52. numguard/_corpus/sigmoid/spec.py +29 -0
  53. numguard/_corpus/sigmoid/task.md +17 -0
  54. numguard/_corpus/softmax/__init__.py +1 -0
  55. numguard/_corpus/softmax/buggy.py +8 -0
  56. numguard/_corpus/softmax/reference.py +13 -0
  57. numguard/_corpus/softmax/spec.py +21 -0
  58. numguard/_corpus/softmax/task.md +16 -0
  59. numguard/_corpus/variance_onepass/__init__.py +1 -0
  60. numguard/_corpus/variance_onepass/buggy.py +11 -0
  61. numguard/_corpus/variance_onepass/reference.py +18 -0
  62. numguard/_corpus/variance_onepass/spec.py +22 -0
  63. numguard/_corpus/variance_onepass/task.md +15 -0
  64. numguard/agent/__init__.py +9 -0
  65. numguard/agent/bench.py +154 -0
  66. numguard/agent/ci.py +258 -0
  67. numguard/agent/common.py +90 -0
  68. numguard/agent/fix.py +288 -0
  69. numguard/agent/llm.py +420 -0
  70. numguard/agent/prompts.py +166 -0
  71. numguard/agent/sandbox.py +244 -0
  72. numguard/agent/session.py +164 -0
  73. numguard/agent/suggest.py +170 -0
  74. numguard/agent/tools.py +185 -0
  75. numguard/agent/worker.py +188 -0
  76. numguard/api.py +365 -0
  77. numguard/backends.py +256 -0
  78. numguard/card.py +241 -0
  79. numguard/carddata.py +119 -0
  80. numguard/cases.py +738 -0
  81. numguard/checks.py +701 -0
  82. numguard/ci.py +44 -0
  83. numguard/cli.py +772 -0
  84. numguard/config.py +191 -0
  85. numguard/corpus.py +305 -0
  86. numguard/dtypes.py +140 -0
  87. numguard/github_api.py +295 -0
  88. numguard/httpclient.py +44 -0
  89. numguard/init_cmd.py +287 -0
  90. numguard/properties.py +275 -0
  91. numguard/py.typed +0 -0
  92. numguard/pytest_plugin.py +216 -0
  93. numguard/reference.py +247 -0
  94. numguard/report.py +235 -0
  95. numguard/resolve.py +170 -0
  96. numguard/result.py +260 -0
  97. numguard/runconfig.py +153 -0
  98. numguard/specdoc.py +681 -0
  99. numguard/strategies.py +176 -0
  100. numguard_ci-1.0.0.dist-info/METADATA +911 -0
  101. numguard_ci-1.0.0.dist-info/RECORD +105 -0
  102. numguard_ci-1.0.0.dist-info/WHEEL +5 -0
  103. numguard_ci-1.0.0.dist-info/entry_points.txt +5 -0
  104. numguard_ci-1.0.0.dist-info/licenses/LICENSE +21 -0
  105. 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,6 @@
1
+ """Allow ``python -m numguard``."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__": # pragma: no cover
6
+ main()
@@ -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,7 @@
1
+ """dot / (||a|| ||b||)."""
2
+
3
+ import numpy as np
4
+
5
+
6
+ def cosine_similarity(a, b):
7
+ return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
@@ -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,7 @@
1
+ """Pythagoras, directly."""
2
+
3
+ import numpy as np
4
+
5
+
6
+ def hypot(x, y):
7
+ return np.sqrt(x * x + y * y)
@@ -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,7 @@
1
+ """Direct formula."""
2
+
3
+ import numpy as np
4
+
5
+
6
+ def log1p(x):
7
+ return np.log(1 + x)
@@ -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,7 @@
1
+ """Direct translation of the formula."""
2
+
3
+ import numpy as np
4
+
5
+
6
+ def logsumexp(x):
7
+ return np.log(np.sum(np.exp(x)))
@@ -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,9 @@
1
+ """Closed form, straight from the definition."""
2
+
3
+ import numpy as np
4
+
5
+
6
+ def loo_mean(x):
7
+ x = np.asarray(x)
8
+ n = x.size
9
+ return ((np.sum(x) - x) / (n - 1)).astype(x.dtype)
@@ -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,9 @@
1
+ """Sum then divide, in the input dtype."""
2
+
3
+ import numpy as np
4
+
5
+
6
+ def mean(x):
7
+ x = np.asarray(x)
8
+ total = np.sum(x, dtype=x.dtype)
9
+ return (total / x.dtype.type(x.size)).astype(x.dtype)
@@ -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,7 @@
1
+ """x / ||x||."""
2
+
3
+ import numpy as np
4
+
5
+
6
+ def normalize(x):
7
+ return x / np.sqrt(np.sum(x * x, axis=-1, keepdims=True))
@@ -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