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 +49 -0
- pylimid/_version.py +24 -0
- pylimid/graph/__init__.py +37 -0
- pylimid/graph/chance_node.py +121 -0
- pylimid/graph/decision_node.py +119 -0
- pylimid/graph/diagram.py +545 -0
- pylimid/graph/node.py +307 -0
- pylimid/graph/render/__init__.py +10 -0
- pylimid/graph/render/_common.py +183 -0
- pylimid/graph/render/graphviz.py +118 -0
- pylimid/graph/render/mermaid.py +95 -0
- pylimid/graph/utility_node.py +113 -0
- pylimid/graph/validation.py +59 -0
- pylimid/inference/__init__.py +43 -0
- pylimid/inference/engine.py +242 -0
- pylimid/inference/numpyro/__init__.py +4 -0
- pylimid/inference/numpyro/model.py +109 -0
- pylimid/inference/numpyro/samplers.py +122 -0
- pylimid/inference/numpyro/solvers/__init__.py +38 -0
- pylimid/inference/numpyro/solvers/_policy.py +114 -0
- pylimid/inference/numpyro/solvers/backward_induction.py +286 -0
- pylimid/inference/numpyro/solvers/batched_scan.py +181 -0
- pylimid/inference/numpyro/solvers/intervention_scan.py +168 -0
- pylimid/inference/numpyro/solvers/regularity.py +150 -0
- pylimid/inference/result.py +174 -0
- pylimid/py.typed +0 -0
- pylimid-0.1.0.dist-info/METADATA +131 -0
- pylimid-0.1.0.dist-info/RECORD +30 -0
- pylimid-0.1.0.dist-info/WHEEL +4 -0
- pylimid-0.1.0.dist-info/licenses/LICENSE.md +202 -0
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}.")
|