Skip to content

Commit 90b5985

Browse files
committed
tranformed GPs, updated AI skill, new light forced docs, new tests
1 parent ff0ccc3 commit 90b5985

15 files changed

Lines changed: 963 additions & 6 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ You are helping scientists design autonomous experiments using gpCAM, a Gaussian
2020
- **Cost functions (travel time)**: `skills/cost-functions/SKILL.md`
2121
- **Large-scale (>10k points)**: `skills/gp2scale-advanced/SKILL.md`
2222
- **Multi-task/multi-output**: `skills/multi-task-advanced/SKILL.md`
23+
- **Constrained observations (y>0 or y∈[0,1])**: `skills/transformed-optimizers-advanced/SKILL.md`
2324

2425
These skills also ship as a Claude Code plugin marketplace (see README.md), so they are available outside this repo once installed.
2526

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ To update later, run `/plugin marketplace update gpcam`; to remove, `/plugin uni
6969
| **cost-functions** | Account for motor travel time, settling, directional costs, and zone-based penalties. |
7070
| **gp2scale-advanced** | Large-scale experiments (>10k points) using sparse kernels and Dask distributed computing. |
7171
| **multi-task-advanced** | Multi-output / function-valued experiments with `fvGPOptimizer`. |
72+
| **transformed-optimizers-advanced** | Constrained observations: `LogGPOptimizer` for `y > 0` and `LogitGPOptimizer` for `y ∈ [0, 1]` — predictions and credible intervals stay inside the constrained range. |
7273

7374
Once installed, the skills activate automatically when you describe an experiment design problem to Claude, or you can invoke one explicitly (e.g. _"use the experiment-designer skill to set up an adaptive XRD scan"_).
7475

docs/source/_static/custom.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
/* Keep native browser controls (scrollbars, form widgets) in their light
2+
variants -- pydata's default_mode="light" handles the rest. */
3+
:root {
4+
color-scheme: light;
5+
}
6+
17
/* ------------------------------------------------------------------ */
28
/* Headshot grid used on contributor/team pages */
39
/* ------------------------------------------------------------------ */

docs/source/api/overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
77
gpOptimizer.md
88
fvgpOptimizer.md
9+
transformedOptimizers.md
910
gpMCMC.md
1011
kernels.md
1112
logging.md
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Transformed Optimizers
2+
3+
Single-task optimizers that fit a Gaussian process to observations after a fixed
4+
output transform (link function), and use
5+
:py:meth:`gpcam.GPOptimizer.evaluate_posterior` to push the latent Gaussian
6+
posterior back through the inverse link to the original observation space.
7+
8+
Use :py:class:`gpcam.LogGPOptimizer` for strictly positive observations in
9+
``(0, inf)`` — predictions and credible intervals are guaranteed positive, and
10+
the original-scale mean/std are available in closed form (lognormal).
11+
12+
Use :py:class:`gpcam.LogitGPOptimizer` for observations bounded in ``[0, 1]``
13+
predictions and credible intervals are guaranteed inside ``(0, 1)``; the
14+
original-scale mean/std are estimated by Monte Carlo (logistic-normal has no
15+
closed form). Observations are clipped to ``[eps, 1 - eps]`` because
16+
``logit(0)``/``logit(1)`` are infinite.
17+
18+
The inherited :py:meth:`posterior_mean` / :py:meth:`posterior_covariance`
19+
operate in the GP's modeling space (log / logit); use
20+
:py:meth:`evaluate_posterior` to obtain a posterior on the original scale.
21+
22+
## LogGPOptimizer
23+
24+
```{eval-rst}
25+
.. autoclass:: gpcam.gp_optimizer.LogGPOptimizer
26+
:members:
27+
```
28+
29+
## LogitGPOptimizer
30+
31+
```{eval-rst}
32+
.. autoclass:: gpcam.gp_optimizer.LogitGPOptimizer
33+
:members:
34+
```

docs/source/conf.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ def setup(app):
7171
# -- Options for HTML output -------------------------------------------------
7272
html_theme = 'pydata_sphinx_theme'
7373

74+
# Force light mode regardless of the visitor's OS preference. pydata-sphinx-theme
75+
# sets data-theme="light" on <html> when default_mode == "light", and the navbar
76+
# theme-switcher is excluded via navbar_end so visitors can't toggle to dark.
77+
html_context = {'default_mode': 'light'}
78+
7479
html_theme_options = {
7580
'logo': {
7681
'image_light': '_static/gpCAM_bright_bg.png',

docs/source/examples/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ GPOptimizer_NonEuclideanInputSpaces
1111
GPOptimizer_gp2ScaleTest
1212
GPOptimizer_MultiTaskTest
1313
GPOptimizer_Optimization
14+
GPOptimizer_LogAndLogit
1415
```
1516

