Skip to content

Commit 8d8f09d

Browse files
authored
Merge pull request #21 from jkitchin/claude/jaxsr-issue-18-ia0azx
feat(basis): add_block for structured Theta(a) * data-column blocks
2 parents aaeb0b4 + 8552940 commit 8d8f09d

6 files changed

Lines changed: 863 additions & 3 deletions

File tree

.claude/skills/jaxsr/SKILL.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,15 @@ Based on the answers, recommend a basis library configuration:
4343
| Large feature space (screening) | `add_constant + add_linear + add_interactions(2)` then use `lasso_path` strategy |
4444
| Response surface (DOE) | `add_constant + add_linear + add_polynomials(2) + add_interactions(2)` — or use `ResponseSurface` directly |
4545
| Categorical factors present | Add `add_categorical_indicators() + add_categorical_interactions()` to any of the above |
46+
| Unknown coefficient *function* multiplying a data column (superposition, implicit dynamics) | `add_block(theta, multiply_by="<column>", block_name=...)` — see `guides/basis-library.md` |
4647

4748
**Key guidance:**
4849
- Start simple. You can always add complexity.
4950
- `add_transcendental(safe=True)` guards against log(0), 1/0, sqrt(<0). Always use `safe=True`.
5051
- `add_ratios(safe=True)` adds x_i/x_j terms. Doubles the library size — only use when ratios are physically meaningful.
5152
- `add_parametric()` enables nonlinear parameters (e.g., `exp(-a*x)`). Powerful but slower to fit.
53+
- `add_block()` multiplies a whole basis by a data column, so a selected coefficient is a term of an
54+
unknown coefficient function. Drop a block with `without_blocks()` to test whether it earned its place.
5255
- If n_features > 5, avoid `add_polynomials(degree>2)` — the library becomes enormous.
5356

5457
### Step 3: Recommend Selection Strategy

.claude/skills/jaxsr/guides/basis-library.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,69 @@ library.add_parametric(
172172
- Parametric fits are slower (requires nonlinear optimization per candidate).
173173
- Only add parametric terms when you have physical motivation.
174174

175+
### `add_block(library, multiply_by=None, block_name=None, complexity_offset=0, feature_map=None)`
176+
177+
Adds every function of another library, optionally multiplied by a column of the
178+
data. This builds design-matrix blocks of the form `Θ(a) ⊙ b`, where `Θ` is a
179+
basis over one variable and `b` is another *column* — typically a measured or
180+
estimated derivative. A coefficient selected in such a block is literally a term
181+
of the unknown coefficient *function* multiplying `b`.
182+
183+
```python
184+
from jaxsr import BasisLibrary
185+
186+
# Basis over the coefficient function's argument
187+
theta = (BasisLibrary(n_features=1, feature_names=["c"])
188+
.add_constant()
189+
.add_linear()
190+
.add_polynomials(max_degree=2))
191+
192+
# y_c = s'(c)*y_x + v'(c): one block per unknown function
193+
library = (BasisLibrary(n_features=2, feature_names=["c", "y_x"])
194+
.add_block(theta, multiply_by="y_x", block_name="horizontal")
195+
.add_block(theta, block_name="vertical"))
196+
197+
library.names
198+
# ['y_x', 'c*y_x', 'c^2*y_x', '1', 'c', 'c^2']
199+
```
200+
201+
**What it does for you:**
202+
203+
- **Names** are generated as `<basis>*<column>`, consistently; the constant term
204+
collapses to just the column name (`1*y_x``y_x`), and a source name that is
205+
a bare sum is parenthesized (`1+c``(1+c)*y_x`).
206+
- **Complexity** is inherited from the source, plus 1 for the multiplication,
207+
plus `complexity_offset`.
208+
- **Feature indices** are remapped: the source is written against its own
209+
columns, and `add_block` re-expresses it on this library's feature space,
210+
matching features by name (use `feature_map={"src": "target"}` when the names
211+
differ).
212+
- **Parametric terms pass through unchanged** — bounds, `log_scale` and the name
213+
template are preserved, so profile-likelihood optimization still applies inside
214+
the block.
215+
- The source library is **copied, not shared**, so one `theta` can seed several
216+
blocks.
217+
218+
**Working with blocks:**
219+
220+
```python
221+
library.blocks
222+
# {'horizontal': [0, 1, 2], 'vertical': [3, 4, 5]}
223+
224+
library.filter_by_block(include="horizontal") # -> [0, 1, 2]
225+
library.filter_by_block(exclude=["vertical"]) # -> [0, 1, 2]
226+
227+
# First diagnostic for a structured library: did the block earn its place?
228+
reduced = library.without_blocks("vertical") # new library, original untouched
229+
```
230+
231+
Comparing the fit of `library` against `library.without_blocks("vertical")` is
232+
the fastest way to check a horizontal/vertical identifiability trade-off, which
233+
is a real hazard whenever two blocks can explain the same variation.
234+
235+
Block functions are not deserializable — like `add_custom`, the library config
236+
saves but the block must be re-added after `load()`.
237+
175238
### `add_categorical_indicators(features=None)`
176239

177240
For categorical features, adds binary indicator (dummy) variables.

0 commit comments

Comments
 (0)