subhaloscript 1.1.2__tar.gz → 1.1.4__tar.gz

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 (36) hide show
  1. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/.claude/skills/galacticus-analysis/SKILL.md +14 -2
  2. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/.claude/skills/galacticus-analysis/references/subscript_functions.md +36 -11
  3. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/.claude/skills/update/SKILL.md +18 -6
  4. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/PKG-INFO +1 -1
  5. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/meta.yaml +1 -1
  6. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/pyproject.toml +1 -1
  7. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/subscript/scripts/nfilters.py +47 -21
  8. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/tests/test_nfilters.py +68 -2
  9. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/.github/workflows/main.yml +0 -0
  10. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/.gitignore +0 -0
  11. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/LICENSE +0 -0
  12. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/README.md +0 -0
  13. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/example-notebooks/basic-usage.ipynb +0 -0
  14. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/example-notebooks/units.ipynb +0 -0
  15. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/subscript/defaults.py +0 -0
  16. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/subscript/external.py +0 -0
  17. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/subscript/macros.py +0 -0
  18. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/subscript/scripts/histograms.py +0 -0
  19. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/subscript/scripts/nodes.py +0 -0
  20. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/subscript/scripts/spatial.py +0 -0
  21. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/subscript/subhalo_timeseries.py +0 -0
  22. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/subscript/tabulatehdf5.py +0 -0
  23. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/subscript/tracking.py +0 -0
  24. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/subscript/units.py +0 -0
  25. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/subscript/util.py +0 -0
  26. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/subscript/wrappers.py +0 -0
  27. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/tests/data/dump.txt +0 -0
  28. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/tests/test_histograms.py +0 -0
  29. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/tests/test_macros.py +0 -0
  30. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/tests/test_nfilters_legacy.py +0 -0
  31. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/tests/test_nodes.py +0 -0
  32. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/tests/test_spatial.py +0 -0
  33. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/tests/test_symphony.py +0 -0
  34. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/tests/test_tabulatehdf5.py +0 -0
  35. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/tests/test_units.py +0 -0
  36. {subhaloscript-1.1.2 → subhaloscript-1.1.4}/tests/test_wrappers.py +0 -0
@@ -3,9 +3,11 @@
3
3
  **SubScript** is a Python library providing ergonomic utility functions for analyzing Galacticus semi-analytic model outputs.
4
4
 
5
5
  **Repository:** https://github.com/cgannonucm/SubScript
6
- **Package:** `subhaloscript` (v1.1.2)
6
+ **Package:** `subhaloscript` (v1.1.4)
7
7
  **Author:** Charles Gannon (cgannon@ucmerced.edu)
8
8
 
9
+ > **Note:** Refer to SubScript as a "library" (preferred), "package", or "toolkit" — never as an "API".
10
+
9
11
  ---
10
12
 
11
13
  ## Quick Start
@@ -223,7 +225,7 @@ nf.subhalos_valid(gout, mass_min=1e9, mass_max=1e12) # All filters combined
223
225
 
224
226
  ### Filter Combinations
225
227
 
226
- Combine filters using boolean logic:
228
+ Combine filters using boolean logic. `logical_and()` and `logical_or()` accept any number of positional arguments:
227
229
 
228
230
  ```python
229
231
  from subscript.scripts.nfilters import logical_and, logical_or, logical_not
@@ -234,9 +236,19 @@ combined = nf.logical_and(
234
236
  nf.r3d(None, 0, 0.05) # Passing None "freezes" arguments
235
237
  )
236
238
 
239
+ # AND: Three or more conditions as positional args
240
+ combined3 = nf.logical_and(
241
+ nf.subhalos,
242
+ nf.r3d(None, 0, 0.05),
243
+ nf.interval(None, 1e9, 1e12, key=ParamKeys.mass_bound)
244
+ )
245
+
237
246
  # OR: Either condition true
238
247
  either = nf.logical_or(filter1, filter2)
239
248
 
249
+ # OR: Multiple conditions as positional args
250
+ any_match = nf.logical_or(filter1, filter2, filter3)
251
+
240
252
  # NOT: Invert condition
241
253
  inverted = nf.logical_not(nf.subhalos) # Select only host halos
242
254
  ```
