ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
AnalyticEvaluator.cpp
Go to the documentation of this file.
1/**
2 * @file AnalyticEvaluator.cpp
3 * @brief Implementation of the closed-form CDF resolution pass.
4 * See @c AnalyticEvaluator.h for the full docstring.
5 */
6#include "AnalyticEvaluator.h"
7
8#include <algorithm>
9#include <cmath>
10#include <limits>
11#include <optional>
12#include <vector>
13
14#include "Aggregation.h" // ComparisonOperator + cmpOpFromOid
15#include "RandomVariable.h" // parse_distribution_spec, parseDoubleStrict
16#include "distributions/Distribution.h" // makeDistribution, comparatorPairLess
17extern "C" {
18#include "provsql_utils.h" // gate_type
19}
20
21namespace provsql {
22
23/* pdfAt / cdfAt are the free-function entry points for the cold callers
24 * (single-RV-vs-constant decide, curve rendering, shape mass). The
25 * per-family closed forms live in the Distribution subclasses; hot
26 * quadrature loops construct the Distribution once and call pdf/cdf on it
27 * directly rather than going through these per-point. */
28double pdfAt(const DistributionSpec &d, double c)
29{
30 return makeDistribution(d)->pdf(c);
31}
32
33double cdfAt(const DistributionSpec &d, double c)
34{
35 return makeDistribution(d)->cdf(c);
36}
37
38namespace {
39
40/* All four ordered comparators reduce to either F(c) or 1 - F(c)
41 * (continuous: @c < and @c <= have the same probability, ditto @c >
42 * and @c >=). EQ / NE on continuous RVs are handled universally by
43 * RangeCheck (P(X = c) = 0, P(X != c) = 1, sound in every semiring
44 * via gate_zero / gate_one); they should never reach this function. */
45double cdfDecide(const DistributionSpec &d, ComparisonOperator op, double c)
46{
47 double cdf_c = cdfAt(d, c);
48 if (std::isnan(cdf_c)) return cdf_c;
49
50 switch (op) {
53 return cdf_c;
56 return 1.0 - cdf_c;
59 /* Should have been handled upstream by RangeCheck; if we still
60 * see one here it means RangeCheck did not run (e.g.
61 * provsql.simplify_on_load is off). Fall through to undecided
62 * rather than silently make an inconsistent choice. */
63 return std::numeric_limits<double>::quiet_NaN();
64 }
65 return std::numeric_limits<double>::quiet_NaN();
66}
67
68/* Mirror @c provsql_having_detail::flip_op without taking the
69 * dependency on @c having_semantics from this file. Used to
70 * normalise @c c @c cmp @c X into @c X @c flip(cmp) @c c. */
72{
73 switch (op) {
80 }
81 return op;
82}
83
84/* P(X op Y) for two independent RVs: the ComparatorRuleRegistry closed
85 * forms (Normal-Normal difference, Exp-Exp rate ratio, Uniform-Uniform
86 * geometric, registered by the family implementations), else the
87 * family-agnostic 1-D quadrature -- both behind comparatorPairLess. NaN if
88 * nothing applies (caller falls back to Monte Carlo). Continuous
89 * throughout, so <,<= share a value and >,>= share its complement; EQ/NE
90 * are handled upstream by RangeCheck. */
91double rvVsRvDecide(const DistributionSpec &X, const DistributionSpec &Y,
93{
94 const auto dX = makeDistribution(X);
95 const auto dY = makeDistribution(Y);
96 double pLess = comparatorPairLess(*dX, *dY); /* P(X < Y) */
97
98 if (std::isnan(pLess))
99 return pLess;
100 if (pLess < 0.0) pLess = 0.0;
101 if (pLess > 1.0) pLess = 1.0;
102 switch (op) {
105 return pLess;
108 return 1.0 - pLess; /* P(X > Y) = 1 - P(X < Y), continuous */
111 return std::numeric_limits<double>::quiet_NaN();
112 }
113 return std::numeric_limits<double>::quiet_NaN();
114}
115
116/* Try to parse a @c gate_value's extra as a double. Returns NaN on
117 * any failure (caller treats NaN as "skip this cmp"). */
118double bareValue(const GenericCircuit &gc, gate_t g)
119{
120 if (gc.getGateType(g) != gate_value)
121 return std::numeric_limits<double>::quiet_NaN();
122 try { return parseDoubleStrict(gc.getExtra(g)); }
123 catch (const CircuitException &) {
124 return std::numeric_limits<double>::quiet_NaN();
125 }
126}
127
128/* Try to parse a @c gate_rv's distribution spec. Returns @c
129 * std::nullopt on any failure. */
130std::optional<DistributionSpec>
131bareRv(const GenericCircuit &gc, gate_t g)
132{
133 if (gc.getGateType(g) != gate_rv)
134 return std::nullopt;
135 return parse_distribution_spec(gc.getExtra(g));
136}
137
138/* Closed-form P(X cmp c) for a categorical-form gate_mixture X. X's
139 * wires are [key, mul_1, ..., mul_n]; each mul_i carries its
140 * probability in set_prob and its outcome value in extra (parsed as
141 * float8). The probability is just the sum of π_i over mulinputs
142 * whose value satisfies the predicate.
143 *
144 * EQ / NE are exact too in this setting (X = c iff some outcome equals
145 * c with positive mass): the RangeCheck pre-pass treats EQ / NE over
146 * continuous RVs as P=0 / P=1, but a categorical is discrete so we
147 * decide them here. Returns NaN if any mulinput's extra fails to
148 * parse as a finite float8 -- the cmp then falls through to MC. */
149double categoricalDecide(const GenericCircuit &gc, gate_t mix,
150 ComparisonOperator op, double c)
151{
152 const auto &wires = gc.getWires(mix);
153 double p = 0.0;
154 for (std::size_t i = 1; i < wires.size(); ++i) {
155 double v;
156 try { v = parseDoubleStrict(gc.getExtra(wires[i])); }
157 catch (const CircuitException &) {
158 return std::numeric_limits<double>::quiet_NaN();
159 }
160 bool hit = false;
161 switch (op) {
162 case ComparisonOperator::LT: hit = v < c; break;
163 case ComparisonOperator::LE: hit = v <= c; break;
164 case ComparisonOperator::GT: hit = v > c; break;
165 case ComparisonOperator::GE: hit = v >= c; break;
166 case ComparisonOperator::EQ: hit = v == c; break;
167 case ComparisonOperator::NE: hit = v != c; break;
168 }
169 if (hit) p += gc.getProb(wires[i]);
170 }
171 return p;
172}
173
174/**
175 * @brief Try to decide @p cmp_gate via a closed-form CDF.
176 *
177 * Recognised shapes:
178 * - @c X @c cmp @c c (X a bare @c gate_rv, c a bare @c gate_value)
179 * - @c c @c cmp @c X (mirror of the above; flip the comparator)
180 * - @c X @c cmp @c Y where both @c X and @c Y are bare normal
181 * @c gate_rv leaves with distinct UUIDs (independence test)
182 *
183 * Returns the analytical probability in [0, 1] when decided,
184 * @c NaN otherwise.
185 */
186double tryAnalyticDecide(const GenericCircuit &gc, gate_t cmp_gate)
187{
188 bool ok = false;
189 ComparisonOperator op = cmpOpFromOid(gc.getInfos(cmp_gate).first, ok);
190 if (!ok) return std::numeric_limits<double>::quiet_NaN();
191
192 const auto &wires = gc.getWires(cmp_gate);
193 if (wires.size() != 2) return std::numeric_limits<double>::quiet_NaN();
194 gate_t lhs = wires[0], rhs = wires[1];
195
196 /* X cmp c */
197 if (auto specX = bareRv(gc, lhs)) {
198 double c = bareValue(gc, rhs);
199 if (!std::isnan(c)) return cdfDecide(*specX, op, c);
200 }
201
202 /* c cmp X */
203 if (auto specX = bareRv(gc, rhs)) {
204 double c = bareValue(gc, lhs);
205 if (!std::isnan(c)) return cdfDecide(*specX, flipCmpOp(op), c);
206 }
207
208 /* Categorical mixture cmp constant: exact sum of mass over the
209 * mulinputs whose value satisfies the predicate. EQ / NE are
210 * meaningful on a discrete distribution and decided here rather
211 * than the continuous-default route RangeCheck takes. */
212 if (gc.isCategoricalMixture(lhs)) {
213 double c = bareValue(gc, rhs);
214 if (!std::isnan(c)) return categoricalDecide(gc, lhs, op, c);
215 }
216 if (gc.isCategoricalMixture(rhs)) {
217 double c = bareValue(gc, lhs);
218 if (!std::isnan(c)) return categoricalDecide(gc, rhs, flipCmpOp(op), c);
219 }
220
221 /* X cmp Y, both bare RVs of the same family with a closed form
222 * (Normal-Normal, Exp-Exp, Uniform-Uniform). The @c X cmp X same-UUID case
223 * is handled upstream by RangeCheck's identity shortcut, so by the time we
224 * get here distinct UUIDs implies independence (each RV constructor mints a
225 * fresh @c uuid_generate_v4 token). */
226 {
227 auto specX = bareRv(gc, lhs);
228 auto specY = bareRv(gc, rhs);
229 if (specX && specY)
230 return rvVsRvDecide(*specX, *specY, op);
231 }
232
233 return std::numeric_limits<double>::quiet_NaN();
234}
235
236} // namespace
237
239{
240 unsigned resolved = 0;
241 const auto nb = gc.getNbGates();
242
243 /* Snapshot the cmp-gate ids so in-place rewrites don't affect the
244 * iteration: same pattern as @c runRangeCheck. */
245 std::vector<gate_t> cmps;
246 for (std::size_t i = 0; i < nb; ++i) {
247 auto g = static_cast<gate_t>(i);
248 if (gc.getGateType(g) == gate_cmp)
249 cmps.push_back(g);
250 }
251
252 for (gate_t c : cmps) {
253 if (gc.getGateType(c) != gate_cmp) continue;
254 double p = tryAnalyticDecide(gc, c);
255 if (!std::isnan(p)) {
256 /* Clamp to [0, 1] defensively: floating-point CDF roundoff
257 * could in principle produce values marginally outside the
258 * unit interval (1 - F(c) for c far in the right tail). */
259 if (p < 0.0) p = 0.0;
260 if (p > 1.0) p = 1.0;
261 gc.resolveCmpToBernoulli(c, p);
262 ++resolved;
263 }
264 }
265
266 return resolved;
267}
268
269} // namespace provsql
ComparisonOperator cmpOpFromOid(Oid op_oid, bool &ok)
Map a PostgreSQL comparison-operator OID to a ComparisonOperator.
Typed aggregation value, operator, and aggregator abstractions.
ComparisonOperator
SQL comparison operators used in gate_cmp circuit gates.
Definition Aggregation.h:39
@ LT
Less than (<).
Definition Aggregation.h:43
@ GT
Greater than (>).
Definition Aggregation.h:45
@ LE
Less than or equal (<=).
Definition Aggregation.h:42
@ NE
Not equal (<>).
Definition Aggregation.h:41
@ GE
Greater than or equal (>=).
Definition Aggregation.h:44
Closed-form CDF resolution for trivial gate_cmp shapes.
gate_t
Strongly-typed gate identifier.
Definition Circuit.h:49
Per-family polymorphic view over a continuous gate_rv distribution (§F.1 class hierarchy).
Continuous random-variable helpers (distribution parsing, moments).
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
std::vector< gate_t >::size_type getNbGates() const
Return the total number of gates in the circuit.
Definition Circuit.h:103
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.
void resolveCmpToBernoulli(gate_t g, double p)
Replace a gate_cmp by a constant Boolean leaf (gate_one for p == 1, gate_zero for p == 0) or by a Ber...
std::pair< unsigned, unsigned > getInfos(gate_t g) const
Return the integer annotation pair for gate g.
double comparatorPairLess(const Distribution &X, const Distribution &Y)
for two independent RVs.
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 pdfAt(const DistributionSpec &d, double c)
Closed-form probability density for a basic distribution.
std::optional< DistributionSpec > parse_distribution_spec(const std::string &s)
Parse the on-disk text encoding of a gate_rv distribution.
double cdfAt(const DistributionSpec &d, double c)
Closed-form CDF for a basic continuous distribution.
unsigned runAnalyticEvaluator(GenericCircuit &gc)
Run the closed-form CDF resolution pass over gc.
Core types, constants, and utilities shared across ProvSQL.
@ gate_rv
Continuous random-variable leaf (extra encodes distribution).
Parsed distribution spec (family + up to two parameters).