pylimid 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pylimid/__init__.py ADDED
@@ -0,0 +1,49 @@
1
+ """
2
+ pylimid — limited-memory influence diagrams with mixed-type variables.
3
+
4
+ Build a diagram from chance, decision and utility nodes, then query it with
5
+ `infer` or find the optimal policy with `solve`::
6
+
7
+ from pylimid import ChanceNode, DecisionNode, InfluenceDiagram, UtilityNode
8
+ from pylimid import infer, solve
9
+
10
+ The graph layer (`pylimid.graph`) holds the node types and the diagram;
11
+ the inference layer (`pylimid.inference`) holds `infer`, `solve` and
12
+ their result types.
13
+ """
14
+
15
+ from pylimid._version import __version__
16
+ from pylimid.graph import (
17
+ ChanceNode,
18
+ DecisionNode,
19
+ InfluenceDiagram,
20
+ UtilityNode,
21
+ )
22
+ from pylimid.inference import (
23
+ InferenceError,
24
+ InferenceResult,
25
+ Policy,
26
+ Posterior,
27
+ Solution,
28
+ SolveMethod,
29
+ SolverName,
30
+ infer,
31
+ solve,
32
+ )
33
+
34
+ __all__ = [
35
+ "ChanceNode",
36
+ "DecisionNode",
37
+ "InferenceError",
38
+ "InferenceResult",
39
+ "InfluenceDiagram",
40
+ "Policy",
41
+ "Posterior",
42
+ "Solution",
43
+ "SolveMethod",
44
+ "SolverName",
45
+ "UtilityNode",
46
+ "__version__",
47
+ "infer",
48
+ "solve",
49
+ ]
pylimid/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1.0'
22
+ __version_tuple__ = version_tuple = (0, 1, 0)
23
+
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,37 @@
1
+ """
2
+ Graph layer: nodes and diagram representation.
3
+
4
+ Backend-agnostic. Nothing in this package imports a probabilistic
5
+ programming system; the ``dist`` callables carried by chance nodes return
6
+ distribution objects whose concrete type is the backend's concern.
7
+
8
+ Node types
9
+ ----------
10
+ Three node kinds share a common base (`Node`):
11
+
12
+ - `ChanceNode` — a random variable, ``P(name | parents)``.
13
+ - `DecisionNode` — a variable the agent controls; ``parents`` are its
14
+ *information set* (observed when deciding), ``states`` its actions.
15
+ - `UtilityNode` — a deterministic payoff ``U(parents)``; always a sink.
16
+
17
+ A Bayesian network is an `InfluenceDiagram` containing only chance nodes.
18
+ """
19
+
20
+ from pylimid.graph.chance_node import ChanceNode
21
+ from pylimid.graph.decision_node import DecisionNode
22
+ from pylimid.graph.diagram import InfluenceDiagram
23
+ from pylimid.graph.node import Consistency, Node, NodeKind
24
+ from pylimid.graph.utility_node import UtilityNode
25
+ from pylimid.graph.validation import DiagramProblem, ProblemKind
26
+
27
+ __all__ = [
28
+ "ChanceNode",
29
+ "Consistency",
30
+ "DecisionNode",
31
+ "DiagramProblem",
32
+ "InfluenceDiagram",
33
+ "Node",
34
+ "NodeKind",
35
+ "ProblemKind",
36
+ "UtilityNode",
37
+ ]
@@ -0,0 +1,121 @@
1
+ """
2
+ Chance node — a random variable conditioned on its parents.
3
+
4
+ A chance node is ``P(name | parents)`` — a random variable whose distribution
5
+ depends on its parents (a causal / statistical dependency, as distinct from a
6
+ decision node's information set). It inherits the shared mutable-node contract
7
+ from `Node` — field-level validation on every assignment, and the `Consistency`
8
+ gate (optional ``dist``, derived consistency) for cross-field checking.
9
+
10
+ The ``dist`` callable receives resolved parent values as **keyword arguments**
11
+ (keyed by parent name), so it is tied to parent *names*, which are stable,
12
+ rather than parent *order*, which is incidental.
13
+ """
14
+
15
+ from collections.abc import Callable
16
+ from dataclasses import dataclass, field
17
+ from typing import Any
18
+
19
+ from pylimid.graph.node import Consistency, Node, NodeKind, _signature_matches
20
+
21
+ #: Factory returning a distribution object given resolved parent values.
22
+ #:
23
+ #: The graph layer is opaque to the concrete distribution type a backend uses,
24
+ #: so the return is typed ``Any`` here. The backend's translator narrows it.
25
+ DistFactory = Callable[..., Any]
26
+
27
+
28
+ @dataclass
29
+ class ChanceNode(Node):
30
+ """
31
+ A random variable in an influence diagram: ``P(name | parents)``.
32
+
33
+ The node is mutable. Field-level validation runs on every assignment
34
+ (including during construction), so a bad value is rejected at the moment
35
+ it is set rather than later. Cross-field consistency (``dist`` signature
36
+ vs ``parents``) is *not* enforced on assignment — it is queryable via
37
+ `consistency` and gated via `validate`.
38
+
39
+ Attributes:
40
+ name: Identifier for the node.
41
+ parents: Names of the nodes this one conditionally depends on. Empty
42
+ for a root node. May be edited after construction.
43
+ dist: Callable invoked as ``dist(**parent_values)`` returning the
44
+ node's distribution. ``None`` (the default) marks the node as
45
+ not-yet-configured.
46
+ states: Human-facing labels for a discrete node's outcomes. ``None``
47
+ marks the node as continuous. Values flowing through the graph are
48
+ integer indices into this tuple.
49
+ """
50
+
51
+ dist: DistFactory | None = field(default=None, kw_only=True)
52
+ states: tuple[str, ...] | None = field(default=None, kw_only=True)
53
+
54
+ def __setattr__(self, key: str, value: Any) -> None:
55
+ """Validate ``dist`` / ``states`` (chance-specific) then defer to base."""
56
+ if key == "dist":
57
+ _validate_dist(value)
58
+ elif key == "states":
59
+ _validate_states(value)
60
+ super().__setattr__(key, value)
61
+
62
+ # --- kind / structure ---------------------------------------------------
63
+
64
+ @property
65
+ def kind(self) -> NodeKind:
66
+ """`NodeKind.CHANCE`."""
67
+ return NodeKind.CHANCE
68
+
69
+ @property
70
+ def is_discrete(self) -> bool:
71
+ """Whether the node has a declared set of discrete states."""
72
+ return self.states is not None
73
+
74
+ # --- consistency --------------------------------------------------------
75
+
76
+ def _compute_consistency(self) -> Consistency:
77
+ if self.dist is None:
78
+ return Consistency.UNCONFIGURED
79
+ if _signature_matches(self.dist, self.parents):
80
+ return Consistency.CONSISTENT
81
+ return Consistency.STALE
82
+
83
+ def consistency_message(self, state: Consistency) -> str:
84
+ """Chance-node wording for a non-CONSISTENT state (see base)."""
85
+ if state is Consistency.UNCONFIGURED:
86
+ return f"Node {self.name!r} has no `dist` configured yet."
87
+ # STALE is the only remaining non-CONSISTENT state.
88
+ return (
89
+ f"Node {self.name!r}'s `dist` signature does not match its "
90
+ f"parents {self.parents!r}; reconfigure `dist` after changing "
91
+ f"the parent set."
92
+ )
93
+
94
+
95
+ # --- chance-specific field validators ---------------------------------------
96
+
97
+
98
+ def _validate_dist(dist: Any) -> None:
99
+ if dist is not None and not callable(dist):
100
+ raise TypeError(
101
+ f"`dist` must be callable or None, "
102
+ f"got object of type {type(dist).__name__}."
103
+ )
104
+
105
+
106
+ def _validate_states(states: Any) -> None:
107
+ if states is None:
108
+ return
109
+ if not isinstance(states, tuple):
110
+ raise TypeError(
111
+ f"`states` must be a tuple or None, got {type(states).__name__}."
112
+ )
113
+ if not states:
114
+ raise ValueError(
115
+ "`states`, when provided, must be non-empty; use `None` for a "
116
+ "continuous node."
117
+ )
118
+ if not all(isinstance(s, str) and s.strip() for s in states):
119
+ raise ValueError(f"`states` must all be non-empty strings, got {states!r}.")
120
+ if len(set(states)) != len(states):
121
+ raise ValueError(f"`states` must be unique, got {states!r}.")
@@ -0,0 +1,119 @@
1
+ """
2
+ Decision node — a variable the agent controls.
3
+
4
+ A decision node has **no distribution**: its value is chosen, not sampled. What
5
+ it does carry is:
6
+
7
+ - an **information set** (the ``parents`` field — the variables observed when
8
+ the decision is made), and
9
+ - an **action space** (``states`` — the available choices).
10
+
11
+ The information set is structural, not causal
12
+ ---------------------------------------------
13
+ A decision's ``parents`` are *not* a statistical dependency. They record what
14
+ the agent observes before acting. This is the standard influence-diagram
15
+ convention: the same ``parents`` tuple that drives topological ordering and
16
+ cycle prevention for chance nodes is reused here, but its meaning is
17
+ "information available at decision time." Keeping one field name means the
18
+ container's shared bookkeeping (validation, ordering) is reused unchanged —
19
+ see `pylimid.graph.node`.
20
+
21
+ Consistency
22
+ -----------
23
+ A decision is `Consistency.CONSISTENT` once its action ``states`` are
24
+ declared; before that it is `Consistency.UNCONFIGURED`. There is no
25
+ `Consistency.STALE` state for a decision: the action space does not depend
26
+ on the information set's size, so adding or removing an information parent
27
+ never invalidates it.
28
+
29
+ Continuous decisions are not supported: ``states=None`` leaves the node
30
+ unconfigured.
31
+ """
32
+
33
+ from dataclasses import dataclass, field
34
+ from typing import Any
35
+
36
+ from pylimid.graph.node import (
37
+ Consistency,
38
+ Node,
39
+ NodeKind,
40
+ )
41
+
42
+
43
+ @dataclass
44
+ class DecisionNode(Node):
45
+ """
46
+ A decision variable in an influence diagram.
47
+
48
+ Attributes:
49
+ name: Identifier for the node.
50
+ parents: The **information set** — names of the variables observed
51
+ when this decision is made. Not a causal dependency. May be edited
52
+ after construction.
53
+ states: Labels for the available actions. ``None`` (the default) marks
54
+ the decision as not-yet-configured. Values flowing through the
55
+ graph are integer indices into this tuple.
56
+ """
57
+
58
+ states: tuple[str, ...] | None = field(default=None, kw_only=True)
59
+
60
+ def __setattr__(self, key: str, value: Any) -> None:
61
+ """Validate ``states`` (decision-specific) then defer to base."""
62
+ if key == "states":
63
+ _validate_states(value)
64
+ super().__setattr__(key, value)
65
+
66
+ # --- kind / structure ---------------------------------------------------
67
+
68
+ @property
69
+ def kind(self) -> NodeKind:
70
+ """`NodeKind.DECISION`."""
71
+ return NodeKind.DECISION
72
+
73
+ @property
74
+ def is_discrete(self) -> bool:
75
+ """
76
+ Whether the decision has a declared, enumerable action space.
77
+
78
+ ``True`` when ``states`` is set (a discrete decision, the kind the
79
+ solvers handle). ``False`` when ``states`` is ``None``, which marks
80
+ the decision as not-yet-configured.
81
+ """
82
+ return self.states is not None
83
+
84
+ # --- consistency --------------------------------------------------------
85
+
86
+ def _compute_consistency(self) -> Consistency:
87
+ if self.states is None:
88
+ return Consistency.UNCONFIGURED
89
+ return Consistency.CONSISTENT
90
+
91
+ def consistency_message(self, state: Consistency) -> str:
92
+ """Decision-node wording for a non-CONSISTENT state (see base)."""
93
+ # UNCONFIGURED is the only non-CONSISTENT state for a decision.
94
+ return (
95
+ f"Decision {self.name!r} has no action space (`states`) declared; "
96
+ f"set `states` to the available actions."
97
+ )
98
+
99
+
100
+ # --- decision-specific field validators -------------------------------------
101
+
102
+
103
+ def _validate_states(states: Any) -> None:
104
+ """Validate the action ``states`` (mirrors the chance-node states check)."""
105
+ if states is None:
106
+ return
107
+ if not isinstance(states, tuple):
108
+ raise TypeError(
109
+ f"`states` must be a tuple or None, got {type(states).__name__}."
110
+ )
111
+ if not states:
112
+ raise ValueError(
113
+ "`states`, when provided, must be non-empty; use `None` for an "
114
+ "undeclared (or future continuous) decision."
115
+ )
116
+ if not all(isinstance(s, str) and s.strip() for s in states):
117
+ raise ValueError(f"`states` must all be non-empty strings, got {states!r}.")
118
+ if len(set(states)) != len(states):
119
+ raise ValueError(f"`states` must be unique, got {states!r}.")