mapFolding 0.2.0__py3-none-any.whl → 0.2.2__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 CHANGED
@@ -1,13 +1,5 @@
1
- """Test concept: Import priority levels. Larger priority values should be imported before smaller priority values.
2
- This seems to be a little silly: no useful information is encoded in the priority value, so I don't know if a
3
- new import should have a lower or higher priority.
4
- Crazy concept: Python doesn't cram at least two import roles into one system, call it `import` and tell us how
5
- awesome Python is. Alternatively, I learn about the secret system for mapping physical names to logical names."""
6
-
7
- # TODO Across the entire package, restructure computationDivisions.
8
- # test modules need updating still
9
-
10
1
  from .theSSOT import *
2
+ from Z0Z_tools import defineConcurrencyLimit, intInnit, oopsieKwargsie
11
3
  from .beDRY import getTaskDivisions, makeConnectionGraph, outfitFoldings, setCPUlimit
12
4
  from .beDRY import getLeavesTotal, parseDimensions, validateListDimensions
13
5
  from .startHere import countFolds
mapFolding/babbage.py CHANGED
@@ -6,7 +6,25 @@ import numba
6
6
  import numpy
7
7
 
8
8
  @numba.jit(cache=True)
9
- def _countFolds(connectionGraph: NDArray[integer[Any]], foldsTotal: NDArray[integer[Any]], mapShape: Tuple[int, ...], my: NDArray[integer[Any]], gapsWhere: NDArray[integer[Any]], the: NDArray[integer[Any]], track: NDArray[integer[Any]]):
9
+ def _countFolds(connectionGraph: NDArray[integer[Any]], foldsTotal: NDArray[integer[Any]], mapShape: Tuple[int, ...], my: NDArray[integer[Any]], gapsWhere: NDArray[integer[Any]], the: NDArray[integer[Any]], track: NDArray[integer[Any]]) -> int:
10
+ """
11
+ What in tarnation is this stupid module and function?
12
+
13
+ - This function is not in the same module as `countFolds` so that we can delay Numba just-in-time (jit) compilation of this function and the finalization of its settings until we are ready.
14
+ - This function is not in the same module as `countFoldsCompiled`, which is the function that does the hard, so that we can delay `numba.jit` compilation of `countFoldsCompiled`.
15
+ - `countFoldsCompiled` is not merely "jitted", it is super jitted, which makes it too arrogant to talk to plebian Python functions. It will, however, reluctantly talk to basic jitted functions.
16
+ - The function in this module is jitted, so it can talk to `countFoldsCompiled`, and because it isn't so arrogant, it will talk to the low-class `countFolds` with only a few restrictions, such as:
17
+ - No `TypedDict`
18
+ - No Python v 3.13
19
+ - The plebs must clean up their own memory problems
20
+ - No oversized integers
21
+ - No global variables, only global constants
22
+ - They don't except pleb nonlocal variables either
23
+ - Python "class": they are all inferior to a jit
24
+ - No `**kwargs`
25
+ - and just a few dozen-jillion other things.
26
+
27
+ """
10
28
  # TODO learn if I really must change this jitted function to get the super jit to recompile
11
29
  # print('babbage')
12
30
  return countFoldsCompiled(connectionGraph, foldsTotal, my, gapsWhere, the, track)
mapFolding/beDRY.py CHANGED
@@ -1,22 +1,24 @@
1
1
  """A relatively stable API for oft-needed functionality."""
2
- from mapFolding.importPackages import intInnit, defineConcurrencyLimit, oopsieKwargsie
2
+ from mapFolding import intInnit, defineConcurrencyLimit, oopsieKwargsie
3
3
  from mapFolding import indexMy, indexThe, indexTrack, computationState
4
+ from mapFolding import dtypeDefault, dtypeLarge, dtypeSmall
4
5
  from typing import Any, List, Optional, Sequence, Type, Union
5
6
  import numpy
6
7
  import numba
7
- import numba.extending
8
- import numpy.typing
8
+ from numpy.typing import NDArray
9
+ from numpy import integer
9
10
  import sys
11
+ import operator
10
12
 
11
13
  def getLeavesTotal(listDimensions: Sequence[int]) -> int:
12
14
  """
13
- Calculate the product of non-zero, non-negative integers in the given list.
15
+ How many leaves are in the map.
14
16
 
15
17
  Parameters:
16
18
  listDimensions: A list of integers representing dimensions.
17
19
 
18
20
  Returns:
19
- productDimensions: The product of all positive integer dimensions. Returns 0 if all dimensions are 0.
21
+ productDimensions: The product of all positive integer dimensions.
20
22
  """
21
23
  listNonNegative = parseDimensions(listDimensions, 'listDimensions')
22
24
  listPositive = [dimension for dimension in listNonNegative if dimension > 0]
@@ -27,57 +29,82 @@ def getLeavesTotal(listDimensions: Sequence[int]) -> int:
27
29
  productDimensions = 1
28
30
  for dimension in listPositive:
29
31
  if dimension > sys.maxsize // productDimensions:
30
- raise OverflowError("Product would exceed maximum integer size")
32
+ raise OverflowError(f"I received {dimension=} in {listDimensions=}, but the product of the dimensions exceeds the maximum size of an integer on this system.")
31
33
  productDimensions *= dimension
32
34
 
33
35
  return productDimensions
34
36
 