1617
The notebooks below walk through common gpCAM use cases.
@@ -23,3 +24,4 @@ Each can be downloaded and run locally after `pip install gpcam`.
2324
- **[gp2Scale](GPOptimizer_gp2ScaleTest.ipynb)** — large-scale sparse GP with Dask distributed computation
2425
- **[Multi-task GP](GPOptimizer_MultiTaskTest.ipynb)** — multi-output regression with the `fvGPOptimizer` class
2526
- **[Function optimization](GPOptimizer_Optimization.ipynb)** — minimizing a known function with the `optimize` loop
27+
- **[Log and Logit GP optimizers](GPOptimizer_LogAndLogit.ipynb)** — fitting strictly-positive data with `LogGPOptimizer` and bounded `[0, 1]` data with `LogitGPOptimizer`

examples/GPOptimizer_LogAndLogit.ipynb

Lines changed: 487 additions & 0 deletions
Large diffs are not rendered by default.

gpcam/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77

88
from .gp_optimizer import GPOptimizer
99
from .gp_optimizer import fvGPOptimizer
10+
from .gp_optimizer import LogGPOptimizer
11+
from .gp_optimizer import LogitGPOptimizer
1012
from .gp_mcmc import gpMCMC, ProposalDistribution
1113
from .autonomous_experimenter import AutonomousExperimenterGP
1214
from .autonomous_experimenter import AutonomousExperimenterFvGP
1315

14-
__all__ = ['GPOptimizer', 'fvGPOptimizer','AutonomousExperimenterGP','AutonomousExperimenterFvGP', 'gpMCMC', 'ProposalDistribution']
16+
__all__ = ['GPOptimizer', 'fvGPOptimizer', 'LogGPOptimizer', 'LogitGPOptimizer',
17+
'AutonomousExperimenterGP', 'AutonomousExperimenterFvGP', 'gpMCMC', 'ProposalDistribution']
1518

1619
logger.disable('gpcam')

gpcam/gp_optimizer.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#!/usr/bin/env python
2+
import warnings
23
import numpy as np
4+
from scipy.special import expit, logit
35
from fvgp import fvGP
46
from .gp_optimizer_base import GPOptimizerBase
57

