Skip to content

Commit b99e683

Browse files
Release Managervbraun
Release Manager
authored andcommitted
Trac #23724: Allow random sampling for unit testing
#16244 removed random sampling, but there are times when it would be nice to have. This ticket adds it back in, but optionally. URL: https://trac.sagemath.org/23724 Reported by: roed Ticket author(s): David Roe Reviewer(s): Julian Rüth
2 parents 205c67a + 5d4f93b commit b99e683

File tree

2 files changed

+86
-25
lines changed

2 files changed

+86
-25
lines changed

src/sage/misc/misc.py

+56-8
Original file line numberDiff line numberDiff line change
@@ -1174,11 +1174,30 @@ def random_sublist(X, s):
11741174
return [a for a in X if random.random() <= s]
11751175

11761176

1177-
def some_tuples(elements, repeat, bound):
1177+
def some_tuples(elements, repeat, bound, max_samples=None):
11781178
r"""
11791179
Return an iterator over at most ``bound`` number of ``repeat``-tuples of
11801180
``elements``.
11811181
1182+
INPUT:
1183+
1184+
- ``elements`` -- an iterable
1185+
- ``repeat`` -- integer (default ``None``), the length of the tuples to be returned.
1186+
If ``None``, just returns entries from ``elements``.
1187+
- ``bound`` -- the maximum number of tuples returned (ignored if ``max_samples`` given)
1188+
- ``max_samples`` -- non-negative integer (default ``None``). If given,
1189+
then a sample of the possible tuples will be returned,
1190+
instead of the first few in the standard order.
1191+
1192+
OUTPUT:
1193+
1194+
If ``max_samples`` is not provided, an iterator over the first
1195+
``bound`` tuples of length ``repeat``, in the standard nested-for-loop order.
1196+
1197+
If ``max_samples`` is provided, a list of at most ``max_samples`` tuples,
1198+
sampled uniformly from the possibilities. In this case, ``elements``
1199+
must be finite.
1200+
11821201
TESTS::
11831202
11841203
sage: from sage.misc.misc import some_tuples
@@ -1192,15 +1211,44 @@ def some_tuples(elements, repeat, bound):
11921211
sage: len(list(l))
11931212
10
11941213
1195-
.. TODO::
1214+
sage: l = some_tuples(range(3), 2, None, max_samples=10)
1215+
sage: len(list(l))
1216+
9
1217+
"""
1218+
if max_samples is None:
1219+
from itertools import islice, product
1220+
P = elements if repeat is None else product(elements, repeat=repeat)
1221+
return islice(P, bound)
1222+
else:
1223+
if not (hasattr(elements, '__len__') and hasattr(elements, '__getitem__')):
1224+
elements = list(elements)
1225+
n = len(elements)
1226+
N = n if repeat is None else n**repeat
1227+
if N <= max_samples:
1228+
from itertools import product
1229+
return elements if repeat is None else product(elements, repeat=repeat)
1230+
return _some_tuples_sampling(elements, repeat, max_samples, n)
11961231

1197-
Currently, this only return an iterator over the first element of the
1198-
Cartesian product. It would be smarter to return something more
1199-
"random like" as it is used in tests. However, this should remain
1200-
deterministic.
1232+
def _some_tuples_sampling(elements, repeat, max_samples, n):
12011233
"""
1202-
from itertools import islice, product
1203-
return islice(product(elements, repeat=repeat), bound)
1234+
Internal function for :func:`some_tuples`.
1235+
1236+
TESTS::
1237+
1238+
sage: from sage.misc.misc import _some_tuples_sampling
1239+
sage: list(_some_tuples_sampling(range(3), 3, 2, 3))
1240+
[(0, 1, 0), (1, 1, 1)]
1241+
sage: list(_some_tuples_sampling(range(20), None, 4, 20))
1242+
[0, 6, 9, 3]
1243+
"""
1244+
from sage.rings.integer import Integer
1245+
N = n if repeat is None else n**repeat
1246+
# We sample on range(N) and create tuples manually since we don't want to create the list of all possible tuples in memory
1247+
for a in random.sample(range(N), max_samples):
1248+
if repeat is None:
1249+
yield elements[a]
1250+
else:
1251+
yield tuple(elements[j] for j in Integer(a).digits(n, padto=repeat))
12041252

