Metadata-Version: 2.5
Name: cadaques
Version: 0.4.0
Summary: Cost-Aware Dual Architecture for QUery-Efficient diScovery: autonomous discovery campaigns — any oracle, any driver, one budget.
Project-URL: Repository, https://github.com/jorgebravoabad/cadaques
Project-URL: Changelog, https://github.com/jorgebravoabad/cadaques/blob/main/CHANGELOG.md
Project-URL: Paper, https://arxiv.org/abs/2607.16127
Project-URL: Issues, https://github.com/jorgebravoabad/cadaques/issues
Author-email: Jorge Bravo-Abad <jorge.bravo@uam.es>
License: MIT
License-File: LICENSE
Keywords: active-learning,autonomous-discovery,bayesian-optimization,cost-aware,materials-science,self-driving-labs
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Physics
Requires-Python: >=3.10
Requires-Dist: numpy>=1.24
Provides-Extra: bo
Requires-Dist: scikit-learn>=1.3; extra == 'bo'
Provides-Extra: dev
Requires-Dist: mypy; extra == 'dev'
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Provides-Extra: docs
Requires-Dist: furo; extra == 'docs'
Requires-Dist: sphinx>=8; extra == 'docs'
Provides-Extra: mcp
Requires-Dist: mcp<3,>=2.3; extra == 'mcp'
Description-Content-Type: text/markdown

# CADAQUES

