implica 0.3.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.

Potentially problematic release.


This version of implica might be problematic. Click here for more details.

implica/graph/graph.py ADDED
@@ -0,0 +1,443 @@
1
+ """
2
+ Graph structure for the implicational logic system.
3
+
4
+ This module defines the Graph class which manages nodes and edges with
5
+ optimized data structures for large graphs.
6
+ """
7
+
8
+ from typing import Iterator, Optional
9
+
10
+ from .connection import Connection
11
+ from .elements import Node, Edge
12
+ from ..core.types import BaseType
13
+
14
+
15
+ class Graph:
16
+ """
17
+ A graph representing the implicational logic model.
18
+
19
+ The graph uses optimized data structures:
20
+ - Dict for O(1) node and edge lookups by uid
21
+ - Sets for O(1) membership checks
22
+ - Adjacency lists for efficient edge traversal
23
+
24
+ The graph maintains type consistency through validation.
25
+ """
26
+
27
+ def __init__(self):
28
+ """Initialize an empty graph."""
29
+ # Core storage: uid -> object
30
+ self._nodes: dict[str, Node] = {}
31
+ self._edges: dict[str, Edge] = {}
32
+
33
+ # Adjacency lists for efficient traversal
34
+ # node_uid -> set of outgoing edge uids
35
+ self._outgoing_edges: dict[str, set[str]] = {}
36
+ # node_uid -> set of incoming edge uids
37
+ self._incoming_edges: dict[str, set[str]] = {}
38
+
39
+ # ========== Low-level API (internal use) ==========
40
+
41
+ def _add_node(self, node: Node) -> None:
42
+ """
43
+ Add a node to the graph (low-level API).
44
+
45
+ Args:
46
+ node: The node to add
47
+
48
+ Raises:
49
+ ValueError: If a node with the same uid already exists
50
+ """
51
+ uid = node.uid
52
+
53
+ if uid in self._nodes:
54
+ raise ValueError(f"Node with uid '{uid}' already exists in the graph")
55
+
56
+ self._nodes[uid] = node
57
+ self._outgoing_edges[uid] = set()
58
+ self._incoming_edges[uid] = set()
59
+
60
+ def _remove_node(self, node_uid: str) -> None:
61
+ """
62
+ Remove a node from the graph (low-level API).
63
+
64
+ This also removes all edges connected to the node.
65
+
66
+ Args:
67
+ node_uid: The uid of the node to remove
68
+
69
+ Raises:
70
+ KeyError: If the node doesn't exist
71
+ """
72
+ if node_uid not in self._nodes:
73
+ raise KeyError(f"Node with uid '{node_uid}' not found in the graph")
74
+
75
+ # Remove all connected edges
76
+ outgoing = list(self._outgoing_edges[node_uid])
77
+ incoming = list(self._incoming_edges[node_uid])
78
+
79
+ for edge_uid in outgoing:
80
+ self._remove_edge(edge_uid)
81
+
82
+ for edge_uid in incoming:
83
+ self._remove_edge(edge_uid)
84
+
85
+ # Remove the node
86
+ del self._nodes[node_uid]
87
+ del self._outgoing_edges[node_uid]
88
+ del self._incoming_edges[node_uid]
89
+
90
+ def _add_edge(self, edge: Edge) -> None:
91
+ """
92
+ Add an edge to the graph (low-level API).
93
+
94
+ Args:
95
+ edge: The edge to add
96
+
97
+ Raises:
98
+ ValueError: If the edge already exists
99
+ KeyError: If source or destination nodes don't exist
100
+ """
101
+ uid = edge.uid
102
+ src_uid = edge.src_node.uid
103
+ dst_uid = edge.dst_node.uid
104
+
105
+ if uid in self._edges:
106
+ raise ValueError(f"Edge with uid '{uid}' already exists in the graph")
107
+
108
+ if src_uid not in self._nodes:
109
+ raise KeyError(f"Source node with uid '{src_uid}' not found in the graph")
110
+
111
+ if dst_uid not in self._nodes:
112
+ raise KeyError(
113
+ f"Destination node with uid '{dst_uid}' not found in the graph"
114
+ )
115
+
116
+ # Verify nodes match
117
+ if self._nodes[src_uid] != edge.src_node:
118
+ raise ValueError(f"Source node mismatch for uid '{src_uid}'")
119
+
120
+ if self._nodes[dst_uid] != edge.dst_node:
121
+ raise ValueError(f"Destination node mismatch for uid '{dst_uid}'")
122
+
123
+ self._edges[uid] = edge
124
+ self._outgoing_edges[src_uid].add(uid)
125
+ self._incoming_edges[dst_uid].add(uid)
126
+
127
+ def _remove_edge(self, edge_uid: str) -> None:
128
+ """
129
+ Remove an edge from the graph (low-level API).
130
+
131
+ Args:
132
+ edge_uid: The uid of the edge to remove
133
+
134
+ Raises:
135
+ KeyError: If the edge doesn't exist
136
+ """
137
+ if edge_uid not in self._edges:
138
+ raise KeyError(f"Edge with uid '{edge_uid}' not found in the graph")
139
+
140
+ edge = self._edges[edge_uid]
141
+ src_uid = edge.src_node.uid
142
+ dst_uid = edge.dst_node.uid
143
+
144
+ del self._edges[edge_uid]
145
+ self._outgoing_edges[src_uid].discard(edge_uid)
146
+ self._incoming_edges[dst_uid].discard(edge_uid)
147
+
148
+ # ========== Public query API ==========
149
+
150
+ def has_node(self, node_uid: str) -> bool:
151
+ """
152
+ Check if a node exists in the graph.
153
+
154
+ Args:
155
+ node_uid: The uid of the node
156
+
157
+ Returns:
158
+ bool: True if the node exists, False otherwise
159
+ """
160
+ return node_uid in self._nodes
161
+
162
+ def get_node(self, node_uid: str) -> Node:
163
+ """
164
+ Get a node by its uid.
165
+
166
+ Args:
167
+ node_uid: The uid of the node
168
+
169
+ Returns:
170
+ Node: The node
171
+
172
+ Raises:
173
+ KeyError: If the node doesn't exist
174
+ """
175
+ if node_uid not in self._nodes:
176
+ raise KeyError(f"Node with uid '{node_uid}' not found in the graph")
177
+ return self._nodes[node_uid]
178
+
179
+ def get_node_by_type(self, type: BaseType) -> Optional[Node]:
180
+ """
181
+ Get a node by its type.
182
+
183
+ Args:
184
+ type: The type to search for
185
+
186
+ Returns:
187
+ Optional[Node]: The node if found, None otherwise
188
+ """
189
+ uid = type.uid
190
+ return self._nodes.get(uid)
191
+
192
+ def has_edge(self, edge_uid: str) -> bool:
193
+ """
194
+ Check if an edge exists in the graph.
195
+
196
+ Args:
197
+ edge_uid: The uid of the edge
198
+
199
+ Returns:
200
+ bool: True if the edge exists, False otherwise
201
+ """
202
+ return edge_uid in self._edges
203
+
204
+ def get_edge(self, edge_uid: str) -> Edge:
205
+ """
206
+ Get an edge by its uid.
207
+
208
+ Args:
209
+ edge_uid: The uid of the edge
210
+
211
+ Returns:
212
+ Edge: The edge
213
+
214
+ Raises:
215
+ KeyError: If the edge doesn't exist
216
+ """
217
+ if edge_uid not in self._edges:
218
+ raise KeyError(f"Edge with uid '{edge_uid}' not found in the graph")
219
+ return self._edges[edge_uid]
220
+
221
+ def get_edges_for_node(self, node_uid: str) -> list[Edge]:
222
+ """
223
+ Get all edges connected to a node (both incoming and outgoing).
224
+
225
+ Args:
226
+ node_uid: The uid of the node
227
+
228
+ Returns:
229
+ list[Edge]: List of connected edges
230
+ """
231
+ if node_uid not in self._nodes:
232
+ return []
233
+
234
+ edge_uids = self._outgoing_edges[node_uid] | self._incoming_edges[node_uid]
235
+ return [self._edges[uid] for uid in edge_uids]
236
+
237
+ def get_outgoing_edges(self, node_uid: str) -> list[Edge]:
238
+ """
239
+ Get all outgoing edges from a node.
240
+
241
+ Args:
242
+ node_uid: The uid of the node
243
+
244
+ Returns:
245
+ list[Edge]: List of outgoing edges
246
+ """
247
+ if node_uid not in self._nodes:
248
+ return []
249
+
250
+ return [self._edges[uid] for uid in self._outgoing_edges[node_uid]]
251
+
252
+ def get_incoming_edges(self, node_uid: str) -> list[Edge]:
253
+ """
254
+ Get all incoming edges to a node.
255
+
256
+ Args:
257
+ node_uid: The uid of the node
258
+
259
+ Returns:
260
+ list[Edge]: List of incoming edges
261
+ """
262
+ if node_uid not in self._nodes:
263
+ return []
264
+
265
+ return [self._edges[uid] for uid in self._incoming_edges[node_uid]]
266
+
267
+ def nodes(self) -> Iterator[Node]:
268
+ """
269
+ Iterate over all nodes in the graph.
270
+
271
+ Returns:
272
+ Iterator[Node]: An iterator over all nodes
273
+ """
274
+ return iter(self._nodes.values())
275
+
276
+ def edges(self) -> Iterator[Edge]:
277
+ """
278
+ Iterate over all edges in the graph.
279
+
280
+ Returns:
281
+ Iterator[Edge]: An iterator over all edges
282
+ """
283
+ return iter(self._edges.values())
284
+
285
+ def node_count(self) -> int:
286
+ """
287
+ Get the number of nodes in the graph.
288
+
289
+ Returns:
290
+ int: The number of nodes
291
+ """
292
+ return len(self._nodes)
293
+
294
+ def edge_count(self) -> int:
295
+ """
296
+ Get the number of edges in the graph.
297
+
298
+ Returns:
299
+ int: The number of edges
300
+ """
301
+ return len(self._edges)
302
+
303
+ # ========== Validation ==========
304
+
305
+ def validate(self) -> bool:
306
+ """
307
+ Validate the graph for consistency.
308
+
309
+ Checks:
310
+ - All edges reference existing nodes
311
+ - Edge node references match stored nodes
312
+ - Adjacency lists are consistent
313
+ - Adjacency lists only reference existing nodes
314
+ - Each edge in adjacency lists matches the correct source/destination
315
+ - No orphaned entries in adjacency structures
316
+
317
+ Returns:
318
+ bool: True if the graph is valid
319
+
320
+ Raises:
321
+ ValueError: If validation fails
322
+ """
323
+ # First: Check that all nodes have corresponding adjacency list entries
324
+ # This must be done before we try to access adjacency lists
325
+ for node_uid in self._nodes:
326
+ if node_uid not in self._outgoing_edges:
327
+ raise ValueError(
328
+ f"Node '{node_uid}' missing from outgoing edges structure"
329
+ )
330
+
331
+ if node_uid not in self._incoming_edges:
332
+ raise ValueError(
333
+ f"Node '{node_uid}' missing from incoming edges structure"
334
+ )
335
+
336
+ # Second: Check that all adjacency list keys correspond to existing nodes
337
+ for node_uid in self._outgoing_edges:
338
+ if node_uid not in self._nodes:
339
+ raise ValueError(
340
+ f"Outgoing edges structure contains orphaned node '{node_uid}'"
341
+ )
342
+
343
+ for node_uid in self._incoming_edges:
344
+ if node_uid not in self._nodes:
345
+ raise ValueError(
346
+ f"Incoming edges structure contains orphaned node '{node_uid}'"
347
+ )
348
+
349
+ # Third: Check all edges reference valid nodes
350
+ for edge_uid, edge in self._edges.items():
351
+ src_uid = edge.src_node.uid
352
+ dst_uid = edge.dst_node.uid
353
+
354
+ if src_uid not in self._nodes:
355
+ raise ValueError(
356
+ f"Edge '{edge_uid}' references non-existent source node '{src_uid}'"
357
+ )
358
+
359
+ if dst_uid not in self._nodes:
360
+ raise ValueError(
361
+ f"Edge '{edge_uid}' references non-existent destination node '{dst_uid}'"
362
+ )
363
+
364
+ # Check node references match
365
+ if self._nodes[src_uid] != edge.src_node:
366
+ raise ValueError(
367
+ f"Edge '{edge_uid}' source node doesn't match stored node '{src_uid}'"
368
+ )
369
+
370
+ if self._nodes[dst_uid] != edge.dst_node:
371
+ raise ValueError(
372
+ f"Edge '{edge_uid}' destination node doesn't match stored node '{dst_uid}'"
373
+ )
374
+
375
+ # Check adjacency lists
376
+ if edge_uid not in self._outgoing_edges[src_uid]:
377
+ raise ValueError(
378
+ f"Edge '{edge_uid}' not in outgoing edges of node '{src_uid}'"
379
+ )
380
+
381
+ if edge_uid not in self._incoming_edges[dst_uid]:
382
+ raise ValueError(
383
+ f"Edge '{edge_uid}' not in incoming edges of node '{dst_uid}'"
384
+ )
385
+
386
+ # Fourth: Check adjacency lists reference valid edges and correct nodes
387
+ for node_uid in self._nodes:
388
+ # Validate outgoing edges
389
+ for edge_uid in self._outgoing_edges[node_uid]:
390
+ if edge_uid not in self._edges:
391
+ raise ValueError(
392
+ f"Outgoing edge list of node '{node_uid}' references "
393
+ f"non-existent edge '{edge_uid}'"
394
+ )
395
+
396
+ # Verify the edge actually originates from this node
397
+ edge = self._edges[edge_uid]
398
+ if edge.src_node.uid != node_uid:
399
+ raise ValueError(
400
+ f"Edge '{edge_uid}' in outgoing list of node '{node_uid}' "
401
+ f"has incorrect source node '{edge.src_node.uid}'"
402
+ )
403
+
404
+ # Validate incoming edges
405
+ for edge_uid in self._incoming_edges[node_uid]:
406
+ if edge_uid not in self._edges:
407
+ raise ValueError(
408
+ f"Incoming edge list of node '{node_uid}' references "
409
+ f"non-existent edge '{edge_uid}'"
410
+ )
411
+
412
+ # Verify the edge actually terminates at this node
413
+ edge = self._edges[edge_uid]
414
+ if edge.dst_node.uid != node_uid:
415
+ raise ValueError(
416
+ f"Edge '{edge_uid}' in incoming list of node '{node_uid}' "
417
+ f"has incorrect destination node '{edge.dst_node.uid}'"
418
+ )
419
+
420
+ return True
421
+
422
+ # ========== Connection API ==========
423
+
424
+ def connect(self) -> Connection:
425
+ """
426
+ Create a connection for transactional graph modifications.
427
+
428
+ Returns:
429
+ Connection: A new connection object
430
+ """
431
+
432
+ return Connection(self)
433
+
434
+ def __str__(self) -> str:
435
+ """Return a string representation of the graph."""
436
+ return f"Graph(nodes={self.node_count()}, edges={self.edge_count()})"
437
+
438
+ def __repr__(self) -> str:
439
+ """Return a detailed representation."""
440
+ return (
441
+ f"Graph(nodes={self.node_count()}, edges={self.edge_count()}, "
442
+ f"node_list={list(self._nodes.keys())})"
443
+ )
@@ -0,0 +1,29 @@
1
+ from .base import Mutation
2
+ from .add_node import AddNode
3
+ from .remove_node import RemoveNode
4
+ from .try_add_node import TryAddNode
5
+ from .try_remove_node import TryRemoveNode
6
+ from .add_edge import AddEdge
7
+ from .remove_edge import RemoveEdge
8
+ from .try_add_edge import TryAddEdge
9
+ from .try_remove_edge import TryRemoveEdge
10
+ from .add_many_nodes import AddManyNodes
11
+ from .add_many_edges import AddManyEdges
12
+ from .remove_many_nodes import RemoveManyNodes
13
+ from .remove_many_edges import RemoveManyEdges
14
+
15
+ __all__ = [
16
+ "Mutation",
17
+ "AddNode",
18
+ "RemoveNode",
19
+ "TryAddNode",
20
+ "TryRemoveNode",
21
+ "AddEdge",
22
+ "RemoveEdge",
23
+ "TryAddEdge",
24
+ "TryRemoveEdge",
25
+ "AddManyNodes",
26
+ "AddManyEdges",
27
+ "RemoveManyNodes",
28
+ "RemoveManyEdges",
29
+ ]
@@ -0,0 +1,45 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from ..graph.elements import Edge
4
+ from .base import Mutation
5
+
6
+ if TYPE_CHECKING:
7
+ from ..graph.graph import Graph
8
+
9
+
10
+ class AddEdge(Mutation):
11
+ """Mutation to add an edge to the graph."""
12
+
13
+ def __init__(self, edge: Edge):
14
+ """
15
+ Initialize the AddEdge mutation.
16
+
17
+ Args:
18
+ edge: The edge to add
19
+ """
20
+ self.edge = edge
21
+
22
+ def forward(self, graph: "Graph") -> None:
23
+ """
24
+ Add the edge to the graph.
25
+
26
+ Args:
27
+ graph: The graph to modify
28
+
29
+ Raises:
30
+ ValueError: If the edge already exists or nodes don't exist
31
+ """
32
+ graph._add_edge(self.edge)
33
+
34
+ def backward(self, graph: "Graph") -> None:
35
+ """
36
+ Remove the edge from the graph.
37
+
38
+ Args:
39
+ graph: The graph to modify
40
+ """
41
+ graph._remove_edge(self.edge.uid)
42
+
43
+ def __str__(self) -> str:
44
+ """Return a string representation."""
45
+ return f"AddEdge({self.edge})"
@@ -0,0 +1,60 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from ..graph.elements import Edge
4
+ from .base import Mutation
5
+
6
+ if TYPE_CHECKING:
7
+ from ..graph.graph import Graph
8
+
9
+
10
+ class AddManyEdges(Mutation):
11
+ """Mutation to add multiple edges to the graph."""
12
+
13
+ def __init__(self, edges: list[Edge]):
14
+ """
15
+ Initialize the AddManyEdges mutation.
16
+
17
+ Args:
18
+ edges: List of edges to add
19
+ """
20
+ self.edges = edges
21
+ self._added_edges: list[Edge] = []
22
+
23
+ def forward(self, graph: "Graph") -> None:
24
+ """
25
+ Add all edges to the graph.
26
+
27
+ If any edge fails to be added, all previously added edges are rolled back.
28
+
29
+ Args:
30
+ graph: The graph to modify
31
+
32
+ Raises:
33
+ ValueError: If any edge already exists or nodes don't exist
34
+ """
35
+ self._added_edges = []
36
+
37
+ try:
38
+ for edge in self.edges:
39
+ graph._add_edge(edge)
40
+ self._added_edges.append(edge)
41
+ except Exception:
42
+ # Rollback: remove all edges that were added
43
+ for edge in self._added_edges:
44
+ graph._remove_edge(edge.uid)
45
+ self._added_edges = []
46
+ raise
47
+
48
+ def backward(self, graph: "Graph") -> None:
49
+ """
50
+ Remove all added edges from the graph.
51
+
52
+ Args:
53
+ graph: The graph to modify
54
+ """
55
+ for edge in reversed(self._added_edges):
56
+ graph._remove_edge(edge.uid)
57
+
58
+ def __str__(self) -> str:
59
+ """Return a string representation."""
60
+ return f"AddManyEdges(count={len(self.edges)})"
@@ -0,0 +1,60 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from ..graph.elements import Node
4
+ from .base import Mutation
5
+
6
+ if TYPE_CHECKING:
7
+ from ..graph.graph import Graph
8
+
9
+
10
+ class AddManyNodes(Mutation):
11
+ """Mutation to add multiple nodes to the graph."""
12
+
13
+ def __init__(self, nodes: list[Node]):
14
+ """
15
+ Initialize the AddManyNodes mutation.
16
+
17
+ Args:
18
+ nodes: List of nodes to add
19
+ """
20
+ self.nodes = nodes
21
+ self._added_nodes: list[Node] = []
22
+
23
+ def forward(self, graph: "Graph") -> None:
24
+ """
25
+ Add all nodes to the graph.
26
+
27
+ If any node fails to be added, all previously added nodes are rolled back.
28
+
29
+ Args:
30
+ graph: The graph to modify
31
+
32
+ Raises:
33
+ ValueError: If any node already exists
34
+ """
35
+ self._added_nodes = []
36
+
37
+ try:
38
+ for node in self.nodes:
39
+ graph._add_node(node)
40
+ self._added_nodes.append(node)
41
+ except Exception:
42
+ # Rollback: remove all nodes that were added
43
+ for node in self._added_nodes:
44
+ graph._remove_node(node.uid)
45
+ self._added_nodes = []
46
+ raise
47
+
48
+ def backward(self, graph: "Graph") -> None:
49
+ """
50
+ Remove all added nodes from the graph.
51
+
52
+ Args:
53
+ graph: The graph to modify
54
+ """
55
+ for node in reversed(self._added_nodes):
56
+ graph._remove_node(node.uid)
57
+
58
+ def __str__(self) -> str:
59
+ """Return a string representation."""
60
+ return f"AddManyNodes(count={len(self.nodes)})"
@@ -0,0 +1,45 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from ..graph import Edge, Node
4
+ from .base import Mutation
5
+
6
+ if TYPE_CHECKING:
7
+ from ..graph import Graph
8
+
9
+
10
+ class AddNode(Mutation):
11
+ """Mutation to add a node to the graph."""
12
+
13
+ def __init__(self, node: Node):
14
+ """
15
+ Initialize the AddNode mutation.
16
+
17
+ Args:
18
+ node: The node to add
19
+ """
20
+ self.node = node
21
+
22
+ def forward(self, graph: "Graph") -> None:
23
+ """
24
+ Add the node to the graph.
25
+
26
+ Args:
27
+ graph: The graph to modify
28
+
29
+ Raises:
30
+ ValueError: If the node already exists
31
+ """
32
+ graph._add_node(self.node)
33
+
34
+ def backward(self, graph: "Graph") -> None:
35
+ """
36
+ Remove the node from the graph.
37
+
38
+ Args:
39
+ graph: The graph to modify
40
+ """
41
+ graph._remove_node(self.node.uid)
42
+
43
+ def __str__(self) -> str:
44
+ """Return a string representation."""
45
+ return f"AddNode({self.node})"