cadaques 0.1.0__py3-none-any.whl
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.
- cadaques/__init__.py +18 -0
- cadaques/core/__init__.py +4 -0
- cadaques/core/campaign.py +146 -0
- cadaques/core/cost.py +138 -0
- cadaques/core/ledger.py +92 -0
- cadaques/core/protocols.py +80 -0
- cadaques/drivers/__init__.py +1 -0
- cadaques/drivers/reference.py +96 -0
- cadaques/oracles/__init__.py +2 -0
- cadaques/oracles/analytic.py +50 -0
- cadaques/oracles/ising.py +134 -0
- cadaques-0.1.0.dist-info/METADATA +116 -0
- cadaques-0.1.0.dist-info/RECORD +15 -0
- cadaques-0.1.0.dist-info/WHEEL +4 -0
- cadaques-0.1.0.dist-info/licenses/LICENSE +21 -0
cadaques/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""CADAQUES — Cost-Aware Dual Architecture for QUery-Efficient diScovery.
|
|
2
|
+
|
|
3
|
+
An open-source framework for autonomous discovery campaigns:
|
|
4
|
+
any oracle, any driver, one budget. Every query counts.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .core.campaign import Campaign, CampaignResult, DriverStopped
|
|
8
|
+
from .core.cost import Budget, BudgetExceeded, BudgetView, Cost
|
|
9
|
+
from .core.ledger import Ledger, Transaction
|
|
10
|
+
from .core.protocols import Driver, Oracle, Query, Result
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"Budget", "BudgetExceeded", "BudgetView", "Campaign", "CampaignResult",
|
|
16
|
+
"Cost", "Driver", "DriverStopped", "Ledger", "Oracle", "Query",
|
|
17
|
+
"Result", "Transaction", "__version__",
|
|
18
|
+
]
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""The Campaign runner: the discovery loop with an accountant inside.
|
|
2
|
+
|
|
3
|
+
The loop is deliberately small. Its one structural commitment is the
|
|
4
|
+
framework's thesis: *every query counts*. Each iteration prices the
|
|
5
|
+
oracle query before committing, meters the driver's decision, and the
|
|
6
|
+
campaign ends when the budget is exhausted — not when an iteration
|
|
7
|
+
counter runs out.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Literal, Sequence
|
|
15
|
+
|
|
16
|
+
from .cost import Budget, Cost
|
|
17
|
+
from .ledger import Ledger
|
|
18
|
+
from .protocols import Driver, Oracle, Result
|
|
19
|
+
|
|
20
|
+
StopReason = Literal["budget_exhausted", "max_queries", "driver_stopped"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DriverStopped(Exception):
|
|
24
|
+
"""A Driver may raise this to end a campaign early."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class CampaignResult:
|
|
29
|
+
best: Result | None
|
|
30
|
+
history: list[Result]
|
|
31
|
+
ledger: Ledger
|
|
32
|
+
budget: Budget
|
|
33
|
+
stop_reason: StopReason
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def n_queries(self) -> int:
|
|
37
|
+
return len(self.history)
|
|
38
|
+
|
|
39
|
+
def trace(self, currency: str = "seconds") -> list[tuple[float, float]]:
|
|
40
|
+
"""(cumulative settled cost, best-so-far value) — the signature
|
|
41
|
+
cost-normalized curve of a CADAQUES campaign."""
|
|
42
|
+
points: list[tuple[float, float]] = []
|
|
43
|
+
best: float | None = None
|
|
44
|
+
for tx, running in self.ledger.cumulative():
|
|
45
|
+
if tx.kind != "oracle":
|
|
46
|
+
continue
|
|
47
|
+
value = tx.meta.get("value")
|
|
48
|
+
if value is None:
|
|
49
|
+
continue
|
|
50
|
+
best = value if best is None else max(best, value) if self._maximize else min(best, value)
|
|
51
|
+
points.append((getattr(running, currency), best))
|
|
52
|
+
return points
|
|
53
|
+
|
|
54
|
+
_maximize: bool = field(default=True, repr=False)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Campaign:
|
|
58
|
+
"""Couple one Oracle to one Driver under one Budget."""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
oracle: Oracle,
|
|
63
|
+
driver: Driver,
|
|
64
|
+
budget: Budget,
|
|
65
|
+
*,
|
|
66
|
+
maximize: bool = True,
|
|
67
|
+
meter_driver: bool = True,
|
|
68
|
+
) -> None:
|
|
69
|
+
self.oracle = oracle
|
|
70
|
+
self.driver = driver
|
|
71
|
+
self.budget = budget
|
|
72
|
+
self.maximize = maximize
|
|
73
|
+
self.meter_driver = meter_driver
|
|
74
|
+
self.ledger = Ledger()
|
|
75
|
+
self.history: list[Result] = []
|
|
76
|
+
|
|
77
|
+
# ------------------------------------------------------------------
|
|
78
|
+
def run(self, max_queries: int | None = None) -> CampaignResult:
|
|
79
|
+
stop_reason: StopReason = "budget_exhausted"
|
|
80
|
+
best: Result | None = None
|
|
81
|
+
|
|
82
|
+
while True:
|
|
83
|
+
if max_queries is not None and len(self.history) >= max_queries:
|
|
84
|
+
stop_reason = "max_queries"
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
# -- driver decision (metered) -------------------------------
|
|
88
|
+
t0 = time.perf_counter()
|
|
89
|
+
try:
|
|
90
|
+
query = self.driver.propose(self.history, self.budget.view())
|
|
91
|
+
except DriverStopped:
|
|
92
|
+
stop_reason = "driver_stopped"
|
|
93
|
+
break
|
|
94
|
+
elapsed = time.perf_counter() - t0
|
|
95
|
+
|
|
96
|
+
driver_cost = Cost()
|
|
97
|
+
if self.meter_driver:
|
|
98
|
+
driver_cost = Cost(seconds=elapsed) + getattr(
|
|
99
|
+
self.driver, "last_proposal_cost", Cost()
|
|
100
|
+
)
|
|
101
|
+
self.budget.charge(driver_cost, settle=True)
|
|
102
|
+
self.ledger.record(
|
|
103
|
+
"driver",
|
|
104
|
+
label=type(self.driver).__name__,
|
|
105
|
+
declared=driver_cost,
|
|
106
|
+
settled=driver_cost,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# -- oracle query: price, afford, evaluate, settle ------------
|
|
110
|
+
declared = self.oracle.price(query)
|
|
111
|
+
if not self.budget.can_afford(declared):
|
|
112
|
+
stop_reason = "budget_exhausted"
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
result = self.oracle.evaluate(query)
|
|
116
|
+
self.budget.charge(result.cost, settle=True)
|
|
117
|
+
self.ledger.record(
|
|
118
|
+
"oracle",
|
|
119
|
+
label=type(self.oracle).__name__,
|
|
120
|
+
declared=declared,
|
|
121
|
+
settled=result.cost,
|
|
122
|
+
value=result.value,
|
|
123
|
+
params=dict(result.query.params),
|
|
124
|
+
fidelity=dict(result.query.fidelity),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
self.history.append(result)
|
|
128
|
+
self.driver.observe(result)
|
|
129
|
+
|
|
130
|
+
if best is None or self._improves(result, best):
|
|
131
|
+
best = result
|
|
132
|
+
|
|
133
|
+
campaign_result = CampaignResult(
|
|
134
|
+
best=best,
|
|
135
|
+
history=self.history,
|
|
136
|
+
ledger=self.ledger,
|
|
137
|
+
budget=self.budget,
|
|
138
|
+
stop_reason=stop_reason,
|
|
139
|
+
)
|
|
140
|
+
campaign_result._maximize = self.maximize
|
|
141
|
+
return campaign_result
|
|
142
|
+
|
|
143
|
+
def _improves(self, candidate: Result, incumbent: Result) -> bool:
|
|
144
|
+
if self.maximize:
|
|
145
|
+
return candidate.value > incumbent.value
|
|
146
|
+
return candidate.value < incumbent.value
|
cadaques/core/cost.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Cost primitives: the first-class citizens of CADAQUES.
|
|
2
|
+
|
|
3
|
+
Every oracle query and every driver decision is priced in a
|
|
4
|
+
multi-currency :class:`Cost` and charged against a single
|
|
5
|
+
:class:`Budget`. Campaigns end when the budget is exhausted,
|
|
6
|
+
not when an iteration counter runs out.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field, replace
|
|
12
|
+
from typing import Iterator, Mapping
|
|
13
|
+
|
|
14
|
+
#: Currencies tracked by the framework. Heterogeneous by design:
|
|
15
|
+
#: a DFT relaxation, a beamline measurement and an LLM call are
|
|
16
|
+
#: not the same kind of expense.
|
|
17
|
+
CURRENCIES: tuple[str, ...] = ("seconds", "cpu_hours", "euros", "tokens")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class Cost:
|
|
22
|
+
"""An immutable multi-currency cost vector.
|
|
23
|
+
|
|
24
|
+
All components default to zero, so ``Cost(seconds=3.0)`` prices a
|
|
25
|
+
3-second query and ``Cost()`` is the free cost.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
seconds: float = 0.0
|
|
29
|
+
cpu_hours: float = 0.0
|
|
30
|
+
euros: float = 0.0
|
|
31
|
+
tokens: float = 0.0
|
|
32
|
+
|
|
33
|
+
# -- arithmetic -------------------------------------------------
|
|
34
|
+
def __add__(self, other: "Cost") -> "Cost":
|
|
35
|
+
return Cost(**{c: getattr(self, c) + getattr(other, c) for c in CURRENCIES})
|
|
36
|
+
|
|
37
|
+
def __sub__(self, other: "Cost") -> "Cost":
|
|
38
|
+
return Cost(**{c: getattr(self, c) - getattr(other, c) for c in CURRENCIES})
|
|
39
|
+
|
|
40
|
+
def scaled(self, factor: float) -> "Cost":
|
|
41
|
+
return Cost(**{c: getattr(self, c) * factor for c in CURRENCIES})
|
|
42
|
+
|
|
43
|
+
# -- queries ----------------------------------------------------
|
|
44
|
+
def items(self) -> Iterator[tuple[str, float]]:
|
|
45
|
+
for c in CURRENCIES:
|
|
46
|
+
yield c, getattr(self, c)
|
|
47
|
+
|
|
48
|
+
def as_dict(self) -> dict[str, float]:
|
|
49
|
+
return dict(self.items())
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def is_free(self) -> bool:
|
|
53
|
+
return all(v == 0 for _, v in self.items())
|
|
54
|
+
|
|
55
|
+
def dominated_by(self, other: "Cost") -> bool:
|
|
56
|
+
"""True if every component of ``self`` is <= that of ``other``."""
|
|
57
|
+
return all(getattr(self, c) <= getattr(other, c) for c in CURRENCIES)
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_dict(cls, data: Mapping[str, float]) -> "Cost":
|
|
61
|
+
unknown = set(data) - set(CURRENCIES)
|
|
62
|
+
if unknown:
|
|
63
|
+
raise ValueError(f"Unknown cost currencies: {sorted(unknown)}")
|
|
64
|
+
return cls(**{k: float(v) for k, v in data.items()})
|
|
65
|
+
|
|
66
|
+
def __repr__(self) -> str: # compact: only non-zero currencies
|
|
67
|
+
nonzero = ", ".join(f"{c}={v:g}" for c, v in self.items() if v)
|
|
68
|
+
return f"Cost({nonzero or '0'})"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class BudgetExceeded(RuntimeError):
|
|
72
|
+
"""Raised when a charge is attempted beyond the campaign budget."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True, slots=True)
|
|
76
|
+
class BudgetView:
|
|
77
|
+
"""A read-only snapshot of the budget, exposed to Drivers.
|
|
78
|
+
|
|
79
|
+
Drivers may adapt their strategy to the remaining budget (e.g.
|
|
80
|
+
explore early, exploit when funds run low) but cannot mutate it.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
total: Cost
|
|
84
|
+
spent: Cost
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def remaining(self) -> Cost:
|
|
88
|
+
return self.total - self.spent
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def fraction_used(self) -> float:
|
|
92
|
+
"""Largest used fraction across the currencies with a limit."""
|
|
93
|
+
fractions = [
|
|
94
|
+
getattr(self.spent, c) / getattr(self.total, c)
|
|
95
|
+
for c in CURRENCIES
|
|
96
|
+
if getattr(self.total, c) > 0
|
|
97
|
+
]
|
|
98
|
+
return max(fractions, default=0.0)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class Budget:
|
|
103
|
+
"""The single budget of a discovery campaign.
|
|
104
|
+
|
|
105
|
+
Only currencies with a positive ``total`` component are limited;
|
|
106
|
+
the rest are tracked but unbounded. Declared costs are checked
|
|
107
|
+
with :meth:`can_afford` *before* a query is issued; settled costs
|
|
108
|
+
are charged unconditionally with ``charge(..., settle=True)`` —
|
|
109
|
+
in the real world one discovers an overrun after paying for it,
|
|
110
|
+
and the ledger keeps the evidence.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
total: Cost
|
|
114
|
+
spent: Cost = field(default_factory=Cost)
|
|
115
|
+
|
|
116
|
+
def can_afford(self, cost: Cost) -> bool:
|
|
117
|
+
prospective = self.spent + cost
|
|
118
|
+
return all(
|
|
119
|
+
getattr(prospective, c) <= getattr(self.total, c)
|
|
120
|
+
for c in CURRENCIES
|
|
121
|
+
if getattr(self.total, c) > 0
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def charge(self, cost: Cost, *, settle: bool = False) -> None:
|
|
125
|
+
if not settle and not self.can_afford(cost):
|
|
126
|
+
raise BudgetExceeded(f"Cannot afford {cost}; remaining {self.remaining}")
|
|
127
|
+
self.spent = self.spent + cost
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def remaining(self) -> Cost:
|
|
131
|
+
return self.total - self.spent
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def exhausted(self) -> bool:
|
|
135
|
+
return not self.can_afford(Cost())
|
|
136
|
+
|
|
137
|
+
def view(self) -> BudgetView:
|
|
138
|
+
return BudgetView(total=replace(self.total), spent=replace(self.spent))
|
cadaques/core/ledger.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""The campaign ledger: every transaction, declared and settled.
|
|
2
|
+
|
|
3
|
+
The ledger is the accounting record of a discovery campaign and,
|
|
4
|
+
almost for free, its provenance and replay substrate: exporting it
|
|
5
|
+
to JSONL yields a complete, reproducible trace of what was asked,
|
|
6
|
+
what it was expected to cost, and what it actually cost.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Iterator, Literal, Mapping
|
|
16
|
+
|
|
17
|
+
from .cost import Cost
|
|
18
|
+
|
|
19
|
+
TransactionKind = Literal["oracle", "driver"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class Transaction:
|
|
24
|
+
kind: TransactionKind
|
|
25
|
+
label: str
|
|
26
|
+
declared: Cost
|
|
27
|
+
settled: Cost
|
|
28
|
+
timestamp: float = field(default_factory=time.time)
|
|
29
|
+
meta: Mapping[str, Any] = field(default_factory=dict)
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def overrun(self) -> Cost:
|
|
33
|
+
"""Settled minus declared: the price of optimism."""
|
|
34
|
+
return self.settled - self.declared
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class Ledger:
|
|
39
|
+
"""Append-only record of all campaign transactions."""
|
|
40
|
+
|
|
41
|
+
transactions: list[Transaction] = field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
def record(
|
|
44
|
+
self,
|
|
45
|
+
kind: TransactionKind,
|
|
46
|
+
label: str,
|
|
47
|
+
declared: Cost,
|
|
48
|
+
settled: Cost,
|
|
49
|
+
**meta: Any,
|
|
50
|
+
) -> Transaction:
|
|
51
|
+
tx = Transaction(kind=kind, label=label, declared=declared, settled=settled, meta=meta)
|
|
52
|
+
self.transactions.append(tx)
|
|
53
|
+
return tx
|
|
54
|
+
|
|
55
|
+
# -- aggregation ------------------------------------------------
|
|
56
|
+
def total(self, kind: TransactionKind | None = None) -> Cost:
|
|
57
|
+
total = Cost()
|
|
58
|
+
for tx in self.transactions:
|
|
59
|
+
if kind is None or tx.kind == kind:
|
|
60
|
+
total = total + tx.settled
|
|
61
|
+
return total
|
|
62
|
+
|
|
63
|
+
def cumulative(self, kind: TransactionKind | None = None) -> Iterator[tuple[Transaction, Cost]]:
|
|
64
|
+
"""Yield (transaction, running settled total) pairs."""
|
|
65
|
+
running = Cost()
|
|
66
|
+
for tx in self.transactions:
|
|
67
|
+
if kind is None or tx.kind == kind:
|
|
68
|
+
running = running + tx.settled
|
|
69
|
+
yield tx, running
|
|
70
|
+
|
|
71
|
+
def __len__(self) -> int:
|
|
72
|
+
return len(self.transactions)
|
|
73
|
+
|
|
74
|
+
# -- persistence ------------------------------------------------
|
|
75
|
+
def to_jsonl(self, path: str | Path) -> Path:
|
|
76
|
+
path = Path(path)
|
|
77
|
+
with path.open("w", encoding="utf-8") as fh:
|
|
78
|
+
for tx in self.transactions:
|
|
79
|
+
fh.write(
|
|
80
|
+
json.dumps(
|
|
81
|
+
{
|
|
82
|
+
"kind": tx.kind,
|
|
83
|
+
"label": tx.label,
|
|
84
|
+
"declared": tx.declared.as_dict(),
|
|
85
|
+
"settled": tx.settled.as_dict(),
|
|
86
|
+
"timestamp": tx.timestamp,
|
|
87
|
+
"meta": dict(tx.meta),
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
+ "\n"
|
|
91
|
+
)
|
|
92
|
+
return path
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""The dual architecture: the :class:`Oracle` and :class:`Driver` protocols.
|
|
2
|
+
|
|
3
|
+
CADAQUES is agnostic on both sides of the discovery loop:
|
|
4
|
+
|
|
5
|
+
* an **Oracle** is anything that answers queries at a declared price —
|
|
6
|
+
a simulator, a laboratory instrument, an analytic function;
|
|
7
|
+
* a **Driver** is anything that decides what to ask next — random
|
|
8
|
+
search, Bayesian optimization, gradient methods, an LLM agent.
|
|
9
|
+
|
|
10
|
+
Both sides are metered: the campaign runner charges oracle queries
|
|
11
|
+
*and* driver decisions against one budget.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any, Mapping, Protocol, Sequence, runtime_checkable
|
|
18
|
+
|
|
19
|
+
from .cost import BudgetView, Cost
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class Query:
|
|
24
|
+
"""A single question to an Oracle.
|
|
25
|
+
|
|
26
|
+
``params`` are the design/search variables; ``fidelity`` are the
|
|
27
|
+
knobs that trade accuracy for cost (lattice size, mesh density,
|
|
28
|
+
integration time, ...). Keeping fidelity explicit is what makes
|
|
29
|
+
multi-fidelity, cost-aware campaigns first-class citizens.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
params: Mapping[str, Any]
|
|
33
|
+
fidelity: Mapping[str, Any] = field(default_factory=dict)
|
|
34
|
+
tag: str = ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class Result:
|
|
39
|
+
"""An Oracle's answer, carrying its *settled* (actual) cost."""
|
|
40
|
+
|
|
41
|
+
query: Query
|
|
42
|
+
value: float
|
|
43
|
+
cost: Cost
|
|
44
|
+
info: Mapping[str, Any] = field(default_factory=dict)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@runtime_checkable
|
|
48
|
+
class Oracle(Protocol):
|
|
49
|
+
"""Anything that answers queries at a price.
|
|
50
|
+
|
|
51
|
+
``price`` declares the expected cost *ex ante* so the runner can
|
|
52
|
+
check affordability before committing; ``evaluate`` performs the
|
|
53
|
+
query and reports the *settled* cost inside the Result. Real
|
|
54
|
+
oracles deviate from their declared price — the ledger records
|
|
55
|
+
both, and that discrepancy is itself an observable.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def price(self, query: Query) -> Cost:
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
def evaluate(self, query: Query) -> Result:
|
|
62
|
+
...
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@runtime_checkable
|
|
66
|
+
class Driver(Protocol):
|
|
67
|
+
"""Anything that decides what to ask next.
|
|
68
|
+
|
|
69
|
+
``propose`` receives the full history and a read-only view of the
|
|
70
|
+
budget, enabling budget-aware strategies. ``observe`` feeds the
|
|
71
|
+
settled result back. Drivers that incur their own accountable
|
|
72
|
+
costs beyond wall time (e.g. LLM tokens) may expose a
|
|
73
|
+
``last_proposal_cost`` attribute, which the runner will charge.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def propose(self, history: Sequence[Result], budget: BudgetView) -> Query:
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
def observe(self, result: Result) -> None:
|
|
80
|
+
...
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .reference import AnnealedLocalDriver, RandomDriver
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Reference drivers.
|
|
2
|
+
|
|
3
|
+
Two deliberately simple strategies that already span the design
|
|
4
|
+
space CADAQUES cares about:
|
|
5
|
+
|
|
6
|
+
* :class:`RandomDriver` — the universal, budget-oblivious baseline
|
|
7
|
+
against which every intelligent driver must justify its cost.
|
|
8
|
+
* :class:`AnnealedLocalDriver` — a budget-*aware* strategy: it
|
|
9
|
+
explores globally while funds are plentiful and contracts around
|
|
10
|
+
the incumbent as the budget runs out, reading the remaining
|
|
11
|
+
fraction from the ``BudgetView`` it receives each iteration.
|
|
12
|
+
|
|
13
|
+
Bayesian optimization, gradient-based and LLM-agent drivers plug in
|
|
14
|
+
through the same two-method protocol.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import Mapping, Sequence
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
from ..core.cost import BudgetView
|
|
25
|
+
from ..core.protocols import Query, Result
|
|
26
|
+
|
|
27
|
+
SearchSpace = Mapping[str, tuple[float, float]]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _uniform_sample(space: SearchSpace, rng: np.random.Generator) -> dict[str, float]:
|
|
31
|
+
return {k: float(rng.uniform(low, high)) for k, (low, high) in space.items()}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class RandomDriver:
|
|
36
|
+
"""Uniform random search over a box-bounded space."""
|
|
37
|
+
|
|
38
|
+
space: SearchSpace
|
|
39
|
+
fidelity: Mapping[str, int] = field(default_factory=dict)
|
|
40
|
+
seed: int | None = None
|
|
41
|
+
|
|
42
|
+
def __post_init__(self) -> None:
|
|
43
|
+
self._rng = np.random.default_rng(self.seed)
|
|
44
|
+
|
|
45
|
+
def propose(self, history: Sequence[Result], budget: BudgetView) -> Query:
|
|
46
|
+
return Query(params=_uniform_sample(self.space, self._rng), fidelity=dict(self.fidelity))
|
|
47
|
+
|
|
48
|
+
def observe(self, result: Result) -> None: # random search learns nothing
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class AnnealedLocalDriver:
|
|
54
|
+
"""Gaussian local search whose step size anneals with the budget.
|
|
55
|
+
|
|
56
|
+
The proposal is a perturbation of the best point seen so far,
|
|
57
|
+
with standard deviation ``sigma_max`` at the start of the
|
|
58
|
+
campaign shrinking linearly to ``sigma_min`` as
|
|
59
|
+
``budget.fraction_used`` approaches 1 — spend on exploration
|
|
60
|
+
while rich, on exploitation while poor.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
space: SearchSpace
|
|
64
|
+
fidelity: Mapping[str, int] = field(default_factory=dict)
|
|
65
|
+
sigma_max: float = 0.5
|
|
66
|
+
sigma_min: float = 0.02
|
|
67
|
+
maximize: bool = True
|
|
68
|
+
seed: int | None = None
|
|
69
|
+
|
|
70
|
+
def __post_init__(self) -> None:
|
|
71
|
+
self._rng = np.random.default_rng(self.seed)
|
|
72
|
+
self._best: Result | None = None
|
|
73
|
+
|
|
74
|
+
# -- Driver protocol ------------------------------------------------
|
|
75
|
+
def propose(self, history: Sequence[Result], budget: BudgetView) -> Query:
|
|
76
|
+
if self._best is None:
|
|
77
|
+
return Query(params=_uniform_sample(self.space, self._rng), fidelity=dict(self.fidelity))
|
|
78
|
+
|
|
79
|
+
frac = min(max(budget.fraction_used, 0.0), 1.0)
|
|
80
|
+
sigma_rel = self.sigma_max + (self.sigma_min - self.sigma_max) * frac
|
|
81
|
+
|
|
82
|
+
params: dict[str, float] = {}
|
|
83
|
+
for key, (low, high) in self.space.items():
|
|
84
|
+
width = high - low
|
|
85
|
+
center = float(self._best.query.params[key])
|
|
86
|
+
proposal = center + self._rng.normal(0.0, sigma_rel * width)
|
|
87
|
+
params[key] = float(np.clip(proposal, low, high))
|
|
88
|
+
return Query(params=params, fidelity=dict(self.fidelity))
|
|
89
|
+
|
|
90
|
+
def observe(self, result: Result) -> None:
|
|
91
|
+
if self._best is None or self._improves(result):
|
|
92
|
+
self._best = result
|
|
93
|
+
|
|
94
|
+
def _improves(self, result: Result) -> bool:
|
|
95
|
+
assert self._best is not None
|
|
96
|
+
return result.value > self._best.value if self.maximize else result.value < self._best.value
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Analytic oracles: exact functions with synthetic cost models.
|
|
2
|
+
|
|
3
|
+
Useful for unit tests and for controlled driver-benchmarking
|
|
4
|
+
experiments where the ground truth and the cost model are both known
|
|
5
|
+
exactly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Callable, Mapping
|
|
13
|
+
|
|
14
|
+
from ..core.cost import Cost
|
|
15
|
+
from ..core.protocols import Query, Result
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class AnalyticOracle:
|
|
20
|
+
"""Wrap ``fn(params) -> float`` as a metered Oracle.
|
|
21
|
+
|
|
22
|
+
``price_fn`` maps a query to its declared cost (default: a flat
|
|
23
|
+
one-second tariff), letting tests exercise heterogeneous-cost
|
|
24
|
+
scenarios deterministically. The settled cost equals the declared
|
|
25
|
+
cost plus the (negligible) measured evaluation wall time, keeping
|
|
26
|
+
the declared/settled distinction alive even in toy settings.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
fn: Callable[[Mapping[str, float]], float]
|
|
30
|
+
price_fn: Callable[[Query], Cost] = field(default=lambda _q: Cost(seconds=1.0))
|
|
31
|
+
name: str = "analytic"
|
|
32
|
+
|
|
33
|
+
def price(self, query: Query) -> Cost:
|
|
34
|
+
return self.price_fn(query)
|
|
35
|
+
|
|
36
|
+
def evaluate(self, query: Query) -> Result:
|
|
37
|
+
t0 = time.perf_counter()
|
|
38
|
+
value = float(self.fn(query.params))
|
|
39
|
+
elapsed = time.perf_counter() - t0
|
|
40
|
+
settled = self.price_fn(query) + Cost(seconds=elapsed)
|
|
41
|
+
return Result(query=query, value=value, cost=settled, info={"oracle": self.name})
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def quadratic_bowl(center: Mapping[str, float]) -> Callable[[Mapping[str, float]], float]:
|
|
45
|
+
"""Negative squared distance to ``center`` (maximum 0 at the center)."""
|
|
46
|
+
|
|
47
|
+
def fn(params: Mapping[str, float]) -> float:
|
|
48
|
+
return -sum((float(params[k]) - float(v)) ** 2 for k, v in center.items())
|
|
49
|
+
|
|
50
|
+
return fn
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Canonical illustrative oracle: the two-dimensional Ising model.
|
|
2
|
+
|
|
3
|
+
The campaign it supports is a textbook discovery task with an exact
|
|
4
|
+
analytic answer: locate the critical temperature of the 2D Ising
|
|
5
|
+
model by maximizing the magnetic susceptibility. Onsager (1944):
|
|
6
|
+
|
|
7
|
+
T_c = 2 / ln(1 + sqrt(2)) ≈ 2.269185 (J = k_B = 1)
|
|
8
|
+
|
|
9
|
+
The oracle is a checkerboard-Metropolis Monte Carlo simulation whose
|
|
10
|
+
*fidelity knobs* — lattice size ``L`` and number of measurement
|
|
11
|
+
``sweeps`` — carry genuine, heterogeneous cost: accuracy is bought
|
|
12
|
+
with compute. This makes it a faithful miniature of real discovery
|
|
13
|
+
oracles, where every answer has a price and better answers cost more.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import time
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
from ..core.cost import Cost
|
|
24
|
+
from ..core.protocols import Query, Result
|
|
25
|
+
|
|
26
|
+
#: Onsager's exact critical temperature (J = k_B = 1).
|
|
27
|
+
T_C_EXACT: float = 2.0 / np.log(1.0 + np.sqrt(2.0))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Ising2DOracle:
|
|
32
|
+
"""Metered Monte Carlo oracle for the 2D Ising model.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
default_L, default_sweeps, default_equilibration:
|
|
37
|
+
Fidelity defaults used when a query does not specify them.
|
|
38
|
+
seconds_per_spin_sweep:
|
|
39
|
+
Ex-ante cost model: declared price is proportional to
|
|
40
|
+
``L**2 * (equilibration + sweeps)``. The settled cost inside
|
|
41
|
+
each Result is the *measured* wall time, so declared/settled
|
|
42
|
+
discrepancies are visible in the ledger by construction.
|
|
43
|
+
seed:
|
|
44
|
+
Base seed; each evaluation derives an independent stream.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
default_L: int = 24
|
|
48
|
+
default_sweeps: int = 400
|
|
49
|
+
default_equilibration: int = 200
|
|
50
|
+
seconds_per_spin_sweep: float = 2.5e-8
|
|
51
|
+
seed: int | None = None
|
|
52
|
+
|
|
53
|
+
def __post_init__(self) -> None:
|
|
54
|
+
self._seed_sequence = np.random.SeedSequence(self.seed)
|
|
55
|
+
|
|
56
|
+
# -- helpers ------------------------------------------------------
|
|
57
|
+
def _fidelity(self, query: Query) -> tuple[int, int, int]:
|
|
58
|
+
fid = query.fidelity
|
|
59
|
+
L = int(fid.get("L", self.default_L))
|
|
60
|
+
sweeps = int(fid.get("sweeps", self.default_sweeps))
|
|
61
|
+
equilibration = int(fid.get("equilibration", self.default_equilibration))
|
|
62
|
+
if L < 2 or sweeps < 1 or equilibration < 0:
|
|
63
|
+
raise ValueError(f"Invalid fidelity: L={L}, sweeps={sweeps}, eq={equilibration}")
|
|
64
|
+
return L, sweeps, equilibration
|
|
65
|
+
|
|
66
|
+
# -- Oracle protocol ----------------------------------------------
|
|
67
|
+
def price(self, query: Query) -> Cost:
|
|
68
|
+
L, sweeps, equilibration = self._fidelity(query)
|
|
69
|
+
seconds = self.seconds_per_spin_sweep * L * L * (sweeps + equilibration)
|
|
70
|
+
return Cost(seconds=seconds, cpu_hours=seconds / 3600.0)
|
|
71
|
+
|
|
72
|
+
def evaluate(self, query: Query) -> Result:
|
|
73
|
+
T = float(query.params["T"])
|
|
74
|
+
if T <= 0:
|
|
75
|
+
raise ValueError(f"Temperature must be positive, got T={T}")
|
|
76
|
+
L, sweeps, equilibration = self._fidelity(query)
|
|
77
|
+
rng = np.random.default_rng(self._seed_sequence.spawn(1)[0])
|
|
78
|
+
|
|
79
|
+
t0 = time.perf_counter()
|
|
80
|
+
chi, abs_m = _susceptibility(T=T, L=L, sweeps=sweeps, equilibration=equilibration, rng=rng)
|
|
81
|
+
elapsed = time.perf_counter() - t0
|
|
82
|
+
|
|
83
|
+
settled = Cost(seconds=elapsed, cpu_hours=elapsed / 3600.0)
|
|
84
|
+
return Result(
|
|
85
|
+
query=query,
|
|
86
|
+
value=chi,
|
|
87
|
+
cost=settled,
|
|
88
|
+
info={"abs_magnetization": abs_m, "L": L, "sweeps": sweeps, "T": T},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ----------------------------------------------------------------------
|
|
93
|
+
# Vectorized checkerboard Metropolis
|
|
94
|
+
# ----------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
def _neighbor_sum(spins: np.ndarray) -> np.ndarray:
|
|
97
|
+
return (
|
|
98
|
+
np.roll(spins, 1, axis=0)
|
|
99
|
+
+ np.roll(spins, -1, axis=0)
|
|
100
|
+
+ np.roll(spins, 1, axis=1)
|
|
101
|
+
+ np.roll(spins, -1, axis=1)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _sweep(spins: np.ndarray, beta: float, masks: tuple[np.ndarray, np.ndarray], rng: np.random.Generator) -> None:
|
|
106
|
+
"""One full Metropolis sweep via two checkerboard half-updates."""
|
|
107
|
+
for mask in masks:
|
|
108
|
+
delta_e = 2.0 * spins * _neighbor_sum(spins)
|
|
109
|
+
accept = (delta_e <= 0) | (rng.random(spins.shape) < np.exp(-beta * delta_e))
|
|
110
|
+
spins[mask & accept] *= -1
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _susceptibility(
|
|
114
|
+
*, T: float, L: int, sweeps: int, equilibration: int, rng: np.random.Generator
|
|
115
|
+
) -> tuple[float, float]:
|
|
116
|
+
"""Magnetic susceptibility per spin, chi = beta * N * (<m^2> - <|m|>^2)."""
|
|
117
|
+
beta = 1.0 / T
|
|
118
|
+
spins = rng.choice(np.array([-1, 1], dtype=np.int8), size=(L, L)).astype(np.int8)
|
|
119
|
+
|
|
120
|
+
ii, jj = np.indices((L, L))
|
|
121
|
+
checkerboard = (ii + jj) % 2 == 0
|
|
122
|
+
masks = (checkerboard, ~checkerboard)
|
|
123
|
+
|
|
124
|
+
for _ in range(equilibration):
|
|
125
|
+
_sweep(spins, beta, masks, rng)
|
|
126
|
+
|
|
127
|
+
m_samples = np.empty(sweeps)
|
|
128
|
+
for k in range(sweeps):
|
|
129
|
+
_sweep(spins, beta, masks, rng)
|
|
130
|
+
m_samples[k] = spins.mean()
|
|
131
|
+
|
|
132
|
+
abs_m = float(np.abs(m_samples).mean())
|
|
133
|
+
chi = beta * L * L * float((m_samples**2).mean() - abs_m**2)
|
|
134
|
+
return chi, abs_m
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cadaques
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cost-Aware Dual Architecture for QUery-Efficient diScovery: autonomous discovery campaigns — any oracle, any driver, one budget.
|
|
5
|
+
Project-URL: Repository, https://github.com/jorgebravoabad/cadaques
|
|
6
|
+
Author-email: Jorge Bravo-Abad <jorge.bravo@uam.es>
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: active-learning,autonomous-discovery,bayesian-optimization,cost-aware,materials-science,self-driving-labs
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: numpy>=1.24
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
21
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# CADAQUES
|
|
25
|
+
|
|
26
|
+
**Cost-Aware Dual Architecture for QUery-Efficient diScovery**
|
|
27
|
+
|
|
28
|
+
*An open-source framework for autonomous discovery campaigns: any oracle, any driver, one budget.*
|
|
29
|
+
|
|
30
|
+
CADAQUES decouples autonomous discovery into two symmetric protocols. A metered
|
|
31
|
+
**Oracle** abstracts anything that answers queries at a price — a simulator, a
|
|
32
|
+
laboratory instrument, an analytic function. A **Driver** abstracts anything that
|
|
33
|
+
decides what to ask next — random search, Bayesian optimization, gradient methods,
|
|
34
|
+
LLM agents. Between them sits the framework's one structural commitment: **cost is
|
|
35
|
+
a first-class primitive**. Every oracle query and every driver decision is priced
|
|
36
|
+
in heterogeneous currencies (wall time, CPU hours, euros, tokens) and charged
|
|
37
|
+
against a single campaign budget. Campaigns end when the budget is exhausted, not
|
|
38
|
+
when an iteration counter runs out, and all results are reported as performance
|
|
39
|
+
per unit cost. *Every query counts.*
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install git+https://github.com/jorgebravoabad/cadaques.git
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or, for development:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
git clone https://github.com/jorgebravoabad/cadaques.git
|
|
51
|
+
cd cadaques
|
|
52
|
+
pip install -e .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A PyPI release is planned; see the roadmap below.
|
|
56
|
+
|
|
57
|
+
## Quickstart: a discovery campaign with an exact answer
|
|
58
|
+
|
|
59
|
+
Locate the critical temperature of the 2D Ising model by maximizing the magnetic
|
|
60
|
+
susceptibility under a fixed compute budget. Onsager's exact result,
|
|
61
|
+
T_c = 2/ln(1+√2) ≈ 2.269, provides the ground truth against which any driver
|
|
62
|
+
can be validated.
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from cadaques import Budget, Campaign, Cost
|
|
66
|
+
from cadaques.drivers import AnnealedLocalDriver
|
|
67
|
+
from cadaques.oracles import Ising2DOracle, T_C_EXACT
|
|
68
|
+
|
|
69
|
+
oracle = Ising2DOracle(seed=0)
|
|
70
|
+
driver = AnnealedLocalDriver(
|
|
71
|
+
space={"T": (1.5, 3.5)},
|
|
72
|
+
fidelity={"L": 24, "sweeps": 400},
|
|
73
|
+
seed=0,
|
|
74
|
+
)
|
|
75
|
+
campaign = Campaign(oracle, driver, Budget(total=Cost(seconds=30.0)))
|
|
76
|
+
|
|
77
|
+
outcome = campaign.run()
|
|
78
|
+
print(f"Best T = {outcome.best.query.params['T']:.3f} (exact: {T_C_EXACT:.3f})")
|
|
79
|
+
print(f"Queries: {outcome.n_queries}, stop reason: {outcome.stop_reason}")
|
|
80
|
+
print(f"Spent: {outcome.budget.spent}")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Design principles
|
|
84
|
+
|
|
85
|
+
- **Dual agnosticism.** Oracles and Drivers are `typing.Protocol` classes with
|
|
86
|
+
two methods each. Anything that speaks the protocol plugs in.
|
|
87
|
+
- **Declared vs. settled cost.** Oracles declare a price *ex ante*
|
|
88
|
+
(`oracle.price(query)`); the actual cost is settled *ex post* inside each
|
|
89
|
+
`Result`. Real oracles deviate from their estimates — the ledger records both,
|
|
90
|
+
and the discrepancy is itself an observable.
|
|
91
|
+
- **Both sides are metered.** Driver decisions cost wall time — and tokens, if the
|
|
92
|
+
driver is an LLM agent. A campaign's economics include the price of intelligence,
|
|
93
|
+
enabling the question: *when does an expensive smart driver beat a cheap dumb one?*
|
|
94
|
+
- **Budget-aware strategies.** Drivers receive a read-only `BudgetView` and may
|
|
95
|
+
adapt: the reference `AnnealedLocalDriver` explores while rich and exploits
|
|
96
|
+
while poor.
|
|
97
|
+
- **The ledger is the provenance.** Every transaction (declared, settled,
|
|
98
|
+
timestamped) exports to JSONL: a complete, replayable trace of the campaign.
|
|
99
|
+
|
|
100
|
+
## Status and roadmap
|
|
101
|
+
|
|
102
|
+
`0.1.0` — core protocols, campaign runner, multi-currency budget and ledger,
|
|
103
|
+
reference drivers, and a canonical Ising-2D oracle with fidelity-dependent cost.
|
|
104
|
+
|
|
105
|
+
This is an early release: the API may evolve until `1.0`. Planned next steps
|
|
106
|
+
include a Bayesian-optimization driver, campaign replay, expanded documentation,
|
|
107
|
+
a PyPI release, and an LLM-agent driver adapter.
|
|
108
|
+
|
|
109
|
+
## Citation
|
|
110
|
+
|
|
111
|
+
If you use CADAQUES in academic work, please cite it (see `CITATION.cff`).
|
|
112
|
+
A Zenodo DOI badge will appear here upon release.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
cadaques/__init__.py,sha256=TetRVyC7IYAxmdTWbxF6yseU1tq1MKaVBIY0zLxFXJI,659
|
|
2
|
+
cadaques/core/__init__.py,sha256=WIiWii9CPJCJZYZsd8O2CkkquhSUBWkzCPyBlhS6qMo,214
|
|
3
|
+
cadaques/core/campaign.py,sha256=4jBRHHCb7qV6Sz5DgirKbdNC-hs0mo4mTXjNiXNog-g,4826
|
|
4
|
+
cadaques/core/cost.py,sha256=kiPTCg-LrCd2fx7BoV6Np-F3Hejzh4zbe3W27pSGpyo,4619
|
|
5
|
+
cadaques/core/ledger.py,sha256=cYUwS5Zy7AEGZ3JNTAWeZWMobgS3nqa0w745w8QaqR0,2933
|
|
6
|
+
cadaques/core/protocols.py,sha256=KWnYeIy7ioBkq8sHq5nzi7Ppjfb2oVG2-Ty8AcffGjk,2518
|
|
7
|
+
cadaques/drivers/__init__.py,sha256=Kn4JwKQdqZPbpzL5DA3I6AU_sBF5lPeuaoWe4I_8S1Y,57
|
|
8
|
+
cadaques/drivers/reference.py,sha256=v7mgzpP6RBMnZFLyTT74DEriNHpSzzJikynZ2xsb38E,3483
|
|
9
|
+
cadaques/oracles/__init__.py,sha256=7kaO5ZYTW0z1bInWd-t4PBjljfuVbcLLkKP_dqoOkTg,97
|
|
10
|
+
cadaques/oracles/analytic.py,sha256=vE6A3GtmOMm9NAyB011968lN5ekBxy9ula3l8ErSYIQ,1730
|
|
11
|
+
cadaques/oracles/ising.py,sha256=USYLUfDoTET_r-OXFwgtXQvQAA9eQUKEMsel9PbWs1g,5002
|
|
12
|
+
cadaques-0.1.0.dist-info/METADATA,sha256=hStmEClW3GM2lbgir1SNZVV2B8KSBXTdPhIowTWGFas,4664
|
|
13
|
+
cadaques-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
14
|
+
cadaques-0.1.0.dist-info/licenses/LICENSE,sha256=4B8R4NG-iP-hN4YOpwtjgYINw_ZeOX_uaLeIYQMrnso,1073
|
|
15
|
+
cadaques-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jorge Bravo-Abad
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|