35
- def getTaskDivisions(CPUlimit, computationDivisions: Optional[Union[int, str]], concurrencyLimit: int, listDimensions, the: numpy.typing.NDArray[numpy.integer[Any]], ):
36
- # TODO remove after restructuring the tests
37
- if isinstance(computationDivisions, bool) and computationDivisions:
38
- computationDivisions = "maximum"
39
-
37
+ def getTaskDivisions(computationDivisions: Optional[Union[int, str]], concurrencyLimit: int, CPUlimit: Optional[Union[bool, float, int]], listDimensions: Sequence[int]):
38
+ """
39
+ Determines whether or how to divide the computation into tasks.
40
+
41
+ Parameters
42
+ ----------
43
+ computationDivisions (None):
44
+ Specifies how to divide computations:
45
+ - None: no division of the computation into tasks; sets task divisions to 0
46
+ - int: direct set the number of task divisions; cannot exceed the map's total leaves
47
+ - "maximum": divides into `leavesTotal`-many `taskDivisions`
48
+ - "cpu": limits the divisions to the number of available CPUs, i.e. `concurrencyLimit`
49
+ concurrencyLimit:
50
+ Maximum number of concurrent tasks allowed
51
+ listDimensions: for error reporting
52
+ CPUlimit: for error reporting
53
+
54
+ Returns
55
+ -------
56
+ taskDivisions:
57
+
58
+ Raises
59
+ ------
60
+ ValueError
61
+ If computationDivisions is an unsupported type or if resulting task divisions exceed total leaves
62
+
63
+ Notes
64
+ -----
65
+ Task divisions cannot exceed total leaves to prevent duplicate counting of folds.
66
+ """
40
67
  if not computationDivisions:
41
- # Coding it this way should cover `None`, `False`, and `0`.
42
- the[indexThe.taskDivisions] = 0
43
- elif isinstance(computationDivisions, int):
44
- the[indexThe.taskDivisions] = computationDivisions
68
+ return 0
69
+ else:
70
+ leavesTotal = getLeavesTotal(listDimensions)
71
+ if isinstance(computationDivisions, int):
72
+ taskDivisions = computationDivisions
45
73
  elif isinstance(computationDivisions, str):
46
74
  computationDivisions = computationDivisions.lower()
47
75
  if computationDivisions == "maximum":
48
- the[indexThe.taskDivisions] = the[indexThe.leavesTotal]
76
+ taskDivisions = leavesTotal
49
77
  elif computationDivisions == "cpu":
50
- the[indexThe.taskDivisions] = min(concurrencyLimit, the[indexThe.leavesTotal])
78
+ taskDivisions = min(concurrencyLimit, leavesTotal)
51
79
  else:
52
80
  raise ValueError(f"I received {computationDivisions} for the parameter, `computationDivisions`, but the so-called programmer didn't implement code for that.")
53
81
 
54
- if the[indexThe.taskDivisions] > the[indexThe.leavesTotal]:
55
- raise ValueError(f"Problem: `taskDivisions`, ({the[indexThe.taskDivisions]}), is greater than `leavesTotal`, ({the[indexThe.leavesTotal]}), which will cause duplicate counting of the folds.\n\nChallenge: you cannot directly set `taskDivisions` or `leavesTotal`. They are derived from parameters that may or may not still be named `computationDivisions`, `CPUlimit` , and `listDimensions` and from dubious-quality Python code.\n\nFor those parameters, I received {computationDivisions=}, {CPUlimit=}, and {listDimensions=}.\n\nPotential solutions: get a different hobby or set `computationDivisions` to a different value.")
82
+ if taskDivisions > leavesTotal:
83
+ raise ValueError(f"Problem: `taskDivisions`, ({taskDivisions}), is greater than `leavesTotal`, ({leavesTotal}), which will cause duplicate counting of the folds.\n\nChallenge: you cannot directly set `taskDivisions` or `leavesTotal`. They are derived from parameters that may or may not still be named `computationDivisions`, `CPUlimit` , and `listDimensions` and from dubious-quality Python code.\n\nFor those parameters, I received {computationDivisions=}, {CPUlimit=}, and {listDimensions=}.\n\nPotential solutions: get a different hobby or set `computationDivisions` to a different value.")
56
84
 
57
- return the
85
+ return taskDivisions
58
86
 
59
- def makeConnectionGraph(listDimensions: Sequence[int], dtype: Optional[Type] = numpy.int64) -> numpy.typing.NDArray[numpy.integer[Any]]:
87
+ def makeConnectionGraph(listDimensions: Sequence[int], **keywordArguments: Optional[Type]) -> NDArray[integer[Any]]:
60
88
  """
61
- Constructs a connection graph for a given list of dimensions.
62
- This function generates a multi-dimensional connection graph based on the provided list of dimensions.
63
- The graph represents the connections between leaves in a Cartesian product decomposition or dimensional product mapping.
89
+ Constructs a multi-dimensional connection graph representing the connections between the leaves of a map with the given dimensions.
90
+ Also called a Cartesian product decomposition or dimensional product mapping.
64
91
 
65
92
  Parameters:
66
- listDimensions: A validated sequence of integers representing the dimensions of the map.
93
+ listDimensions: A sequence of integers representing the dimensions of the map.
67
94
  Returns:
68
95
  connectionGraph: A 3D numpy array with shape of (dimensionsTotal + 1, leavesTotal + 1, leavesTotal + 1).
69
96
  """