12051253
def powerset(X):
12061254
r"""

src/sage/misc/sage_unittest.py

+30-17
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import unittest
1616
import sys
1717
import traceback
18+
import random
1819

1920
class TestSuite(object):
2021
"""
@@ -76,7 +77,7 @@ class TestSuite(object):
7677
7778
Debugging tip: in case of failure of some test, use ``%pdb on`` to
7879
turn on automatic debugging on error. Run the failing test
79-
independtly: the debugger will stop right where the first
80+
independently: the debugger will stop right where the first
8081
assertion fails. Then, introspection can be used to analyse what
8182
exactly the problem is. See also the ``catch = False`` option to
8283
:meth:`.run`.
@@ -269,16 +270,16 @@ def _test_b(self, tester): tester.fail()
269270
...
270271
AssertionError: None
271272
272-
In conjunction with ``%pdb on``, this allows for the debbuger
273+
In conjunction with ``%pdb on``, this allows for the debugger
273274
to jump directly to the first failure location.
274275
"""
275276
if isinstance(skip, str):
276277
skip = [skip]
277278
else:
278279
skip = tuple(skip)
279280

280-
# The class of exceptions that will be catched and reported;
281-
# other exceptions will get trough. None catches nothing.
281+
# The class of exceptions that will be caught and reported;
282+
# other exceptions will get through. None catches nothing.
282283
catch_exception = Exception if catch else None
283284

284285
tester = instance_tester(self._instance, **options)
@@ -373,7 +374,7 @@ class InstanceTester(unittest.TestCase):
373374
Testing utilities for Rational Field
374375
"""
375376

376-
def __init__(self, instance, elements = None, verbose = False, prefix = "", max_runs = 4096, **options):
377+
def __init__(self, instance, elements = None, verbose = False, prefix = "", max_runs = 4096, max_samples = None, **options):
377378
"""
378379
A gadget attached to an instance providing it with testing utilities.
379380
@@ -383,7 +384,7 @@ def __init__(self, instance, elements = None, verbose = False, prefix = "", max_
383384
sage: InstanceTester(instance = ZZ, verbose = True, elements = [1,2,3])
384385
Testing utilities for Integer Ring
385386
386-
This is used by ``SageObject._tester``, which see::
387+
This is used by ``SageObject._tester``, for example::
387388
388389
sage: QQ._tester()
389390
Testing utilities for Rational Field
@@ -394,6 +395,7 @@ def __init__(self, instance, elements = None, verbose = False, prefix = "", max_
394395
self._elements = elements
395396
self._prefix = prefix
396397
self._max_runs = max_runs
398+
self._max_samples = max_samples
397399

398400
def runTest(self):
399401
"""
@@ -448,9 +450,9 @@ def __repr__(self):
448450
return "Testing utilities for %s"%self._instance
449451

450452

451-
def some_elements(self, S=None):
453+
def some_elements(self, S=None, repeat=None):
452454
"""
453-
Returns a list (or iterable) of elements of ``self`` on which
455+
Returns a list (or iterable) of elements of the instance on which
454456
the tests should be run. This is only meaningful for container
455457
objects like parents.
456458
@@ -461,9 +463,13 @@ def some_elements(self, S=None):
461463
time, or the result of :meth:`.some_elements` if no elements
462464
were specified.
463465
466+
- ``repeat`` -- integer (default: None). If given, instead returns
467+
a list of tuples of length ``repeat`` from ``S``.
468+
464469
OUTPUT:
465470
466-
A list of at most ``self._max_runs`` elements of ``S``.
471+
A list of at most ``self._max_runs`` elements of ``S^r``,
472+
or a sample of at most ``self._max_samples`` if that is not ``None``.
467473
468474
EXAMPLES:
469475
@@ -520,6 +526,18 @@ def some_elements(self, S=None):
520526
sage: list(tester.some_elements())
521527
[0, 1, 2]
522528
529+
The ``repeat`` keyword can give pairs or triples from ``S``::
530+
531+
sage: list(tester.some_elements(repeat=2))
532+
[(0, 0), (0, 1), (0, 2), (1, 0), (1, 1)]
533+
534+
You can use ``max_samples`` to sample at random, instead of in order::
535+
536+
sage: tester = InstanceTester(ZZ, elements = srange(8), max_samples = 4)
537+
sage: list(tester.some_elements())
538+
[0, 3, 7, 1]
539+
sage: list(tester.some_elements(repeat=2))
540+
[(1, 4), (3, 1), (4, 5), (5, 0)]
523541
524542
Test for :trac:`15919`, :trac:`16244`::
525543
@@ -539,14 +557,9 @@ def some_elements(self, S=None):
539557
sage: list(tester.some_elements())
540558
[(0, 0, 0, 0), (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)]
541559
"""
542-
if S is None:
543-
if self._elements is None:
544-
S = self._instance.some_elements()
545-
else:
546-
S = self._elements
547-
import itertools
548-
return list(itertools.islice(S,0,self._max_runs))
549-
560+
S = S or self._elements or self._instance.some_elements()
561+
from sage.misc.misc import some_tuples
562+
return list(some_tuples(S, repeat, self._max_runs, self._max_samples))
550563

551564
class PythonObjectWithTests(object):
552565
"""

0 commit comments

Comments
 (0)