@@ -1,8 +1,8 @@
1
1
  # SubScript Function Reference
2
2
 
3
- Complete documentation of all functions in the SubScript library (v1.1.2), organized by module.
3
+ Complete documentation of all functions in the SubScript library (v1.1.4), organized by module.
4
4
 
5
- **Last Updated:** 2026-02-20
5
+ **Last Updated:** 2026-02-23
6
6
  **Repository:** https://github.com/cgannonucm/SubScript
7
7
 
8
8
  ---
@@ -541,36 +541,53 @@ valid_mass = nodedata(gout, ParamKeys.mass_bound, nfilter=valid_filter)
541
541
  **Signature:**
542
542
  ```python
543
543
  def logical_and(arg1: (np.ndarray[bool] | Callable),
544
- arg2: (np.ndarray[bool] | Callable)) → Callable
544
+ arg2: (np.ndarray[bool] | Callable),
545
+ *args: (np.ndarray[bool] | Callable)) → Callable
545
546
  ```
546
547
 
547
- **Description:** Create logical AND of two filters or boolean arrays.
548
+ **Description:** Create logical AND of two or more filters or boolean arrays. Accepts any number of additional positional arguments.
549
+
550
+ **Parameters:**
551
+ - `arg1` (np.ndarray[bool] | Callable): First condition
552
+ - `arg2` (np.ndarray[bool] | Callable): Second condition
553
+ - `*args` (np.ndarray[bool] | Callable): Additional conditions to AND together
548
554
 
549
555
  ```python
550
- from subscript.scripts.nfilters import logical_and, subhalos, r3d
556
+ from subscript.scripts.nfilters import logical_and, subhalos, r3d, interval
551
557
 
552
- # Combine filters
558
+ # Combine two filters
553
559
  inner_subhalos = logical_and(subhalos, r3d(None, 0, 0.1))
554
560
  mass = nodedata(gout, ParamKeys.mass_bound, nfilter=inner_subhalos)
555
561
 
556
- # Combine arrays
557
- array_and = logical_and(bool_array1, bool_array2)
562
+ # Combine three or more filters as positional args
563
+ combined = logical_and(
564
+ subhalos,
565
+ r3d(None, 0, 0.1),
566
+ interval(None, 1e9, 1e12, key=ParamKeys.mass_bound)
567
+ )
558
568
  ```
559
569
 
560
570
  **Notes:**
561
571
  - Works with both callables (filters) and arrays
562
572
  - Returns callable that can be used as `nfilter`
563
- - Implements: `arg1(...) & arg2(...)`
573
+ - Raises `ValueError` if argument shapes mismatch
574
+ - Implements: `arg1(...) & arg2(...) & args[0](...) & ...`
564
575
 
565
576
  #### logical_or()
566
577
 
567
578
  **Signature:**
568
579
  ```python
569
580
  def logical_or(arg1: (np.ndarray[bool] | Callable),
570
- arg2: (np.ndarray[bool] | Callable)) → Callable
581
+ arg2: (np.ndarray[bool] | Callable),
582
+ *args: (np.ndarray[bool] | Callable)) → Callable
571
583
  ```
572
584
 
573
- **Description:** Create logical OR of two filters or boolean arrays.
585
+ **Description:** Create logical OR of two or more filters or boolean arrays. Accepts any number of additional positional arguments.
586
+
587
+ **Parameters:**
588
+ - `arg1` (np.ndarray[bool] | Callable): First condition
589
+ - `arg2` (np.ndarray[bool] | Callable): Second condition
590
+ - `*args` (np.ndarray[bool] | Callable): Additional conditions to OR together
574
591
 
575
592
  ```python
576
593
  # Nodes either in inner region OR above mass threshold
@@ -578,8 +595,16 @@ inner_or_massive = logical_or(
578
595
  r3d(None, 0, 0.05),
579
596
  interval(None, 1e11, np.inf, key=ParamKeys.mass_bound)
580
597
  )
598
+
599
+ # Three or more conditions as positional args
600
+ any_match = logical_or(filter1, filter2, filter3, filter4)
581
601
  ```