70
- leavesTotal = getLeavesTotal(listDimensions)
71
- arrayDimensions = numpy.array(listDimensions, dtype=dtype)
97
+ datatype = keywordArguments.get('datatype', dtypeDefault)
98
+ mapShape = validateListDimensions(listDimensions)
99
+ leavesTotal = getLeavesTotal(mapShape)
100
+ arrayDimensions = numpy.array(mapShape, dtype=datatype)
72
101
  dimensionsTotal = len(arrayDimensions)
73
102
 
74
103
  # Step 1: find the cumulative product of the map's dimensions
75
- cumulativeProduct = numpy.ones(dimensionsTotal + 1, dtype=dtype)
76
- for index in range(1, dimensionsTotal + 1):
77
- cumulativeProduct[index] = cumulativeProduct[index - 1] * arrayDimensions[index - 1]
104
+ cumulativeProduct = numpy.multiply.accumulate([1] + mapShape, dtype=datatype)
78
105
 
79
106
  # Step 2: create a coordinate system
80
- coordinateSystem = numpy.zeros((dimensionsTotal + 1, leavesTotal + 1), dtype=dtype)
107
+ coordinateSystem = numpy.zeros((dimensionsTotal + 1, leavesTotal + 1), dtype=datatype)
81
108
 
82
109
  for dimension1ndex in range(1, dimensionsTotal + 1):
83
110
  for leaf1ndex in range(1, leavesTotal + 1):
@@ -87,7 +114,7 @@ def makeConnectionGraph(listDimensions: Sequence[int], dtype: Optional[Type] = n
87
114
  )
88
115
 
89
116
  # Step 3: create and fill the connection graph
90
- connectionGraph = numpy.zeros((dimensionsTotal + 1, leavesTotal + 1, leavesTotal + 1), dtype=dtype)
117
+ connectionGraph = numpy.zeros((dimensionsTotal + 1, leavesTotal + 1, leavesTotal + 1), dtype=datatype)
91
118
 
92
119
  for dimension1ndex in range(1, dimensionsTotal + 1):
93
120
  for activeLeaf1ndex in range(1, leavesTotal + 1):
