ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
RvHistogram.cpp
Go to the documentation of this file.
1/**
2 * @file RvHistogram.cpp
3 * @brief SQL function `provsql.rv_histogram(token, bins)`.
4 *
5 * Returns an empirical histogram of the scalar values produced by the
6 * sub-circuit rooted at @c token, binned into @p bins equal-width
7 * intervals between the observed minimum and maximum. The sample
8 * count is taken from @c provsql.rv_mc_samples; pinning
9 * @c provsql.monte_carlo_seed makes the result reproducible.
10 *
11 * Output: a jsonb array of objects @c {bin_lo, bin_hi, count}, one per
12 * non-empty bin (zero-count bins are still emitted so the client can
13 * draw a flat baseline). Returning jsonb rather than @c SETOF record
14 * keeps the C++ side free of SRF / FuncCallContext mechanics and
15 * matches the pattern used by @c simplified_circuit_subgraph.
16 *
17 * Accepted root gate types:
18 * - @c gate_value: degenerate Dirac at the constant. Emits a single
19 * bin at @c (v, v) with count equal to the would-be sample count,
20 * so the client can normalise to a probability mass without a
21 * special case.
22 * - @c gate_rv: sampled from the leaf's distribution.
23 * - @c gate_arith: sampled by recursing through the arithmetic DAG,
24 * reusing @c gate_rv draws within an iteration so shared leaves
25 * are correctly correlated.
26 * - @c gate_mixture: sampled by recursing through the mixture's
27 * Bernoulli (gate_input) wire and the selected scalar branch,
28 * reusing per-iteration caches so a shared p_token across the
29 * circuit produces coupled draws.
30 * - @c gate_agg: sampled per Monte Carlo iteration by walking the
31 * gate_semimod children, applying their Boolean filter, and
32 * finalising the chosen aggregator (COUNT / SUM / AVG / MIN /
33 * MAX). Same machinery probability evaluation uses for HAVING
34 * under MC; see @c MonteCarloSampler::evalScalar.
35 * - @c gate_semimod: a bare per-row contribution interpreted as
36 * value · 1_{k fires}, i.e. zero in worlds where the row's
37 * Boolean filter does not fire and the scalar value otherwise.
38 *
39 * Any other gate type still raises: Boolean-valued gates fall under
40 * @c probability_evaluate, and unsupported scalar shapes (delta…)
41 * have no defined sampling semantics.
42 */
43extern "C" {
44#include "postgres.h"
45#include "fmgr.h"
46#include "utils/jsonb.h"
47#include "utils/fmgrprotos.h"
48#include "utils/uuid.h"
49#include "provsql_utils.h"
50#include "provsql_error.h"
51
52PG_FUNCTION_INFO_V1(rv_histogram);
53}
54
55#include "CircuitFromMMap.h"
56#include "Expectation.h"
57#include "GenericCircuit.h"
58#include "MonteCarloSampler.h"
59#include "RandomVariable.h"
60#include "RangeCheck.h"
61#include "provsql_utils_cpp.h"
62
63#include <algorithm>
64#include <cmath>
65#include <iomanip>
66#include <optional>
67#include <sstream>
68#include <string>
69#include <tuple>
70#include <utility>
71#include <vector>
72
73namespace {
74
75void emit_bin(std::ostringstream &out, bool &first,
76 double lo, double hi, double count)
77{
78 if (!first) out << ',';
79 first = false;
80 out << "{\"bin_lo\":" << lo
81 << ",\"bin_hi\":" << hi
82 << ",\"count\":" << count << '}';
83}
84
85} // namespace
86
87extern "C" Datum
88rv_histogram(PG_FUNCTION_ARGS)
89{
90 pg_uuid_t *root_arg = (pg_uuid_t *) PG_GETARG_POINTER(0);
91 int bins = PG_GETARG_INT32(1);
92 pg_uuid_t *prov_arg = (pg_uuid_t *) PG_GETARG_POINTER(2);
93
94 if (bins <= 0)
95 provsql_error("rv_histogram: bins must be positive (got %d)", bins);
96
97 std::ostringstream out;
98 /* setprecision(17) preserves full double round-trip through the
99 * jsonb_in parser, so the client sees the same bin edges that the
100 * sampler produced. */
101 out << std::setprecision(17);
102 out << '[';
103 bool first = true;
104
105 try {
106 /* Always go through getJointCircuit: when prov is gate_one() the
107 * joint loader still produces a valid single-root closure (the
108 * gate_one leaf is just an extra disconnected node). This keeps
109 * a single code path for shared-leaf coupling between the
110 * indicator (event_gate) and the value (root_gate) in the
111 * conditional case. */
112 gate_t root_gate, event_gate;
114 try {
115 gc = getJointCircuit(*root_arg, *prov_arg, root_gate, event_gate);
116 } catch (const CircuitException &) {
117 out << ']';
118 Datum json = DirectFunctionCall1(
119 jsonb_in, CStringGetDatum(pstrdup(out.str().c_str())));
120 PG_RETURN_DATUM(json);
121 }
122
123 /* gate_one event = unconditional. */
124 std::optional<gate_t> event_opt;
125 if (gc.getGateType(event_gate) != gate_one) event_opt = event_gate;
126 /* A stored "X | C" arrives as a conditioned root: peel it to the bare
127 * scalar target and fold the condition into the event, so the histogram
128 * is of the conditional (truncated) distribution. */
129 root_gate = provsql::lift_conditioning(gc, root_gate, event_opt);
130 const bool conditional = event_opt.has_value();
131
132 const gate_type t = gc.getGateType(root_gate);
133
134 if (t == gate_value) {
135 /* Dirac: parse the literal, emit one degenerate bin. Count
136 * mirrors rv_mc_samples (or 1 if MC is disabled) so the client
137 * can compute relative mass the same way for every gate kind. */
138 const double v = provsql::parseDoubleStrict(gc.getExtra(root_gate));
139 const unsigned n = provsql_rv_mc_samples > 0
140 ? static_cast<unsigned>(provsql_rv_mc_samples)
141 : 1u;
142 emit_bin(out, first, v, v, n);
143 } else if (t == gate_rv || t == gate_arith || t == gate_mixture
144 || t == gate_agg || t == gate_semimod) {
145 std::vector<double> samples;
146 /* Exact analytical histogram: when the (peeled) root matches a
147 * closed-form distribution -- a (truncated) Gaussian, a sum of
148 * Normals, a mixture of bare RVs -- the per-bin probability mass is
149 * cdf(hi) - cdf(lo), exact and sampling-free. This is the right
150 * answer whatever provsql.rv_mc_samples is (in particular 0):
151 * truncating a Gaussian has an exact distribution. Composites with
152 * no closed form (general gate_arith, gate_agg) fall through to
153 * Monte Carlo below. */
154 auto shape = provsql::matchClosedFormDistribution(gc, root_gate, event_opt);
155 std::optional<std::vector<std::tuple<double, double, double>>> ahist;
156 if (shape) ahist = provsql::analyticalHistogram(*shape, bins);
157
158 if (ahist) {
159 for (const auto &[bl, bh, mass] : *ahist)
160 emit_bin(out, first, bl, bh, mass);
161 } else if (provsql_rv_mc_samples <= 0) {
162 /* No closed form and sampling disabled: empty histogram rather
163 * than an error, so a distribution-profile composed with the
164 * closed-form support / moments still succeeds. */
165 ereport(NOTICE,
166 (errmsg("rv_histogram: no closed-form shape and "
167 "provsql.rv_mc_samples = 0; returning an empty "
168 "histogram")));
169 } else {
170 const unsigned N = static_cast<unsigned>(provsql_rv_mc_samples);
171
172 if (conditional) {
173 /* Closed-form truncation fast path: when the root is a bare
174 * gate_rv of a supported family and the event reduces to an
175 * interval on it, draw exactly @c N samples from the truncated
176 * distribution. 100% acceptance, so the "accepted 0 of N"
177 * error below no longer fires on tight events that previously
178 * starved the MC rejection path. */
180 gc, root_gate, *event_opt, N);
181 if (direct) {
182 samples = std::move(*direct);
183 } else {
185 gc, root_gate, *event_opt, N);
186 if (cs.accepted.empty())
188 "rv_histogram: conditional MC accepted 0 of %u samples; "
189 "raise provsql.rv_mc_samples or check that the event is "
190 "satisfiable",
191 cs.attempted);
192 samples = std::move(cs.accepted);
193 }
194 } else {
195 samples = provsql::monteCarloScalarSamples(gc, root_gate, N);
196 }
197 } /* end: rv_mc_samples > 0 */
198
199 if (!samples.empty()) {
200 /* Pick the bin range per side: when @c compute_support proves
201 * a finite support endpoint we use it verbatim (Uniform / sums
202 * of Uniforms / mixtures of bounded RVs / Exponential's lower
203 * end at 0 / etc.), because the analytical bound is tighter
204 * than any sample-based estimate. When the support is open
205 * on a side (Normal, sums involving Normal, the upper tail of
206 * Exponential / Erlang) the raw empirical extreme would be
207 * an outlier draw -- ~±4σ for a Normal at rv_mc_samples = 10000
208 * -- which stretches the histogram so the bulk of the mass
209 * concentrates in middle bins and the edge bins look empty.
210 * Trim the outermost 0.1% of samples on that side; samples
211 * outside the trimmed range still get pooled into the edge
212 * bin (the clamp below), so total counts stay conserved. */
213 std::sort(samples.begin(), samples.end());
214 const std::size_t n = samples.size();
215 const std::size_t lo_idx = n / 1000; /* 0.1% */
216 const std::size_t hi_idx = n - 1 - lo_idx;
217 auto support = provsql::compute_support(gc, root_gate, event_opt);
218 double smin = std::isfinite(support.first)
219 ? support.first
220 : samples[lo_idx];
221 double smax = std::isfinite(support.second)
222 ? support.second
223 : samples[hi_idx];
224 if (smin == smax) {
225 /* Degenerate: trimmed range collapsed to a point (every
226 * non-tail draw is the same value, or the simplifier
227 * elided everything but a single point-mass leaf). */
228 emit_bin(out, first, smin, smax,
229 static_cast<unsigned>(samples.size()));
230 } else {
231 std::vector<unsigned> counts(bins, 0);
232 const double width = (smax - smin) / bins;
233 for (double x : samples) {
234 int b = static_cast<int>((x - smin) / width);
235 if (b < 0) b = 0;
236 if (b >= bins) b = bins - 1;
237 counts[b] += 1;
238 }
239 for (int i = 0; i < bins; ++i) {
240 const double lo = smin + i * width;
241 const double hi = (i == bins - 1) ? smax : lo + width;
242 emit_bin(out, first, lo, hi, counts[i]);
243 }
244 }
245 }
246 } else {
247 const char *type_name = (t < nb_gate_types)
248 ? gate_type_name[t] : "invalid";
250 "rv_histogram: root gate type '%s' is not a scalar "
251 "(expected gate_value, gate_rv, gate_arith, gate_mixture, "
252 "gate_agg, or gate_semimod)",
253 type_name);
254 }
255 } catch (const std::exception &e) {
256 provsql_error("rv_histogram: %s", e.what());
257 } catch (...) {
258 provsql_error("rv_histogram: unknown exception");
259 }
260
261 out << ']';
262 Datum json = DirectFunctionCall1(
263 jsonb_in, CStringGetDatum(pstrdup(out.str().c_str())));
264 PG_RETURN_DATUM(json);
265}
GenericCircuit getJointCircuit(pg_uuid_t root_token, pg_uuid_t event_token, gate_t &root_gate, gate_t &event_gate)
Build a GenericCircuit containing the closures of two roots, with shared subgraphs unified.
Build in-memory circuits from the mmap-backed persistent store.
gate_t
Strongly-typed gate identifier.
Definition Circuit.h:49
Analytical expectation / variance / moment evaluator over RV circuits.
Semiring-agnostic in-memory provenance circuit.
Monte Carlo sampling over a GenericCircuit, RV-aware.
Continuous random-variable helpers (distribution parsing, moments).
Support-based bound check for continuous-RV comparators.
Datum rv_histogram(PG_FUNCTION_ARGS)
Exception type thrown by circuit operations on invalid input.
Definition Circuit.h:206
gateType getGateType(gate_t g) const
Return the type of gate g.
Definition Circuit.h:130
In-memory provenance circuit with semiring-generic evaluation.
std::string getExtra(gate_t g) const
Return the string extra for gate g.
std::pair< double, double > compute_support(const GenericCircuit &gc, gate_t root, std::optional< gate_t > event_root)
Compute the [lo, hi] support interval of a scalar sub-circuit rooted at root.
std::optional< ClosedFormShape > matchClosedFormDistribution(const GenericCircuit &gc, gate_t root, std::optional< gate_t > event_root)
Detect any of the closed-form shapes supported by rv_analytical_curves.
gate_t lift_conditioning(GenericCircuit &gc, gate_t root, std::optional< gate_t > &event_opt)
Lift conditioning out of a scalar arithmetic expression.
std::optional< std::vector< std::tuple< double, double, double > > > analyticalHistogram(const ClosedFormShape &shape, int bins)
Exact histogram (bin_lo, bin_hi, probability mass) of a closed-form shape, in bins equal-width bins o...
double parseDoubleStrict(const std::string &s)
Strictly parse s as a double.
ConditionalScalarSamples monteCarloConditionalScalarSamples(const GenericCircuit &gc, gate_t root, gate_t event_root, unsigned samples)
Rejection-sample root conditioned on event_root.
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< std::vector< double > > try_truncated_closed_form_sample(const GenericCircuit &gc, gate_t root, gate_t event_root, unsigned n)
Try to draw n exact samples from the conditional distribution of root given event_root via closed-for...
int provsql_rv_mc_samples
Default sample count for analytical-evaluator MC fallbacks; 0 disables fallback (callers raise instea...
Definition provsql.c:96
Uniform error-reporting macros for ProvSQL.
#define provsql_error(fmt,...)
Report a fatal ProvSQL error and abort the current transaction.
const char * gate_type_name[]
Names of gate types.
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...
@ gate_arith
n-ary arithmetic gate over scalar-valued children (info1 holds operator tag)
C++ utility functions for UUID manipulation.
UUID structure.