ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
InformationTheory.cpp
Go to the documentation of this file.
1/**
2 * @file InformationTheory.cpp
3 * @brief Entropy / KL divergence / mutual information readouts.
4 */
5#include "InformationTheory.h"
6
7#include "CircuitFromMMap.h"
8#include "ConjugatePosterior.h" // conjugatePosterior (observe-evidence entropy)
9#include "Expectation.h" // lift_conditioning, evaluateBooleanProbability
10#include "MonteCarloSampler.h"
11#include "PivotIntegration.h" // simpsonIntegrate, kSimpsonPanels
12#include "RandomVariable.h" // parse_distribution_spec
14#include "provsql_utils_cpp.h"
15
16extern "C" {
17#include "postgres.h"
18#include "fmgr.h"
19#include "utils/uuid.h"
20#include "provsql_utils.h"
21#include "provsql_error.h"
22
23PG_FUNCTION_INFO_V1(rv_entropy);
24PG_FUNCTION_INFO_V1(rv_kl);
25PG_FUNCTION_INFO_V1(rv_mutual_information);
26}
27
28#include <algorithm>
29#include <cmath>
30#include <functional>
31#include <map>
32#include <memory>
33#include <set>
34#include <string>
35#include <vector>
36
37namespace provsql {
38
39namespace {
40
41constexpr double kInf = std::numeric_limits<double>::infinity();
42
43unsigned mc_budget_or_throw(const std::string &what)
44{
45 const int n = provsql_rv_mc_samples;
46 if (n <= 0)
47 throw CircuitException(
48 what + " has no closed form for this shape and "
49 "provsql.rv_mc_samples = 0 disables the Monte Carlo fallback");
50 return static_cast<unsigned>(n);
51}
52
53/* ------------------------------------------------------------------
54 * Density views.
55 *
56 * A gate resolves to a closed density view when its distribution is
57 * expressible in one of two exact forms: a finite pmf (gate_value,
58 * categorical mixture) or a continuous pdf callable over a finite
59 * integration window (bare gate_rv, and Bernoulli mixture trees over
60 * INDEPENDENT arms -- the gmm cascade -- whose pdf is the weighted sum
61 * of the arms'). Everything else (arith composites, shared-leaf
62 * mixtures, conditioned roots) has no closed view and the caller falls
63 * back to MC or raises.
64 * ------------------------------------------------------------------ */
65struct DensityView {
66 bool discrete;
67 std::map<double, double> pmf; /* discrete */
68 std::function<double(double)> pdf; /* continuous */
69 double lo = 0.0, hi = 0.0; /* continuous window */
70};
71
72/* Stochastic leaves reachable from g (gate_rv / gate_input /
73 * gate_mulinput): the footprint used for the structural-independence
74 * shortcuts. Deterministic gate_value leaves are deliberately not
75 * collected -- they are shared by v5-keying and carry no randomness. */
76void collectStochasticLeaves(const GenericCircuit &gc, gate_t g,
77 std::set<gate_t> &out, std::set<gate_t> &seen)
78{
79 if (!seen.insert(g).second) return;
80 const auto t = gc.getGateType(g);
81 if (t == gate_rv || t == gate_input || t == gate_mulinput)
82 out.insert(g);
83 for (gate_t c : gc.getWires(g))
84 collectStochasticLeaves(gc, c, out, seen);
85}
86
87std::set<gate_t> stochasticLeaves(const GenericCircuit &gc, gate_t g)
88{
89 std::set<gate_t> out, seen;
90 collectStochasticLeaves(gc, g, out, seen);
91 return out;
92}
93
94std::optional<DensityView>
95resolveDensity(const GenericCircuit &gc, gate_t g)
96{
97 const auto t = gc.getGateType(g);
98
99 if (t == gate_value) {
100 double v;
101 try { v = parseDoubleStrict(gc.getExtra(g)); }
102 catch (const CircuitException &) { return std::nullopt; }
103 DensityView view;
104 view.discrete = true;
105 view.pmf[v] = 1.0;
106 return view;
107 }
108
109 if (t == gate_rv) {
110 auto spec = parse_distribution_spec(gc.getExtra(g));
111 if (!spec) return std::nullopt;
112 std::shared_ptr<Distribution> dist = makeDistribution(*spec);
113 DensityView view;
114 view.discrete = false;
115 if (!dist->integrationRange(view.lo, view.hi)) return std::nullopt;
116 view.pdf = [dist](double x) { return dist->pdf(x); };
117 return view;
118 }
119
120 if (t == gate_mixture) {
121 const auto &wires = gc.getWires(g);
122 if (gc.isCategoricalMixture(g)) {
123 DensityView view;
124 view.discrete = true;
125 for (std::size_t i = 1; i < wires.size(); ++i) {
126 double v;
127 try { v = parseDoubleStrict(gc.getExtra(wires[i])); }
128 catch (const CircuitException &) { return std::nullopt; }
129 view.pmf[v] += gc.getProb(wires[i]);
130 }
131 return view;
132 }
133 /* Bernoulli mixture(p, x, y). The selection coin must be a bare
134 * pinned input and the arms must be leaf-disjoint (a shared leaf
135 * makes the arms correlated, so the pdf is no longer the weighted
136 * sum of the arm pdfs). */
137 if (wires.size() != 3) return std::nullopt;
138 if (gc.getGateType(wires[0]) != gate_input) return std::nullopt;
139 const double pi = gc.getProb(wires[0]);
140 if (std::isnan(pi)) return std::nullopt;
141 auto ax = stochasticLeaves(gc, wires[1]);
142 auto ay = stochasticLeaves(gc, wires[2]);
143 for (gate_t l : ax)
144 if (ay.count(l)) return std::nullopt;
145 auto vx = resolveDensity(gc, wires[1]);
146 auto vy = resolveDensity(gc, wires[2]);
147 if (!vx || !vy) return std::nullopt;
148 if (vx->discrete && vy->discrete) {
149 DensityView view;
150 view.discrete = true;
151 for (const auto &[v, p] : vx->pmf) view.pmf[v] += pi * p;
152 for (const auto &[v, p] : vy->pmf) view.pmf[v] += (1.0 - pi) * p;
153 return view;
154 }
155 if (!vx->discrete && !vy->discrete) {
156 DensityView view;
157 view.discrete = false;
158 view.lo = std::min(vx->lo, vy->lo);
159 view.hi = std::max(vx->hi, vy->hi);
160 auto fx = vx->pdf, fy = vy->pdf;
161 view.pdf = [pi, fx, fy](double x) {
162 const double a = fx(x), b = fy(x);
163 if (std::isnan(a) || std::isnan(b))
164 return std::numeric_limits<double>::quiet_NaN();
165 return pi * a + (1.0 - pi) * b;
166 };
167 return view;
168 }
169 /* Mixed discrete/continuous mixture: neither a pmf nor a pdf. */
170 return std::nullopt;
171 }
172
173 return std::nullopt;
174}
175
176/* Shannon entropy of a pmf / differential entropy of a pdf, in nats. */
177double entropyOfView(const DensityView &view)
178{
179 if (view.discrete) {
180 double h = 0.0;
181 for (const auto &[v, p] : view.pmf)
182 if (p > 0.0) h -= p * std::log(p);
183 return h;
184 }
185 return simpsonIntegrate(view.lo, view.hi, kSimpsonPanels,
186 [&](double x) {
187 const double f = view.pdf(x);
188 if (std::isnan(f)) return std::numeric_limits<double>::quiet_NaN();
189 if (f <= 0.0) return 0.0; /* x ln x -> 0 as x -> 0 */
190 return -f * std::log(f);
191 });
192}
193
194/* Histogram plug-in estimate of the (differential) entropy from raw
195 * draws: Scott-rule bin width, H^ = -sum (n_i/m) ln(n_i / (m h)). A
196 * degenerate sample (zero spread) is a point mass: Shannon entropy 0. */
197double entropyFromSamples(std::vector<double> xs, const std::string &what)
198{
199 xs.erase(std::remove_if(xs.begin(), xs.end(),
200 [](double v) { return std::isnan(v); }),
201 xs.end());
202 const std::size_t m = xs.size();
203 if (m == 0)
204 throw CircuitException(what + ": no defined Monte Carlo draws");
205 double mean = 0.0;
206 for (double v : xs) mean += v;
207 mean /= static_cast<double>(m);
208 double var = 0.0;
209 for (double v : xs) var += (v - mean) * (v - mean);
210 var /= static_cast<double>(m);
211 const double sd = std::sqrt(var);
212 if (!(sd > 0.0)) return 0.0; /* point mass */
213 const double h =
214 3.49 * sd / std::cbrt(static_cast<double>(m)); /* Scott's rule */
215 const auto [mn_it, mx_it] = std::minmax_element(xs.begin(), xs.end());
216 const double lo = *mn_it, hi = *mx_it;
217 const std::size_t nbins =
218 std::max<std::size_t>(1, static_cast<std::size_t>((hi - lo) / h) + 1);
219 std::vector<std::size_t> counts(nbins, 0);
220 for (double v : xs) {
221 std::size_t b = static_cast<std::size_t>((v - lo) / h);
222 if (b >= nbins) b = nbins - 1;
223 ++counts[b];
224 }
225 double H = 0.0;
226 for (std::size_t c : counts) {
227 if (c == 0) continue;
228 const double p = static_cast<double>(c) / static_cast<double>(m);
229 H -= p * std::log(p / h);
230 }
231 return H;
232}
233
234} // namespace
235
236double computeEntropy(const GenericCircuit &gc, gate_t root,
237 std::optional<gate_t> event)
238{
239 if (!event) {
240 if (auto view = resolveDensity(gc, root)) {
241 const double h = entropyOfView(*view);
242 if (!std::isnan(h)) return h;
243 }
244 return entropyFromSamples(
245 monteCarloScalarSamples(gc, root, mc_budget_or_throw("entropy")),
246 "entropy");
247 }
248 /* Conjugate observe-evidence: the posterior is a bare distribution of
249 * the prior's family, so its (differential) entropy is the exact
250 * quadrature over the family pdf -- no sampling. */
251 if (auto post = conjugatePosterior(gc, root, *event)) {
252 std::shared_ptr<Distribution> dist = makeDistribution(*post);
253 DensityView view;
254 view.discrete = false;
255 if (dist->integrationRange(view.lo, view.hi)) {
256 view.pdf = [dist](double x) { return dist->pdf(x); };
257 const double h = entropyOfView(view);
258 if (!std::isnan(h)) return h;
259 }
260 }
261 /* Conditional entropy of X | A: plug-in estimate over the rejection
262 * sampler's conditional draws (the coupled joint circuit keeps a leaf
263 * shared between root and event on one draw per iteration). */
265 gc, root, *event, mc_budget_or_throw("conditional entropy"));
266 if (cs.accepted.empty())
267 throw CircuitException(
268 "conditional entropy: the conditioning event was never satisfied "
269 "in the Monte Carlo pass (probability ~ 0?)");
270 return entropyFromSamples(std::move(cs.accepted), "conditional entropy");
271}
272
273double computeKL(const GenericCircuit &gc, gate_t p_root, gate_t q_root)
274{
275 auto vp = resolveDensity(gc, p_root);
276 auto vq = resolveDensity(gc, q_root);
277 if (!vp || !vq)
278 throw CircuitException(
279 "kl: both arguments must resolve to a closed-form density (a bare "
280 "distribution constructor, a constant, a categorical, or a mixture "
281 "of those with independent arms); arithmetic composites and "
282 "conditioned variables are not supported");
283
284 if (vp->discrete && vq->discrete) {
285 double kl = 0.0;
286 for (const auto &[v, p] : vp->pmf) {
287 if (p <= 0.0) continue;
288 auto it = vq->pmf.find(v);
289 const double q = (it == vq->pmf.end()) ? 0.0 : it->second;
290 if (q <= 0.0) return kInf; /* P not abs. continuous w.r.t. Q */
291 kl += p * std::log(p / q);
292 }
293 return kl;
294 }
295 if (vp->discrete != vq->discrete)
296 return kInf; /* atoms vs a density: never absolutely continuous */
297
298 /* Continuous-continuous: integrate f_p ln(f_p / f_q) over P's window.
299 * f_q underflowing to 0 where f_p has mass yields +Infinity, which
300 * flows through the quadrature. */
301 return simpsonIntegrate(vp->lo, vp->hi, kSimpsonPanels,
302 [&](double x) {
303 const double fp = vp->pdf(x);
304 if (std::isnan(fp)) return std::numeric_limits<double>::quiet_NaN();
305 if (fp <= 0.0) return 0.0;
306 const double fq = vq->pdf(x);
307 if (std::isnan(fq)) return std::numeric_limits<double>::quiet_NaN();
308 if (fq <= 0.0) return kInf;
309 return fp * std::log(fp / fq);
310 });
311}
312
314 gate_t y_root)
315{
316 if (x_root == y_root) {
317 /* I(X; X) = H(X): finite (Shannon) for a discrete X, +Infinity for a
318 * continuous one. */
319 if (auto view = resolveDensity(gc, x_root))
320 if (view->discrete) return entropyOfView(*view);
321 return kInf;
322 }
323
324 /* Structural independence: disjoint stochastic leaves => I = 0. */
325 {
326 auto lx = stochasticLeaves(gc, x_root);
327 auto ly = stochasticLeaves(gc, y_root);
328 bool shared = false;
329 for (gate_t l : lx)
330 if (ly.count(l)) { shared = true; break; }
331 if (!shared) return 0.0;
332 }
333
334 /* Correlated pair: 2-D histogram plug-in over coupled joint draws.
335 * B ~ m^(1/3) bins per axis; I^ = sum p_ij ln(p_ij / (p_i q_j)),
336 * non-negative by construction. */
337 const unsigned m0 = mc_budget_or_throw("mutual_information");
338 auto [xs, ys] = monteCarloScalarPairSamples(gc, x_root, y_root, m0);
339 std::vector<std::pair<double, double>> pairs;
340 pairs.reserve(xs.size());
341 for (std::size_t i = 0; i < xs.size(); ++i)
342 if (!std::isnan(xs[i]) && !std::isnan(ys[i]))
343 pairs.emplace_back(xs[i], ys[i]);
344 const std::size_t m = pairs.size();
345 if (m == 0)
346 throw CircuitException("mutual_information: no defined Monte Carlo draws");
347
348 double xlo = pairs[0].first, xhi = xlo, ylo = pairs[0].second, yhi = ylo;
349 for (const auto &[a, b] : pairs) {
350 xlo = std::min(xlo, a); xhi = std::max(xhi, a);
351 ylo = std::min(ylo, b); yhi = std::max(yhi, b);
352 }
353 if (!(xhi > xlo) || !(yhi > ylo))
354 return 0.0; /* a degenerate marginal carries no information */
355
356 const std::size_t B = std::max<std::size_t>(
357 4, static_cast<std::size_t>(std::cbrt(static_cast<double>(m))));
358 auto binOf = [B](double v, double lo, double hi) {
359 auto b = static_cast<std::size_t>((v - lo) / (hi - lo)
360 * static_cast<double>(B));
361 return (b >= B) ? B - 1 : b;
362 };
363 std::vector<std::size_t> joint(B * B, 0), margx(B, 0), margy(B, 0);
364 for (const auto &[a, b] : pairs) {
365 const std::size_t bx = binOf(a, xlo, xhi), by = binOf(b, ylo, yhi);
366 ++joint[bx * B + by];
367 ++margx[bx];
368 ++margy[by];
369 }
370 const double dm = static_cast<double>(m);
371 double mi = 0.0;
372 for (std::size_t bx = 0; bx < B; ++bx)
373 for (std::size_t by = 0; by < B; ++by) {
374 const std::size_t c = joint[bx * B + by];
375 if (c == 0) continue;
376 const double pij = static_cast<double>(c) / dm;
377 const double px = static_cast<double>(margx[bx]) / dm;
378 const double py = static_cast<double>(margy[by]) / dm;
379 mi += pij * std::log(pij / (px * py));
380 }
381 return std::max(mi, 0.0);
382}
383
384} // namespace provsql
385
386/* ------------------------------------------------------------------
387 * SQL-callable entry points, mirroring rv_moment's plumbing.
388 * ------------------------------------------------------------------ */
389
390/**
391 * @brief SQL: rv_entropy(token uuid, prov uuid) -> float8
392 */
393Datum rv_entropy(PG_FUNCTION_ARGS)
394{
395 try {
396 pg_uuid_t *token = PG_GETARG_UUID_P(0);
397 pg_uuid_t *prov = PG_GETARG_UUID_P(1);
398
399 gate_t root_gate, event_gate;
400 auto gc = getJointCircuit(*token, *prov, root_gate, event_gate);
401
402 std::optional<gate_t> event_opt;
403 if (gc.getGateType(event_gate) != gate_one)
404 event_opt = event_gate;
405 root_gate = provsql::lift_conditioning(gc, root_gate, event_opt);
406
407 return Float8GetDatum(
408 provsql::computeEntropy(gc, root_gate, event_opt));
409 } catch (const std::exception &e) {
410 provsql_error("rv_entropy: %s", e.what());
411 } catch (...) {
412 provsql_error("rv_entropy: unknown exception");
413 }
414 PG_RETURN_NULL();
415}
416
417/**
418 * @brief SQL: rv_kl(p uuid, q uuid) -> float8
419 */
420Datum rv_kl(PG_FUNCTION_ARGS)
421{
422 try {
423 pg_uuid_t *p = PG_GETARG_UUID_P(0);
424 pg_uuid_t *q = PG_GETARG_UUID_P(1);
425
426 gate_t p_gate, q_gate;
427 auto gc = getJointCircuit(*p, *q, p_gate, q_gate);
428
429 return Float8GetDatum(provsql::computeKL(gc, p_gate, q_gate));
430 } catch (const std::exception &e) {
431 provsql_error("rv_kl: %s", e.what());
432 } catch (...) {
433 provsql_error("rv_kl: unknown exception");
434 }
435 PG_RETURN_NULL();
436}
437
438/**
439 * @brief SQL: rv_mutual_information(x uuid, y uuid) -> float8
440 */
441Datum rv_mutual_information(PG_FUNCTION_ARGS)
442{
443 try {
444 pg_uuid_t *x = PG_GETARG_UUID_P(0);
445 pg_uuid_t *y = PG_GETARG_UUID_P(1);
446
447 gate_t x_gate, y_gate;
448 auto gc = getJointCircuit(*x, *y, x_gate, y_gate);
449
450 return Float8GetDatum(
451 provsql::computeMutualInformation(gc, x_gate, y_gate));
452 } catch (const std::exception &e) {
453 provsql_error("rv_mutual_information: %s", e.what());
454 } catch (...) {
455 provsql_error("rv_mutual_information: unknown exception");
456 }
457 PG_RETURN_NULL();
458}
GenericCircuit getJointCircuit(const std::vector< pg_uuid_t > &tokens, std::vector< gate_t > &gates)
Multi-root variant of getJointCircuit.
Build in-memory circuits from the mmap-backed persistent store.
gate_t
Strongly-typed gate identifier.
Definition Circuit.h:49
Exact conjugate-prior posteriors for observe-evidence circuits.
Per-family polymorphic view over a continuous gate_rv distribution (§F.1 class hierarchy).
Analytical expectation / variance / moment evaluator over RV circuits.
Datum rv_kl(PG_FUNCTION_ARGS)
SQL: rv_kl(p uuid, q uuid) -> float8.
Datum rv_entropy(PG_FUNCTION_ARGS)
SQL: rv_entropy(token uuid, prov uuid) -> float8.
Datum rv_mutual_information(PG_FUNCTION_ARGS)
SQL: rv_mutual_information(x uuid, y uuid) -> float8.
Information-theoretic readouts over scalar RV sub-circuits: entropy, Kullback-Leibler divergence,...
Monte Carlo sampling over a GenericCircuit, RV-aware.
Shared 1-D quadrature core for the pivot-conjunction and order-statistic closed forms.
Continuous random-variable helpers (distribution parsing, moments).
Exception type thrown by circuit operations on invalid input.
Definition Circuit.h:206
std::vector< gate_t > & getWires(gate_t g)
Return a mutable reference to the child-wire list of gate g.
Definition Circuit.h:140
gateType getGateType(gate_t g) const
Return the type of gate g.
Definition Circuit.h:130
In-memory provenance circuit with semiring-generic evaluation.
bool isCategoricalMixture(gate_t g) const
Test whether g is a categorical-form gate_mixture (the explicit provsql.categorical output).
std::string getExtra(gate_t g) const
Return the string extra for gate g.
double getProb(gate_t g) const
Return the probability for gate g.
double computeKL(const GenericCircuit &gc, gate_t p_root, gate_t q_root)
Kullback-Leibler divergence KL(P || Q) in nats.
gate_t lift_conditioning(GenericCircuit &gc, gate_t root, std::optional< gate_t > &event_opt)
Lift conditioning out of a scalar arithmetic expression.
std::pair< std::vector< double >, std::vector< double > > monteCarloScalarPairSamples(const GenericCircuit &gc, gate_t root_a, gate_t root_b, unsigned samples)
Coupled per-iteration draws of two scalar roots.
double parseDoubleStrict(const std::string &s)
Strictly parse s as a double.
std::unique_ptr< Distribution > makeDistribution(const DistributionSpec &spec)
Construct the per-family Distribution for a parsed spec.
double computeEntropy(const GenericCircuit &gc, gate_t root, std::optional< gate_t > event)
Entropy of root in nats: Shannon for a discrete view, differential for a continuous one.
constexpr double kInf
double simpsonIntegrate(double lo, double hi, int N, F &&f)
Composite-Simpson with N panels.
double computeMutualInformation(const GenericCircuit &gc, gate_t x_root, gate_t y_root)
Mutual information I(X; Y) in nats: exactly 0 for structurally independent roots, H(X) / Infinity for...
ConditionalScalarSamples monteCarloConditionalScalarSamples(const GenericCircuit &gc, gate_t root, gate_t event_root, unsigned samples)
Rejection-sample root conditioned on event_root.
std::optional< DistributionSpec > conjugatePosterior(const GenericCircuit &gc, gate_t target, gate_t evidence)
The exact posterior of target given evidence, as a resolved distribution spec, when the circuit match...
std::vector< double > monteCarloScalarSamples(const GenericCircuit &gc, gate_t root, unsigned samples)
Sample a scalar sub-circuit samples times and return the draws.
std::optional< DistributionSpec > parse_distribution_spec(const std::string &s)
Parse the on-disk text encoding of a gate_rv distribution.
constexpr int kSimpsonPanels
Panel count shared by every composite-Simpson quadrature over a distribution's integration range: exa...
int provsql_rv_mc_samples
Default sample count for analytical-evaluator MC fallbacks; 0 disables fallback (callers raise instea...
Definition provsql.c:100
Uniform error-reporting macros for ProvSQL.
#define provsql_error(fmt,...)
Report a fatal ProvSQL error and abort the current transaction.
Core types, constants, and utilities shared across ProvSQL.
@ gate_rv
Continuous random-variable leaf (extra encodes distribution).
@ gate_mixture
Probabilistic mixture: three wires [p_token (gate_input Bernoulli), x_token, y_token]; samples x when...
C++ utility functions for UUID manipulation.
UUID structure.