From 215a639479802717a4cc9059d982d51e4dc853cc Mon Sep 17 00:00:00 2001 From: yfflood Date: Sat, 27 Jun 2026 12:22:53 -0700 Subject: [PATCH 1/2] Handle zero-probability decision contexts --- pycid/core/causal_bayesian_network.py | 9 +++++++++ pycid/core/macid_base.py | 4 +++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pycid/core/causal_bayesian_network.py b/pycid/core/causal_bayesian_network.py index fc623b5..f77946a 100644 --- a/pycid/core/causal_bayesian_network.py +++ b/pycid/core/causal_bayesian_network.py @@ -199,6 +199,7 @@ def expected_value( variables: Iterable[str], context: Dict[str, Outcome], intervention: Optional[Dict[str, Outcome]] = None, + zero_prob_value: Optional[float] = None, ) -> List[float]: """Compute the expected value of a real-valued variable for a given context, under an optional intervention @@ -210,8 +211,16 @@ def expected_value( context: Node values to condition upon. A dictionary mapping of node => value. intervention: Interventions to apply. A dictionary mapping node => value. + + zero_prob_value: Value to return for each variable if context has zero probability. """ factor = self.query(variables, context, intervention=intervention) + # Conditional expectations on zero-probability contexts are undefined. + # pgmpy may represent these as all-NaN factors, or as zero-mass factors. + if np.isnan(factor.values).all() or np.sum(factor.values) == 0: + if zero_prob_value is not None: + return [zero_prob_value for _ in factor.variables] + raise RuntimeError(f"query {variables} | {context} conditions on a zero-probability event") factor.normalize() # make probs add to one ev = np.array([0.0 for _ in factor.variables]) diff --git a/pycid/core/macid_base.py b/pycid/core/macid_base.py index 94c97b1..6a4b590 100644 --- a/pycid/core/macid_base.py +++ b/pycid/core/macid_base.py @@ -394,7 +394,9 @@ def opt_policy(**parent_values: Outcome) -> Outcome: eu = {} for d in domain: parent_values[decision] = d - eu[d] = sum(copy.expected_value(descendant_utility_nodes, parent_values)) + # Unreachable decision contexts do not affect expected utility, so + # use a neutral fallback while constructing the decision rule. + eu[d] = sum(copy.expected_value(descendant_utility_nodes, parent_values, zero_prob_value=0.0)) return max(eu, key=eu.get) # type: ignore self.add_cpds(StochasticFunctionCPD(decision, opt_policy, self, domain=domain, label="opt")) From 31b2aa0a8108b2608734fea93d57427514e472ad Mon Sep 17 00:00:00 2001 From: yfflood Date: Sat, 27 Jun 2026 13:35:33 -0700 Subject: [PATCH 2/2] Confirm zero-probability context before fallback --- pycid/core/causal_bayesian_network.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/pycid/core/causal_bayesian_network.py b/pycid/core/causal_bayesian_network.py index f77946a..0cdf6cb 100644 --- a/pycid/core/causal_bayesian_network.py +++ b/pycid/core/causal_bayesian_network.py @@ -7,7 +7,7 @@ import networkx as nx import numpy as np from pgmpy.factors.discrete import TabularCPD -from pgmpy.inference.ExactInference import BeliefPropagation +from pgmpy.inference.ExactInference import BeliefPropagation, VariableElimination from pgmpy.models import BayesianNetwork from pgmpy.sampling import BayesianModelSampling @@ -194,6 +194,23 @@ def intervene(self, intervention: Dict[str, Outcome]) -> None: new_dist[intervention[variable]] = 1 self.model[variable] = new_dist + def _context_has_zero_probability( + self, context: Dict[str, Outcome], intervention: Optional[Dict[str, Outcome]] = None + ) -> bool: + """Return whether the conditioning context has probability zero.""" + if not context: + return False + + cbn = self.copy() + if intervention: + cbn.intervene(intervention) + + # Test event probability without conditioning on the event. Decision + # domains still provide valid CPDs for this diagnostic check. + factor = VariableElimination(cbn).query(list(context), show_progress=False) + index = tuple(factor.state_names[variable].index(context[variable]) for variable in factor.variables) + return bool(factor.values[index] == 0) + def expected_value( self, variables: Iterable[str], @@ -216,8 +233,8 @@ def expected_value( """ factor = self.query(variables, context, intervention=intervention) # Conditional expectations on zero-probability contexts are undefined. - # pgmpy may represent these as all-NaN factors, or as zero-mass factors. - if np.isnan(factor.values).all() or np.sum(factor.values) == 0: + # Confirm that cause directly, instead of treating every NaN as this case. + if np.isnan(factor.values).all() and self._context_has_zero_probability(context, intervention): if zero_prob_value is not None: return [zero_prob_value for _ in factor.variables] raise RuntimeError(f"query {variables} | {context} conditions on a zero-probability event")