Skip to content

Commit a60bdcb

Browse files
authored
Merge pull request #48 from ronpandolfi/claude
Add Claude Code skills for autonomous experiment design
2 parents beae1b3 + b39cea1 commit a60bdcb

10 files changed

Lines changed: 1534 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# gpCAM Claude Skills
2+
3+
You are helping scientists design autonomous experiments using gpCAM, a Gaussian Process-based Bayesian optimization toolkit.
4+
5+
## Skills
6+
7+
Read the appropriate skill file before generating code:
8+
9+
- **Designing an experiment**: `skills/experiment-designer/SKILL.md`
10+
- **Custom kernels**: `skills/kernel-designer/SKILL.md`
11+
- **Acquisition functions**: `skills/acquisition-functions/SKILL.md`
12+
- **Prior mean functions**: `skills/prior-mean-functions/SKILL.md`
13+
- **Noise models**: `skills/noise-functions/SKILL.md`
14+
- **Cost functions (travel time)**: `skills/cost-functions/SKILL.md`
15+
- **Large-scale (>10k points)**: `skills/gp2scale-advanced/SKILL.md`
16+
- **Multi-task/multi-output**: `skills/multi-task-advanced/SKILL.md`
17+
18+
## Reference Materials
19+
20+
- [gpCAM documentation](https://gpcam.readthedocs.io) — full API reference and mathematical background
21+
- `gpcam/kernels.py` — re-exports the fvGP kernel library (`from fvgp.kernels import *`)
22+
23+
## Key Principles
24+
25+
1. **Generate complete, runnable scripts** — not fragments
26+
2. **Target audience is scientists**, not GP experts — explain choices in plain language
27+
3. **Always document the hyperparameter layout** — which index maps to what
28+
4. **Hyperparameter bounds must match** the total hyperparameter count across kernel + mean + noise
29+
5. **Default kernel is usually fine** — only suggest custom when there's a clear reason
30+
6. **Use vectorized numpy** — no Python loops over data points
31+
7. **Return dict keys**: `posterior_mean()` returns `"m(x)"`, `posterior_covariance()` returns `"v(x)"` and `"S"`. NOT `"f(x)"`.
32+
8. **`get_data()` keys use spaces**: `"x data"`, `"y data"`, `"hyperparameters"`, `"measurement variances"`. NOT underscores.

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,26 @@ for i in range(100):
3838
```
3939

4040

41+
## Designing experiments with Claude Code
42+
43+
gpCAM ships with a set of [Claude Code](https://docs.anthropic.com/en/docs/claude-code) skills that guide an AI assistant through designing autonomous experiments — custom kernels, acquisition functions, noise models, and the full ask/tell/train loop. Experimentalists who want smart, autonomous data acquisition without deep knowledge of GP math or the gpCAM API can use these skills to design autonomous experiments.
44+
45+
When you clone this repo, the root `CLAUDE.md` and `skills/` directory are picked up automatically by Claude Code. Available skills:
46+
47+
| Skill | Description |
48+
|-------|-------------|
49+
| **experiment-designer** | End-to-end autonomous experiment design. Translates a scientist's description of their measurement into a complete gpCAM script. |
50+
| **kernel-designer** | Design and compose custom kernel functions that encode domain knowledge (smoothness, periodicity, symmetry, anisotropy). |
51+
| **acquisition-functions** | Write custom acquisition functions that encode experimental priorities (exploration vs exploitation, multi-objective, constraints). |
52+
| **prior-mean-functions** | Encode known physics or expected trends as prior mean functions. |
53+
| **noise-functions** | Model position-dependent or heteroscedastic noise from detector characteristics. |
54+
| **cost-functions** | Account for motor travel time, settling, directional costs, and zone-based penalties. |
55+
| **gp2scale-advanced** | Large-scale experiments (>10k points) using sparse kernels and Dask distributed computing. |
56+
| **multi-task-advanced** | Multi-output / function-valued experiments with `fvGPOptimizer`. |
57+
58+
These skills are also compatible with other agentic platforms (e.g. [OpenClaw](https://openclaw.ai), or any harness that can read `SKILL.md` files) — point your assistant at the `skills/` directory.
59+
60+
4161
## Credits
4262

4363
Main Developer: Marcus Noack ([MarcusNoack@lbl.gov](mailto:MarcusNoack@lbl.gov))
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
# Skill: gpCAM Acquisition Functions
2+
3+
Design custom acquisition functions that control where gpCAM measures next.
4+
5+
## When to Use
6+
7+
When a user needs acquisition behavior beyond the built-in options:
8+
- Balancing exploration and exploitation
9+
- Multi-objective optimization
10+
- Constrained search (avoid forbidden regions)
11+
- Cost-aware acquisition (expensive moves)
12+
- Upper/lower confidence bounds
13+
- Probability of improvement
14+
15+
## Acquisition Function Contract
16+
17+
```python
18+
def my_acquisition(x, gp_optimizer):
19+
"""
20+
Parameters
21+
----------
22+
x : np.ndarray, shape (V, D)
23+
Candidate points to evaluate.
24+
gp_optimizer : GPOptimizer
25+
The GP model. Use its posterior_mean() and posterior_covariance() methods.
26+
27+
Returns
28+
-------
29+
scores : np.ndarray, shape (V,)
30+
Score for each candidate. HIGHER = more desirable (function is MAXIMIZED).
31+
"""
32+
```
33+
34+
**Key rule:** The acquisition function is **maximized**. Return higher values for points you want to measure.
35+
36+
## Built-in Options
37+
38+
Pass these as strings to `gpo.ask(acquisition_function=...)`:
39+
40+
| Name | String key | Best for |
41+
|------|-----------|----------|
42+
| Variance | `"variance"` | Pure exploration / mapping |
43+
| Expected Improvement | `"expected improvement"` | Optimization (find max) |
44+
| Probability of Improvement | `"probability of improvement"` | Optimization, risk-averse |
45+
| Upper Confidence Bound | `"ucb"` | Maximization with tunable exploration/exploitation |
46+
| Lower Confidence Bound | `"lcb"` | Minimization with tunable exploration/exploitation |
47+
| Predicted Maximum | `"maximum"` | Pure exploitation — mean only, no uncertainty |
48+
| Predicted Minimum | `"minimum"` | Pure exploitation for minimization |
49+
| Gradient | `"gradient"` | Seek steepest regions of the posterior mean |
50+
| Target Probability | `"target probability"` | Find points with output near a target value |
51+
| Relative Information Entropy | `"relative information entropy"` | Information-theoretic exploration |
52+
| RIE Set | `"relative information entropy set"` | Batch acquisition |
53+
| Total Correlation | `"total correlation"` | Batch acquisition |
54+
55+
To sanity-check any built-in or custom acquisition on a grid of candidates without calling `ask()`:
56+
```python
57+
scores = gpo.evaluate_acquisition_function(x_grid, acquisition_function="ucb")
58+
```
59+
60+
## Custom Acquisition Recipes
61+
62+
### Upper Confidence Bound (UCB)
63+
Available as the built-in string `"ucb"` — pass directly to `gpo.ask(acquisition_function="ucb")`. Write the callable form below only when you need to tune `beta` or otherwise customize the score:
64+
```python
65+
def ucb(x, gpo):
66+
"""
67+
beta controls exploration/exploitation tradeoff:
68+
beta=0: pure exploitation (just go to predicted max)
69+
beta=1: mild exploration
70+
beta=3: strong exploration (~95% confidence)
71+
"""
72+
beta = 2.0 # TUNE THIS
73+
mean = gpo.posterior_mean(x)["m(x)"]
74+
var = gpo.posterior_covariance(x, variance_only=True)["v(x)"]
75+
return mean + beta * np.sqrt(var)
76+
```
77+
78+
### Lower Confidence Bound (for minimization)
79+
gpCAM maximizes acquisition, so flip the sign for minimization:
80+
```python
81+
def lcb(x, gpo):
82+
"""Find the minimum of the function."""
83+
beta = 2.0
84+
mean = gpo.posterior_mean(x)["m(x)"]
85+
var = gpo.posterior_covariance(x, variance_only=True)["v(x)"]
86+
return -(mean - beta * np.sqrt(var)) # note the negation
87+
```
88+
89+
### Expected Improvement (custom version with minimization)
90+
```python
91+
from scipy.stats import norm
92+
93+
def expected_improvement_minimize(x, gpo):
94+
"""Expected improvement for finding the MINIMUM."""
95+
mean = gpo.posterior_mean(x)["m(x)"]
96+
var = gpo.posterior_covariance(x, variance_only=True)["v(x)"]
97+
std = np.sqrt(np.maximum(var, 1e-10))
98+
99+
y_best = np.min(gpo.y_data) # current best (minimum)
100+
z = (y_best - mean) / std
101+
ei = std * (z * norm.cdf(z) + norm.pdf(z))
102+
return ei
103+
```
104+
105+
### Probability of Improvement
106+
```python
107+
from scipy.stats import norm
108+
109+
def probability_of_improvement(x, gpo):
110+
"""Probability that measurement improves on current best."""
111+
mean = gpo.posterior_mean(x)["m(x)"]
112+
var = gpo.posterior_covariance(x, variance_only=True)["v(x)"]
113+
std = np.sqrt(np.maximum(var, 1e-10))
114+
115+
y_best = np.max(gpo.y_data)
116+
z = (mean - y_best) / std
117+
return norm.cdf(z)
118+
```
119+
120+
### Constrained Acquisition (Avoid Regions)
121+
```python
122+
def constrained_variance(x, gpo):
123+
"""Explore but avoid a circular forbidden zone."""
124+
var = gpo.posterior_covariance(x, variance_only=True)["v(x)"]
125+
126+
# Forbidden zone: circle at (5, 5) with radius 1
127+
center = np.array([5.0, 5.0])
128+
dist = np.linalg.norm(x - center, axis=1)
129+
penalty = np.where(dist < 1.0, -1e6, 0.0)
130+
131+
return var + penalty
132+
```
133+
134+
### Multi-Objective (Weighted)
135+
```python
136+
def multi_objective(x, gpo):
137+
"""Balance finding the max with reducing uncertainty."""
138+
mean = gpo.posterior_mean(x)["m(x)"]
139+
var = gpo.posterior_covariance(x, variance_only=True)["v(x)"]
140+
141+
w_exploit = 0.7 # weight on exploitation
142+
w_explore = 0.3 # weight on exploration
143+
144+
# Normalize each component to [0, 1]
145+
mean_norm = (mean - mean.min()) / (mean.max() - mean.min() + 1e-10)
146+
var_norm = (var - var.min()) / (var.max() - var.min() + 1e-10)
147+
148+
return w_exploit * mean_norm + w_explore * var_norm
149+
```
150+
151+
### Threshold Finder (Find Boundary)
152+
Useful when searching for where a signal crosses a threshold:
153+
```python
154+
def threshold_finder(x, gpo):
155+
"""Find the boundary where f(x) = threshold."""
156+
threshold = 0.5 # EDIT THIS
157+
158+
mean = gpo.posterior_mean(x)["m(x)"]
159+
var = gpo.posterior_covariance(x, variance_only=True)["v(x)"]
160+
std = np.sqrt(np.maximum(var, 1e-10))
161+
162+
# Score is high near the threshold AND where uncertainty is high
163+
distance_to_threshold = np.abs(mean - threshold)
164+
return std / (distance_to_threshold + 0.01)
165+
```
166+
167+
## Usage in the Experiment Loop
168+
169+
```python
170+
# Built-in string or a callable are both accepted:
171+
result = gpo.ask(
172+
input_set=parameter_bounds,
173+
acquisition_function=ucb, # or "ucb", "expected improvement", ...
174+
)
175+
```
176+
177+
### Useful `ask()` options
178+
179+
| Argument | Meaning |
180+
|----------|---------|
181+
| `n=N` | Request `N` points at once (batch). For vectorized single-task, use a batch-aware acquisition like `"relative information entropy set"` or `"total correlation"`. |
182+
| `vectorized=True` (default) | The acquisition function is called once with all candidate points, shape `(V, D)` — required for custom callables written against the contract above. |
183+
| `vectorized=False` | Candidates are evaluated one at a time (list of 1-D arrays). Used for non-vectorizable acquisition or non-Euclidean inputs. |
184+
| `method="global"\|"local"\|"hgdl"\|"hgdlAsync"` | Inner optimizer that searches for the argmax of the acquisition over `input_set`. `hgdl` requires `dask_client=`; `hgdlAsync` starts a background search and returns an `opt_obj` you can poll or `kill_client()`. |
185+
| `dask_client=client` | Distribute the inner optimization across Dask workers. |
186+
| `batch_size=B` | When candidates are a list, evaluate them in chunks of `B` on the cluster. |
187+
| `max_iter`, `pop_size`, `info=True` | Inner optimizer controls. |
188+
189+
`input_set` can be continuous bounds (`np.array([[lo,hi], ...])`), a list of candidate points (discrete finite set), or a list of arbitrary objects (non-Euclidean — strings, graphs — provided your kernel handles them).
190+
191+
## Hyperparameter Coordination
192+
193+
Acquisition functions don't add hyperparameters — they read the GP state via `gpo.posterior_mean()` and `gpo.posterior_covariance()`. However:
194+
195+
- If you access `gpo.y_data` directly (e.g., for `y_best`), make sure it's up to date after `tell()`
196+
- The GP must be trained before acquisition makes sense — always call `train()` first
197+
- For `variance_only=True`: faster, returns just diagonal variances (usually what you want)
198+
- For full covariance: use `variance_only=False` but this is O(V²) memory
199+
200+
## Common Pitfalls
201+
202+
1. **Returning negative scores for points you want**: Remember, acquisition is MAXIMIZED.
203+
2. **Division by zero in std**: Always use `np.maximum(var, 1e-10)` before taking sqrt.
204+
3. **Not handling edge cases**: Early in the loop with few points, the GP posterior can be unreliable.
205+
4. **Expensive acquisition functions**: They're evaluated many times during optimization. Keep them fast.

0 commit comments

Comments
 (0)