[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21293589.svg)](https://doi.org/10.5281/zenodo.21293589)
[![PyPI](https://img.shields.io/pypi/v/cadaques.svg)](https://pypi.org/project/cadaques/)
[![CI](https://github.com/jorgebravoabad/cadaques/actions/workflows/ci.yml/badge.svg)](https://github.com/jorgebravoabad/cadaques/actions/workflows/ci.yml)

**Cost-Aware Dual Architecture for QUery-Efficient diScovery**

*An open-source framework for autonomous discovery campaigns: any oracle, any driver, one budget.*

CADAQUES decouples autonomous discovery into two symmetric protocols. A metered
**Oracle** abstracts anything that answers queries at a price — a simulator, a
laboratory instrument, an analytic function. A **Driver** abstracts anything that
decides what to ask next — random search, Bayesian optimization, gradient methods,
LLM agents. Between them sits the framework's one structural commitment: **cost is
a first-class primitive**. Every oracle query and every driver decision is priced
in heterogeneous currencies (wall time, CPU hours, euros, tokens) and charged
against a single campaign budget. Campaigns end when the budget is exhausted, not
when an iteration counter runs out, and all results are reported as performance
per unit cost. *Every query counts.*

## Installation

```bash
pip install cadaques          # core: NumPy only
pip install "cadaques[bo]"    # + the Gaussian-process driver (scikit-learn)
pip install "cadaques[mcp]"   # + the MCP server for agents (0.4)
```

Or, for development, install the latest version from source:

```bash
git clone https://github.com/jorgebravoabad/cadaques.git
cd cadaques
pip install -e .
```

## Quickstart: a discovery campaign with an exact answer

Locate the critical temperature of the 2D Ising model by maximizing the magnetic
susceptibility under a fixed compute budget. Onsager's exact result,
T_c = 2/ln(1+√2) ≈ 2.269, provides the ground truth against which any driver
can be validated.

```python
from cadaques import Budget, Campaign, Cost
from cadaques.drivers import AnnealedLocalDriver
from cadaques.oracles import Ising2DOracle, T_C_EXACT

oracle = Ising2DOracle(seed=0)
driver = AnnealedLocalDriver(
    space={"T": (1.5, 3.5)},
    fidelity={"L": 24, "sweeps": 400},
    seed=0,
)
campaign = Campaign(oracle, driver, Budget(total=Cost(seconds=30.0)))

outcome = campaign.run()
print(f"Best T = {outcome.best.query.params['T']:.3f}  (exact: {T_C_EXACT:.3f})")
print(f"Queries: {outcome.n_queries}, stop reason: {outcome.stop_reason}")
print(f"Spent: {outcome.budget.spent}")
```

## The campaign is a durable object (since 0.2)

Since 0.2 a campaign is not a script you ran once — it is a declarative,
inspectable, replayable object:

```python
from cadaques import Budget, Campaign, Cost, Task, checkpoint, replay, resume
from cadaques.drivers import RandomDriver
from cadaques.oracles import Ising2DOracle

task = Task.from_bounds({"T": (1.5, 3.5)}, name="ising_tc")
campaign = Campaign(
    Ising2DOracle(), RandomDriver(space=task.space.bounds),
    Budget(total=Cost(seconds=30.0)),
    task=task,      # direction, bounds, constraints, success criterion
    seed=42,        # one campaign seed rules all randomness (ADR-0012)
)

spec = campaign.to_spec()            # the campaign as portable JSON
outcome = campaign.run()
outcome.events.to_jsonl("run.jsonl") # append-only event log: the source of truth

# audit later, with no re-execution — derived state equals the live state:
assert replay(spec, "run.jsonl") == outcome.state

# or kill it and continue exactly where it stopped:
checkpoint(campaign, "ckpt/")
revived = resume("ckpt/")
```

What this buys, concretely:

- **Event-sourced runtime.** Every transition — start, proposal, rejection,
  result, failure, stop — is an immutable event (JSONL, schema-versioned).
  The accounting ledger is a *derived view* of the event log.
- **Failure is a result.** An oracle exception becomes a FAILED `Result`
  that settles its declared cost and stays on the record; out-of-task
  proposals are recorded rejections, never crashes.
- **Replay and resume.** Final state derives from `(spec, events)` alone;
  a checkpointed campaign continues query-for-query identically to an
  uninterrupted one (rng states included).
- **Specs refuse to lie.** Participants holding callables are not
  spec-serializable and are refused loudly — a spec is a portable
  declaration, never a pickled blob.
- **Conformance suites.** `cadaques.testing` ships the Oracle/Driver/
  Resource contract checks; external adapters are encouraged to run them.

## From a spreadsheet to the next experiments (0.3)

The first domain vertical. A chemist declares column roles once — composition
descriptors, reaction conditions, an objective, a recorded cost per run — and
the table compiles to kernel objects:

```python
from cadaques.verticals import ReactionTable

table = ReactionTable.from_csv(
    "reactions.csv",
    objective="c2_yield",
    composition=("x_Mn", "x_Ce"),
    conditions=("T", "P"),
    cost_columns={"seconds": "run_s"},
)

# Recommendation mode: which experiments should the lab run next?
ranking = table.recommend_next(k=5, seed=42)     # requires: pip install "cadaques[bo]"
print(ranking.summary())
ranking.to_csv("next_experiments.csv")           # for the lab notebook

# Campaign mode: would Bayesian optimization have beaten random on this table?
from cadaques import Budget, Cost
from cadaques.drivers import BayesianDriver, RandomDriver
from cadaques.stats import paired_campaigns

result = paired_campaigns(
    lambda seed: table.campaign(BayesianDriver(space=table.oracle().bounds),
                                Budget(total=Cost(seconds=25000)), seed=seed),
    lambda seed: table.campaign(RandomDriver(space=table.oracle().bounds),
                                Budget(total=Cost(seconds=25000)), seed=seed),
    seeds=range(20), metric=lambda o: o.best.value, alternative="greater",
)
print(result.summary())   # paired bootstrap CI + Wilcoxon, quotable as-is
```

Behind this sit the 0.3 components: **`DatasetOracle`** (a measured table as a
hidden oracle, with a *declared* miss policy — a dataset miss is a FAILED result
that settles its declared cost — and deterministic economics that can replay
recorded per-row costs); **`BayesianDriver`** (Matérn GP with Expected
Improvement *per predicted unit cost*, learned from the driver's own settled
history, exploration annealed with `fraction_used`); **`cadaques.stats`** (paired
bootstrap and Wilcoxon on NumPy alone, exact null to n=25); and **recommendation
mode** (`recommend()` → a ranked, seeded, bit-for-bit reproducible object whose
provenance records the driver's declaration, a fingerprint of the history, and
the measured *thinking time* — the price of intelligence, attached to the advice
it produced). A frozen retrospective-benchmark protocol over four public
datasets lives in `studies/`.

## Campaigns operated by agents (0.4)

A language-model agent can operate a campaign through typed tools instead of
writing its own loop. The kernel keeps the definitions — what counts as a cost,
when work may be admitted, what a failed evaluation consumes — and every event
records who caused it. The same eleven tools are served over MCP and as a JSON
command line:

```bash
cadaques-agent --root ws schemas                         # every tool, with its JSON Schema
cadaques-agent --root ws call describe_capabilities      # drivers, oracles, error codes
cadaques-agent --root ws serve-mcp                       # requires: pip install "cadaques[mcp]"
```

For work that runs outside the process — a batch job on a cluster, a
measurement on an instrument — the agent reserves an evaluation, runs it, and
settles it with what it really cost:

```python
from cadaques.agent import Toolbox

tb = Toolbox("ws")
me = {"kind": "agent", "id": "my-agent", "model": "<model id>"}
tb.call("create_campaign", {"campaign_id": "pc1d", "actor": me, "request_id": "c1", "spec": {
    "oracle": {"class": "cadaques.oracles.external.ExternalOracle",       # prices, never runs
               "params": {"tariff": {"seconds": 30.0}, "scaling": {"resolution": 1.0},
                          "reference": {"resolution": 16}}},
    "driver": {"class": "cadaques.drivers.reference.RandomDriver",
               "params": {"space": {"d_H": [0.05, 0.95]}}},
    "budget_total": {"seconds": 600.0},
    "task": {"bounds": {"d_H": [0.05, 0.95]}, "direction": "maximize", "name": "pc1d"},
    "seed": 0}})

job = tb.call("reserve_evaluation", {"campaign_id": "pc1d", "actor": me, "request_id": "r1",
                                     "fidelity": {"resolution": 32}})["result"]
# ... submit the job yourself; when it ends:
tb.call("settle_evaluation", {"campaign_id": "pc1d", "actor": me, "request_id": "s1",
                              "evaluation_id": job["evaluation_id"], "outcome": "completed",
                              "value": 0.71, "settled_cost": {"seconds": 75.0},
                              "artifacts": ["slurm:123456"]})
```

Every response is an envelope (`ok`, `result` or a structured `error` with
`retryable` and `next_actions`). Admission counts outstanding reservations, so
concurrent operators cannot overspend; failures and cancellations are still
charged; repeating a call with the same `request_id` returns the original
response; and campaigns live on disk, so they outlive the session that started
them. Specs arriving through tools may name only allowlisted participants.

## Design principles

- **Dual agnosticism.** Oracles and Drivers are `typing.Protocol` classes with
  two methods each. Anything that speaks the protocol plugs in. Since 0.2 the
  dual architecture has an operational counterpart: `Resource`
  (submit/status/collect) with an `OracleResource` adapter.
- **Declared vs. settled cost.** Oracles declare a price *ex ante*
  (`oracle.price(query)`); the actual cost is settled *ex post* inside each
  `Result`. Real oracles deviate from their estimates — the ledger records both,
  and the discrepancy is itself an observable.
- **Both sides are metered.** Driver decisions cost wall time — and tokens, if the
  driver is an LLM agent. A campaign's economics include the price of intelligence,
  enabling the question: *when does an expensive smart driver beat a cheap dumb one?*
- **Failure is a result.** A failed evaluation — an oracle exception, a dataset
  miss — settles its declared cost and stays on the record, exactly like a wasted
  experiment.
- **Budget-aware strategies.** Drivers receive a read-only `BudgetView` and may
  adapt: the reference `AnnealedLocalDriver` explores while rich and exploits
  while poor; `BayesianDriver` anneals its exploration margin the same way.
- **The event log is the provenance.** Every transition (declared, settled,
  timestamped, schema-versioned) exports to JSONL: a complete, replayable trace
  of the campaign, from which the accounting ledger is derived.

## Status, stability and roadmap

**Status: research software, pre-1.0.** The claim of record is the latest
tagged release — this README describes shipped capability only, and the
roadmap below is a plan, not a feature list.

*Stability policy (ADR-0010).* Semantic versioning; nothing public breaks
without a deprecation shim spanning at least two minor releases. The exact
implementation accompanying v1 of the arXiv paper is permanently tagged
(`v0.1.0-paper`), and its imports run against every release via compatibility
shims (`cadaques.core.protocols`, `cadaques.core.campaign`, `CampaignResult`).
Durable design decisions live in `docs/adr/`.

*Kernel (shipped in 0.2).* Task · Query/Result and the general
Action/Observation envelopes · Driver · Oracle · Resource (with the
`OracleResource` adapter) · Campaign · Budget · Event · Artifact ·
Outcome · CampaignSpec — the twelve objects of the campaign
architecture, with event-sourced state, seed streams, replay,
checkpoint/resume and public conformance suites (`cadaques.testing`).

*Generality proof (shipped in 0.3).* `DatasetOracle` for retrospective
campaigns over measured tables; the cost-aware `BayesianDriver` (extra
`cadaques[bo]`); the audited `cadaques.stats` module; recommendation mode
(`recommend()`, `RankedCandidates`); the chemistry vertical
(`ReactionTable`); and the frozen study protocol in `studies/`.

*Agent surface (shipped in 0.4).* `cadaques.agent`: eleven typed tools over
persistent campaigns, structured errors, `request_id` idempotency, actor
attribution on every event (`cadaques.events/2`), kernel-enforced reservations
for evaluations run outside the process (`ExternalOracle`, `ExternalDriver`),
the `cadaques-agent` command line and an MCP server (extra `cadaques[mcp]`).

*Roadmap (not yet shipped).* Asynchronous executors and a Slurm-backed
workflow resource behind the frozen Resource lifecycle (ADR-0007); a
portfolio Driver allocating budget across strategies as a cost-aware
bandit; native categorical and mixed search spaces; declarative
constraint vocabulary for specs; plugin entry points for external
drivers and oracles; an LLM-agent driver adapter with token metering.
See `ARCHITECTURE.md` and `docs/adr/` for boundaries and decision gates.

## Citation

If you use CADAQUES in academic work, please cite it (see `CITATION.cff`).
Concept DOI (always resolves to the latest version):
[10.5281/zenodo.21293589](https://doi.org/10.5281/zenodo.21293589).
Version DOIs are listed per release in [`CHANGELOG.md`](CHANGELOG.md).
Paper: [arXiv:2607.16127](https://arxiv.org/abs/2607.16127).

## License

MIT.
