ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
ConjugatePosterior.cpp
Go to the documentation of this file.
1/**
2 * @file ConjugatePosterior.cpp
3 * @brief Implementation of the conjugate-posterior recogniser.
4 */
6
7#include "Circuit.h" // CircuitException
8#include "distributions/Distribution.h" // lookupConjugateRule
9
10#include <cmath>
11#include <cstring>
12#include <vector>
13
14extern "C" {
15#include "provsql_utils.h"
16}
17
18namespace provsql {
19
20namespace {
21
22/**
23 * @brief Flatten the @c gate_times conjunction spine of @p g into its
24 * @c gate_observe atoms.
25 *
26 * Multiplicity-preserving (no seen-set): the importance-sampling weight
27 * walk multiplies one density factor per wire occurrence, so a repeated
28 * atom must fold twice here too. @c gate_one factors are the times
29 * neutral and are skipped. Returns @c false on any other factor -- a
30 * Boolean event, a @c gate_cmp, anything -- declining the recognition.
31 */
32bool collectObserveAtoms(const GenericCircuit &gc, gate_t g,
33 std::vector<gate_t> &atoms)
34{
35 const auto type = gc.getGateType(g);
36 if (type == gate_observe) {
37 atoms.push_back(g);
38 return true;
39 }
40 if (type == gate_one)
41 return true;
42 if (type == gate_times) {
43 for (gate_t c : gc.getWires(g))
44 if (!collectObserveAtoms(gc, c, atoms))
45 return false;
46 return true;
47 }
48 return false;
49}
50
51/** @brief One parsed @c gate_observe atom of the recognised shape. */
52struct ParsedObservation {
53 DistributionTemplate lik; ///< The observed leaf's template.
54 int wired_param; ///< Which parameter is the latent (0 = p1, 1 = p2).
55 gate_t latent; ///< The gate the wired slot resolves to.
56 double datum; ///< The observed datum (the observe gate's extra).
57};
58
59/**
60 * @brief Parse one @c gate_observe atom: a single bare @c gate_rv child
61 * whose template has exactly one wired slot, with the datum in
62 * the observe gate's @c extra. @c std::nullopt on any mismatch.
63 */
64std::optional<ParsedObservation>
65parseObservation(const GenericCircuit &gc, gate_t obs)
66{
67 const auto &wires = gc.getWires(obs);
68 if (wires.size() != 1)
69 return std::nullopt;
70 const gate_t leaf = wires[0];
71 if (gc.getGateType(leaf) != gate_rv)
72 return std::nullopt;
73
74 auto tmpl = parse_distribution_template(gc.getExtra(leaf));
75 if (!tmpl || !tmpl->family)
76 return std::nullopt;
77
78 /* Exactly one wired slot, and that wire must be the latent gate
79 * itself -- not an arith composition of it (the wire points wherever
80 * the parameter token resolved to; a composed parameter wires the
81 * arith gate, which the caller's target-identity check refuses). */
82 const bool w1 = tmpl->p1.wire_slot >= 0;
83 const bool w2 = tmpl->p2.wire_slot >= 0;
84 if (w1 == w2)
85 return std::nullopt;
86
87 const auto &leaf_wires = gc.getWires(leaf);
88 const int slot = w1 ? tmpl->p1.wire_slot : tmpl->p2.wire_slot;
89 if (slot < 0 || static_cast<std::size_t>(slot) >= leaf_wires.size())
90 return std::nullopt;
91
92 ParsedObservation out;
93 out.lik = *tmpl;
94 out.wired_param = w1 ? 0 : 1;
95 out.latent = leaf_wires[slot];
96
97 try {
98 out.datum = parseDoubleStrict(gc.getExtra(obs));
99 } catch (const CircuitException &) {
100 return std::nullopt;
101 }
102 if (!std::isfinite(out.datum))
103 return std::nullopt;
104
105 return out;
106}
107
108/**
109 * @brief Canonicalise a prior spec into the conjugate registry's carrier
110 * family: the SQL @c gamma constructor stores an integer-shape
111 * prior as an @c erlang leaf, and @c Exponential(λ) is
112 * @c Gamma(1, λ), so rate priors always fold in the @c gamma
113 * carrier. The substituted spec is the identical distribution;
114 * only the update-rule key (and the readout dispatch) changes.
115 */
116void canonicalisePrior(DistributionSpec &spec)
117{
118 const char *n = spec.family->name;
119 const bool is_erlang = std::strcmp(n, "erlang") == 0;
120 const bool is_exponential = std::strcmp(n, "exponential") == 0;
121 if (!is_erlang && !is_exponential)
122 return;
123 if (const DistributionFamily *g = lookupDistributionFamily("gamma")) {
124 if (is_exponential) {
125 spec.p2 = spec.p1;
126 spec.p1 = 1.0;
127 }
128 spec.family = g;
129 }
130}
131
132/**
133 * @brief The shared fold: parse every atom, check the common latent /
134 * prior shape, and run the registry updates over the running
135 * posterior spec. When @p log_evidence is non-null, also
136 * accumulate each rule's log predictive density (declining if
137 * any rule lacks one). @p target, when supplied, must be the
138 * latent every observation wires; otherwise the latent is
139 * inferred from the first atom.
140 */
141std::optional<DistributionSpec>
142foldConjugate(const GenericCircuit &gc, std::optional<gate_t> target,
143 gate_t evidence, double *log_evidence)
144{
145 std::vector<gate_t> atoms;
146 if (!collectObserveAtoms(gc, evidence, atoms) || atoms.empty())
147 return std::nullopt;
148
149 std::optional<gate_t> latent = target;
150 DistributionSpec post;
151 bool have_prior = false;
152 double log_m = 0.0;
153
154 for (gate_t obs : atoms) {
155 auto o = parseObservation(gc, obs);
156 if (!o)
157 return std::nullopt;
158 if (!latent)
159 latent = o->latent;
160 else if (o->latent != *latent)
161 return std::nullopt; /* two distinct latents (or not the target) */
162
163 if (!have_prior) {
164 /* The prior: a bare, all-literal gate_rv. A parametric
165 * (hierarchical) prior has no constant-parameter spec and
166 * declines here. */
167 if (gc.getGateType(*latent) != gate_rv)
168 return std::nullopt;
169 auto prior = parse_distribution_spec(gc.getExtra(*latent));
170 if (!prior || !prior->family)
171 return std::nullopt;
172 post = *prior;
173 canonicalisePrior(post);
174 have_prior = true;
175 }
176
178 o->lik.family->name, o->wired_param, post.family->name);
179 if (!rule)
180 return std::nullopt;
181 if (log_evidence) {
182 if (!rule->log_predictive)
183 return std::nullopt;
184 const double l = rule->log_predictive(post.p1, post.p2,
185 o->lik, o->datum);
186 if (std::isnan(l))
187 return std::nullopt;
188 log_m += l;
189 }
190 if (!rule->update(post.p1, post.p2, o->lik, o->datum))
191 return std::nullopt;
192 }
193
194 if (log_evidence)
195 *log_evidence = log_m;
196 return post;
197}
198
199} // namespace
200
201std::optional<DistributionSpec>
202conjugatePosterior(const GenericCircuit &gc, gate_t target, gate_t evidence)
203{
204 return foldConjugate(gc, target, evidence, nullptr);
205}
206
207std::optional<double>
209{
210 double log_m;
211 if (!foldConjugate(gc, std::nullopt, evidence, &log_m))
212 return std::nullopt;
213 return log_m;
214}
215
216} // namespace provsql
Generic directed-acyclic-graph circuit template and gate identifier.
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).
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.
std::string getExtra(gate_t g) const
Return the string extra for gate g.
std::optional< double > conjugateLogEvidence(const GenericCircuit &gc, gate_t evidence)
The exact log marginal likelihood of a conjugate-shaped evidence circuit; std::nullopt on any shape ...
const ConjugateRule * lookupConjugateRule(const std::string &likelihood_family, int wired_param, const std::string &prior_family)
Look up the conjugate rule for (likelihood family, wired parameter position, prior family); nullptr o...
double parseDoubleStrict(const std::string &s)
Strictly parse s as a double.
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::optional< DistributionSpec > parse_distribution_spec(const std::string &s)
Parse the on-disk text encoding of a gate_rv distribution.
std::optional< DistributionTemplate > parse_distribution_template(const std::string &s)
Parse the on-disk text encoding of a gate_rv distribution, keeping wired (token) parameters as wire r...
const DistributionFamily * lookupDistributionFamily(const std::string &name)
Look up a family by its on-disk name token.
Core types, constants, and utilities shared across ProvSQL.
@ gate_observe
Latent-variable observation (likelihood-weighting evidence): one wire → an observed bare gate_rv leaf...
@ gate_rv
Continuous random-variable leaf (extra encodes distribution).
The conjugate update for one observation of a given likelihood family against the running posterior (...
A registered family's descriptor: its complete identity.
Parsed distribution spec (family + up to two parameters).