@@ -687,3 +689,111 @@ def __init__(
687689
logging=logging,
688690
multi_task=True,
689691
args=args)
692+
693+
694+
class LogGPOptimizer(GPOptimizer):
695+
"""
696+
A single-task :py:class:`GPOptimizer` for strictly positive observations in (0, inf).
697+
698+
Observations are modeled in log-space (the GP sees ``log(y)``), and posterior
699+
predictions are mapped back to the original scale with ``exp`` via
700+
:py:meth:`evaluate_posterior`, which guarantees strictly positive predictions and
701+
credible intervals. ``exp`` of a Gaussian is lognormal, so the original-scale mean
702+
and standard deviation are available in closed form.
703+
704+
All constructor arguments are identical to :py:class:`GPOptimizer`. Note that the
705+
inherited :py:meth:`posterior_mean` / :py:meth:`posterior_covariance` operate in
706+
log-space; use :py:meth:`evaluate_posterior` for the original (positive) scale.
707+
708+
Acquisition functions: :py:meth:`ask` optimizes the GP in log-space. Because ``log``
709+
is monotone increasing, ranking acquisitions (``variance``, ``ucb``, ``lcb``,
710+
``maximum``, ``minimum``) still identify the same locations as on the original scale.
711+
For ``target probability``, pass bounds already in log-space (``np.log(a)``, ``np.log(b)``).
712+
"""
713+
714+
def _prepare(self, y):
715+
y = np.asarray(y, dtype=float)
716+
if np.any(y <= 0.0):
717+
raise ValueError("LogGPOptimizer requires strictly positive observations (y > 0).")
718+
return y
719+
720+
def _forward(self, y):
721+
return np.log(y)
722+
723+
def _inverse(self, z):
724+
return np.exp(z)
725+
726+
def _forward_deriv(self, y):
727+
return 1.0 / y
728+
729+
def _moments(self, mu, var):
730+
# exp(Normal(mu, var)) is lognormal
731+
mean = np.exp(mu + var / 2.0)
732+
std = np.sqrt((np.exp(var) - 1.0) * np.exp(2.0 * mu + var))
733+
return mean, std
734+
735+
736+
class LogitGPOptimizer(GPOptimizer):
737+
"""
738+
A single-task :py:class:`GPOptimizer` for observations bounded in [0, 1].
739+
740+
Observations are modeled in logit (log-odds) space (the GP sees ``logit(y)``), and
741+
posterior predictions are mapped back with the logistic/sigmoid via
742+
:py:meth:`evaluate_posterior`, which guarantees predictions and credible intervals
743+
inside (0, 1). Because ``logit(0)`` / ``logit(1)`` are infinite, observations are
744+
clipped to ``[eps, 1 - eps]`` (a warning is emitted when clipping occurs). The
745+
logistic-normal distribution has no closed-form moments, so the original-scale mean
746+
and standard deviation are estimated by Monte-Carlo.
747+
748+
Parameters
749+
----------
750+
eps : float, optional
751+
Clipping margin for the open interval; observations are clipped to
752+
``[eps, 1 - eps]`` before the logit transform. The default is 1e-6.
753+
n_samples : int, optional
754+
Number of Monte-Carlo samples used to estimate the original-scale mean/std in
755+
:py:meth:`evaluate_posterior`. The default is 10000.
756+
757+
Notes
758+
-----
759+
All other constructor arguments are identical to :py:class:`GPOptimizer`. The
760+
inherited :py:meth:`posterior_mean` / :py:meth:`posterior_covariance` operate in
761+
logit-space; use :py:meth:`evaluate_posterior` for the original (0, 1) scale. The
762+
acquisition-function note for :py:class:`LogGPOptimizer` applies here too (pass
763+
``target probability`` bounds in logit-space).
764+
"""
765+
766+
def __init__(self, x_data=None, y_data=None, eps=1e-6, n_samples=10000, **kwargs):
767+
self.eps = eps
768+
self.n_samples = n_samples
769+
super().__init__(x_data=x_data, y_data=y_data, **kwargs)
770+
771+
def _prepare(self, y):
772+
y = np.asarray(y, dtype=float)
773+
if np.any(y < self.eps) or np.any(y > 1.0 - self.eps):
774+
warnings.warn("LogitGPOptimizer clipped observations to "
775+
f"[{self.eps}, {1.0 - self.eps}] before the logit transform.")
776+
return np.clip(y, self.eps, 1.0 - self.eps)
777+
778+
def _forward(self, y):
779+
return logit(y)
780+
781+
def _inverse(self, z):
782+
return expit(z)
783+
784+
def _forward_deriv(self, y):
785+
return 1.0 / (y * (1.0 - y))
786+
787+
def _moments(self, mu, var):
788+
# sigmoid(Normal(mu, var)) is logistic-normal -> no closed form, estimate by MC
789+
mu = np.asarray(mu).reshape(-1)
790+
sd = np.sqrt(np.asarray(var).reshape(-1))
791+
samples = expit(np.random.normal(loc=mu[:, None], scale=sd[:, None],
792+
size=(mu.shape[0], self.n_samples)))
793+
return samples.mean(axis=1), samples.std(axis=1)
794+
795+
def __getstate__(self):
796+
state = super().__getstate__()
797+
state["eps"] = self.eps
798+
state["n_samples"] = self.n_samples
799+
return state

0 commit comments

Comments
 (0)