mapFolding 0.2.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.
- mapFolding/JAX/lunnanJAX.py +206 -0
- mapFolding/JAX/taskJAX.py +313 -0
- mapFolding/__init__.py +21 -0
- mapFolding/babbage.py +12 -0
- mapFolding/beDRY.py +219 -0
- mapFolding/benchmarks/benchmarking.py +66 -0
- mapFolding/benchmarks/test_benchmarks.py +74 -0
- mapFolding/importPackages.py +5 -0
- mapFolding/lovelace.py +121 -0
- mapFolding/oeis.py +299 -0
- mapFolding/reference/hunterNumba.py +132 -0
- mapFolding/reference/irvineJavaPort.py +120 -0
- mapFolding/reference/lunnan.py +153 -0
- mapFolding/reference/lunnanNumpy.py +123 -0
- mapFolding/reference/lunnanWhile.py +121 -0
- mapFolding/reference/rotatedEntryPoint.py +240 -0
- mapFolding/startHere.py +54 -0
- mapFolding/theSSOT.py +62 -0
- mapFolding-0.2.0.dist-info/METADATA +170 -0
- mapFolding-0.2.0.dist-info/RECORD +28 -0
- mapFolding-0.2.0.dist-info/WHEEL +5 -0
- mapFolding-0.2.0.dist-info/entry_points.txt +4 -0
- mapFolding-0.2.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/conftest.py +262 -0
- tests/test_oeis.py +195 -0
- tests/test_other.py +71 -0
- tests/test_tasks.py +18 -0
tests/test_oeis.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from .conftest import *
|
|
2
|
+
from contextlib import redirect_stdout
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
import io
|
|
5
|
+
import os
|
|
6
|
+
import pathlib
|
|
7
|
+
import pytest
|
|
8
|
+
import random
|
|
9
|
+
import re as regex
|
|
10
|
+
import unittest
|
|
11
|
+
import unittest.mock
|
|
12
|
+
import urllib.error
|
|
13
|
+
import urllib.request
|
|
14
|
+
|
|
15
|
+
def test_aOFn_calculate_value(oeisID: str):
|
|
16
|
+
for n in settingsOEIS[oeisID]['valuesTestValidation']:
|
|
17
|
+
standardComparison(settingsOEIS[oeisID]['valuesKnown'][n], oeisIDfor_n, oeisID, n)
|
|
18
|
+
|
|
19
|
+
@pytest.mark.parametrize("badID", ["A999999", " A999999 ", "A999999extra"])
|
|
20
|
+
def test__validateOEISid_invalid_id(badID: str):
|
|
21
|
+
standardComparison(KeyError, _validateOEISid, badID)
|
|
22
|
+
|
|
23
|
+
def test__validateOEISid_partially_valid(oeisID_1random: str):
|
|
24
|
+
standardComparison(KeyError, _validateOEISid, f"{oeisID_1random}extra")
|
|
25
|
+
|
|
26
|
+
def test__validateOEISid_valid_id(oeisID: str):
|
|
27
|
+
standardComparison(oeisID, _validateOEISid, oeisID)
|
|
28
|
+
|
|
29
|
+
def test__validateOEISid_valid_id_case_insensitive(oeisID: str):
|
|
30
|
+
standardComparison(oeisID.upper(), _validateOEISid, oeisID.lower())
|
|
31
|
+
standardComparison(oeisID.upper(), _validateOEISid, oeisID.upper())
|
|
32
|
+
standardComparison(oeisID.upper(), _validateOEISid, oeisID.swapcase())
|
|
33
|
+
|
|
34
|
+
parameters_test_aOFn_invalid_n = [
|
|
35
|
+
# (2, "ok"), # test the test template
|
|
36
|
+
(-random.randint(1, 100), "randomNegative"),
|
|
37
|
+
("foo", "string"),
|
|
38
|
+
(1.5, "float")
|
|
39
|
+
]
|
|
40
|
+
badValues, badValuesIDs = zip(*parameters_test_aOFn_invalid_n)
|
|
41
|
+
@pytest.mark.parametrize("badN", badValues, ids=badValuesIDs)
|
|
42
|
+
def test_aOFn_invalid_n(oeisID_1random: str, badN):
|
|
43
|
+
"""Check that negative or non-integer n raises ValueError."""
|
|
44
|
+
standardComparison(ValueError, oeisIDfor_n, oeisID_1random, badN)
|
|
45
|
+
|
|
46
|
+
def test_aOFn_zeroDim_A001418():
|
|
47
|
+
standardComparison(ArithmeticError, oeisIDfor_n, 'A001418', 0)
|
|
48
|
+
|
|
49
|
+
# ===== OEIS Cache Tests =====
|
|
50
|
+
@pytest.mark.parametrize("cacheExists", [True, False])
|
|
51
|
+
@unittest.mock.patch('pathlib.Path.exists')
|
|
52
|
+
@unittest.mock.patch('pathlib.Path.unlink')
|
|
53
|
+
def test_clearOEIScache(mock_unlink: unittest.mock.MagicMock, mock_exists: unittest.mock.MagicMock, cacheExists: bool):
|
|
54
|
+
"""Test OEIS cache clearing with both existing and non-existing cache."""
|
|
55
|
+
mock_exists.return_value = cacheExists
|
|
56
|
+
clearOEIScache()
|
|
57
|
+
|
|
58
|
+
if cacheExists:
|
|
59
|
+
assert mock_unlink.call_count == len(settingsOEIS)
|
|
60
|
+
mock_unlink.assert_has_calls([unittest.mock.call(missing_ok=True)] * len(settingsOEIS))
|
|
61
|
+
else:
|
|
62
|
+
mock_exists.assert_called_once()
|
|
63
|
+
mock_unlink.assert_not_called()
|
|
64
|
+
|
|
65
|
+
def testCacheMiss(pathCacheTesting: pathlib.Path, oeisID_1random: str):
|
|
66
|
+
"""Test cache miss scenario - cache file doesn't exist."""
|
|
67
|
+
# No setup function needed - we want to test missing cache
|
|
68
|
+
standardCacheTest(settingsOEIS[oeisID_1random]['valuesKnown'], None, oeisID_1random, pathCacheTesting)
|
|
69
|
+
|
|
70
|
+
def testCacheExpired(pathCacheTesting: pathlib.Path, oeisID_1random: str):
|
|
71
|
+
"""Test expired cache scenario."""
|
|
72
|
+
def setupExpiredCache(pathCache: pathlib.Path, oeisID: str) -> None:
|
|
73
|
+
pathCache.write_text("# Old cache content")
|
|
74
|
+
oldModificationTime = datetime.now() - timedelta(days=30)
|
|
75
|
+
os.utime(pathCache, times=(oldModificationTime.timestamp(), oldModificationTime.timestamp()))
|
|
76
|
+
|
|
77
|
+
standardCacheTest(settingsOEIS[oeisID_1random]['valuesKnown'], setupExpiredCache, oeisID_1random, pathCacheTesting)
|
|
78
|
+
|
|
79
|
+
def testInvalidCache(pathCacheTesting: pathlib.Path, oeisID_1random: str):
|
|
80
|
+
"""Test invalid cache content scenario."""
|
|
81
|
+
def setupInvalidCache(pathCache: pathlib.Path, oeisID: str) -> None:
|
|
82
|
+
pathCache.write_text("Invalid content")
|
|
83
|
+
|
|
84
|
+
standardCacheTest(settingsOEIS[oeisID_1random]['valuesKnown'], setupInvalidCache, oeisID_1random, pathCacheTesting)
|
|
85
|
+
|
|
86
|
+
def testInvalidFileContent(pathCacheTesting: pathlib.Path, oeisID_1random: str):
|
|
87
|
+
pathFilenameCache = pathCacheTesting / _formatFilenameCache.format(oeisID=oeisID_1random)
|
|
88
|
+
|
|
89
|
+
# Write invalid content to cache
|
|
90
|
+
pathFilenameCache.write_text("# A999999\n1 1\n2 2\n")
|
|
91
|
+
modificationTimeOriginal = pathFilenameCache.stat().st_mtime
|
|
92
|
+
|
|
93
|
+
# Function should detect invalid content, fetch fresh data, and update cache
|
|
94
|
+
OEISsequence = _getOEISidValues(oeisID_1random)
|
|
95
|
+
|
|
96
|
+
# Verify the function succeeded
|
|
97
|
+
assert OEISsequence is not None
|
|
98
|
+
# Verify cache was updated (modification time changed)
|
|
99
|
+
assert pathFilenameCache.stat().st_mtime > modificationTimeOriginal
|
|
100
|
+
# Verify cache now contains correct sequence ID
|
|
101
|
+
assert f"# {oeisID_1random}" in pathFilenameCache.read_text()
|
|
102
|
+
|
|
103
|
+
def testNetworkError(monkeypatch: pytest.MonkeyPatch, pathCacheTesting: pathlib.Path):
|
|
104
|
+
def mockUrlopen(*args, **kwargs):
|
|
105
|
+
raise urllib.error.URLError("Network error")
|
|
106
|
+
|
|
107
|
+
monkeypatch.setattr(urllib.request, 'urlopen', mockUrlopen)
|
|
108
|
+
with pytest.raises(urllib.error.URLError):
|
|
109
|
+
_getOEISidValues(next(iter(settingsOEIS)))
|
|
110
|
+
|
|
111
|
+
def testParseContentErrors():
|
|
112
|
+
"""Test invalid content parsing."""
|
|
113
|
+
standardComparison(ValueError, _parseBFileOEIS, "Invalid content\n1 2\n", 'A001415')
|
|
114
|
+
|
|
115
|
+
def testExtraComments(pathCacheTesting: pathlib.Path, oeisID_1random: str):
|
|
116
|
+
pathFilenameCache = pathCacheTesting / _formatFilenameCache.format(oeisID=oeisID_1random)
|
|
117
|
+
|
|
118
|
+
# Write content with extra comment lines
|
|
119
|
+
contentWithExtraComments = f"""# {oeisID_1random}
|
|
120
|
+
# Extra comment line 1
|
|
121
|
+
# Extra comment line 2
|
|
122
|
+
1 2
|
|
123
|
+
2 4
|
|
124
|
+
3 6
|
|
125
|
+
# Another comment in the middle
|
|
126
|
+
4 8
|
|
127
|
+
5 10"""
|
|
128
|
+
pathFilenameCache.write_text(contentWithExtraComments)
|
|
129
|
+
|
|
130
|
+
OEISsequence = _getOEISidValues(oeisID_1random)
|
|
131
|
+
# Verify sequence values are correct despite extra comments
|
|
132
|
+
standardComparison(2, lambda d: d[1], OEISsequence) # First value
|
|
133
|
+
standardComparison(8, lambda d: d[4], OEISsequence) # Value after mid-sequence comment
|
|
134
|
+
standardComparison(10, lambda d: d[5], OEISsequence) # Last value
|
|
135
|
+
|
|
136
|
+
# ===== Command Line Interface Tests =====
|
|
137
|
+
def testHelpText():
|
|
138
|
+
"""Test that help text is complete and examples are valid."""
|
|
139
|
+
outputStream = io.StringIO()
|
|
140
|
+
with redirect_stdout(outputStream):
|
|
141
|
+
getOEISids()
|
|
142
|
+
|
|
143
|
+
helpText = outputStream.getvalue()
|
|
144
|
+
|
|
145
|
+
# Verify content
|
|
146
|
+
for oeisID in oeisIDsImplemented:
|
|
147
|
+
assert oeisID in helpText
|
|
148
|
+
assert settingsOEIS[oeisID]['description'] in helpText
|
|
149
|
+
|
|
150
|
+
# Extract and verify examples
|
|
151
|
+
|
|
152
|
+
cliMatch = regex.search(r'OEIS_for_n (\w+) (\d+)', helpText)
|
|
153
|
+
pythonMatch = regex.search(r"oeisIDfor_n\('(\w+)', (\d+)\)", helpText)
|
|
154
|
+
|
|
155
|
+
assert cliMatch and pythonMatch, "Help text missing examples"
|
|
156
|
+
oeisID, n = pythonMatch.groups()
|
|
157
|
+
n = int(n)
|
|
158
|
+
|
|
159
|
+
# Verify CLI and Python examples use same values
|
|
160
|
+
assert cliMatch.groups() == (oeisID, str(n)), "CLI and Python examples inconsistent"
|
|
161
|
+
|
|
162
|
+
# Verify the example works
|
|
163
|
+
expectedValue = oeisIDfor_n(oeisID, n)
|
|
164
|
+
|
|
165
|
+
# Test CLI execution of the example
|
|
166
|
+
with unittest.mock.patch('sys.argv', ['OEIS_for_n', oeisID, str(n)]):
|
|
167
|
+
outputStream = io.StringIO()
|
|
168
|
+
with redirect_stdout(outputStream):
|
|
169
|
+
OEIS_for_n()
|
|
170
|
+
standardComparison(expectedValue, lambda: int(outputStream.getvalue().strip().split()[0]))
|
|
171
|
+
|
|
172
|
+
def testCLI_InvalidInputs():
|
|
173
|
+
"""Test CLI error handling."""
|
|
174
|
+
testCases = [
|
|
175
|
+
(['OEIS_for_n'], "missing arguments"),
|
|
176
|
+
(['OEIS_for_n', 'A999999', '1'], "invalid OEIS ID"),
|
|
177
|
+
(['OEIS_for_n', 'A001415', '-1'], "negative n"),
|
|
178
|
+
(['OEIS_for_n', 'A001415', 'abc'], "non-integer n"),
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
for arguments, testID in testCases:
|
|
182
|
+
with unittest.mock.patch('sys.argv', arguments):
|
|
183
|
+
expectSystemExit("error", OEIS_for_n)
|
|
184
|
+
|
|
185
|
+
def testCLI_HelpFlag():
|
|
186
|
+
"""Verify --help output contains required information."""
|
|
187
|
+
with unittest.mock.patch('sys.argv', ['OEIS_for_n', '--help']):
|
|
188
|
+
outputStream = io.StringIO()
|
|
189
|
+
with redirect_stdout(outputStream):
|
|
190
|
+
expectSystemExit("nonError", OEIS_for_n)
|
|
191
|
+
|
|
192
|
+
helpOutput = outputStream.getvalue()
|
|
193
|
+
assert "Available OEIS sequences:" in helpOutput
|
|
194
|
+
assert "Usage examples:" in helpOutput
|
|
195
|
+
assert all(oeisID in helpOutput for oeisID in oeisIDsImplemented)
|
tests/test_other.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from .conftest import *
|
|
2
|
+
import pytest
|
|
3
|
+
import sys
|
|
4
|
+
# TODO test `outfitFoldings`, especially that `listDimensions` is sorted.
|
|
5
|
+
|
|
6
|
+
@pytest.mark.parametrize("listDimensions,expected_intInnit,expected_parseListDimensions,expected_validateListDimensions,expected_getLeavesTotal", [
|
|
7
|
+
(None, ValueError, ValueError, ValueError, ValueError), # None instead of list
|
|
8
|
+
(['a'], ValueError, ValueError, ValueError, ValueError), # string
|
|
9
|
+
([-4, 2], [-4, 2], ValueError, ValueError, ValueError), # negative
|
|
10
|
+
([-3], [-3], ValueError, ValueError, ValueError), # negative
|
|
11
|
+
([0, 0], [0, 0], [0, 0], NotImplementedError, 0), # no positive dimensions
|
|
12
|
+
([0, 5, 6], [0, 5, 6], [0, 5, 6], [5, 6], 30), # zeros ignored
|
|
13
|
+
([0], [0], [0], NotImplementedError, 0), # edge case
|
|
14
|
+
([1, 2, 3, 4, 5], [1, 2, 3, 4, 5], [1, 2, 3, 4, 5], [1, 2, 3, 4, 5], 120), # sequential
|
|
15
|
+
([1, sys.maxsize], [1, sys.maxsize], [1, sys.maxsize], [1, sys.maxsize], sys.maxsize), # maxint
|
|
16
|
+
([7.5], ValueError, ValueError, ValueError, ValueError), # float
|
|
17
|
+
([1] * 1000, [1] * 1000, [1] * 1000, [1] * 1000, 1), # long list
|
|
18
|
+
([11], [11], [11], NotImplementedError, 11), # single dimension
|
|
19
|
+
([13, 0, 17], [13, 0, 17], [13, 0, 17], [13, 17], 221), # zeros handled
|
|
20
|
+
([2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], 16), # repeated dimensions
|
|
21
|
+
([2, 3, 4], [2, 3, 4], [2, 3, 4], [2, 3, 4], 24),
|
|
22
|
+
([2, 3], [2, 3], [2, 3], [2, 3], 6),
|
|
23
|
+
([2] * 11, [2] * 11, [2] * 11, [2] * 11, 2048), # power of 2
|
|
24
|
+
([3, 2], [3, 2], [3, 2], [3, 2], 6), # return value is the input when valid
|
|
25
|
+
([3] * 5, [3] * 5, [3] * 5, [3, 3, 3, 3, 3], 243), # power of 3
|
|
26
|
+
([None], TypeError, TypeError, TypeError, TypeError), # None
|
|
27
|
+
([True], TypeError, TypeError, TypeError, TypeError), # bool
|
|
28
|
+
([[17, 39]], TypeError, TypeError, TypeError, TypeError), # nested
|
|
29
|
+
([], ValueError, ValueError, ValueError, ValueError), # empty
|
|
30
|
+
([complex(1,1)], ValueError, ValueError, ValueError, ValueError), # complex number
|
|
31
|
+
([float('inf')], ValueError, ValueError, ValueError, ValueError), # infinity
|
|
32
|
+
([float('nan')], ValueError, ValueError, ValueError, ValueError), # NaN
|
|
33
|
+
([sys.maxsize - 1, 1], [sys.maxsize - 1, 1], [sys.maxsize - 1, 1], [sys.maxsize - 1, 1], sys.maxsize - 1), # near maxint
|
|
34
|
+
([sys.maxsize // 2, sys.maxsize // 2, 2], [sys.maxsize // 2, sys.maxsize // 2, 2], [sys.maxsize // 2, sys.maxsize // 2, 2], [sys.maxsize // 2, sys.maxsize // 2, 2], OverflowError), # overflow protection
|
|
35
|
+
([sys.maxsize, sys.maxsize], [sys.maxsize, sys.maxsize], [sys.maxsize, sys.maxsize], [sys.maxsize, sys.maxsize], OverflowError), # overflow protection
|
|
36
|
+
(range(3, 7), [3, 4, 5, 6], [3, 4, 5, 6], [3, 4, 5, 6], 360), # range sequence type
|
|
37
|
+
(tuple([3, 5, 7]), [3, 5, 7], [3, 5, 7], [3, 5, 7], 105), # tuple sequence type
|
|
38
|
+
])
|
|
39
|
+
def test_listDimensionsAsParameter(listDimensions: None | list[str] | list[int] | list[float] | list[None] | list[bool] | list[list[int]] | list[complex] | range | tuple[int, ...], expected_intInnit: type[ValueError] | list[int] | type[TypeError], expected_parseListDimensions: type[ValueError] | list[int] | type[TypeError], expected_validateListDimensions: type[ValueError] | type[NotImplementedError] | list[int] | type[TypeError], expected_getLeavesTotal: type[ValueError] | int | type[TypeError] | type[OverflowError]) -> None:
|
|
40
|
+
"""Test both validateListDimensions and getLeavesTotal with the same inputs."""
|
|
41
|
+
standardComparison(expected_intInnit, intInnit, listDimensions)
|
|
42
|
+
standardComparison(expected_parseListDimensions, parseDimensions, listDimensions)
|
|
43
|
+
standardComparison(expected_validateListDimensions, validateListDimensions, listDimensions)
|
|
44
|
+
standardComparison(expected_getLeavesTotal, getLeavesTotal, listDimensions)
|
|
45
|
+
|
|
46
|
+
def test_getLeavesTotal_edge_cases() -> None:
|
|
47
|
+
"""Test edge cases for getLeavesTotal."""
|
|
48
|
+
# Order independence
|
|
49
|
+
standardComparison(getLeavesTotal([2, 3, 4]), getLeavesTotal, [4, 2, 3])
|
|
50
|
+
|
|
51
|
+
# Immutability
|
|
52
|
+
listOriginal = [2, 3]
|
|
53
|
+
standardComparison(6, getLeavesTotal, listOriginal)
|
|
54
|
+
standardComparison([2, 3], lambda x: x, listOriginal) # Check that the list wasn't modified
|
|
55
|
+
|
|
56
|
+
# ===== Parse Integers Tests =====
|
|
57
|
+
def test_intInnit() -> None:
|
|
58
|
+
"""Test integer parsing using the test suite generator."""
|
|
59
|
+
for testName, testFunction in makeTestSuiteIntInnit(intInnit).items():
|
|
60
|
+
testFunction()
|
|
61
|
+
|
|
62
|
+
def test_oopsieKwargsie() -> None:
|
|
63
|
+
"""Test handling of unexpected keyword arguments."""
|
|
64
|
+
for testName, testFunction in makeTestSuiteOopsieKwargsie(oopsieKwargsie).items():
|
|
65
|
+
testFunction()
|
|
66
|
+
|
|
67
|
+
def test_countFolds_invalid_computationDivisions() -> None:
|
|
68
|
+
standardComparison(ValueError, countFolds, [2, 2], {"wrong": "value"})
|
|
69
|
+
|
|
70
|
+
def test_parseListDimensions_noDimensions() -> None:
|
|
71
|
+
standardComparison(ValueError, parseDimensions, [])
|
tests/test_tasks.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .conftest import *
|
|
2
|
+
import pytest
|
|
3
|
+
from typing import List, Dict, Tuple
|
|
4
|
+
|
|
5
|
+
# TODO add a test. `C` = number of logical cores available. `n = C + 1`. Ensure that `[2,n]` is computed correctly.
|
|
6
|
+
|
|
7
|
+
def test_foldings_computationDivisions(listDimensionsTest_countFolds: List[int], foldsTotalKnown: Dict[Tuple[int, ...], int]) -> None:
|
|
8
|
+
standardComparison(foldsTotalKnown[tuple(listDimensionsTest_countFolds)], countFolds, listDimensionsTest_countFolds, True)
|
|
9
|
+
|
|
10
|
+
def test_defineConcurrencyLimit() -> None:
|
|
11
|
+
testSuite = makeTestSuiteConcurrencyLimit(defineConcurrencyLimit)
|
|
12
|
+
for testName, testFunction in testSuite.items():
|
|
13
|
+
testFunction()
|
|
14
|
+
|
|
15
|
+
@pytest.mark.parametrize("cpuLimitValue", [{"invalid": True}, ["weird"]])
|
|
16
|
+
def test_countFolds_cpuLimitOopsie(cpuLimitValue: Dict[str, bool] | List[str]) -> None:
|
|
17
|
+
# This forces CPUlimit = oopsieKwargsie(cpuLimitValue).
|
|
18
|
+
standardComparison(ValueError, countFolds, [2, 2], True, cpuLimitValue)
|