582
602
 
603
+ **Notes:**
604
+ - Works with both callables (filters) and arrays
605
+ - Returns callable that can be used as `nfilter`
606
+ - Raises `ValueError` if argument shapes mismatch
607
+
583
608
  #### logical_not()
584
609
 
585
610
  **Signature:**
@@ -24,19 +24,22 @@ When invoked, this skill will:
24
24
  - Also update the Summary Table at the bottom of that file
25
25
  - If the function fits an existing section in `SKILL.md`, add a usage snippet there too
26
26
  6. **Update the `Last Updated` date** and version number at the top of `subscript_functions.md`
27
- 7. **Install package** in the `.venv` virtual environment:
27
+ 7. **If new features were detected** (new functions, new parameters, changed signatures), ask the user via `AskUserQuestion`:
28
+ - "Yes" — write tests for the new features in the appropriate `tests/test_*.py` file, following the existing test style (mock data dicts, `numpy.testing.assert_equal`, etc.)
29
+ - "No" — skip test writing
30
+ 8. **Install package** in the `.venv` virtual environment:
28
31
  ```bash
29
32
  .venv/bin/python -m pip install -e .
30
33
  ```
31
- 8. **Run tests** to verify everything passes:
34
+ 9. **Run tests** to verify everything passes:
32
35
  ```bash
33
36
  .venv/bin/python -m pytest
34
37
  ```
35
38
  - If any tests fail, fix the issues before proceeding to commit
36
- 9. **Draft a commit message** and present it to the user using `AskUserQuestion`, offering options:
37
- - "Use as-is" — commit with the drafted message
38
- - "Edit message" — let the user provide a custom message
39
- 10. **Stage and commit** all changed files with the finalized message
39
+ 10. **Draft a commit message** and present it to the user using `AskUserQuestion`, offering options:
40
+ - "Use as-is" — commit with the drafted message
41
+ - "Edit message" — let the user provide a custom message
42
+ 11. **Stage and commit** all changed files with the finalized message
40
43
 
41
44
  ## Files Updated
42
45
 
@@ -112,6 +115,15 @@ You can then push to GitHub:
112
115
  git push