@@ -113,30 +140,56 @@ def makeConnectionGraph(listDimensions: Sequence[int], dtype: Optional[Type] = n
113
140
 
114
141
  return connectionGraph
115
142
 
116
- def outfitFoldings(
117
- listDimensions: Sequence[int],
118
- computationDivisions: Optional[Union[int, str]] = None,
119
- CPUlimit: Optional[Union[int, float, bool]] = None,
120
- dtypeDefault: Optional[Type] = numpy.int64, # TODO consider allowing a type or a "signal", such as "minimum", "safe", "maximum"
121
- dtypeLarge: Optional[Type] = numpy.int64, # Can/should I use numba types?
122
- ) -> computationState:
123
- the = numpy.zeros(len(indexThe), dtype=dtypeDefault)
143
+ def makeDataContainer(shape, datatype: Optional[Type] = None):
144
+ """Create a container, probably numpy.ndarray, with the given shape and datatype."""
145
+ if datatype is None:
146
+ datatype = dtypeDefault
147
+ return numpy.zeros(shape, dtype=datatype)
148
+
149
+ def outfitFoldings(listDimensions: Sequence[int], computationDivisions: Optional[Union[int, str]] = None, CPUlimit: Optional[Union[bool, float, int]] = None, **keywordArguments: Optional[Type]) -> computationState:
150
+ """
151
+ Initializes and configures the computation state for map folding computations.
152
+
153
+ Parameters
154
+ ----------
155
+ listDimensions:
156
+ The dimensions of the map to be folded
157
+ computationDivisions (None):
158
+ Specifies how to divide the computation tasks
159
+ CPUlimit (None):
160
+ Limits the CPU usage for computations
161
+
162
+ Returns
163
+ -------
164
+ computationState
165
+ An initialized computation state containing:
166
+ - connectionGraph: Graph representing connections in the map
167
+ - foldsTotal: Array tracking total folds
168
+ - mapShape: Validated and sorted dimensions of the map
169
+ - my: Array for internal state tracking
170
+ - gapsWhere: Array tracking gap positions
171
+ - the: Static settings and metadata
172
+ - track: Array for tracking computation progress
173
+ """
174
+ datatypeDefault = keywordArguments.get('datatypeDefault', dtypeDefault)
175
+ datatypeLarge = keywordArguments.get('datatypeLarge', dtypeLarge)
176
+
177
+ the = makeDataContainer(len(indexThe), datatypeDefault)
124
178
 
125
179
  mapShape = tuple(sorted(validateListDimensions(listDimensions)))
126
180
  the[indexThe.leavesTotal] = getLeavesTotal(mapShape)
127
181
  the[indexThe.dimensionsTotal] = len(mapShape)
128
182
  concurrencyLimit = setCPUlimit(CPUlimit)
129
-
130
- the = getTaskDivisions(CPUlimit, computationDivisions, concurrencyLimit, listDimensions, the)
131
-
183
+ the[indexThe.taskDivisions] = getTaskDivisions(computationDivisions, concurrencyLimit, CPUlimit, listDimensions)
184
+
132
185
  stateInitialized = computationState(
133
- connectionGraph = makeConnectionGraph(mapShape, dtype=dtypeDefault),
134
- foldsTotal = numpy.zeros(the[indexThe.leavesTotal], dtype=numpy.int64),
186
+ connectionGraph = makeConnectionGraph(mapShape, datatype=datatypeDefault),
187
+ foldsTotal = makeDataContainer(the[indexThe.leavesTotal], datatypeLarge),
135
188
  mapShape = mapShape,
136
- my = numpy.zeros(len(indexMy), dtype=dtypeLarge),
137
- gapsWhere = numpy.zeros(int(the[indexThe.leavesTotal]) * int(the[indexThe.leavesTotal]) + 1, dtype=dtypeDefault),
189
+ my = makeDataContainer(len(indexMy), datatypeLarge),
190
+ gapsWhere = makeDataContainer(int(the[indexThe.leavesTotal]) * int(the[indexThe.leavesTotal]) + 1, datatypeDefault),
138
191
  the = the,
139
- track = numpy.zeros((len(indexTrack), the[indexThe.leavesTotal] + 1), dtype=dtypeLarge)
192
+ track = makeDataContainer((len(indexTrack), the[indexThe.leavesTotal] + 1), datatypeLarge)
140
193
  )
141
194
 
142
195
  stateInitialized['my'][indexMy.leaf1ndex.value] = 1
@@ -145,10 +198,10 @@ def outfitFoldings(
145
198
 
146
199
  def parseDimensions(dimensions: Sequence[int], parameterName: str = 'unnamed parameter') -> List[int]:
147
200
  """
148
- Parse and validate a list of dimensions.
201
+ Parse and validate dimensions are non-negative integers.
149
202
 
150
203
  Parameters:
151
- listDimensions: List of integers representing dimensions
204
+ dimensions: Sequence of integers representing dimensions
152
205
  parameterName ('unnamed parameter'): Name of the parameter for error messages. Defaults to 'unnamed parameter'
153
206
  Returns:
154
207
  listNonNegative: List of validated non-negative integers
@@ -168,43 +221,41 @@ def parseDimensions(dimensions: Sequence[int], parameterName: str = 'unnamed par
168
221
 
169
222
  return listNonNegative
170
223
 
171
- def setCPUlimit(CPUlimit: Union[int, float, bool, None]):
172
- """Sets CPU limit for concurrent operations using Numba.
173
- This function configures the number of CPU threads that Numba can use for parallel execution.
174
- Note that this setting only affects Numba-jitted functions that have not yet been imported.
224
+ def setCPUlimit(CPUlimit: Union[bool, float, int, None]) -> int:
225
+ """Sets CPU limit for Numba concurrent operations. Note that it can only affect Numba-jitted functions that have not yet been imported.
226
+
175
227
  Parameters:
176
- CPUlimit (Union[int, float, bool, None]): The CPU limit to set.
177
- - If int/float: Specifies number of CPU threads to use
178
- - If bool: True uses all available CPUs, False uses 1 CPU
179
- - If None: Uses system default
228
+ CPUlimit: whether and how to limit the CPU usage. See notes for details.
180
229
  Returns:
181
230
  concurrencyLimit: The actual concurrency limit that was set
182
231
  Raises:
183
232
  TypeError: If CPUlimit is not of the expected types
233
+
234
+ Limits on CPU usage `CPUlimit`:
235
+ - `False`, `None`, or `0`: No limits on CPU usage; uses all available CPUs. All other values will potentially limit CPU usage.
236
+ - `True`: Yes, limit the CPU usage; limits to 1 CPU.
237
+ - Integer `>= 1`: Limits usage to the specified number of CPUs.
238
+ - Decimal value (`float`) between 0 and 1: Fraction of total CPUs to use.
239
+ - Decimal value (`float`) between -1 and 0: Fraction of CPUs to *not* use.
240
+ - Integer `<= -1`: Subtract the absolute value from total CPUs.
184
241
  """
185
242
  if not (CPUlimit is None or isinstance(CPUlimit, (bool, int, float))):
186
243
  CPUlimit = oopsieKwargsie(CPUlimit)
187
244
 
188
245
  concurrencyLimit = defineConcurrencyLimit(CPUlimit)
189
- # NOTE `set_num_threads` only affects "jitted" functions that have _not_ yet been "imported"
190
246
  numba.set_num_threads(concurrencyLimit)
191
247
 
192
248
  return concurrencyLimit
193
249
 
194
250
  def validateListDimensions(listDimensions: Sequence[int]) -> List[int]:
195
251
  """
196
- Validates and processes a list of dimensions.
197
-
198
- This function ensures that the input list of dimensions is not None,
199
- parses it to ensure all dimensions are non-negative, and then filters
200
- out any dimensions that are not greater than zero. If the resulting
201
- list has fewer than two dimensions, a NotImplementedError is raised.
252
+ Validates and sorts a sequence of at least two positive dimensions.
202
253
 
203
254
  Parameters:
204
- listDimensions: A list of integer dimensions to be validated.
255
+ listDimensions: A sequence of integer dimensions to be validated.
205
256
 
206
257
  Returns:
207
- validDimensions: A list, with at least two elements, of only positive integers.
258
+ dimensionsValidSorted: A list, with at least two elements, of only positive integers.
208
259
 
209
260
  Raises:
210
261
  ValueError: If the input listDimensions is None.
@@ -213,7 +264,7 @@ def validateListDimensions(listDimensions: Sequence[int]) -> List[int]:
213
264
  if not listDimensions:
214
265
  raise ValueError(f"listDimensions is a required parameter.")
215
266
  listNonNegative = parseDimensions(listDimensions, 'listDimensions')
216
- validDimensions = [dimension for dimension in listNonNegative if dimension > 0]
217
- if len(validDimensions) < 2:
267
+ dimensionsValid = [dimension for dimension in listNonNegative if dimension > 0]
268
+ if len(dimensionsValid) < 2:
218
269
  raise NotImplementedError(f"This function requires listDimensions, {listDimensions}, to have at least two dimensions greater than 0. You may want to look at https://oeis.org/.")
219
- return validDimensions
270
+ return sorted(dimensionsValid)
mapFolding/lovelace.py CHANGED
@@ -1,27 +1,43 @@
1
+ """
2
+ The algorithm for counting folds.
3
+
4
+ Starting from established data structures, the algorithm initializes some baseline values. The initialization uses a loop that is not used after the first fold is counted.
5
+
6
+ After initialization, the folds are either counted sequentially or counted with inefficiently divided parallel tasks.
7
+
8
+ All three of these actions--initialization, sequential counting, and parallel counting--use nearly identical logic. Without Numba, all of the logic is in one function with exactly one additional
9
+ conditional statement for initialization and exactly one additional conditional statement for parallel counting.
10
+
11
+ Numba's just-in-time (jit) compiler, especially super jit, is capable of radically increasing throughput and dramatically reducing the size of the compiled code, especially by ejecting unused code.
12
+
13
+ The complexity of this module is due to me allegedly applying Numba's features. Allegedly.
14
+
15
+ (The flow starts with the last function.)
16
+ """
1
17
  from mapFolding import indexMy, indexThe, indexTrack
2
18
  from numpy import integer
3
19
  from numpy.typing import NDArray
4
- from typing import Any, Optional
20
+ from typing import Any, Tuple, Optional
5
21
  import numba
6
22
  import numpy
7
23
 
8
24
  @numba.jit(parallel=False, _nrt=True, boundscheck=False, error_model='numpy', fastmath=True, forceinline=True, looplift=False, no_cfunc_wrapper=True, no_cpython_wrapper=True, nogil=True, nopython=True)
9
- def ifComputationDivisions(my: NDArray[integer[Any]], the: NDArray[integer[Any]]):
25
+ def ifComputationDivisions(my: NDArray[integer[Any]], the: NDArray[integer[Any]]) -> bool:
10
26
  if the[indexThe.taskDivisions.value] == 0:
11
27
  return True
12
28
  return my[indexMy.leaf1ndex.value] != the[indexThe.taskDivisions.value] or \
13
29
  (my[indexMy.leafConnectee.value] % the[indexThe.taskDivisions.value]) == my[indexMy.taskIndex.value]
14
30
 
15
31
  @numba.jit(parallel=False, _nrt=True, boundscheck=False, error_model='numpy', fastmath=True, forceinline=True, looplift=False, no_cfunc_wrapper=True, no_cpython_wrapper=True, nogil=True, nopython=True)
16
- def insertUnconstrainedLeaf(my: NDArray[integer[Any]], the: NDArray[integer[Any]], Z0Z_initializeUnconstrainedLeaf: Optional[bool]):
17
- if Z0Z_initializeUnconstrainedLeaf:
32
+ def insertUnconstrainedLeaf(my: NDArray[integer[Any]], the: NDArray[integer[Any]], initializeUnconstrainedLeaf: Optional[bool]) -> bool:
33
+ if initializeUnconstrainedLeaf:
18
34
  return my[indexMy.dimensionsUnconstrained.value] == the[indexThe.dimensionsTotal.value]
19
35
  else:
20
36
  return False
21
37
 
22
38
  @numba.jit(parallel=False, _nrt=True, boundscheck=False, error_model='numpy', fastmath=True, forceinline=True, looplift=False, no_cfunc_wrapper=True, no_cpython_wrapper=True, nogil=True, nopython=True)
23
- def initializationConditionUnconstrainedLeaf(my: NDArray[integer[Any]], Z0Z_initializeUnconstrainedLeaf: Optional[bool]):
24
- if Z0Z_initializeUnconstrainedLeaf is None or Z0Z_initializeUnconstrainedLeaf is False:
39
+ def initializationConditionUnconstrainedLeaf(my: NDArray[integer[Any]], initializeUnconstrainedLeaf: Optional[bool]) -> bool:
40
+ if initializeUnconstrainedLeaf is None or initializeUnconstrainedLeaf is False:
25
41
  return False
26
42
  else:
27
43
  if my[indexMy.gap1ndex.value] > 0:
@@ -30,7 +46,7 @@ def initializationConditionUnconstrainedLeaf(my: NDArray[integer[Any]], Z0Z_init
30
46
  return False
31
47
 
32
48
  @numba.jit(parallel=False, _nrt=True, boundscheck=False, error_model='numpy', fastmath=True, forceinline=True, looplift=False, no_cfunc_wrapper=True, no_cpython_wrapper=True, nogil=True, nopython=True)
33
- def doWhile(connectionGraph: NDArray[integer[Any]], foldsTotal: NDArray[integer[Any]], my: NDArray[integer[Any]], gapsWhere: NDArray[integer[Any]], the: NDArray[integer[Any]], track: NDArray[integer[Any]], Z0Z_initializeUnconstrainedLeaf: Optional[bool] ):
49
+ def doWhile(connectionGraph: NDArray[integer[Any]], foldsTotal: NDArray[integer[Any]], my: NDArray[integer[Any]], gapsWhere: NDArray[integer[Any]], the: NDArray[integer[Any]], track: NDArray[integer[Any]], initializeUnconstrainedLeaf: Optional[bool]) -> Tuple[NDArray[integer[Any]], NDArray[integer[Any]], NDArray[integer[Any]], NDArray[integer[Any]]]:
34
50
  while my[indexMy.leaf1ndex.value] > 0:
35
51
  if my[indexMy.leaf1ndex.value] <= 1 or track[indexTrack.leafBelow.value, 0] == 1:
36
52
  if my[indexMy.leaf1ndex.value] > the[indexThe.leavesTotal.value]:
@@ -45,6 +61,7 @@ def doWhile(connectionGraph: NDArray[integer[Any]], foldsTotal: NDArray[integer[
45
61
  else:
46
62
  my[indexMy.leafConnectee.value] = connectionGraph[my[indexMy.dimension1ndex.value], my[indexMy.leaf1ndex.value], my[indexMy.leaf1ndex.value]]
47
63
  while my[indexMy.leafConnectee.value] != my[indexMy.leaf1ndex.value]:
64
+ # NOTE This conditional check should only be in the parallel counting branch
48
65
  if ifComputationDivisions(my, the):
49
66
  gapsWhere[my[indexMy.gap1ndexCeiling.value]] = my[indexMy.leafConnectee.value]
50
67
  if track[indexTrack.countDimensionsGapped.value, my[indexMy.leafConnectee.value]] == 0:
@@ -52,7 +69,8 @@ def doWhile(connectionGraph: NDArray[integer[Any]], foldsTotal: NDArray[integer[
52
69
  track[indexTrack.countDimensionsGapped.value, my[indexMy.leafConnectee.value]] += 1
53
70
  my[indexMy.leafConnectee.value] = connectionGraph[my[indexMy.dimension1ndex.value], my[indexMy.leaf1ndex.value], track[indexTrack.leafBelow.value, my[indexMy.leafConnectee.value]]]
54
71
  my[indexMy.dimension1ndex.value] += 1
55
- if insertUnconstrainedLeaf(my, the, Z0Z_initializeUnconstrainedLeaf):
72
+ # NOTE This `if` statement and `while` loop should be absent from the code that does the counting
73
+ if insertUnconstrainedLeaf(my, the, initializeUnconstrainedLeaf):
56
74
  my[indexMy.indexLeaf.value] = 0
57
75
  while my[indexMy.indexLeaf.value] < my[indexMy.leaf1ndex.value]:
58
76
  gapsWhere[my[indexMy.gap1ndexCeiling.value]] = my[indexMy.indexLeaf.value]
@@ -77,13 +95,16 @@ def doWhile(connectionGraph: NDArray[integer[Any]], foldsTotal: NDArray[integer[
77
95
  track[indexTrack.leafAbove.value, track[indexTrack.leafBelow.value, my[indexMy.leaf1ndex.value]]] = my[indexMy.leaf1ndex.value]
78
96
  track[indexTrack.gapRangeStart.value, my[indexMy.leaf1ndex.value]] = my[indexMy.gap1ndex.value]
79
97
  my[indexMy.leaf1ndex.value] += 1
80
- if initializationConditionUnconstrainedLeaf(my, Z0Z_initializeUnconstrainedLeaf):
98
+ # NOTE This check and break should be absent from the code that does the counting
99
+ if initializationConditionUnconstrainedLeaf(my, initializeUnconstrainedLeaf):
81
100
  break
82
101
  return foldsTotal, my, gapsWhere, track
83
102
 
84
103
  @numba.jit(parallel=True, _nrt=True, boundscheck=False, error_model='numpy', fastmath=True, forceinline=True, looplift=False, no_cfunc_wrapper=True, no_cpython_wrapper=True, nogil=True, nopython=True)
85
- def doTaskIndices(connectionGraph: NDArray[integer[Any]], foldsTotal: NDArray[integer[Any]], my: NDArray[integer[Any]], gapsWhere: NDArray[integer[Any]], the: NDArray[integer[Any]], track: NDArray[integer[Any]]):
86
-
104
+ def doTaskIndices(connectionGraph: NDArray[integer[Any]], foldsTotal: NDArray[integer[Any]], my: NDArray[integer[Any]], gapsWhere: NDArray[integer[Any]], the: NDArray[integer[Any]], track: NDArray[integer[Any]]) -> NDArray[integer[Any]]:
105
+ """This is the only function with the `parallel=True` option.
106
+ Make a copy of the initialized state because all task divisions can start from this baseline.
107
+ Run the counting algorithm but with conditional execution of a few lines of code, so each task has an incomplete count that does not overlap with other tasks."""
87
108
  stateFoldsSubTotal = foldsTotal.copy()
88
109
  stateMy = my.copy()
89
110
  statePotentialGaps = gapsWhere.copy()
@@ -92,18 +113,17 @@ def doTaskIndices(connectionGraph: NDArray[integer[Any]], foldsTotal: NDArray[in
92
113
  for indexSherpa in numba.prange(the[indexThe.taskDivisions.value]):
93
114
  my = stateMy.copy()
94
115
  my[indexMy.taskIndex.value] = indexSherpa
95
- foldsSubTotal, _1, _2, _3 = doWhile(connectionGraph, stateFoldsSubTotal.copy(), my, statePotentialGaps.copy(), the, stateTrack.copy(), Z0Z_initializeUnconstrainedLeaf=False)
116
+ foldsSubTotal, _1, _2, _3 = doWhile(connectionGraph, stateFoldsSubTotal.copy(), my, statePotentialGaps.copy(), the, stateTrack.copy(), initializeUnconstrainedLeaf=False)
96
117
 
97
118
  foldsTotal[indexSherpa] = foldsSubTotal[indexSherpa]
98
119
 
99
120
  return foldsTotal
100
121
 
101
122
  @numba.jit(parallel=False, _nrt=True, boundscheck=False, error_model='numpy', fastmath=True, forceinline=True, looplift=False, no_cfunc_wrapper=True, no_cpython_wrapper=True, nogil=True, nopython=True)
102
- def countFoldsCompileBranch(connectionGraph: NDArray[integer[Any]], foldsTotal: NDArray[integer[Any]],
103
- my: NDArray[integer[Any]], gapsWhere: NDArray[integer[Any]], the: NDArray[integer[Any]], track: NDArray[integer[Any]],
104
- obviousFlagForNumba: bool):
123
+ def countFoldsCompileBranch(connectionGraph: NDArray[integer[Any]], foldsTotal: NDArray[integer[Any]], my: NDArray[integer[Any]], gapsWhere: NDArray[integer[Any]], the: NDArray[integer[Any]], track: NDArray[integer[Any]], obviousFlagForNumba: bool) -> NDArray[integer[Any]]:
124
+ """Allegedly, `obviousFlagForNumba` allows Numba to compile two versions: one for parallel execution and one leaner version for sequential execution."""
105
125
  if obviousFlagForNumba:
106
- foldsTotal, _1, _2, _3 = doWhile(connectionGraph, foldsTotal, my, gapsWhere, the, track, Z0Z_initializeUnconstrainedLeaf=False)
126
+ foldsTotal, _1, _2, _3 = doWhile(connectionGraph, foldsTotal, my, gapsWhere, the, track, initializeUnconstrainedLeaf=False)
107
127
  else:
108
128
  foldsTotal = doTaskIndices(connectionGraph, foldsTotal, my, gapsWhere, the, track)
109
129
 
@@ -111,11 +131,15 @@ def countFoldsCompileBranch(connectionGraph: NDArray[integer[Any]], foldsTotal:
111
131
 
112
132
  @numba.jit(parallel=False, _nrt=True, boundscheck=False, error_model='numpy', fastmath=True, forceinline=True, looplift=False, no_cfunc_wrapper=True, no_cpython_wrapper=True, nogil=True, nopython=True)
113
133
  def countFoldsCompiled(connectionGraph: NDArray[integer[Any]], foldsTotal: NDArray[integer[Any]], my: NDArray[integer[Any]], gapsWhere: NDArray[integer[Any]], the: NDArray[integer[Any]], track: NDArray[integer[Any]]) -> int:
134
+ # ^ Receive the data structures.
114
135
 
115
- _0, my, gapsWhere, track = doWhile(connectionGraph, foldsTotal, my, gapsWhere, the, track, Z0Z_initializeUnconstrainedLeaf=True)
136
+ # Initialize baseline values primarily to eliminate the need for the logic of `insertUnconstrainedLeaf`
137
+ _0, my, gapsWhere, track = doWhile(connectionGraph, foldsTotal, my, gapsWhere, the, track, initializeUnconstrainedLeaf=True)
116
138
 
117
139
  obviousFlagForNumba = the[indexThe.taskDivisions.value] == int(False)
118
140
 
141
+ # Call the function that will branch to sequential or parallel counting
119
142
  foldsTotal = countFoldsCompileBranch(connectionGraph, foldsTotal, my, gapsWhere, the, track, obviousFlagForNumba)
120
143
 
144
+ # Return an `int` integer
121
145
  return numpy.sum(foldsTotal).item()
mapFolding/oeis.py CHANGED
@@ -65,6 +65,40 @@ settingsOEIShardcodedValues = {
65
65
  oeisIDsImplemented: Final[List[str]] = sorted([oeisID.upper().strip() for oeisID in settingsOEIShardcodedValues.keys()])
66
66
  """Directly implemented OEIS IDs; standardized, e.g., 'A001415'."""
67
67
 
68
+ def _validateOEISid(oeisIDcandidate: str):
69
+ """
70
+ Validates an OEIS sequence ID against implemented sequences.
71
+
72
+ If the provided ID is recognized within the application's implemented
73
+ OEIS sequences, the function returns the verified ID in uppercase.
74
+ Otherwise, a KeyError is raised indicating that the sequence is not
75
+ directly supported.
76
+
77
+ Parameters:
78
+ oeisIDcandidate: The OEIS sequence identifier to validate.
79
+
80
+ Returns:
81
+ oeisID: The validated and possibly modified OEIS sequence ID, if recognized.
82
+
83
+ Raises:
84
+ KeyError: If the provided sequence ID is not directly implemented.
85
+ """
86
+ if oeisIDcandidate in oeisIDsImplemented:
87
+ return oeisIDcandidate
88
+ else:
89
+ oeisIDcleaned = str(oeisIDcandidate).upper().strip()
90
+ if oeisIDcleaned in oeisIDsImplemented:
91
+ return oeisIDcleaned
92
+ else:
93
+ raise KeyError(
94
+ f"OEIS ID {oeisIDcandidate} is not directly implemented.\n"
95
+ f"Available sequences:\n{_formatOEISsequenceInfo()}"
96
+ )
97
+
98
+ def _getFilenameOEISbFile(oeisID: str) -> str:
99
+ oeisID = _validateOEISid(oeisID)
100
+ return f"b{oeisID[1:]}.txt"
101
+
68
102
  def _parseBFileOEIS(OEISbFile: str, oeisID: str) -> Dict[int, int]:
69
103
  """
70
104
  Parses the content of an OEIS b-file for a given sequence ID.
@@ -101,8 +135,6 @@ try:
101
135
  except NameError:
102
136
  _pathCache = pathlib.Path.home() / ".mapFoldingCache"
103
137
 
104
- _formatFilenameCache = "{oeisID}.txt"
105
-
106
138
  def _getOEISidValues(oeisID: str) -> Dict[int, int]:
107
139
  """
108
140
  Retrieves the specified OEIS sequence as a dictionary mapping integer indices
@@ -122,7 +154,7 @@ def _getOEISidValues(oeisID: str) -> Dict[int, int]:
122
154
  IOError: If there is an error reading from or writing to the local cache.
123
155
  """
124
156
 
125
- pathFilenameCache = _pathCache / _formatFilenameCache.format(oeisID=oeisID)
157
+ pathFilenameCache = _pathCache / _getFilenameOEISbFile(oeisID)
126
158
  cacheDays = 7
127
159
 
128
160
  tryCache = False
@@ -137,7 +169,7 @@ def _getOEISidValues(oeisID: str) -> Dict[int, int]:
137
169
  except (ValueError, IOError):
138
170
  tryCache = False
139
171
 
140
- urlOEISbFile = f"https://oeis.org/{oeisID}/b{oeisID[1:]}.txt"
172
+ urlOEISbFile = f"https://oeis.org/{oeisID}/{_getFilenameOEISbFile(oeisID)}"
141
173
  httpResponse: urllib.response.addinfourl = urllib.request.urlopen(urlOEISbFile)
142
174
  OEISbFile = httpResponse.read().decode('utf-8')
143
175
 
@@ -148,6 +180,28 @@ def _getOEISidValues(oeisID: str) -> Dict[int, int]:
148
180
  return _parseBFileOEIS(OEISbFile, oeisID)
149
181
 
150
182
  def makeSettingsOEIS() -> Dict[str, SettingsOEIS]:
183
+ """
184
+ Creates a dictionary mapping OEIS IDs to their corresponding settings.
185
+
186
+ This function initializes settings for each implemented OEIS sequence by combining
187
+ hardcoded values with dynamically retrieved OEIS sequence values.
188
+
189
+ Returns:
190
+ Dict[str, SettingsOEIS]: A dictionary where:
191
+ - Keys are OEIS sequence IDs (str)
192
+ - Values are SettingsOEIS objects containing:
193
+ - description: Text description of the sequence
194
+ - getDimensions: Function to get dimensions
195
+ - valuesBenchmark: Benchmark values
196
+ - valuesKnown: Known values from OEIS
197
+ - valuesTestValidation: Values for test validation
198
+ - valueUnknown: First unknown value in sequence
199
+
200
+ Note:
201
+ Relies on global variables:
202
+ - oeisIDsImplemented: List of implemented OEIS sequence IDs
203
+ - settingsOEIShardcodedValues: Dictionary of hardcoded settings per sequence
204
+ """
151
205
  settingsTarget = {}
152
206
  for oeisID in oeisIDsImplemented:
153
207
  valuesKnownSherpa = _getOEISidValues(oeisID)
@@ -190,36 +244,6 @@ def _formatOEISsequenceInfo() -> str:
190
244
  for oeisID in oeisIDsImplemented
191
245
  )
192
246
 
193
- def _validateOEISid(oeisIDcandidate: str):
194
- """
195
- Validates an OEIS sequence ID against implemented sequences.
196
-
197
- If the provided ID is recognized within the application's implemented
198
- OEIS sequences, the function returns the verified ID in uppercase.
199
- Otherwise, a KeyError is raised indicating that the sequence is not
200
- directly supported.
201
-
202
- Parameters:
203
- oeisIDcandidate: The OEIS sequence identifier to validate.
204
-
205
- Returns:
206
- oeisID: The validated and possibly modified OEIS sequence ID, if recognized.
207
-
208
- Raises:
209
- KeyError: If the provided sequence ID is not directly implemented.
210
- """
211
- if oeisIDcandidate in oeisIDsImplemented:
212
- return oeisIDcandidate
213
- else:
214
- oeisIDcleaned = str(oeisIDcandidate).upper().strip()
215
- if oeisIDcleaned in oeisIDsImplemented:
216
- return oeisIDcleaned
217
- else:
218
- raise KeyError(
219
- f"OEIS ID {oeisIDcandidate} is not directly implemented.\n"
220
- f"Available sequences:\n{_formatOEISsequenceInfo()}"
221
- )
222
-
223
247
  """
224
248
  Section: public functions"""
225
249
 
@@ -286,7 +310,7 @@ def clearOEIScache() -> None:
286
310
  return
287
311
  else:
288
312
  for oeisID in settingsOEIS:
289
- pathFilenameCache = _pathCache / _formatFilenameCache.format(oeisID=oeisID)
313
+ pathFilenameCache = _pathCache / _getFilenameOEISbFile(oeisID)
290
314
  pathFilenameCache.unlink(missing_ok=True)
291
315
 
292
316
  print(f"Cache cleared from {_pathCache}")