pelutils 4.2.1__cp315-cp315-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. _pelutils_c.cp315-win_amd64.pyd +0 -0
  2. pelutils/__init__.py +3 -0
  3. pelutils/__version__.py +2 -0
  4. pelutils/_c/__init__.py +29 -0
  5. pelutils/_entry_points/__init__.py +0 -0
  6. pelutils/_entry_points/linecounter.py +149 -0
  7. pelutils/array/__init__.py +48 -0
  8. pelutils/array/_blob.py +173 -0
  9. pelutils/array/_unique.py +77 -0
  10. pelutils/array/_unique.pyi +80 -0
  11. pelutils/array/_utils.py +31 -0
  12. pelutils/job_parser/__init__.py +61 -0
  13. pelutils/job_parser/_job_parser.py +362 -0
  14. pelutils/job_parser/_structs.py +268 -0
  15. pelutils/logging/__init__.py +38 -0
  16. pelutils/logging/_logger.py +331 -0
  17. pelutils/logging/_rich_string.py +49 -0
  18. pelutils/logging/_rotate.py +154 -0
  19. pelutils/misc/__init__.py +29 -0
  20. pelutils/misc/_conditional_import.py +21 -0
  21. pelutils/misc/_files.py +60 -0
  22. pelutils/misc/_git.py +31 -0
  23. pelutils/misc/_misc.py +13 -0
  24. pelutils/misc/_platform.py +74 -0
  25. pelutils/misc/_table.py +113 -0
  26. pelutils/plots/__init__.py +50 -0
  27. pelutils/plots/_figure.py +116 -0
  28. pelutils/plots/_histogram.py +50 -0
  29. pelutils/plots/_utils.py +36 -0
  30. pelutils/py.typed +0 -0
  31. pelutils/serialization/__init__.py +51 -0
  32. pelutils/serialization/_jsonl.py +38 -0
  33. pelutils/serialization/_pretty_json.py +274 -0
  34. pelutils/serialization/_universal_json_model.py +54 -0
  35. pelutils/stats/__init__.py +53 -0
  36. pelutils/stats/_distributions.py +93 -0
  37. pelutils/stats/_stats.py +26 -0
  38. pelutils/tests/__init__.py +32 -0
  39. pelutils/tests/_tests.py +88 -0
  40. pelutils/ticktock/__init__.py +42 -0
  41. pelutils/ticktock/_ticktock.py +450 -0
  42. pelutils/types/__init__.py +25 -0
  43. pelutils/types/_types.py +17 -0
  44. pelutils-4.2.1.dist-info/METADATA +387 -0
  45. pelutils-4.2.1.dist-info/RECORD +49 -0
  46. pelutils-4.2.1.dist-info/WHEEL +5 -0
  47. pelutils-4.2.1.dist-info/entry_points.txt +2 -0
  48. pelutils-4.2.1.dist-info/licenses/pelutils/_c/hashmap/LICENSE +20 -0
  49. pelutils-4.2.1.dist-info/top_level.txt +2 -0
