mapFolding 0.3.12__py3-none-any.whl → 0.4.1__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/__init__.py +40 -38
- mapFolding/basecamp.py +50 -50
- mapFolding/beDRY.py +336 -336
- mapFolding/oeis.py +262 -262
- mapFolding/reference/flattened.py +294 -293
- mapFolding/reference/hunterNumba.py +126 -126
- mapFolding/reference/irvineJavaPort.py +99 -99
- mapFolding/reference/jax.py +153 -153
- mapFolding/reference/lunnan.py +148 -148
- mapFolding/reference/lunnanNumpy.py +115 -115
- mapFolding/reference/lunnanWhile.py +114 -114
- mapFolding/reference/rotatedEntryPoint.py +183 -183
- mapFolding/reference/total_countPlus1vsPlusN.py +203 -203
- mapFolding/someAssemblyRequired/__init__.py +5 -1
- mapFolding/someAssemblyRequired/getLLVMforNoReason.py +12 -12
- mapFolding/someAssemblyRequired/makeJob.py +46 -52
- mapFolding/someAssemblyRequired/synthesizeModuleJAX.py +17 -17
- mapFolding/someAssemblyRequired/synthesizeNumba.py +343 -633
- mapFolding/someAssemblyRequired/synthesizeNumbaGeneralized.py +325 -0
- mapFolding/someAssemblyRequired/synthesizeNumbaJob.py +173 -0
- mapFolding/someAssemblyRequired/synthesizeNumbaModules.py +77 -0
- mapFolding/syntheticModules/__init__.py +0 -0
- mapFolding/syntheticModules/numba_countInitialize.py +4 -4
- mapFolding/syntheticModules/numba_countParallel.py +4 -4
- mapFolding/syntheticModules/numba_countSequential.py +4 -4
- mapFolding/syntheticModules/numba_doTheNeedful.py +7 -7
- mapFolding/theDao.py +165 -165
- mapFolding/theSSOT.py +177 -173
- mapFolding/theSSOTnumba.py +90 -74
- mapFolding-0.4.1.dist-info/METADATA +154 -0
- mapFolding-0.4.1.dist-info/RECORD +42 -0
- tests/conftest.py +253 -129
- tests/test_computations.py +79 -0
- tests/test_oeis.py +76 -85
- tests/test_other.py +136 -224
- tests/test_tasks.py +19 -23
- tests/test_types.py +2 -2
- mapFolding/someAssemblyRequired/synthesizeNumbaHardcoding.py +0 -188
- mapFolding-0.3.12.dist-info/METADATA +0 -155
- mapFolding-0.3.12.dist-info/RECORD +0 -40
- tests/conftest_tmpRegistry.py +0 -62
- tests/conftest_uniformTests.py +0 -53
- {mapFolding-0.3.12.dist-info → mapFolding-0.4.1.dist-info}/LICENSE +0 -0
- {mapFolding-0.3.12.dist-info → mapFolding-0.4.1.dist-info}/WHEEL +0 -0
- {mapFolding-0.3.12.dist-info → mapFolding-0.4.1.dist-info}/entry_points.txt +0 -0
- {mapFolding-0.3.12.dist-info → mapFolding-0.4.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: mapFolding
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: Count distinct ways to fold a map (or a strip of stamps)
|
|
5
|
+
Author-email: Hunter Hogan <HunterHogan@pm.me>
|
|
6
|
+
License: CC-BY-NC-4.0
|
|
7
|
+
Project-URL: Donate, https://www.patreon.com/integrated
|
|
8
|
+
Project-URL: Homepage, https://github.com/hunterhogan/mapFolding
|
|
9
|
+
Project-URL: Repository, https://github.com/hunterhogan/mapFolding.git
|
|
10
|
+
Keywords: A001415,A001416,A001417,A001418,A195646,combinatorics,folding,map folding,OEIS,optimization,stamp folding
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Education
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: Intended Audience :: Other Audience
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: Natural Language :: English
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3
|
|
24
|
+
Classifier: Programming Language :: Python
|
|
25
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Requires-Dist: numba
|
|
32
|
+
Requires-Dist: numpy
|
|
33
|
+
Requires-Dist: Z0Z_tools
|
|
34
|
+
Provides-Extra: testing
|
|
35
|
+
Requires-Dist: autoflake; extra == "testing"
|
|
36
|
+
Requires-Dist: more_itertools; extra == "testing"
|
|
37
|
+
Requires-Dist: pytest-cov; extra == "testing"
|
|
38
|
+
Requires-Dist: pytest-env; extra == "testing"
|
|
39
|
+
Requires-Dist: pytest-xdist; extra == "testing"
|
|
40
|
+
Requires-Dist: pytest; extra == "testing"
|
|
41
|
+
Requires-Dist: python_minifier; extra == "testing"
|
|
42
|
+
Requires-Dist: updateCitation; extra == "testing"
|
|
43
|
+
|
|
44
|
+
# mapFolding: Algorithms for enumerating distinct map/stamp folding patterns 🗺️
|
|
45
|
+
|
|
46
|
+
[](https://pypi.org/project/mapFolding/)
|
|
47
|
+
[](https://youtu.be/g6f_miE91mk&t=4)
|
|
48
|
+
[](https://github.com/hunterhogan/mapFolding/actions/workflows/pythonTests.yml)
|
|
49
|
+

|
|
50
|
+
[](https://creativecommons.org/licenses/by-nc/4.0/)
|
|
51
|
+

|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Quick start
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
pip install mapFolding
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`OEIS_for_n` will run a computation from the command line.
|
|
63
|
+
|
|
64
|
+
```cmd
|
|
65
|
+
(mapFolding) C:\apps\mapFolding> OEIS_for_n A001418 5
|
|
66
|
+
186086600 distinct folding patterns.
|
|
67
|
+
Time elapsed: 1.605 seconds
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Use `mapFolding.oeisIDfor_n()` to compute a(n) for an OEIS ID.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from mapFolding import oeisIDfor_n
|
|
74
|
+
foldsTotal = oeisIDfor_n( 'A001418', 4 )
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Features
|
|
80
|
+
|
|
81
|
+
### 1. Simple, easy usage based on OEIS IDs
|
|
82
|
+
|
|
83
|
+
`mapFolding` directly implements some IDs from [_The On-Line Encyclopedia of Integer Sequences_](https://oeis.org/) ([BibTex](https://github.com/hunterhogan/mapFolding/blob/main/citations/oeis.bibtex) citation).
|
|
84
|
+
|
|
85
|
+
Use `getOEISids` to get the most up-to-date list of available OEIS IDs.
|
|
86
|
+
|
|
87
|
+
```cmd
|
|
88
|
+
(mapFolding) C:\apps\mapFolding> getOEISids
|
|
89
|
+
|
|
90
|
+
Available OEIS sequences:
|
|
91
|
+
A001415: Number of ways of folding a 2 X n strip of stamps.
|
|
92
|
+
A001416: Number of ways of folding a 3 X n strip of stamps.
|
|
93
|
+
A001417: Number of ways of folding a 2 X 2 X ... X 2 n-dimensional map.
|
|
94
|
+
A001418: Number of ways of folding an n X n sheet of stamps.
|
|
95
|
+
A195646: Number of ways of folding a 3 X 3 X ... X 3 n-dimensional map.
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 2. **Algorithm Zoo** 🦒
|
|
99
|
+
|
|
100
|
+
- **Lunnon’s 1971 Algorithm**: A painstakingly debugged version of [the original typo-riddled code](https://github.com/hunterhogan/mapFolding/blob/mapFolding/reference/foldings.txt)
|
|
101
|
+
- The /reference directory.
|
|
102
|
+
- **Numba-JIT Accelerated**: Up to 1000× faster than pure Python ([benchmarks](https://github.com/hunterhogan/mapFolding/blob/mapFolding/notes/Speed%20highlights.md))
|
|
103
|
+
|
|
104
|
+
### 3. **For Researchers** 🔬
|
|
105
|
+
|
|
106
|
+
- Change multiple minute settings, such as the bit width of the data types.
|
|
107
|
+
- Transform the algorithm using AST
|
|
108
|
+
- Create hyper-optimized modules to compute a specific map.
|
|
109
|
+
|
|
110
|
+
### 4. **Customizing your algorithm**
|
|
111
|
+
|
|
112
|
+
- mapFolding\someAssemblyRequired\synthesizeNumbaJob.py (and/or synthesizeNumba____.py, as applicable)
|
|
113
|
+
- Synthesize a Numba-optimized module for a specific mapShape
|
|
114
|
+
- Synthesize _from_ a module in mapFolding\syntheticModules or from any source you select
|
|
115
|
+
- Use the existing transformation options
|
|
116
|
+
- Or create new ways of transforming the algorithm from its source to a specific job
|
|
117
|
+
- mapFolding\someAssemblyRequired\makeJob.py
|
|
118
|
+
- Initialize data for a specific mapShape
|
|
119
|
+
- mapFolding\someAssemblyRequired\synthesizeNumbaModules.py (and/or synthesizeNumba____.py, as applicable)
|
|
120
|
+
- Synthesize one or more Numba-optimized modules for parallel or sequential computation
|
|
121
|
+
- Overwrite the modules in mapFolding\syntheticModules or save the module(s) to a custom path
|
|
122
|
+
- Synthesize _from_ the algorithm(s) in mapFolding\theDao.py or from any source you select
|
|
123
|
+
- Use the existing transformation options
|
|
124
|
+
- Or create new ways of transforming the algorithm from its source to a new module
|
|
125
|
+
- Use your new module in synthesizeNumbaJob.py, above, as the source to create a mapShape-specific job module
|
|
126
|
+
- mapFolding\theDao.py
|
|
127
|
+
- Modify the algorithms for initializing values, parallel computation, and/or sequential computation
|
|
128
|
+
- Use the modified algorithm(s) in synthesizeNumbaModules.py, above, to create Numba-optimized version(s)
|
|
129
|
+
- Then use a Numba-optimized version in synthesizeNumbaJob.py, above, to create a hyper-optimized version for a specific mapShape
|
|
130
|
+
- mapFolding\theSSOT.py (and/or theSSOTnumba.py and/ or theSSOT____.py, if they exist)
|
|
131
|
+
- Modify broad settings or find functions to modify broad settings, such as data structures and their data types
|
|
132
|
+
- Create new settings or groups of settings
|
|
133
|
+
- mapFolding\beDRY.py
|
|
134
|
+
- Functions to handle common tasks, such as parsing parameters or creating the `connectionGraph` for a mapShape (a Cartesian product decomposition)
|
|
135
|
+
- mapFolding\someAssemblyRequired
|
|
136
|
+
- Create new transformations to optimize the algorithm, such as for JAX, CuPy, or CUDA
|
|
137
|
+
- (mapFolding\reference\jax.py has a once-functional JAX implementation, and synthesizeModuleJAX.py might be a useful starting point)
|
|
138
|
+
|
|
139
|
+
## Map-folding Video
|
|
140
|
+
|
|
141
|
+
~~This caused my neurosis:~~ I enjoyed the following video, which is what introduced me to map folding.
|
|
142
|
+
|
|
143
|
+
"How Many Ways Can You Fold a Map?" by Physics for the Birds, 2024 November 13 ([BibTex](https://github.com/hunterhogan/mapFolding/blob/main/citations/Physics_for_the_Birds.bibtex) citation)
|
|
144
|
+
|
|
145
|
+
[](https://www.youtube.com/watch?v=sfH9uIY3ln4)
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## My recovery
|
|
150
|
+
|
|
151
|
+
[](https://HunterThinks.com/support)
|
|
152
|
+
[](https://www.youtube.com/@HunterHogan)
|
|
153
|
+
|
|
154
|
+
[](https://creativecommons.org/licenses/by-nc/4.0/)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
mapFolding/__init__.py,sha256=_YjoypHXmWLmEWwhFVgKO83Uf28ksesT9F73oJoAIPE,1323
|
|
2
|
+
mapFolding/basecamp.py,sha256=v0VCF_Zgm_XBHcz4bqblsxfHwAxZKgenW6um77quWLk,3751
|
|
3
|
+
mapFolding/beDRY.py,sha256=XVtLraG9VnC4yG2HkaFwZRh2td4ZHMjTQvnbcD_W130,17133
|
|
4
|
+
mapFolding/oeis.py,sha256=3hv71o8bhckjY0nsSY5JTJ2LrpJcuhZ9j3mP6LWLIQc,11124
|
|
5
|
+
mapFolding/theDao.py,sha256=SmyTbP1iwRAnpuq2ngdJKooXUA1_PR0VRHQ4fcJskMY,12713
|
|
6
|
+
mapFolding/theSSOT.py,sha256=QrEMPREjEbt1H8HcrM2Nm_hv7JsFWRG3lHdUU0Jrv-w,10238
|
|
7
|
+
mapFolding/theSSOTnumba.py,sha256=zGq2zlZZeuxiNSO2Fs_AqV6UhybJAJuDw-2lMVvDS2w,5133
|
|
8
|
+
mapFolding/reference/flattened.py,sha256=S6D9wiFTlbeoetEqaMLOcA-R22BHOzjqPRujffNxxUM,14875
|
|
9
|
+
mapFolding/reference/hunterNumba.py,sha256=jDS0ORHkIhcJ1rzA5hT49sZHKf3rgJOoGesUCcbKFFY,6054
|
|
10
|
+
mapFolding/reference/irvineJavaPort.py,sha256=7GvBU0tnS6wpFgkYad3465do9jBQW-2bYvbCYyABPHM,3341
|
|
11
|
+
mapFolding/reference/jax.py,sha256=7ji9YWia6Kof0cjcNdiS1GG1rMbC5SBjcyVr_07AeUk,13845
|
|
12
|
+
mapFolding/reference/lunnan.py,sha256=iAbJELfW6RKNMdPcBY9b6rGQ-z1zoRf-1XCurCRMOo8,3951
|
|
13
|
+
mapFolding/reference/lunnanNumpy.py,sha256=rwVP3WIDXimpAuaxhRIuBYU56nVDTKlfGiclw_FkgUU,3765
|
|
14
|
+
mapFolding/reference/lunnanWhile.py,sha256=uRrMT23jTJvoQDlD_FzeIQe_pfMXJG6_bRvs7uhC8z0,3271
|
|
15
|
+
mapFolding/reference/rotatedEntryPoint.py,sha256=USZY3n3zwhSE68ATscUuN66t1qShuEbMI790Gz9JFTw,9352
|
|
16
|
+
mapFolding/reference/total_countPlus1vsPlusN.py,sha256=wpgay-uqPOBd64Z4Pg6tg40j7-4pzWHGMM6v0bnmjhE,6288
|
|
17
|
+
mapFolding/someAssemblyRequired/__init__.py,sha256=wtec_hIz-AKz0_hGdXsWnCKTcCxdMV9-WK6SiIriAeU,396
|
|
18
|
+
mapFolding/someAssemblyRequired/getLLVMforNoReason.py,sha256=nX8tghZClYt7zJd6RpZBXhE_h-CGRHOS17biqiEdf-o,855
|
|
19
|
+
mapFolding/someAssemblyRequired/makeJob.py,sha256=c9sTRUK90snTCcXCvs86VKBH6z_nt3OVFjNs_WgCoIg,2422
|
|
20
|
+
mapFolding/someAssemblyRequired/synthesizeModuleJAX.py,sha256=jatvtYhK5ZJK-YmCKATt7w3icFXXO79cZDAYVrU9bgA,1258
|
|
21
|
+
mapFolding/someAssemblyRequired/synthesizeNumba.py,sha256=mPCjp4N-dOJRC4TvZGkqAqFKDWEPhWH9v0Cq5AWHlBA,17279
|
|
22
|
+
mapFolding/someAssemblyRequired/synthesizeNumbaGeneralized.py,sha256=k8IaCT74ZPhHyra0MbCRdt_5k0Ov3vJgXlN5tbLVnf4,13998
|
|
23
|
+
mapFolding/someAssemblyRequired/synthesizeNumbaJob.py,sha256=2sKZgc5kyyz2KaoApcazj_37UgBqAkxORFeROWWU5tk,9038
|
|
24
|
+
mapFolding/someAssemblyRequired/synthesizeNumbaModules.py,sha256=_iRXjMASB_BnYJeH8Rt7FlC-GE7lkZ1Hy292XTaUCu4,3785
|
|
25
|
+
mapFolding/syntheticModules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
|
+
mapFolding/syntheticModules/numba_countInitialize.py,sha256=geHketfekZTgu5gbc8E3SShPmbW3gDybg5PCBpXdsa8,4274
|
|
27
|
+
mapFolding/syntheticModules/numba_countParallel.py,sha256=kOI5PU90AExPvlWwU0BHVVFjlHlmUFp1KdtlmthQ71E,5517
|
|
28
|
+
mapFolding/syntheticModules/numba_countSequential.py,sha256=zFFRv9oLtOih9TpbtARpVAPt-NfZxh0ygXuj-wfPjUg,3732
|
|
29
|
+
mapFolding/syntheticModules/numba_doTheNeedful.py,sha256=6WuXKDMVa_C56dLlmXNvFl04MlU8-WVasqbAaxsgI-o,1368
|
|
30
|
+
tests/__init__.py,sha256=eg9smg-6VblOr0kisM40CpGnuDtU2JgEEWGDTFVOlW8,57
|
|
31
|
+
tests/conftest.py,sha256=7Ims3QcOzqBXu_k0kX9bt6PieC-OoIpc7OGxzdT2ELc,11826
|
|
32
|
+
tests/test_computations.py,sha256=qBha4IggMfr6ZH06W3M66enTA6PWsx8vkDp5eqYFM9M,4765
|
|
33
|
+
tests/test_oeis.py,sha256=31kdO1vnu2Lon43vM-YJVS4g40Ic03DWNER-cJcpxX4,4916
|
|
34
|
+
tests/test_other.py,sha256=u0vINT5EyVsXTNTR2DZIMpWCg4FH471jjHLRzC2JX7U,8351
|
|
35
|
+
tests/test_tasks.py,sha256=iq6_dh43JQkC2vAWXua0Xe915BKFGbvRJAkmbco854A,2389
|
|
36
|
+
tests/test_types.py,sha256=58tmPG9WOeGGAQbdQK_h_7t4SnENnZugH4WXlI8-L-M,171
|
|
37
|
+
mapFolding-0.4.1.dist-info/LICENSE,sha256=NxH5Y8BdC-gNU-WSMwim3uMbID2iNDXJz7fHtuTdXhk,19346
|
|
38
|
+
mapFolding-0.4.1.dist-info/METADATA,sha256=iJiWfEzXVheLtyLLWT2BNis5xsisnhllS17hnPwiRws,7633
|
|
39
|
+
mapFolding-0.4.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
40
|
+
mapFolding-0.4.1.dist-info/entry_points.txt,sha256=F3OUeZR1XDTpoH7k3wXuRb3KF_kXTTeYhu5AGK1SiOQ,146
|
|
41
|
+
mapFolding-0.4.1.dist-info/top_level.txt,sha256=1gP2vFaqPwHujGwb3UjtMlLEGN-943VSYFR7V4gDqW8,17
|
|
42
|
+
mapFolding-0.4.1.dist-info/RECORD,,
|
tests/conftest.py
CHANGED
|
@@ -2,182 +2,306 @@
|
|
|
2
2
|
|
|
3
3
|
# TODO learn how to run tests and coverage analysis without `env = ["NUMBA_DISABLE_JIT=1"]`
|
|
4
4
|
|
|
5
|
-
from tests.conftest_tmpRegistry import (
|
|
6
|
-
pathCacheTesting,
|
|
7
|
-
pathDataSamples,
|
|
8
|
-
pathFilenameFoldsTotalTesting,
|
|
9
|
-
pathTempTesting,
|
|
10
|
-
setupTeardownTestData,
|
|
11
|
-
)
|
|
12
|
-
from tests.conftest_uniformTests import (
|
|
13
|
-
uniformTestMessage,
|
|
14
|
-
standardizedEqualTo,
|
|
15
|
-
standardizedSystemExit,
|
|
16
|
-
)
|
|
17
5
|
from mapFolding import *
|
|
18
|
-
from mapFolding import basecamp
|
|
19
|
-
from mapFolding import getAlgorithmCallable, getDispatcherCallable
|
|
6
|
+
from mapFolding import basecamp, getAlgorithmDispatcher, getDispatcherCallable
|
|
20
7
|
from mapFolding.beDRY import *
|
|
21
|
-
from mapFolding.
|
|
8
|
+
from mapFolding.someAssemblyRequired import *
|
|
9
|
+
from mapFolding.oeis import _getFilenameOEISbFile, _getOEISidInformation, _getOEISidValues
|
|
22
10
|
from mapFolding.oeis import *
|
|
11
|
+
from types import ModuleType
|
|
12
|
+
from typing import Any, Callable, ContextManager, Dict, Generator, List, Literal, NoReturn, Optional, Sequence, Set, Tuple, Type, Union
|
|
23
13
|
from Z0Z_tools.pytestForYourUse import PytestFor_defineConcurrencyLimit, PytestFor_intInnit, PytestFor_oopsieKwargsie
|
|
24
|
-
from typing import Any, Callable, ContextManager, Dict, Generator, List, Optional, Sequence, Set, Tuple, Type, Union
|
|
25
14
|
import pathlib
|
|
26
15
|
import pytest
|
|
27
16
|
import random
|
|
17
|
+
import shutil
|
|
28
18
|
import unittest.mock
|
|
19
|
+
import uuid
|
|
20
|
+
|
|
21
|
+
# SSOT for test data paths and filenames
|
|
22
|
+
pathDataSamples = pathlib.Path("tests/dataSamples")
|
|
23
|
+
# NOTE `tmp` is not a diminutive form of temporary: it signals a technical term. And "temp" is strongly disfavored.
|
|
24
|
+
pathTmpRoot = pathDataSamples / "tmp"
|
|
25
|
+
|
|
26
|
+
# The registrar maintains the register of temp files
|
|
27
|
+
registerOfTemporaryFilesystemObjects: Set[pathlib.Path] = set()
|
|
28
|
+
|
|
29
|
+
def registrarRecordsTmpObject(path: pathlib.Path) -> None:
|
|
30
|
+
"""The registrar adds a tmp file to the register."""
|
|
31
|
+
registerOfTemporaryFilesystemObjects.add(path)
|
|
32
|
+
|
|
33
|
+
def registrarDeletesTmpObjects() -> None:
|
|
34
|
+
"""The registrar cleans up tmp files in the register."""
|
|
35
|
+
for pathTmp in sorted(registerOfTemporaryFilesystemObjects, reverse=True):
|
|
36
|
+
try:
|
|
37
|
+
if pathTmp.is_file():
|
|
38
|
+
pathTmp.unlink(missing_ok=True)
|
|
39
|
+
elif pathTmp.is_dir():
|
|
40
|
+
shutil.rmtree(pathTmp, ignore_errors=True)
|
|
41
|
+
except Exception as ERRORmessage:
|
|
42
|
+
print(f"Warning: Failed to clean up {pathTmp}: {ERRORmessage}")
|
|
43
|
+
registerOfTemporaryFilesystemObjects.clear()
|
|
44
|
+
|
|
45
|
+
@pytest.fixture(scope="session", autouse=True)
|
|
46
|
+
def setupTeardownTmpObjects() -> Generator[None, None, None]:
|
|
47
|
+
"""Auto-fixture to setup test data directories and cleanup after."""
|
|
48
|
+
pathDataSamples.mkdir(exist_ok=True)
|
|
49
|
+
pathTmpRoot.mkdir(exist_ok=True)
|
|
50
|
+
yield
|
|
51
|
+
registrarDeletesTmpObjects()
|
|
52
|
+
|
|
53
|
+
@pytest.fixture
|
|
54
|
+
def pathTmpTesting(request: pytest.FixtureRequest) -> pathlib.Path:
|
|
55
|
+
# "Z0Z_" ensures the directory name does not start with a number, which would make it an invalid Python identifier
|
|
56
|
+
pathTmp = pathTmpRoot / ("Z0Z_" + str(uuid.uuid4().hex))
|
|
57
|
+
pathTmp.mkdir(parents=True, exist_ok=False)
|
|
58
|
+
|
|
59
|
+
registrarRecordsTmpObject(pathTmp)
|
|
60
|
+
return pathTmp
|
|
61
|
+
|
|
62
|
+
@pytest.fixture
|
|
63
|
+
def pathFilenameTmpTesting(request: pytest.FixtureRequest) -> pathlib.Path:
|
|
64
|
+
try:
|
|
65
|
+
extension = request.param
|
|
66
|
+
except AttributeError:
|
|
67
|
+
extension = ".txt"
|
|
68
|
+
|
|
69
|
+
# "Z0Z_" ensures the name does not start with a number, which would make it an invalid Python identifier
|
|
70
|
+
uuidHex = uuid.uuid4().hex
|
|
71
|
+
subpath = "Z0Z_" + uuidHex[0:-8]
|
|
72
|
+
filenameStem = "Z0Z_" + uuidHex[-8:None]
|
|
73
|
+
|
|
74
|
+
pathFilenameTmp = pathlib.Path(pathTmpRoot, subpath, filenameStem + extension)
|
|
75
|
+
pathFilenameTmp.parent.mkdir(parents=True, exist_ok=False)
|
|
76
|
+
|
|
77
|
+
registrarRecordsTmpObject(pathFilenameTmp)
|
|
78
|
+
return pathFilenameTmp
|
|
79
|
+
|
|
80
|
+
@pytest.fixture
|
|
81
|
+
def pathCacheTesting(pathTmpTesting: pathlib.Path) -> Generator[pathlib.Path, Any, None]:
|
|
82
|
+
"""Temporarily replace the OEIS cache directory with a test directory."""
|
|
83
|
+
from mapFolding import oeis as there_must_be_a_better_way
|
|
84
|
+
pathCacheOriginal = there_must_be_a_better_way._pathCache
|
|
85
|
+
there_must_be_a_better_way._pathCache = pathTmpTesting
|
|
86
|
+
yield pathTmpTesting
|
|
87
|
+
there_must_be_a_better_way._pathCache = pathCacheOriginal
|
|
88
|
+
|
|
89
|
+
@pytest.fixture
|
|
90
|
+
def pathFilenameFoldsTotalTesting(pathTmpTesting: pathlib.Path) -> pathlib.Path:
|
|
91
|
+
return pathTmpTesting.joinpath("foldsTotalTest.txt")
|
|
29
92
|
|
|
30
93
|
def makeDictionaryFoldsTotalKnown() -> Dict[Tuple[int,...], int]:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
94
|
+
"""Returns a dictionary mapping dimension tuples to their known folding totals."""
|
|
95
|
+
dictionaryMapDimensionsToFoldsTotalKnown: Dict[Tuple[int, ...], int] = {}
|
|
96
|
+
|
|
97
|
+
for settings in settingsOEIS.values():
|
|
98
|
+
sequence = settings['valuesKnown']
|
|
99
|
+
|
|
100
|
+
for n, foldingsTotal in sequence.items():
|
|
101
|
+
dimensions = settings['getMapShape'](n)
|
|
102
|
+
dimensions.sort()
|
|
103
|
+
dictionaryMapDimensionsToFoldsTotalKnown[tuple(dimensions)] = foldingsTotal
|
|
104
|
+
|
|
105
|
+
# Are we in a place that has jobs?
|
|
106
|
+
pathJobDEFAULT = getPathJobRootDEFAULT()
|
|
107
|
+
if pathJobDEFAULT.exists():
|
|
108
|
+
# Are there foldsTotal files?
|
|
109
|
+
for pathFilenameFoldsTotal in pathJobDEFAULT.rglob('*.foldsTotal'):
|
|
110
|
+
if pathFilenameFoldsTotal.is_file():
|
|
111
|
+
try:
|
|
112
|
+
listDimensions = eval(pathFilenameFoldsTotal.stem)
|
|
113
|
+
except Exception:
|
|
114
|
+
continue
|
|
115
|
+
# Are the dimensions in the dictionary?
|
|
116
|
+
if isinstance(listDimensions, list) and all(isinstance(dimension, int) for dimension in listDimensions):
|
|
117
|
+
listDimensions.sort()
|
|
118
|
+
if tuple(listDimensions) in dictionaryMapDimensionsToFoldsTotalKnown:
|
|
119
|
+
continue
|
|
120
|
+
# Are the contents a reasonably large integer?
|
|
121
|
+
try:
|
|
122
|
+
foldsTotal = pathFilenameFoldsTotal.read_text()
|
|
123
|
+
except Exception:
|
|
124
|
+
continue
|
|
125
|
+
# Why did I sincerely believe this would only be three lines of code?
|
|
126
|
+
if foldsTotal.isdigit():
|
|
127
|
+
foldsTotalInteger = int(foldsTotal)
|
|
128
|
+
if foldsTotalInteger > 85109616 * 10**3:
|
|
129
|
+
# You made it this far, so fuck it: put it in the dictionary
|
|
130
|
+
dictionaryMapDimensionsToFoldsTotalKnown[tuple(listDimensions)] = foldsTotalInteger
|
|
131
|
+
dictionaryMapDimensionsToFoldsTotalKnown[tuple(listDimensions)] = foldsTotalInteger
|
|
132
|
+
# The sunk-costs fallacy claims another victim!
|
|
133
|
+
|
|
134
|
+
return dictionaryMapDimensionsToFoldsTotalKnown
|
|
72
135
|
|
|
73
136
|
"""
|
|
74
137
|
Section: Fixtures"""
|
|
75
138
|
|
|
76
139
|
@pytest.fixture(autouse=True)
|
|
77
140
|
def setupWarningsAsErrors() -> Generator[None, Any, None]:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
141
|
+
"""Convert all warnings to errors for all tests."""
|
|
142
|
+
import warnings
|
|
143
|
+
warnings.filterwarnings("error")
|
|
144
|
+
yield
|
|
145
|
+
warnings.resetwarnings()
|
|
83
146
|
|
|
84
147
|
@pytest.fixture
|
|
85
148
|
def foldsTotalKnown() -> Dict[Tuple[int,...], int]:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
149
|
+
"""Returns a dictionary mapping dimension tuples to their known folding totals.
|
|
150
|
+
NOTE I am not convinced this is the best way to do this.
|
|
151
|
+
Advantage: I call `makeDictionaryFoldsTotalKnown()` from modules other than test modules.
|
|
152
|
+
Preference: I _think_ I would prefer a SSOT function available to any module
|
|
153
|
+
similar to `foldsTotalKnown = getFoldsTotalKnown(listDimensions)`."""
|
|
154
|
+
return makeDictionaryFoldsTotalKnown()
|
|
92
155
|
|
|
93
156
|
@pytest.fixture
|
|
94
157
|
def listDimensionsTestCountFolds(oeisID: str) -> List[int]:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
158
|
+
"""For each `oeisID` from the `pytest.fixture`, returns `listDimensions` from `valuesTestValidation`
|
|
159
|
+
if `validateListDimensions` approves. Each `listDimensions` is suitable for testing counts."""
|
|
160
|
+
while True:
|
|
161
|
+
n = random.choice(settingsOEIS[oeisID]['valuesTestValidation'])
|
|
162
|
+
if n < 2:
|
|
163
|
+
continue
|
|
164
|
+
listDimensionsCandidate = settingsOEIS[oeisID]['getMapShape'](n)
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
return validateListDimensions(listDimensionsCandidate)
|
|
168
|
+
except (ValueError, NotImplementedError):
|
|
169
|
+
pass
|
|
107
170
|
|
|
108
171
|
@pytest.fixture
|
|
109
172
|
def listDimensionsTestFunctionality(oeisID_1random: str) -> List[int]:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
173
|
+
"""To test functionality, get one `listDimensions` from `valuesTestValidation` if
|
|
174
|
+
`validateListDimensions` approves. The algorithm can count the folds of the returned
|
|
175
|
+
`listDimensions` in a short enough time suitable for testing."""
|
|
176
|
+
while True:
|
|
177
|
+
n = random.choice(settingsOEIS[oeisID_1random]['valuesTestValidation'])
|
|
178
|
+
if n < 2:
|
|
179
|
+
continue
|
|
180
|
+
listDimensionsCandidate = settingsOEIS[oeisID_1random]['getMapShape'](n)
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
return validateListDimensions(listDimensionsCandidate)
|
|
184
|
+
except (ValueError, NotImplementedError):
|
|
185
|
+
pass
|
|
123
186
|
|
|
124
187
|
@pytest.fixture
|
|
125
188
|
def listDimensionsTestParallelization(oeisID: str) -> List[int]:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
189
|
+
"""For each `oeisID` from the `pytest.fixture`, returns `listDimensions` from `valuesTestParallelization`"""
|
|
190
|
+
n = random.choice(settingsOEIS[oeisID]['valuesTestParallelization'])
|
|
191
|
+
return settingsOEIS[oeisID]['getMapShape'](n)
|
|
129
192
|
|
|
130
193
|
@pytest.fixture
|
|
131
194
|
def mockBenchmarkTimer() -> Generator[unittest.mock.MagicMock | unittest.mock.AsyncMock, Any, None]:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
195
|
+
"""Mock time.perf_counter_ns for consistent benchmark timing."""
|
|
196
|
+
with unittest.mock.patch('time.perf_counter_ns') as mockTimer:
|
|
197
|
+
mockTimer.side_effect = [0, 1e9] # Start and end times for 1 second
|
|
198
|
+
yield mockTimer
|
|
136
199
|
|
|
137
200
|
@pytest.fixture
|
|
138
201
|
def mockFoldingFunction() -> Callable[..., Callable[..., None]]:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
202
|
+
"""Creates a mock function that simulates _countFolds behavior."""
|
|
203
|
+
def make_mock(foldsValue: int, listDimensions: List[int]) -> Callable[..., None]:
|
|
204
|
+
mock_array = makeDataContainer(2)
|
|
205
|
+
mock_array[0] = foldsValue
|
|
206
|
+
mock_array[-1] = getLeavesTotal(listDimensions)
|
|
144
207
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
208
|
+
def mock_countFolds(**keywordArguments: Any) -> None:
|
|
209
|
+
keywordArguments['foldGroups'][:] = mock_array
|
|
210
|
+
return None
|
|
148
211
|
|
|
149
|
-
|
|
150
|
-
|
|
212
|
+
return mock_countFolds
|
|
213
|
+
return make_mock
|
|
151
214
|
|
|
152
215
|
@pytest.fixture
|
|
153
216
|
def mockDispatcher() -> Callable[[Any], ContextManager[Any]]:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
217
|
+
"""Context manager for mocking dispatcher callable."""
|
|
218
|
+
def wrapper(mockFunction: Any) -> ContextManager[Any]:
|
|
219
|
+
dispatcherCallable = getDispatcherCallable()
|
|
220
|
+
return unittest.mock.patch(
|
|
221
|
+
f"{dispatcherCallable.__module__}.{dispatcherCallable.__name__}",
|
|
222
|
+
side_effect=mockFunction
|
|
223
|
+
)
|
|
224
|
+
return wrapper
|
|
162
225
|
|
|
163
226
|
@pytest.fixture(params=oeisIDsImplemented)
|
|
164
227
|
def oeisID(request: pytest.FixtureRequest) -> Any:
|
|
165
|
-
|
|
228
|
+
return request.param
|
|
166
229
|
|
|
167
230
|
@pytest.fixture
|
|
168
231
|
def oeisID_1random() -> str:
|
|
169
|
-
|
|
170
|
-
|
|
232
|
+
"""Return one random valid OEIS ID."""
|
|
233
|
+
return random.choice(oeisIDsImplemented)
|
|
234
|
+
|
|
235
|
+
@pytest.fixture
|
|
236
|
+
def useThisDispatcher():
|
|
237
|
+
"""A fixture providing a context manager for temporarily replacing the dispatcher.
|
|
238
|
+
|
|
239
|
+
Returns
|
|
240
|
+
A context manager for patching the dispatcher
|
|
241
|
+
"""
|
|
242
|
+
dispatcherOriginal = basecamp.getDispatcherCallable
|
|
243
|
+
|
|
244
|
+
def patchDispatcher(callableTarget: Callable) -> None:
|
|
245
|
+
def callableParameterized(*arguments: Any, **keywordArguments: Any) -> Callable:
|
|
246
|
+
return callableTarget
|
|
247
|
+
basecamp.getDispatcherCallable = callableParameterized
|
|
248
|
+
|
|
249
|
+
yield patchDispatcher
|
|
250
|
+
basecamp.getDispatcherCallable = dispatcherOriginal
|
|
171
251
|
|
|
172
252
|
@pytest.fixture
|
|
173
|
-
def
|
|
174
|
-
|
|
175
|
-
|
|
253
|
+
def useAlgorithmSourceDispatcher(useThisDispatcher: Callable) -> Generator[None, None, None]:
|
|
254
|
+
"""Temporarily patches getDispatcherCallable to return the algorithm dispatcher."""
|
|
255
|
+
useThisDispatcher(getAlgorithmDispatcher())
|
|
256
|
+
yield
|
|
257
|
+
|
|
258
|
+
def uniformTestMessage(expected: Any, actual: Any, functionName: str, *arguments: Any) -> str:
|
|
259
|
+
"""Format assertion message for any test comparison."""
|
|
260
|
+
return (f"\nTesting: `{functionName}({', '.join(str(parameter) for parameter in arguments)})`\n"
|
|
261
|
+
f"Expected: {expected}\n"
|
|
262
|
+
f"Got: {actual}")
|
|
263
|
+
|
|
264
|
+
def standardizedEqualTo(expected: Any, functionTarget: Callable, *arguments: Any) -> None:
|
|
265
|
+
"""Template for tests expecting an error."""
|
|
266
|
+
if type(expected) is Type[Exception]:
|
|
267
|
+
messageExpected = expected.__name__
|
|
268
|
+
else:
|
|
269
|
+
messageExpected = expected
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
messageActual = actual = functionTarget(*arguments)
|
|
273
|
+
except Exception as actualError:
|
|
274
|
+
messageActual = type(actualError).__name__
|
|
275
|
+
actual = type(actualError)
|
|
276
|
+
|
|
277
|
+
assert actual == expected, uniformTestMessage(messageExpected, messageActual, functionTarget.__name__, *arguments)
|
|
278
|
+
|
|
279
|
+
def standardizedSystemExit(expected: Union[str, int, Sequence[int]], functionTarget: Callable, *arguments: Any) -> None:
|
|
280
|
+
"""Template for tests expecting SystemExit.
|
|
176
281
|
|
|
177
|
-
|
|
178
|
-
|
|
282
|
+
Parameters
|
|
283
|
+
expected: Exit code expectation:
|
|
284
|
+
- "error": any non-zero exit code
|
|
285
|
+
- "nonError": specifically zero exit code
|
|
286
|
+
- int: exact exit code match
|
|
287
|
+
- Sequence[int]: exit code must be one of these values
|
|
288
|
+
functionTarget: The function to test
|
|
289
|
+
arguments: Arguments to pass to the function
|
|
290
|
+
"""
|
|
291
|
+
with pytest.raises(SystemExit) as exitInfo:
|
|
292
|
+
functionTarget(*arguments)
|
|
179
293
|
|
|
180
|
-
|
|
294
|
+
exitCode = exitInfo.value.code
|
|
181
295
|
|
|
182
|
-
|
|
183
|
-
|
|
296
|
+
if expected == "error":
|
|
297
|
+
assert exitCode != 0, \
|
|
298
|
+
f"Expected error exit (non-zero) but got code {exitCode}"
|
|
299
|
+
elif expected == "nonError":
|
|
300
|
+
assert exitCode == 0, \
|
|
301
|
+
f"Expected non-error exit (0) but got code {exitCode}"
|
|
302
|
+
elif isinstance(expected, (list, tuple)):
|
|
303
|
+
assert exitCode in expected, \
|
|
304
|
+
f"Expected exit code to be one of {expected} but got {exitCode}"
|
|
305
|
+
else:
|
|
306
|
+
assert exitCode == expected, \
|
|
307
|
+
f"Expected exit code {expected} but got {exitCode}"
|