113
116
  ```
114
117
 
118
+ ## Test Writing
119
+
120
+ When the user opts to write tests for new features:
121
+
122
+ - Add tests to the existing `tests/test_<module>.py` file that corresponds to the changed module (e.g., changes in `nfilters.py` → `tests/test_nfilters.py`)
123
+ - Follow the existing test style: mock data dicts, `numpy.testing.assert_equal`, simple assertions
124
+ - Test new parameters, new functions, and edge cases (e.g., shape mismatches raising `ValueError`)
125
+ - Include the new test file in the staged files for the commit
126
+
115
127
  ## Implementation Notes
116
128
 
117
129
  - Current version is read from `pyproject.toml` (source of truth)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: subhaloscript
3
- Version: 1.1.2
3
+ Version: 1.1.4
4
4
  Summary: Utility functions for analyzing subhalo distributions.
5
5
  Author-email: Charles Gannon <cgannon@ucmerced.edu>
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  package:
2
2
  name: subhaloscript
3
- version: "1.1.2"
3
+ version: "1.1.4"
4
4
 
5
5
  source:
6
6
  path: . # Or use `git_url`/`url` if not building from local files
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
5
5
  [project]
6
6
  dependencies = ["numpy","pandas","scipy","h5py", "scikit-learn", "astropy"]
7
7
  name = "subhaloscript"
8
- version = "1.1.2"
8
+ version = "1.1.4"
9
9
  authors = [
10
10
  { name="Charles Gannon", email="cgannon@ucmerced.edu" },
11
11
  ]
@@ -2,14 +2,14 @@
2
2
  import numpy as np
3
3
  import numpy.testing
4
4
  import h5py
5
- from typing import Callable
5
+ from typing import Callable, Iterable
6
6
 
7
7
  from subscript.scripts.spatial import project3d, project2d
8
8
  from subscript.wrappers import gscript
9
9
  from subscript.defaults import ParamKeys
10
10
  from subscript.util import deprecated
11
11
 
12
- def logical_or(arg1: (np.ndarray[bool] | Callable), arg2: (np.ndarray[bool] | Callable)):
12
+ def logical_or(arg1: (np.ndarray[bool] | Callable), arg2: (np.ndarray[bool] | Callable), *args:(np.ndarray[bool] | Callable)):
13
13
  """
14
14
  Create a logical OR function from two nodefilters or boolean arrays.
15
15
 
@@ -19,19 +19,34 @@ def logical_or(arg1: (np.ndarray[bool] | Callable), arg2: (np.ndarray[bool] | Ca
19
19
  First condition array or callable returning a boolean array.
20
20
  arg2 : np.ndarray of bool or Callable
21
21
  Second condition array or callable returning a boolean array.
22
+ args : Iterable of np.ndarray of bool or Callable
22
23
 
23
24
  Returns
24
25
  -------
25
26
  Callable
26
- A function that returns the element-wise logical OR of `arg1` and `arg2` when called.
27
+ A function that returns the element-wise logical OR arg1, arg2, and all additional args when called.
27
28
  """
28
- _a1 = arg1
29
- if isinstance(arg1, np.ndarray):
30
- _a1 = lambda *a, **k: arg1
31
- _a2 = arg2
32
- if isinstance(arg2, np.ndarray):
33
- _a2 = lambda *a, **k: arg2
34
- return lambda *a, **k: _a1(*a, **k) | _a2(*a, **k)
29
+
30
+ def eval_or(*a, **k):
31
+ _args = [arg1, arg2] + list(args)
32
+ _arg_eval = None
33
+
34
+ for _arg in _args:
35
+ if isinstance(_arg, np.ndarray):
36
+ _eval = _arg
37
+ else:
38
+ _eval = _arg(*a, **k)
39
+
40
+ if _arg_eval is None:
41
+ _arg_eval = _eval
42
+ else:
43
+ if _arg_eval.shape != _eval.shape:
44
+ raise ValueError(f"Shape mismatch between arguments: {_arg_eval.shape} and {_eval.shape}")
45
+ _arg_eval |= _eval
46
+
47
+ return _arg_eval
48
+
49
+ return eval_or
35
50
 
36
51
  @deprecated("Use logical_or() instead")
37
52
  def nfor(*args, **kwargs):
@@ -40,7 +55,7 @@ def nfor(*args, **kwargs):
40
55
  """
41
56
  return logical_or(*args, **kwargs)
42
57
 
43
- def logical_and(arg1: (np.ndarray[bool] | Callable), arg2: (np.ndarray[bool] | Callable)):
58
+ def logical_and(arg1: (np.ndarray[bool] | Callable), arg2: (np.ndarray[bool] | Callable), *args: (np.ndarray[bool] | Callable)):
44
59
  """
45
60
  Create a logical AND function from two nodefilters or boolean arrays.
46
61
 
@@ -50,19 +65,30 @@ def logical_and(arg1: (np.ndarray[bool] | Callable), arg2: (np.ndarray[bool] | C
50
65
  First condition array or callable returning a boolean array.
51
66
  arg2 : np.ndarray of bool or Callable
52
67
  Second condition array or callable returning a boolean array.
53
-
68
+ args : Iterable of np.ndarray of bool or Callable
54
69
  Returns
55
70
  -------
56
71
  Callable
57
- A function that returns the element-wise logical AND of `arg1` and `arg2` when called.
58
- """
59
- _a1 = arg1
60
- if isinstance(arg1, np.ndarray):
61
- _a1 = lambda *a, **k: arg1
62
- _a2 = arg2
63
- if isinstance(arg2, np.ndarray):
64
- _a2 = lambda *a, **k: arg2
65
- return lambda *a, **k: _a1(*a, **k) & _a2(*a, **k)
72
+ A function that returns the element-wise logical AND of `arg1` and `arg2` and any aditional args when called.
73
+ """
74
+ def eval_and(*a, **k):
75
+ _args = [arg1, arg2] + list(args)
76
+ _arg_eval = None
77
+
78
+ for _arg in _args:
79
+ if isinstance(_arg, np.ndarray):
80
+ _eval = _arg
81
+ else:
82
+ _eval = _arg(*a, **k)
83
+
84
+ if _arg_eval is None:
85
+ _arg_eval = _eval
86
+ else:
87
+ if _arg_eval.shape != _eval.shape:
88
+ raise ValueError(f"Shape mismatch between arguments: {_arg_eval.shape} and {_eval.shape}")
89
+ _arg_eval &= _eval
90
+ return _arg_eval
91
+ return eval_and
66
92
 
67
93
 
68
94
  @deprecated("Use logical_and() instead")
@@ -43,11 +43,11 @@ def test_nfilter_logical_and():
43
43
  out_expected = np.zeros(5, dtype=bool)
44
44
  testing.assert_equal(out_actual, out_expected)
45
45
 
46
- def test_nfilter_logical_and():
46
+ def test_nfilter_logical_or():
47
47
  mockdata = {
48
48
  "nodeIsIsolated": np.asarray((1.0, 0.0, 1.0, 1.0, 0.0))
49
49
  }
50
-
50
+
51
51
  test = logical_or(hosthalos, subhalos)
52
52
  out_actual = test(mockdata)
53
53
  out_expected = np.ones(5, dtype=bool)
@@ -116,3 +116,69 @@ def test_nfilter_project2d():
116
116
  testing.assert_equal(filter_actual, filter_expected)
117
117
 
118
118
 
119
+ def test_logical_and_varargs():
120
+ mockdata = {
121
+ "nodeIsIsolated": np.asarray((1.0, 0.0, 1.0, 1.0, 0.0))
122
+ }
123
+
124
+ # Three callable args
125
+ test = logical_and(hosthalos, hosthalos, hosthalos)
126
+ out_actual = test(mockdata)
127
+ out_expected = hosthalos(mockdata)
128
+ testing.assert_equal(out_actual, out_expected)
129
+
130
+ # Mix of callables and arrays
131
+ test = logical_and(hosthalos, hosthalos(mockdata), subhalos)
132
+ out_actual = test(mockdata)
133
+ out_expected = np.zeros(5, dtype=bool)
134
+ testing.assert_equal(out_actual, out_expected)
135
+
136
+ # Four args: all True AND hosthalos AND subhalos → all False
137
+ all_true = np.ones(5, dtype=bool)
138
+ test = logical_and(hosthalos, subhalos, all_true)
139
+ out_actual = test(mockdata)
140
+ out_expected = np.zeros(5, dtype=bool)
141
+ testing.assert_equal(out_actual, out_expected)
142
+
143
+
144
+ def test_logical_or_varargs():
145
+ mockdata = {
146
+ "nodeIsIsolated": np.asarray((1.0, 0.0, 1.0, 1.0, 0.0))
147
+ }
148
+
149
+ # Three callable args: hosthalos OR subhalos OR hosthalos → all True
150
+ test = logical_or(hosthalos, subhalos, hosthalos)
151
+ out_actual = test(mockdata)
152
+ out_expected = np.ones(5, dtype=bool)
153
+ testing.assert_equal(out_actual, out_expected)
154
+
155
+ # All-false array OR hosthalos → hosthalos
156
+ all_false = np.zeros(5, dtype=bool)
157
+ test = logical_or(all_false, all_false, hosthalos)
158
+ out_actual = test(mockdata)
159
+ out_expected = hosthalos(mockdata)
160
+ testing.assert_equal(out_actual, out_expected)
161
+
162
+
163
+ def test_logical_and_shape_mismatch():
164
+ a = np.ones(5, dtype=bool)
165
+ b = np.ones(3, dtype=bool)
166
+
167
+ test = logical_and(a, a, b)
168
+ try:
169
+ test()
170
+ assert False, "Expected ValueError"
171
+ except ValueError:
172
+ pass
173
+
174
+
175
+ def test_logical_or_shape_mismatch():
176
+ a = np.ones(5, dtype=bool)
177
+ b = np.ones(3, dtype=bool)
178
+
179
+ test = logical_or(a, a, b)
180
+ try:
181
+ test()
182
+ assert False, "Expected ValueError"
183
+ except ValueError:
184
+ pass
File without changes
File without changes
File without changes