Binary file
pelutils/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .__version__ import __version__
2
+
3
+ __all__ = ("__version__",)
@@ -0,0 +1,2 @@
1
+ # Do not put anything else in this file
2
+ __version__ = "4.2.1"
@@ -0,0 +1,29 @@
1
+ """Utilities for pelutils C api, including a helpful way of passing numpy arrays and torch tensors to C."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ import numpy.typing as npt
7
+
8
+ from pelutils.misc._conditional_import import import_torch
9
+ from pelutils.types import AnyArray as AnyArray
10
+
11
+ torch = import_torch()
12
+
13
+
14
+ # Data pointer, num dims, dimensions pointer, strides pointer
15
+ class ArrayArgs:
16
+ def __init__(self, arr: npt.ArrayLike):
17
+ if torch is not None and isinstance(arr, torch.Tensor):
18
+ arr = arr.numpy()
19
+ if not isinstance(arr, np.ndarray):
20
+ raise TypeError(f"Array cannot be of type {type(arr)}")
21
+ # Store attributes on self to prevent garbage collector cleaning them up until the object no longer exists
22
+ self._arr = arr
23
+ self._dims = np.array(self._arr.shape, dtype=np.uint64)
24
+ self._strides = np.array(self._arr.strides, dtype=np.uint64)
25
+
26
+ self.array_ptr = self._arr.ctypes.data
27
+ self.ndim = len(self._dims)
28
+ self.dims_ptr = self._dims.ctypes.data
29
+ self.strides_ptr = self._strides.ctypes.data
File without changes
@@ -0,0 +1,149 @@
1
+ import os
2
+ from argparse import ArgumentParser
3
+
4
+ import git
5
+ import matplotlib.pyplot as plt
6
+ import numpy as np
7
+ from tqdm import tqdm
8
+
9
+ from pelutils.misc import git_repo_info
10
+ from pelutils.plots import Figure, get_dateticks
11
+ from pelutils.types import FloatArray, IntArray
12
+
13
+ _default_extensions = ", ".join((
14
+ ".py",
15
+ ".pyw",
16
+ ".c",
17
+ ".cpp",
18
+ ".cc",
19
+ ".h",
20
+ ".hpp",
21
+ ".tex",
22
+ ".html",
23
+ ".css",
24
+ ".js",
25
+ ".jsx",
26
+ ".ts",
27
+ ".java",
28
+ ".kt",
29
+ ".dart",
30
+ ".swift",
31
+ ".rs",
32
+ ".go",
33
+ ".r",
34
+ ".m",
35
+ ".sh",
36
+ ".zsh",
37
+ ".bash",
38
+ ))
39
+
40
+
41
+ def _count(repo: git.Repo, branch: git.Head, exts: list[str]) -> tuple[FloatArray, dict[str, IntArray]]:
42
+ """Count lines of all files recursively in the current working directory.
43
+
44
+ Return an array of commit epoch times and a dict mapping file extensions to line counts
45
+ """
46
+ commits = list(reversed(list(repo.iter_commits()))) # List of commit from oldest to newest
47
+ times = np.array([c.committed_date for c in commits])
48
+ lines = {ext: np.zeros_like(times) for ext in exts}
49
+ try:
50
+ for i, commit in enumerate(tqdm(commits)):
51
+ try:
52
+ repo.git.checkout(str(commit))
53
+ except git.GitCommandError as e:
54
+ # This error happens if there is a file that has been moved in and out of .gitignore
55
+ # For safety, the commit is ignored
56
+ if "error: The following untracked working tree files would be overwritten by checkout:" in str(e):
57
+ continue
58
+ raise
59
+ for path, __ in repo.index.entries:
60
+ try:
61
+ ext = next(e for e in exts if path.endswith(e) and os.path.isfile(path)) # pyright: ignore[reportAttributeAccessIssue]
62
+ except StopIteration:
63
+ continue
64
+ with open(path) as f:
65
+ lines[ext][i] += len([line for line in f if line.strip()])
66
+ finally:
67
+ repo.git.checkout(branch)
68
+
69
+ return times, lines
70
+
71
+
72
+ def _fuse_times(all_times: list[FloatArray]) -> FloatArray:
73
+ """Merge all time arrays together into a single array that is also sorted."""
74
+ n = sum(times.size for times in all_times)
75
+ times = np.empty(n, dtype=int)
76
+ idcs = [0] * len(all_times)
77
+ for i in range(n):
78
+ current_times = [time[idx] if idx < len(time) else float("inf") for time, idx in zip(all_times, idcs, strict=True)]
79
+ min_time = np.argmin(current_times)
80
+ times[i] = current_times[min_time]
81
+ idcs[min_time] += 1
82
+ return times
83
+
84
+
85
+ def _last_initial_zero(values: IntArray) -> int:
86
+ if values[0] != 0:
87
+ return 0
88
+ return np.where(values != 0)[0][0] - 1
89
+
90
+
91
+ def linecounter(repos: list[str], output: str, extensions: str, date_format: str, no_repo_name: bool):
92
+ file_extensions = [x.strip() for x in extensions.split(",")]
93
+ file_extensions = [x if x.startswith(".") else "." + x for x in file_extensions]
94
+ wd = os.getcwd()
95
+ repo_names, all_times, all_counts = list(), list(), list()
96
+ for possible_repo_path in repos:
97
+ repo_path, __ = git_repo_info(possible_repo_path)
98
+ if repo_path is None:
99
+ raise ValueError(f"{repo_path} is not a git repository")
100
+ os.chdir(repo_path)
101
+
102
+ repo = git.Repo()
103
+ branch = repo.active_branch
104
+ try:
105
+ times, lines = _count(repo, branch, file_extensions)
106
+ finally:
107
+ # Reset repo
108
+ repo.git.checkout(branch)
109
+
110
+ repo_names.append(os.path.split(repo_path)[-1])
111
+ all_times.append(times)
112
+ all_counts.append(lines)
113
+ os.chdir(wd)
114
+
115
+ with Figure(output):
116
+ repo_name: str | None = None
117
+ for _i, (repo_name, times, counts) in enumerate(zip(repo_names, all_times, all_counts, strict=True)):
118
+ for ext, line_counts in counts.items():
119
+ if not line_counts.any():
120
+ continue
121
+ non_zero = line_counts != 0
122
+ non_zero[_last_initial_zero(line_counts)] = 1
123
+ lab = ext + (f" ({repo_name})" if len(repo_names) > 1 and not no_repo_name else "")
124
+ plt.plot(times[non_zero], line_counts[non_zero], marker=".", ms=8, lw=2, label=lab)
125
+
126
+ fused_times = _fuse_times(all_times)
127
+ plt.xticks(*get_dateticks(fused_times, date_format=date_format))
128
+ title = "Line count over time" + (f" ({repo_name})" if len(repo_names) == 1 and not no_repo_name else "")
129
+ plt.title(title)
130
+ plt.xlabel("Date of commit")
131
+ plt.ylabel("Number of non-empty lines")
132
+ plt.legend(loc=2)
133
+ plt.grid()
134
+
135
+
136
+ def run():
137
+ parser = ArgumentParser()
138
+ parser.add_argument("repos", nargs="+")
139
+ parser.add_argument("-o", "--output", default="linecount.png", help="Path to produce output figure to.")
140
+ parser.add_argument("-e", "--extensions", default=_default_extensions, help="Comma separated list of file extensions to look for.")
141
+ parser.add_argument("-d", "--date-format", default="%y-%m-%d", help="How to format axis labels.")
142
+ parser.add_argument("-n", "--no-repo-name", action="store_true", help="Disable repository name from the produced figure.")
143
+ args = parser.parse_args()
144
+
145
+ linecounter(args.repos, args.output, args.extensions, args.date_format, args.no_repo_name)
146
+
147
+
148
+ if __name__ == "__main__":
149
+ run()
@@ -0,0 +1,48 @@
1
+ """Utilities and various algorithms for working with numpy arrays.
2
+
3
+ The flagship is :func:`unique`, a drop-in for ``numpy.unique`` that runs in linear time
4
+ (rather than sorting), making it dramatically faster on large arrays.
5
+
6
+ Quick start
7
+ -----------
8
+
9
+ .. code-block:: python
10
+
11
+ import numpy as np
12
+ from pelutils.array import unique
13
+
14
+ x = np.random.randint(0, 100, size=10_000_000)
15
+ values = unique(x)
16
+ values, index, inverse, counts = unique(
17
+ x, return_index=True, return_inverse=True, return_counts=True,
18
+ )
19
+
20
+ :class:`SparseGridBlobDetection` is used for detecting blobs in a sparse, N-dimensional grid.
21
+ It also works for dense grids (effetively boolean numpy arrays), which can trivially be converted
22
+ to sparse grids using ``np.where`` and ``np.column_stack``, as shown in the example.
23
+
24
+ A blob is defined as an area of touching grid entries which are all ``True``. An obvious example
25
+ is finding areas that are left in an image after thresholding on pixel values.
26
+
27
+ .. code-block:: python
28
+
29
+ greyscale_image = ... # Numpy array of shape height x width
30
+ thresholded = greyscale_image > 100
31
+ # Find the indices in the thresholded image which are True and stack them into an n x 2 array
32
+ coords_above_threshold = np.column_stack(np.where(thresholded))
33
+
34
+ blob_detector = SparseGridBlobDetection(coords_above_threshold)
35
+ blobs = blob_detector.find_all_blobs()
36
+
37
+ # The pixel coordinates in the first blob are fetched with
38
+ coords_above_threshold[blobs[0]]
39
+
40
+ Both :func:`unique` and :class:`SparseGridBlobDetection` are implemented in C, making them
41
+ blazingly fast compared to what you usually expect from Python.
42
+ """
43
+
44
+ from ._blob import SparseGridBlobDetection
45
+ from ._unique import unique
46
+ from ._utils import array_bytes, array_ptr
47
+
48
+ __all__ = ("SparseGridBlobDetection", "array_bytes", "array_ptr", "unique")
@@ -0,0 +1,173 @@
1
+ from __future__ import annotations
2
+
3
+ import _pelutils_c as _c
4
+ import numpy as np
5
+
6
+ import pelutils._c as c_utils
7
+ from pelutils.types import BoolArray, IntArray
8
+
9
+
10
+ class SparseGridBlobDetection:
11
+ """Detect blobs (continuous regions) in a sparse grid, implemented in C for high performance.
12
+
13
+ The grids can have an arbitrary number of dimensions, but a common usecase might be in image analysis.
14
+ Imaging thresholding an image based on pixel values. This class offers blazingly fast detection of all
15
+ blobs in the resulting boolean image.
16
+
17
+ While images are a prime use case, this class works for any dimensionality of arrays.
18
+ It provides two methods: ``find_all_blobs`` and ``find_single_blob``. The first, as demonstrated above, detects,
19
+ for each blobs, all pixels belonging to that blob. The second finds only a single blob and optionally accepts a
20
+ starting pixel.
21
+
22
+ Adjacency is defined by a plus-shaped kernel (generalised to the dimensions of the array), not a solid square-shaped
23
+ kernel. To get the behaviour of a square-shaped kernel, run a dilation with a square kernel over the array first.
24
+
25
+ Note that this class is stateful, meaning that for each call to either of the afforementioned methods, trying to
26
+ detected an already detected blob will raise a ``RuntimeError``.
27
+
28
+ Parameters
29
+ ----------
30
+ grid_coords : IntArray
31
+ Coordinates in the grid that are part of any blob. For an ``d``-dimensional grid, it has shape ``n x d`` where
32
+ ``n`` is the number of nodes in the grid belonging to any blob. For a grid representated by a boolean numpy
33
+ array, the coordinates can be gotten with ``np.column_stack(np.where(grid))``.
34
+
35
+ Note that even though ``grid_coords`` commonly would represent axis coordinates in a numpy array,
36
+ negative coordinates are fully supported.
37
+
38
+ wrap_axes : dict[int, int] | None, optional
39
+ A dictionary mapping axis numbers to their corresponding axis lengths
40
+ in the grid. Values will be wrapped on that axis. Negative axis numbers are supported.
41
+
42
+ As an example, if ``wrap_axes = {2: 10}``, the third column in ``grid_coords`` will be wrapped to
43
+ the range (0, 9), and the coordinates 0 and 9 will be considered next to each other. This is
44
+ effectively the same as doing ``grid_coords[:, 2] %= 10``, and pretending the ends touch each other.
45
+ """
46
+
47
+ def __init__(self, grid_coords: IntArray, wrap_axes: dict[int, int] | None = None):
48
+ """Initialise the blob detection by building necessary data structures."""
49
+ self._validate_grid_coords(grid_coords)
50
+
51
+ # Ensure correct dtype and make a copy to prevent both inside and outside changes to the array unintentionally causing a ruckus
52
+ self._grid_coords = grid_coords.astype(np.int64).copy()
53
+ # Boolean array of visited nodes
54
+ # After each call to `find_blob`, the found indices are set to True
55
+ self._visited = np.zeros(self._grid_coords.shape[0], dtype=bool)
56
+ # Next row in _grid_coords from which to start a search
57
+ self._next_index = 0
58
+ # For each axes that should be wrapped, the size of the axis in the grid is stored
59
+ # For axes which should not be wrapped, 0 is used
60
+ self._wrap_axis_sizes = np.zeros(self._grid_coords.shape[1], dtype=np.uint64)
61
+ wrap_axes = wrap_axes or dict()
62
+ for axis, axis_size in wrap_axes.items():
63
+ self._wrap_axis_sizes[axis] = axis_size
64
+
65
+ # The pointers array is used to store pointers from objects allocated in the C code and pass them between calls
66
+ # This is very ugly and hacky but it works and is arguably simpler that using the Python C API correctly in this
67
+ # use case - at least I'm lazy enough to assume so
68
+ # This first element is the pointer to the coord-to-index hashmap
69
+ self._pointers = np.empty(1, dtype=np.uint64)
70
+
71
+ self._index_args = c_utils.ArrayArgs(self._grid_coords)
72
+ self._pointer_args = c_utils.ArrayArgs(self._pointers)
73
+ self._wrap_axis_sizes_args = c_utils.ArrayArgs(self._wrap_axis_sizes)
74
+ _c.build_lookup_table(
75
+ self._pointer_args.array_ptr,
76
+ self._wrap_axis_sizes_args.array_ptr,
77
+ self._index_args.array_ptr,
78
+ self._grid_coords.shape[0],
79
+ self._grid_coords.shape[1],
80
+ )
81
+
82
+ @property
83
+ def done(self) -> bool:
84
+ """True if all coordinates have been explored and there are no more blobs to discover."""
85
+ return self._next_index == len(self._grid_coords)
86
+
87
+ @property
88
+ def next_index(self) -> int:
89
+ """Return the index of the first unvisited coordinate.
90
+
91
+ This starts at ``0`` and advances as blobs are detected, whether by :meth:`find_single_blob` or :meth:`find_all_blobs`.
92
+ It reaches ``len(grid_coords)`` when all coordinates have been visited, and there are no more blobs to be found.
93
+ This is equivalent to :meth:`done` being ``True``.
94
+
95
+ When a blob is detected, if the current value is part of that blob, it will increase to the index of the first coordinate
96
+ which has not been visited.
97
+ """
98
+ return self._next_index
99
+
100
+ @property
101
+ def visited_coords(self) -> BoolArray:
102
+ """Return 1D boolean array of length ``len(grid_coords)`` indicating which grid coordinates have been visited."""
103
+ visited_coords = self._visited.view()
104
+ visited_coords.flags.writeable = False
105
+ return visited_coords
106
+
107
+ def _validate_grid_coords(self, grid_coords: IntArray):
108
+ """Raise an exception if given ``grid_coords`` are invalid."""
109
+ if not np.issubdtype(grid_coords.dtype, np.integer):
110
+ raise TypeError(f"`grid_coords` must have an integer dtype, not {grid_coords.dtype}")
111
+ if len(grid_coords.shape) != 2:
112
+ raise ValueError(f"`grid_coords` must have shape n x d but has {len(grid_coords.shape)} axes")
113
+ if grid_coords.shape[1] == 0:
114
+ raise ValueError("Coordinates cannot be 0-dimensional")
115
+
116
+ def _mark_visited(self, indices: IntArray):
117
+ self._visited[indices] = True
118
+ while self._next_index < len(self._visited) and self._visited[self._next_index]:
119
+ self._next_index += 1
120
+
121
+ def find_single_blob(self, init_index: int | None = None) -> IntArray:
122
+ """Detect one blob starting at ``grid_coords[init_index]``, or ``grid_coords[self.next_index]`` if ``init_index`` is not provided.
123
+
124
+ Return the index of each row in ``grid_coords`` which is part of the blob.
125
+ """
126
+ if init_index is None:
127
+ init_index = self.next_index
128
+ if init_index >= len(self._grid_coords):
129
+ raise IndexError(f"Cannot start blob search from index {init_index} as there are only {len(self._grid_coords)} coordinates")
130
+ if self._visited[init_index]:
131
+ raise RuntimeError(f"Blob at index {init_index} ({self._grid_coords[init_index]}) has already been visited")
132
+ blob_indices: list[int] = list()
133
+ _c.find_blob(
134
+ self._pointer_args.array_ptr,
135
+ self._wrap_axis_sizes_args.array_ptr,
136
+ self._index_args.array_ptr,
137
+ init_index,
138
+ self._grid_coords.shape[0],
139
+ self._grid_coords.shape[1],
140
+ blob_indices,
141
+ )
142
+
143
+ blob_indices_arr = np.array(blob_indices)
144
+ self._mark_visited(blob_indices_arr)
145
+ return blob_indices_arr
146
+
147
+ def find_all_blobs(self) -> list[IntArray]:
148
+ """Detect all blobs in ``grid_coords``.
149
+
150
+ For each detected blob, an array is returned containing the rows in ``grid_coords`` which are part of that blob.
151
+ """
152
+ if len(self._grid_coords) == 0:
153
+ return list()
154
+ if self.done:
155
+ raise RuntimeError("Blob detection has already been run")
156
+
157
+ indices = np.array([], dtype=np.int64)
158
+ blobs: list[IntArray] = list()
159
+ while not self.done:
160
+ indices = self.find_single_blob()
161
+ blobs.append(indices)
162
+ self._mark_visited(indices)
163
+
164
+ return blobs
165
+
166
+ def __del__(self):
167
+ """Free the lookup table when the instance is no longer referenced to prevent memory leak."""
168
+ try:
169
+ _c.free_lookup_table(self._pointer_args.array_ptr)
170
+ except AttributeError:
171
+ # If the error happens early in init (e.g. wrong call to __init__), _pointer_args has not yet been defined,
172
+ # and the hashmap has not been created
173
+ pass
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TypeVar
4
+
5
+ import _pelutils_c as _c
6
+ import numpy as np
7
+ import numpy.typing as npt
8
+
9
+ import pelutils._c as c_utils
10
+ from pelutils.types import AnyArray
11
+
12
+ _ArrayT = TypeVar("_ArrayT", bound=AnyArray)
13
+
14
+
15
+ def unique(
16
+ array: npt.ArrayLike,
17
+ *,
18
+ return_index: bool = False,
19
+ return_inverse: bool = False,
20
+ return_counts: bool = False,
21
+ axis: int = 0,
22
+ ):
23
+ """Return unique elements in a given numpy array.
24
+
25
+ This function works very similar to ``np.unique``, but it runs in linear time, making it significantly faster
26
+ for large arrays. Unlike with ``np.unique``, the returned unique values are unsorted. Because of this, it can
27
+ also be used for detecting uniqueness along axes when ordering along the respective axes matters.
28
+ """
29
+ array = np.asarray(array)
30
+ if np.issubdtype(array.dtype, np.object_):
31
+ raise TypeError(f"Unsupported array dtype {array.dtype}")
32
+
33
+ if array.ndim == 0:
34
+ raise ValueError("unique does not work for shapeless arrays")
35
+
36
+ if axis:
37
+ axes = list(range(len(array.shape)))
38
+ axes[0] = axis
39
+ axes[axis] = 0
40
+ array = array.transpose(axes)
41
+ else:
42
+ axes = None
43
+ if not array.flags["C_CONTIGUOUS"]:
44
+ array = np.ascontiguousarray(array)
45
+
46
+ index = np.empty(len(array), dtype=np.int64)
47
+ inverse = np.empty(len(array), dtype=np.int64) if return_inverse else None
48
+ counts = np.empty(len(array), dtype=np.int64) if return_counts else None
49
+
50
+ array_args = c_utils.ArrayArgs(array)
51
+ c = _c.unique(
52
+ array_args.array_ptr,
53
+ array_args.ndim,
54
+ array_args.dims_ptr,
55
+ array_args.strides_ptr,
56
+ index.ctypes.data,
57
+ inverse.ctypes.data if inverse is not None else 0,
58
+ counts.ctypes.data if counts is not None else 0,
59
+ )
60
+
61
+ index = index[:c]
62
+ if axis:
63
+ array = array[index]
64
+ array = np.ascontiguousarray(array.transpose(axes))
65
+ else:
66
+ array = array[index]
67
+ ret = [array]
68
+ if return_index:
69
+ ret.append(index)
70
+ if return_inverse:
71
+ assert inverse is not None
72
+ ret.append(inverse)
73
+ if return_counts:
74
+ assert counts is not None
75
+ ret.append(counts[index])
76
+
77
+ return tuple(ret) if len(ret) > 1 else ret[0]
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal, overload
4
+
5
+ import numpy.typing as npt
6
+
7
+ from pelutils.types import AnyArray, IntArray
8
+
9
+ @overload
10
+ def unique(
11
+ array: npt.ArrayLike,
12
+ *,
13
+ return_index: Literal[False] = False,
14
+ return_inverse: Literal[False] = False,
15
+ return_counts: Literal[False] = False,
16
+ axis: int = 0,
17
+ ) -> AnyArray: ...
18
+ @overload
19
+ def unique(
20
+ array: npt.ArrayLike,
21
+ *,
22
+ return_index: Literal[True],
23
+ return_inverse: Literal[False] = False,
24
+ return_counts: Literal[False] = False,
25
+ axis: int = 0,
26
+ ) -> tuple[AnyArray, IntArray]: ...
27
+ @overload
28
+ def unique(
29
+ array: npt.ArrayLike,
30
+ *,
31
+ return_index: Literal[False] = False,
32
+ return_inverse: Literal[True],
33
+ return_counts: Literal[False] = False,
34
+ axis: int = 0,
35
+ ) -> tuple[AnyArray, IntArray]: ...
36
+ @overload
37
+ def unique(
38
+ array: npt.ArrayLike,
39
+ *,
40
+ return_index: Literal[True],
41
+ return_inverse: Literal[True],
42
+ return_counts: Literal[False] = False,
43
+ axis: int = 0,
44
+ ) -> tuple[AnyArray, IntArray, IntArray]: ...
45
+ @overload
46
+ def unique(
47
+ array: npt.ArrayLike,
48
+ *,
49
+ return_index: Literal[False] = False,
50
+ return_inverse: Literal[False] = False,
51
+ return_counts: Literal[True],
52
+ axis: int = 0,
53
+ ) -> tuple[AnyArray, IntArray]: ...
54
+ @overload
55
+ def unique(
56
+ array: npt.ArrayLike,
57
+ *,
58
+ return_index: Literal[True],
59
+ return_inverse: Literal[False] = False,
60
+ return_counts: Literal[True],
61
+ axis: int = 0,
62
+ ) -> tuple[AnyArray, IntArray, IntArray]: ...
63
+ @overload
64
+ def unique(
65
+ array: npt.ArrayLike,
66
+ *,
67
+ return_index: Literal[False] = False,
68
+ return_inverse: Literal[True],
69
+ return_counts: Literal[True],
70
+ axis: int = 0,
71
+ ) -> tuple[AnyArray, IntArray, IntArray]: ...
72
+ @overload
73
+ def unique(
74
+ array: npt.ArrayLike,
75
+ *,
76
+ return_index: Literal[True],
77
+ return_inverse: Literal[True],
78
+ return_counts: Literal[True],
79
+ axis: int = 0,
80
+ ) -> tuple[AnyArray, IntArray, IntArray, IntArray]: ...
@@ -0,0 +1,31 @@
1
+ import ctypes
2
+
3
+ import numpy as np
4
+ import numpy.typing as npt
5
+
6
+ from pelutils.misc._conditional_import import import_torch
7
+
8
+ torch = import_torch()
9
+
10
+
11
+ def array_ptr(arr: npt.ArrayLike) -> ctypes.c_void_p:
12
+ """Return a pointer to a numpy array or torch tensor which can be used to interact with it in low-level languages like C/C++/Rust.
13
+
14
+ This function is mostly useful when not using Python's C api and instead interfacing with .so files directly with ctypes.
15
+ """
16
+ if torch is not None and isinstance(arr, torch.Tensor):
17
+ return ctypes.c_void_p(arr.data_ptr())
18
+ if not isinstance(arr, np.ndarray):
19
+ raise TypeError(f"Array should be of type np.ndarray or torch.Tensor, not {type(arr)}")
20
+ if not arr.flags.c_contiguous:
21
+ raise ValueError("Array must be C-contiguous")
22
+ return ctypes.c_void_p(arr.ctypes.data)
23
+
24
+
25
+ def array_bytes(x: npt.ArrayLike) -> int:
26
+ """Calculate the size of a numpy array or torch tensor in bytes."""
27
+ if torch is not None and isinstance(x, torch.Tensor):
28
+ x = x.numpy()
29
+ if isinstance(x, np.ndarray):
30
+ return x.nbytes
31
+ raise TypeError(f"`x` of type {type(x)} is not a numpy array or torch tensor")
@@ -0,0 +1,61 @@
1
+ """Typed argument and configuration parsing that treats a run as a *job*.
2
+
3
+ ``argparse`` handles command-line flags but knows nothing about config files, so the
4
+ moment you want to save a run's settings, reproduce it later, or launch a sweep of
5
+ several runs, you end up hand-rolling a config file parser, merging it with the CLI args by
6
+ hand, and writing your own record of what was actually run. :class:`JobParser` does all
7
+ of that: you declare your arguments once with a type, and it resolves values from the
8
+ command line *and* an INI config file into one or more :class:`JobDescription` objects —
9
+ CLI values overriding config values, which override declared defaults. A single config
10
+ file can describe many named jobs, and every resolved job can write out an exact,
11
+ human-readable record of how it was invoked.
12
+
13
+ Quick start
14
+ -----------
15
+
16
+ .. code-block:: python
17
+
18
+ from pathlib import Path
19
+ from pelutils.job_parser import Flag, JobParser, OptionalArg, RequiredArg
20
+
21
+ parser = JobParser(
22
+ RequiredArg("data-path", help="Training data directory"),
23
+ OptionalArg("learning-rate", default=1e-4, type=float, help="Optimizer learning rate"),
24
+ Flag("fp16", help="Use mixed precision"),
25
+ multiple_jobs=True,
26
+ )
27
+
28
+ for job in parser.parse_jobs():
29
+ # Names are --kebab-case on the CLI and snake_case attributes on the job
30
+ print(job.name, job.data_path, job.learning_rate, job.fp16)
31
+ job.write_documentation(Path("runs") / job.name / "arguments.ini")
32
+ ... # Run your application with the resolved job values
33
+
34
+ Declare :class:`RequiredArg` for values every job must supply, :class:`OptionalArg` for
35
+ values with a default, and :class:`Flag` for booleans. Point the parser at a config file
36
+ with ``--config-file config.ini``; a file may contain a shared ``[DEFAULT]`` section plus
37
+ named sections that each become a job. Target a single section directly with
38
+ ``--config-file config.ini:section-name``. A config file for the parser above might look
39
+ like:
40
+
41
+ .. code-block:: ini
42
+
43
+ [DEFAULT]
44
+ data-path = /data/train # Shared by every job below
45
+
46
+ [baseline]
47
+ # Inherits the default learning rate; fp16 as a bare key means True
48
+ fp16
49
+
50
+ [high-lr]
51
+ learning-rate = 5e-4
52
+
53
+ For the common single-job case, drop ``multiple_jobs=True`` and call :meth:`JobParser.parse_job`
54
+ to get one :class:`JobDescription` back. See :class:`JobParser` for the full API and
55
+ :meth:`JobDescription.write_documentation` for auto-documenting a run.
56
+ """
57
+
58
+ from ._job_parser import JobParser
59
+ from ._structs import ArgumentTypes, ConfigError, Flag, JobDescription, JobParserError, OptionalArg, RequiredArg
60
+
61
+ __all__ = ("ArgumentTypes", "ConfigError", "Flag", "JobDescription", "JobParser", "JobParserError", "OptionalArg", "RequiredArg")