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/__init__.py +44 -0
- implica/core/__init__.py +4 -0
- implica/core/combinator.py +149 -0
- implica/core/types.py +140 -0
- implica/graph/__init__.py +5 -0
- implica/graph/connection.py +389 -0
- implica/graph/elements.py +194 -0
- implica/graph/graph.py +443 -0
- implica/mutations/__init__.py +29 -0
- implica/mutations/add_edge.py +45 -0
- implica/mutations/add_many_edges.py +60 -0
- implica/mutations/add_many_nodes.py +60 -0
- implica/mutations/add_node.py +45 -0
- implica/mutations/base.py +51 -0
- implica/mutations/remove_edge.py +54 -0
- implica/mutations/remove_many_edges.py +64 -0
- implica/mutations/remove_many_nodes.py +69 -0
- implica/mutations/remove_node.py +62 -0
- implica/mutations/try_add_edge.py +53 -0
- implica/mutations/try_add_node.py +50 -0
- implica/mutations/try_remove_edge.py +58 -0
- implica/mutations/try_remove_node.py +62 -0
- implica-0.3.0.dist-info/LICENSE +21 -0
- implica-0.3.0.dist-info/METADATA +945 -0
- implica-0.3.0.dist-info/RECORD +26 -0
- implica-0.3.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,945 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: implica
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A package for working with graphs representing minimal implicational logic models
|
|
5
|
+
Author: Carlos Fernandez
|
|
6
|
+
Author-email: carlos.ferlo@outlook.com
|
|
7
|
+
Requires-Python: >=3.10,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Requires-Dist: pydantic (>=2.0.0,<3.0.0)
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Implica
|
|
16
|
+
|
|
17
|
+
A Python package for working with graphs representing minimal implicational logic models. This library provides tools for constructing and manipulating type systems based on combinatory logic, with support for transactional graph operations.
|
|
18
|
+
|
|
19
|
+
**Import as `imp` for a clean, concise API!**
|
|
20
|
+
|
|
21
|
+
[](https://www.python.org/downloads/)
|
|
22
|
+
[](LICENSE)
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- 🎯 **Type System**: Build complex type expressions using variables and applications (function types)
|
|
27
|
+
- 🧩 **Combinators**: Work with S and K combinators from combinatory logic
|
|
28
|
+
- 📊 **Graph Structure**: Represent type transformations as nodes and edges in a directed graph
|
|
29
|
+
- 🔄 **Transactional Operations**: Safely modify graphs with automatic rollback on failure
|
|
30
|
+
- 📦 **Bulk Operations**: Add multiple nodes or edges atomically with `add_many_nodes` and `add_many_edges`
|
|
31
|
+
- 🛡️ **Idempotent Mutations**: Use `try_add_node` and `try_add_edge` for safe, duplicate-tolerant operations
|
|
32
|
+
- ✅ **Validation**: Ensure graph consistency with built-in validation
|
|
33
|
+
- 🚀 **Performance**: Optimized data structures for O(1) lookups and efficient traversal
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
### Using Poetry (recommended)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
poetry add implica
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Using pip
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install implica
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### From source
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git clone https://github.com/CarlosFerLo/implicational-logic-graph.git
|
|
53
|
+
cd implicational-logic-graph
|
|
54
|
+
poetry install
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import implica as imp
|
|
61
|
+
|
|
62
|
+
# Create type variables
|
|
63
|
+
A = imp.var("A")
|
|
64
|
+
B = imp.var("B")
|
|
65
|
+
|
|
66
|
+
# Create a function type: A -> B
|
|
67
|
+
func_type = imp.app(A, B)
|
|
68
|
+
|
|
69
|
+
# Create a graph with nodes
|
|
70
|
+
graph = imp.Graph()
|
|
71
|
+
with graph.connect() as conn:
|
|
72
|
+
conn.add_node(imp.node(A))
|
|
73
|
+
conn.add_node(imp.node(B))
|
|
74
|
+
|
|
75
|
+
print(f"Graph has {graph.node_count()} nodes")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Core Concepts
|
|
79
|
+
|
|
80
|
+
### Types
|
|
81
|
+
|
|
82
|
+
The library provides a type system for minimal implicational logic:
|
|
83
|
+
|
|
84
|
+
- **Variable**: Atomic type variables (e.g., `A`, `B`, `C`)
|
|
85
|
+
- **Application**: Function types representing `input_type -> output_type`
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import implica as imp
|
|
89
|
+
|
|
90
|
+
# Simple type variables
|
|
91
|
+
A = imp.var("A")
|
|
92
|
+
B = imp.var("B")
|
|
93
|
+
C = imp.var("C")
|
|
94
|
+
|
|
95
|
+
# Function type: A -> B
|
|
96
|
+
simple_function = imp.app(A, B)
|
|
97
|
+
|
|
98
|
+
# Complex type: (A -> B) -> C
|
|
99
|
+
complex_function = imp.app(imp.app(A, B), C)
|
|
100
|
+
|
|
101
|
+
# Nested type: A -> (B -> C)
|
|
102
|
+
nested_function = imp.app(A, imp.app(B, C))
|
|
103
|
+
|
|
104
|
+
print(simple_function) # Output: A -> B
|
|
105
|
+
print(complex_function) # Output: (A -> B) -> C
|
|
106
|
+
print(nested_function) # Output: A -> B -> C
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Combinators
|
|
110
|
+
|
|
111
|
+
Combinators represent transformations between types. The library includes the S and K combinators from combinatory logic:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
import implica as imp
|
|
115
|
+
|
|
116
|
+
A = imp.var("A")
|
|
117
|
+
B = imp.var("B")
|
|
118
|
+
C = imp.var("C")
|
|
119
|
+
|
|
120
|
+
# S combinator: (A -> B -> C) -> (A -> B) -> A -> C
|
|
121
|
+
s_comb = imp.S(A, B, C)
|
|
122
|
+
print(s_comb) # Output: S: (A -> B -> C) -> (A -> B) -> A -> C
|
|
123
|
+
|
|
124
|
+
# K combinator: A -> B -> A
|
|
125
|
+
k_comb = imp.K(A, B)
|
|
126
|
+
print(k_comb) # Output: K: A -> B -> A
|
|
127
|
+
|
|
128
|
+
# Custom combinator
|
|
129
|
+
identity = imp.Combinator(name="I", type=imp.app(A, A))
|
|
130
|
+
print(identity) # Output: I: A -> A
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Graph Elements
|
|
134
|
+
|
|
135
|
+
#### Nodes
|
|
136
|
+
|
|
137
|
+
Nodes represent types in the graph:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
import implica as imp
|
|
141
|
+
|
|
142
|
+
A = imp.var("A")
|
|
143
|
+
B = imp.var("B")
|
|
144
|
+
|
|
145
|
+
# Create nodes
|
|
146
|
+
node_a = imp.node(A)
|
|
147
|
+
node_b = imp.node(B)
|
|
148
|
+
node_func = imp.node(imp.app(A, B))
|
|
149
|
+
|
|
150
|
+
print(node_a) # Output: Node(A)
|
|
151
|
+
print(node_func) # Output: Node(A -> B)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### Edges
|
|
155
|
+
|
|
156
|
+
Edges represent combinator transformations between types:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
import implica as imp
|
|
160
|
+
|
|
161
|
+
A = imp.var("A")
|
|
162
|
+
B = imp.var("B")
|
|
163
|
+
|
|
164
|
+
# Create nodes
|
|
165
|
+
n1 = imp.node(A)
|
|
166
|
+
n2 = imp.node(B)
|
|
167
|
+
|
|
168
|
+
# Create a combinator that transforms A to B
|
|
169
|
+
comb = imp.Combinator(name="f", type=imp.app(A, B))
|
|
170
|
+
|
|
171
|
+
# Create an edge
|
|
172
|
+
e = imp.edge(n1, n2, comb)
|
|
173
|
+
print(e) # Output: A --[f: A -> B]--> B
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Graph Operations
|
|
177
|
+
|
|
178
|
+
#### Creating and Modifying Graphs
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
import implica as imp
|
|
182
|
+
|
|
183
|
+
# Create an empty graph
|
|
184
|
+
graph = imp.Graph()
|
|
185
|
+
|
|
186
|
+
# Create types and nodes
|
|
187
|
+
A = imp.var("A")
|
|
188
|
+
B = imp.var("B")
|
|
189
|
+
C = imp.var("C")
|
|
190
|
+
|
|
191
|
+
n_a = imp.node(A)
|
|
192
|
+
n_b = imp.node(B)
|
|
193
|
+
n_c = imp.node(C)
|
|
194
|
+
|
|
195
|
+
# Use transactional connections to modify the graph
|
|
196
|
+
with graph.connect() as conn:
|
|
197
|
+
conn.add_node(n_a)
|
|
198
|
+
conn.add_node(n_b)
|
|
199
|
+
conn.add_node(n_c)
|
|
200
|
+
|
|
201
|
+
print(f"Nodes: {graph.node_count()}") # Output: Nodes: 3
|
|
202
|
+
|
|
203
|
+
# Add edges
|
|
204
|
+
comb_ab = imp.Combinator(name="f", type=imp.app(A, B))
|
|
205
|
+
comb_bc = imp.Combinator(name="g", type=imp.app(B, C))
|
|
206
|
+
|
|
207
|
+
with graph.connect() as conn:
|
|
208
|
+
conn.add_edge(imp.edge(n_a, n_b, comb_ab))
|
|
209
|
+
conn.add_edge(imp.edge(n_b, n_c, comb_bc))
|
|
210
|
+
|
|
211
|
+
print(f"Edges: {graph.edge_count()}") # Output: Edges: 2
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
#### Querying Graphs
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
# Get node by UID
|
|
218
|
+
node_retrieved = graph.get_node(n_a.uid)
|
|
219
|
+
|
|
220
|
+
# Get node by type
|
|
221
|
+
node_by_type = graph.get_node_by_type(A)
|
|
222
|
+
|
|
223
|
+
# Check if node exists
|
|
224
|
+
exists = graph.has_node(n_a.uid)
|
|
225
|
+
|
|
226
|
+
# Get outgoing edges from a node
|
|
227
|
+
outgoing = graph.get_outgoing_edges(n_a.uid)
|
|
228
|
+
print(f"Outgoing from A: {len(outgoing)}")
|
|
229
|
+
|
|
230
|
+
# Get incoming edges to a node
|
|
231
|
+
incoming = graph.get_incoming_edges(n_c.uid)
|
|
232
|
+
print(f"Incoming to C: {len(incoming)}")
|
|
233
|
+
|
|
234
|
+
# Iterate over all nodes
|
|
235
|
+
for n in graph.nodes():
|
|
236
|
+
print(f"Node: {n.type}")
|
|
237
|
+
|
|
238
|
+
# Iterate over all edges
|
|
239
|
+
for e in graph.edges():
|
|
240
|
+
print(f"Edge: {e}")
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
#### Graph Validation
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
# Validate graph consistency
|
|
247
|
+
try:
|
|
248
|
+
graph.validate()
|
|
249
|
+
print("Graph is valid!")
|
|
250
|
+
except ValueError as e:
|
|
251
|
+
print(f"Graph validation failed: {e}")
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Advanced Mutations
|
|
255
|
+
|
|
256
|
+
The library provides several mutation types for flexible graph modifications:
|
|
257
|
+
|
|
258
|
+
#### Bulk Mutations
|
|
259
|
+
|
|
260
|
+
Add multiple nodes or edges in a single atomic operation:
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
import implica as imp
|
|
264
|
+
|
|
265
|
+
graph = imp.Graph()
|
|
266
|
+
|
|
267
|
+
# Create multiple nodes
|
|
268
|
+
nodes = [imp.node(imp.var(f"T{i}")) for i in range(5)]
|
|
269
|
+
|
|
270
|
+
# Add all nodes atomically - if any fails, all are rolled back
|
|
271
|
+
with graph.connect() as conn:
|
|
272
|
+
conn.add_many_nodes(nodes)
|
|
273
|
+
|
|
274
|
+
# Create multiple edges
|
|
275
|
+
A, B, C = imp.var("A"), imp.var("B"), imp.var("C")
|
|
276
|
+
combinators = [
|
|
277
|
+
imp.Combinator("f1", imp.app(A, B)),
|
|
278
|
+
imp.Combinator("f2", imp.app(B, C)),
|
|
279
|
+
]
|
|
280
|
+
|
|
281
|
+
# Add all edges atomically
|
|
282
|
+
with graph.connect() as conn:
|
|
283
|
+
conn.add_many_edges(combinators)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Benefits:**
|
|
287
|
+
|
|
288
|
+
- All-or-nothing semantics: if any operation fails, all are rolled back
|
|
289
|
+
- More efficient than individual additions
|
|
290
|
+
- Cleaner code for batch operations
|
|
291
|
+
|
|
292
|
+
**Removal Operations:**
|
|
293
|
+
|
|
294
|
+
The same bulk operations are available for removing nodes and edges:
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
# Remove multiple nodes at once
|
|
298
|
+
node_uids = [n.uid for n in nodes[:3]]
|
|
299
|
+
with graph.connect() as conn:
|
|
300
|
+
conn.remove_many_nodes(node_uids)
|
|
301
|
+
|
|
302
|
+
# Remove multiple edges at once
|
|
303
|
+
edge_uids = [e.uid for e in some_edges]
|
|
304
|
+
with graph.connect() as conn:
|
|
305
|
+
conn.remove_many_edges(edge_uids)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
#### Idempotent Mutations
|
|
309
|
+
|
|
310
|
+
Use safe mutations that don't fail when items already exist or don't exist:
|
|
311
|
+
|
|
312
|
+
```python
|
|
313
|
+
import implica as imp
|
|
314
|
+
|
|
315
|
+
graph = imp.Graph()
|
|
316
|
+
|
|
317
|
+
A = imp.var("A")
|
|
318
|
+
n_a = imp.node(A)
|
|
319
|
+
|
|
320
|
+
# Regular add would fail on duplicate
|
|
321
|
+
with graph.connect() as conn:
|
|
322
|
+
conn.add_node(n_a)
|
|
323
|
+
|
|
324
|
+
# try_add_node won't fail if node exists
|
|
325
|
+
with graph.connect() as conn:
|
|
326
|
+
conn.try_add_node(n_a) # No error, even though n_a exists
|
|
327
|
+
|
|
328
|
+
# Same with edges
|
|
329
|
+
B = imp.var("B")
|
|
330
|
+
with graph.connect() as conn:
|
|
331
|
+
conn.try_add_node(imp.node(B))
|
|
332
|
+
|
|
333
|
+
comb = imp.Combinator("f", imp.app(A, B))
|
|
334
|
+
with graph.connect() as conn:
|
|
335
|
+
conn.try_add_edge(comb) # Adds the edge
|
|
336
|
+
|
|
337
|
+
with graph.connect() as conn:
|
|
338
|
+
conn.try_add_edge(comb) # No error, edge already exists
|
|
339
|
+
|
|
340
|
+
# Safe removal operations
|
|
341
|
+
with graph.connect() as conn:
|
|
342
|
+
conn.try_remove_node(n_a.uid) # Removes the node
|
|
343
|
+
|
|
344
|
+
with graph.connect() as conn:
|
|
345
|
+
conn.try_remove_node(n_a.uid) # No error, node doesn't exist anymore
|
|
346
|
+
|
|
347
|
+
# Same with edges
|
|
348
|
+
with graph.connect() as conn:
|
|
349
|
+
conn.try_remove_edge("some_edge_uid") # No error even if edge doesn't exist
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**Use Cases:**
|
|
353
|
+
|
|
354
|
+
- Building graphs from multiple sources where duplicates may occur
|
|
355
|
+
- Idempotent initialization routines
|
|
356
|
+
- Avoiding explicit existence checks before adding or removing elements
|
|
357
|
+
- Incremental graph construction without duplicate errors
|
|
358
|
+
- Cleanup operations that should be safe to run multiple times
|
|
359
|
+
|
|
360
|
+
## Usage Examples
|
|
361
|
+
|
|
362
|
+
### Example 1: Simple Type Chain
|
|
363
|
+
|
|
364
|
+
Build a chain of type transformations:
|
|
365
|
+
|
|
366
|
+
```python
|
|
367
|
+
import implica as imp
|
|
368
|
+
|
|
369
|
+
# Create graph
|
|
370
|
+
graph = imp.Graph()
|
|
371
|
+
|
|
372
|
+
# Define types
|
|
373
|
+
types = [imp.var(name) for name in ["A", "B", "C", "D"]]
|
|
374
|
+
nodes = [imp.node(t) for t in types]
|
|
375
|
+
|
|
376
|
+
# Add nodes
|
|
377
|
+
with graph.connect() as conn:
|
|
378
|
+
for n in nodes:
|
|
379
|
+
conn.add_node(n)
|
|
380
|
+
|
|
381
|
+
# Create transformation chain: A -> B -> C -> D
|
|
382
|
+
with graph.connect() as conn:
|
|
383
|
+
for i in range(len(nodes) - 1):
|
|
384
|
+
comb = imp.Combinator(
|
|
385
|
+
name=f"f{i}",
|
|
386
|
+
type=imp.app(types[i], types[i + 1])
|
|
387
|
+
)
|
|
388
|
+
conn.add_edge(imp.edge(nodes[i], nodes[i + 1], comb))
|
|
389
|
+
|
|
390
|
+
print(f"Created chain with {graph.node_count()} nodes and {graph.edge_count()} edges")
|
|
391
|
+
|
|
392
|
+
# Query the chain
|
|
393
|
+
a_node = graph.get_node_by_type(imp.var("A"))
|
|
394
|
+
outgoing = graph.get_outgoing_edges(a_node.uid)
|
|
395
|
+
print(f"A has {len(outgoing)} outgoing edge(s)")
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Example 2: Complex Type Structure
|
|
399
|
+
|
|
400
|
+
Work with higher-order functions:
|
|
401
|
+
|
|
402
|
+
```python
|
|
403
|
+
import implica as imp
|
|
404
|
+
|
|
405
|
+
# Create graph
|
|
406
|
+
graph = imp.Graph()
|
|
407
|
+
|
|
408
|
+
# Define type variables
|
|
409
|
+
A = imp.var("A")
|
|
410
|
+
B = imp.var("B")
|
|
411
|
+
C = imp.var("C")
|
|
412
|
+
|
|
413
|
+
# Create complex types
|
|
414
|
+
# Type 1: A
|
|
415
|
+
# Type 2: B
|
|
416
|
+
# Type 3: A -> B
|
|
417
|
+
# Type 4: (A -> B) -> C
|
|
418
|
+
t1 = A
|
|
419
|
+
t2 = B
|
|
420
|
+
t3 = imp.app(A, B)
|
|
421
|
+
t4 = imp.app(t3, C)
|
|
422
|
+
|
|
423
|
+
nodes = [imp.node(t) for t in [t1, t2, t3, t4]]
|
|
424
|
+
|
|
425
|
+
# Add nodes
|
|
426
|
+
with graph.connect() as conn:
|
|
427
|
+
for n in nodes:
|
|
428
|
+
conn.add_node(n)
|
|
429
|
+
|
|
430
|
+
# Add S and K combinators as edges
|
|
431
|
+
s_comb = imp.S(A, B, C)
|
|
432
|
+
k_comb = imp.K(A, B)
|
|
433
|
+
|
|
434
|
+
# Note: You would connect these based on your specific logic model
|
|
435
|
+
print(f"Created graph with {graph.node_count()} nodes")
|
|
436
|
+
print(f"S combinator type: {s_comb.type}")
|
|
437
|
+
print(f"K combinator type: {k_comb.type}")
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Example 3: Transactional Rollback
|
|
441
|
+
|
|
442
|
+
Demonstrate automatic rollback on failure:
|
|
443
|
+
|
|
444
|
+
```python
|
|
445
|
+
import implica as imp
|
|
446
|
+
|
|
447
|
+
graph = imp.Graph()
|
|
448
|
+
|
|
449
|
+
A = imp.var("A")
|
|
450
|
+
B = imp.var("B")
|
|
451
|
+
n_a = imp.node(A)
|
|
452
|
+
n_b = imp.node(B)
|
|
453
|
+
|
|
454
|
+
# Add initial nodes
|
|
455
|
+
with graph.connect() as conn:
|
|
456
|
+
conn.add_node(n_a)
|
|
457
|
+
conn.add_node(n_b)
|
|
458
|
+
|
|
459
|
+
print(f"Initial nodes: {graph.node_count()}") # Output: 2
|
|
460
|
+
|
|
461
|
+
# Try to add invalid edge (will fail and rollback)
|
|
462
|
+
try:
|
|
463
|
+
with graph.connect() as conn:
|
|
464
|
+
# This will succeed
|
|
465
|
+
C = imp.var("C")
|
|
466
|
+
n_c = imp.node(C)
|
|
467
|
+
conn.add_node(n_c)
|
|
468
|
+
|
|
469
|
+
# This will fail (trying to add duplicate node)
|
|
470
|
+
conn.add_node(n_a) # Already exists!
|
|
471
|
+
except RuntimeError as e:
|
|
472
|
+
print(f"Transaction failed: {e}")
|
|
473
|
+
|
|
474
|
+
# Graph remains unchanged
|
|
475
|
+
print(f"Nodes after failed transaction: {graph.node_count()}") # Output: 2
|
|
476
|
+
print(f"Has C: {graph.get_node_by_type(imp.var('C')) is not None}") # Output: False
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Example 4: Building a Proof Tree
|
|
480
|
+
|
|
481
|
+
Create a structure representing logical derivations:
|
|
482
|
+
|
|
483
|
+
```python
|
|
484
|
+
import implica as imp
|
|
485
|
+
|
|
486
|
+
# Create a graph representing a proof
|
|
487
|
+
graph = imp.Graph()
|
|
488
|
+
|
|
489
|
+
# Axioms (base types)
|
|
490
|
+
P = imp.var("P")
|
|
491
|
+
Q = imp.var("Q")
|
|
492
|
+
R = imp.var("R")
|
|
493
|
+
|
|
494
|
+
# Derived types (implications)
|
|
495
|
+
PQ = imp.app(P, Q) # P -> Q
|
|
496
|
+
QR = imp.app(Q, R) # Q -> R
|
|
497
|
+
PR = imp.app(P, R) # P -> R (conclusion)
|
|
498
|
+
|
|
499
|
+
# Create nodes for each type in the proof
|
|
500
|
+
nodes_dict = {
|
|
501
|
+
"P": imp.node(P),
|
|
502
|
+
"Q": imp.node(Q),
|
|
503
|
+
"R": imp.node(R),
|
|
504
|
+
"P->Q": imp.node(PQ),
|
|
505
|
+
"Q->R": imp.node(QR),
|
|
506
|
+
"P->R": imp.node(PR),
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
# Add all nodes
|
|
510
|
+
with graph.connect() as conn:
|
|
511
|
+
for n in nodes_dict.values():
|
|
512
|
+
conn.add_node(n)
|
|
513
|
+
|
|
514
|
+
# Add inference rules as edges
|
|
515
|
+
with graph.connect() as conn:
|
|
516
|
+
# Modus Ponens: P, P->Q ⊢ Q
|
|
517
|
+
mp1 = imp.Combinator(name="MP1", type=imp.app(P, Q))
|
|
518
|
+
conn.add_edge(imp.edge(nodes_dict["P"], nodes_dict["Q"], mp1))
|
|
519
|
+
|
|
520
|
+
# Modus Ponens: Q, Q->R ⊢ R
|
|
521
|
+
mp2 = imp.Combinator(name="MP2", type=imp.app(Q, R))
|
|
522
|
+
conn.add_edge(imp.edge(nodes_dict["Q"], nodes_dict["R"], mp2))
|
|
523
|
+
|
|
524
|
+
# Composition: P->Q, Q->R ⊢ P->R
|
|
525
|
+
comp = imp.Combinator(name="Comp", type=imp.app(P, R))
|
|
526
|
+
conn.add_edge(imp.edge(nodes_dict["P"], nodes_dict["R"], comp))
|
|
527
|
+
|
|
528
|
+
print(f"Proof tree has {graph.node_count()} types")
|
|
529
|
+
print(f"Proof tree has {graph.edge_count()} inference rules")
|
|
530
|
+
|
|
531
|
+
# Validate the proof structure
|
|
532
|
+
graph.validate()
|
|
533
|
+
print("Proof structure is valid!")
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### Example 5: Exploring Graph Structure
|
|
537
|
+
|
|
538
|
+
Navigate and analyze the graph:
|
|
539
|
+
|
|
540
|
+
```python
|
|
541
|
+
import implica as imp
|
|
542
|
+
|
|
543
|
+
# Build a diamond-shaped graph
|
|
544
|
+
# A
|
|
545
|
+
# / \
|
|
546
|
+
# B C
|
|
547
|
+
# \ /
|
|
548
|
+
# D
|
|
549
|
+
|
|
550
|
+
graph = imp.Graph()
|
|
551
|
+
|
|
552
|
+
A, B, C, D = imp.var("A"), imp.var("B"), imp.var("C"), imp.var("D")
|
|
553
|
+
n_a, n_b, n_c, n_d = imp.node(A), imp.node(B), imp.node(C), imp.node(D)
|
|
554
|
+
|
|
555
|
+
with graph.connect() as conn:
|
|
556
|
+
conn.add_node(n_a)
|
|
557
|
+
conn.add_node(n_b)
|
|
558
|
+
conn.add_node(n_c)
|
|
559
|
+
conn.add_node(n_d)
|
|
560
|
+
|
|
561
|
+
# A -> B, A -> C
|
|
562
|
+
conn.add_edge(imp.edge(n_a, n_b, imp.Combinator("f1", imp.app(A, B))))
|
|
563
|
+
conn.add_edge(imp.edge(n_a, n_c, imp.Combinator("f2", imp.app(A, C))))
|
|
564
|
+
|
|
565
|
+
# B -> D, C -> D
|
|
566
|
+
conn.add_edge(imp.edge(n_b, n_d, imp.Combinator("g1", imp.app(B, D))))
|
|
567
|
+
conn.add_edge(imp.edge(n_c, n_d, imp.Combinator("g2", imp.app(C, D))))
|
|
568
|
+
|
|
569
|
+
# Analyze the structure
|
|
570
|
+
print("=== Graph Analysis ===")
|
|
571
|
+
print(f"Total nodes: {graph.node_count()}")
|
|
572
|
+
print(f"Total edges: {graph.edge_count()}")
|
|
573
|
+
|
|
574
|
+
# Find all paths from A
|
|
575
|
+
print("\nFrom A:")
|
|
576
|
+
for e in graph.get_outgoing_edges(n_a.uid):
|
|
577
|
+
print(f" -> {e.dst_node.type} via {e.combinator.name}")
|
|
578
|
+
|
|
579
|
+
# Find all paths to D
|
|
580
|
+
print("\nTo D:")
|
|
581
|
+
for e in graph.get_incoming_edges(n_d.uid):
|
|
582
|
+
print(f" <- {e.src_node.type} via {e.combinator.name}")
|
|
583
|
+
|
|
584
|
+
# Check connectivity
|
|
585
|
+
print("\nNode connectivity:")
|
|
586
|
+
for n in graph.nodes():
|
|
587
|
+
incoming = len(graph.get_incoming_edges(n.uid))
|
|
588
|
+
outgoing = len(graph.get_outgoing_edges(n.uid))
|
|
589
|
+
print(f" {n.type}: {incoming} in, {outgoing} out")
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
### Example 6: Bulk Operations
|
|
593
|
+
|
|
594
|
+
Add multiple nodes and edges efficiently:
|
|
595
|
+
|
|
596
|
+
```python
|
|
597
|
+
import implica as imp
|
|
598
|
+
|
|
599
|
+
graph = imp.Graph()
|
|
600
|
+
|
|
601
|
+
# Create many type variables at once
|
|
602
|
+
type_vars = [imp.var(f"T{i}") for i in range(10)]
|
|
603
|
+
nodes = [imp.node(t) for t in type_vars]
|
|
604
|
+
|
|
605
|
+
# Add all nodes in a single transaction
|
|
606
|
+
with graph.connect() as conn:
|
|
607
|
+
conn.add_many_nodes(nodes)
|
|
608
|
+
|
|
609
|
+
print(f"Added {graph.node_count()} nodes in one operation")
|
|
610
|
+
|
|
611
|
+
# Create a chain of transformations
|
|
612
|
+
combinators = []
|
|
613
|
+
for i in range(len(type_vars) - 1):
|
|
614
|
+
comb = imp.Combinator(
|
|
615
|
+
name=f"f{i}",
|
|
616
|
+
type=imp.app(type_vars[i], type_vars[i + 1])
|
|
617
|
+
)
|
|
618
|
+
combinators.append(comb)
|
|
619
|
+
|
|
620
|
+
# Add all edges in a single transaction
|
|
621
|
+
with graph.connect() as conn:
|
|
622
|
+
conn.add_many_edges(combinators)
|
|
623
|
+
|
|
624
|
+
print(f"Added {graph.edge_count()} edges in one operation")
|
|
625
|
+
|
|
626
|
+
# If any node or edge fails, all are rolled back atomically
|
|
627
|
+
try:
|
|
628
|
+
with graph.connect() as conn:
|
|
629
|
+
# This will fail because nodes[0] already exists
|
|
630
|
+
conn.add_many_nodes([nodes[0], imp.node(imp.var("NEW"))])
|
|
631
|
+
except RuntimeError:
|
|
632
|
+
print("Transaction rolled back - no new nodes added")
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
### Example 7: Idempotent Operations
|
|
636
|
+
|
|
637
|
+
Use safe mutations that don't fail on duplicates:
|
|
638
|
+
|
|
639
|
+
```python
|
|
640
|
+
import implica as imp
|
|
641
|
+
|
|
642
|
+
graph = imp.Graph()
|
|
643
|
+
|
|
644
|
+
A = imp.var("A")
|
|
645
|
+
B = imp.var("B")
|
|
646
|
+
n_a = imp.node(A)
|
|
647
|
+
n_b = imp.node(B)
|
|
648
|
+
|
|
649
|
+
# Add nodes normally
|
|
650
|
+
with graph.connect() as conn:
|
|
651
|
+
conn.add_node(n_a)
|
|
652
|
+
conn.add_node(n_b)
|
|
653
|
+
|
|
654
|
+
print(f"Initial nodes: {graph.node_count()}") # Output: 2
|
|
655
|
+
|
|
656
|
+
# Try to add nodes again - won't fail!
|
|
657
|
+
with graph.connect() as conn:
|
|
658
|
+
conn.try_add_node(n_a) # Already exists, but no error
|
|
659
|
+
conn.try_add_node(n_b) # Already exists, but no error
|
|
660
|
+
conn.try_add_node(imp.node(imp.var("C"))) # New node, will be added
|
|
661
|
+
|
|
662
|
+
print(f"Nodes after try_add: {graph.node_count()}") # Output: 3
|
|
663
|
+
|
|
664
|
+
# Same with edges
|
|
665
|
+
comb_ab = imp.Combinator("f", imp.app(A, B))
|
|
666
|
+
|
|
667
|
+
with graph.connect() as conn:
|
|
668
|
+
conn.try_add_edge(comb_ab) # Will be added
|
|
669
|
+
|
|
670
|
+
print(f"Edges: {graph.edge_count()}") # Output: 1
|
|
671
|
+
|
|
672
|
+
with graph.connect() as conn:
|
|
673
|
+
conn.try_add_edge(comb_ab) # Already exists, but no error
|
|
674
|
+
|
|
675
|
+
print(f"Edges after try_add: {graph.edge_count()}") # Output: 1
|
|
676
|
+
|
|
677
|
+
# Useful for idempotent operations and avoiding duplicate checks
|
|
678
|
+
def ensure_basic_types(graph, type_names):
|
|
679
|
+
"""Ensure all basic types exist in the graph."""
|
|
680
|
+
with graph.connect() as conn:
|
|
681
|
+
for name in type_names:
|
|
682
|
+
conn.try_add_node(imp.node(imp.var(name)))
|
|
683
|
+
|
|
684
|
+
# Can call this multiple times safely
|
|
685
|
+
ensure_basic_types(graph, ["A", "B", "C", "D"])
|
|
686
|
+
ensure_basic_types(graph, ["C", "D", "E", "F"]) # C and D won't cause errors
|
|
687
|
+
|
|
688
|
+
print(f"Final nodes: {graph.node_count()}") # Output: 6 (A, B, C, D, E, F)
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
### Example 8: Bulk Removal Operations
|
|
692
|
+
|
|
693
|
+
Remove multiple elements efficiently:
|
|
694
|
+
|
|
695
|
+
```python
|
|
696
|
+
import implica as imp
|
|
697
|
+
|
|
698
|
+
graph = imp.Graph()
|
|
699
|
+
|
|
700
|
+
# Build a graph with multiple nodes and edges
|
|
701
|
+
types = [imp.var(f"T{i}") for i in range(10)]
|
|
702
|
+
nodes = [imp.node(t) for t in types]
|
|
703
|
+
|
|
704
|
+
with graph.connect() as conn:
|
|
705
|
+
conn.add_many_nodes(nodes)
|
|
706
|
+
|
|
707
|
+
# Create edges
|
|
708
|
+
combinators = []
|
|
709
|
+
for i in range(len(types) - 1):
|
|
710
|
+
comb = imp.Combinator(f"f{i}", imp.app(types[i], types[i + 1]))
|
|
711
|
+
combinators.append(comb)
|
|
712
|
+
|
|
713
|
+
with graph.connect() as conn:
|
|
714
|
+
conn.add_many_edges(combinators)
|
|
715
|
+
|
|
716
|
+
print(f"Initial: {graph.node_count()} nodes, {graph.edge_count()} edges")
|
|
717
|
+
# Output: Initial: 10 nodes, 9 edges
|
|
718
|
+
|
|
719
|
+
# Remove multiple edges at once
|
|
720
|
+
edge_uids = [graph.get_outgoing_edges(nodes[i].uid)[0].uid for i in range(3)]
|
|
721
|
+
with graph.connect() as conn:
|
|
722
|
+
conn.remove_many_edges(edge_uids)
|
|
723
|
+
|
|
724
|
+
print(f"After edge removal: {graph.edge_count()} edges")
|
|
725
|
+
# Output: After edge removal: 6 edges
|
|
726
|
+
|
|
727
|
+
# Remove multiple nodes at once (and their connected edges)
|
|
728
|
+
node_uids = [nodes[i].uid for i in range(5)]
|
|
729
|
+
with graph.connect() as conn:
|
|
730
|
+
conn.remove_many_nodes(node_uids)
|
|
731
|
+
|
|
732
|
+
print(f"After node removal: {graph.node_count()} nodes, {graph.edge_count()} edges")
|
|
733
|
+
# Output: After node removal: 5 nodes, 0 edges (edges were connected to removed nodes)
|
|
734
|
+
|
|
735
|
+
# Safe removal with try_remove (won't fail if already removed)
|
|
736
|
+
with graph.connect() as conn:
|
|
737
|
+
conn.try_remove_node(nodes[0].uid) # Already removed, no error
|
|
738
|
+
conn.try_remove_edge("nonexistent_uid") # Doesn't exist, no error
|
|
739
|
+
|
|
740
|
+
print(f"Final: {graph.node_count()} nodes") # Output: Final: 5 nodes
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
### Example 9: Safe Cleanup Operations
|
|
744
|
+
|
|
745
|
+
Use idempotent removal for cleanup tasks:
|
|
746
|
+
|
|
747
|
+
```python
|
|
748
|
+
import implica as imp
|
|
749
|
+
|
|
750
|
+
def cleanup_temporary_nodes(graph, temp_node_uids):
|
|
751
|
+
"""
|
|
752
|
+
Remove temporary nodes if they exist.
|
|
753
|
+
This function is safe to call multiple times.
|
|
754
|
+
"""
|
|
755
|
+
with graph.connect() as conn:
|
|
756
|
+
for uid in temp_node_uids:
|
|
757
|
+
conn.try_remove_node(uid)
|
|
758
|
+
|
|
759
|
+
graph = imp.Graph()
|
|
760
|
+
|
|
761
|
+
# Add some nodes
|
|
762
|
+
A, B, C = imp.var("A"), imp.var("B"), imp.var("C")
|
|
763
|
+
n_a, n_b, n_c = imp.node(A), imp.node(B), imp.node(C)
|
|
764
|
+
|
|
765
|
+
with graph.connect() as conn:
|
|
766
|
+
conn.add_many_nodes([n_a, n_b, n_c])
|
|
767
|
+
|
|
768
|
+
print(f"Initial nodes: {graph.node_count()}") # Output: 3
|
|
769
|
+
|
|
770
|
+
# Clean up - safe to call multiple times
|
|
771
|
+
temp_uids = [n_a.uid, n_b.uid]
|
|
772
|
+
cleanup_temporary_nodes(graph, temp_uids)
|
|
773
|
+
print(f"After cleanup: {graph.node_count()}") # Output: 1
|
|
774
|
+
|
|
775
|
+
# Call again - won't fail even though nodes are already removed
|
|
776
|
+
cleanup_temporary_nodes(graph, temp_uids)
|
|
777
|
+
print(f"After second cleanup: {graph.node_count()}") # Output: 1
|
|
778
|
+
|
|
779
|
+
# Combine try_remove with try_add for flexible graph updates
|
|
780
|
+
def ensure_graph_state(graph, required_nodes, forbidden_nodes):
|
|
781
|
+
"""
|
|
782
|
+
Ensure graph has required nodes and doesn't have forbidden ones.
|
|
783
|
+
"""
|
|
784
|
+
with graph.connect() as conn:
|
|
785
|
+
# Add required nodes (idempotent)
|
|
786
|
+
for n in required_nodes:
|
|
787
|
+
conn.try_add_node(n)
|
|
788
|
+
|
|
789
|
+
# Remove forbidden nodes (idempotent)
|
|
790
|
+
for uid in forbidden_nodes:
|
|
791
|
+
conn.try_remove_node(uid)
|
|
792
|
+
|
|
793
|
+
# Can call this function repeatedly to maintain desired state
|
|
794
|
+
ensure_graph_state(
|
|
795
|
+
graph,
|
|
796
|
+
required_nodes=[imp.node(imp.var("X")), imp.node(imp.var("Y"))],
|
|
797
|
+
forbidden_nodes=[n_c.uid]
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
print(f"Final state: {graph.node_count()} nodes") # Output: 2 (X and Y)
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
## API Reference
|
|
804
|
+
|
|
805
|
+
### Core Module (`implica.core`)
|
|
806
|
+
|
|
807
|
+
**Types:**
|
|
808
|
+
|
|
809
|
+
- `var(name: str) -> Variable`: Create a type variable
|
|
810
|
+
- `app(input_type: BaseType, output_type: BaseType) -> Application`: Create a function type
|
|
811
|
+
- `Variable`: Atomic type variable
|
|
812
|
+
- `Application`: Function application type
|
|
813
|
+
|
|
814
|
+
**Combinators:**
|
|
815
|
+
|
|
816
|
+
- `S(A, B, C) -> Combinator`: Create S combinator
|
|
817
|
+
- `K(A, B) -> Combinator`: Create K combinator
|
|
818
|
+
- `Combinator`: Generic combinator with name and type
|
|
819
|
+
|
|
820
|
+
### Graph Module (`implica.graph`)
|
|
821
|
+
|
|
822
|
+
**Elements:**
|
|
823
|
+
|
|
824
|
+
- `node(type: BaseType) -> Node`: Create a node
|
|
825
|
+
- `edge(src: Node, dst: Node, comb: Combinator) -> Edge`: Create an edge
|
|
826
|
+
- `Node`: Graph node representing a type
|
|
827
|
+
- `Edge`: Graph edge representing a transformation
|
|
828
|
+
|
|
829
|
+
**Graph:**
|
|
830
|
+
|
|
831
|
+
- `Graph()`: Create a new empty graph
|
|
832
|
+
- `graph.connect() -> Connection`: Create a transactional connection
|
|
833
|
+
- `graph.validate() -> bool`: Validate graph consistency
|
|
834
|
+
- `graph.has_node(uid: str) -> bool`: Check if node exists
|
|
835
|
+
- `graph.get_node(uid: str) -> Node`: Get node by UID
|
|
836
|
+
- `graph.get_node_by_type(type: BaseType) -> Optional[Node]`: Get node by type
|
|
837
|
+
- `graph.get_outgoing_edges(uid: str) -> list[Edge]`: Get outgoing edges
|
|
838
|
+
- `graph.get_incoming_edges(uid: str) -> list[Edge]`: Get incoming edges
|
|
839
|
+
- `graph.nodes() -> Iterator[Node]`: Iterate over nodes
|
|
840
|
+
- `graph.edges() -> Iterator[Edge]`: Iterate over edges
|
|
841
|
+
- `graph.node_count() -> int`: Get number of nodes
|
|
842
|
+
- `graph.edge_count() -> int`: Get number of edges
|
|
843
|
+
|
|
844
|
+
**Connection:**
|
|
845
|
+
|
|
846
|
+
- `Connection`: Transactional graph modification context
|
|
847
|
+
- `conn.add_node(node: Node) -> Connection`: Queue node addition
|
|
848
|
+
- `conn.add_edge(edge: Edge) -> Connection`: Queue edge addition
|
|
849
|
+
- `conn.remove_node(uid: str) -> Connection`: Queue node removal
|
|
850
|
+
- `conn.remove_edge(uid: str) -> Connection`: Queue edge removal
|
|
851
|
+
- `conn.add_many_nodes(nodes: list[Node]) -> Connection`: Queue multiple node additions
|
|
852
|
+
- `conn.add_many_edges(combinators: list[Combinator]) -> Connection`: Queue multiple edge additions
|
|
853
|
+
- `conn.try_add_node(node: Node) -> Connection`: Queue node addition (no error if exists)
|
|
854
|
+
- `conn.try_add_edge(combinator: Combinator) -> Connection`: Queue edge addition (no error if exists)
|
|
855
|
+
- `conn.remove_many_nodes(node_uids: list[str]) -> Connection`: Queue multiple node removals
|
|
856
|
+
- `conn.remove_many_edges(edge_uids: list[str]) -> Connection`: Queue multiple edge removals
|
|
857
|
+
- `conn.try_remove_node(node_uid: str) -> Connection`: Queue node removal (no error if not exists)
|
|
858
|
+
- `conn.try_remove_edge(edge_uid: str) -> Connection`: Queue edge removal (no error if not exists)
|
|
859
|
+
- `conn.commit()`: Apply all queued operations
|
|
860
|
+
- `conn.rollback()`: Discard all queued operations
|
|
861
|
+
|
|
862
|
+
### Mutations Module (`implica.mutations`)
|
|
863
|
+
|
|
864
|
+
- `Mutation`: Abstract base class for all mutations
|
|
865
|
+
- `AddNode(node)`: Add a single node
|
|
866
|
+
- `RemoveNode(node_uid)`: Remove a single node
|
|
867
|
+
- `AddEdge(edge)`: Add a single edge
|
|
868
|
+
- `RemoveEdge(edge_uid)`: Remove a single edge
|
|
869
|
+
- `AddManyNodes(nodes)`: Add multiple nodes atomically
|
|
870
|
+
- `AddManyEdges(edges)`: Add multiple edges atomically
|
|
871
|
+
- `TryAddNode(node)`: Add a node or do nothing if it exists
|
|
872
|
+
- `TryAddEdge(edge)`: Add an edge or do nothing if it exists
|
|
873
|
+
- `RemoveManyNodes(node_uids)`: Remove multiple nodes atomically
|
|
874
|
+
- `RemoveManyEdges(edge_uids)`: Remove multiple edges atomically
|
|
875
|
+
- `TryRemoveNode(node_uid)`: Remove a node or do nothing if it doesn't exist
|
|
876
|
+
- `TryRemoveEdge(edge_uid)`: Remove an edge or do nothing if it doesn't exist
|
|
877
|
+
|
|
878
|
+
## Development
|
|
879
|
+
|
|
880
|
+
### Setup
|
|
881
|
+
|
|
882
|
+
```bash
|
|
883
|
+
# Clone the repository
|
|
884
|
+
git clone https://github.com/carlosFerLo/implicational-logic-graph.git
|
|
885
|
+
cd implicational-logic-graph
|
|
886
|
+
|
|
887
|
+
# Install dependencies
|
|
888
|
+
poetry install
|
|
889
|
+
|
|
890
|
+
# Run tests
|
|
891
|
+
poetry run pytest
|
|
892
|
+
|
|
893
|
+
# Run tests with coverage
|
|
894
|
+
poetry run pytest --cov=src/implicational_logic_graph
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
### Running Tests
|
|
898
|
+
|
|
899
|
+
```bash
|
|
900
|
+
# Run all tests
|
|
901
|
+
poetry run pytest
|
|
902
|
+
|
|
903
|
+
# Run specific test file
|
|
904
|
+
poetry run pytest tests/test_graph.py
|
|
905
|
+
|
|
906
|
+
# Run with verbose output
|
|
907
|
+
poetry run pytest -v
|
|
908
|
+
|
|
909
|
+
# Run with coverage report
|
|
910
|
+
poetry run pytest --cov=src/implica --cov-report=html
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
## Contributing
|
|
914
|
+
|
|
915
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
916
|
+
|
|
917
|
+
1. Fork the repository
|
|
918
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
919
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
920
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
921
|
+
5. Open a Pull Request
|
|
922
|
+
|
|
923
|
+
## License
|
|
924
|
+
|
|
925
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
926
|
+
|
|
927
|
+
## Acknowledgments
|
|
928
|
+
|
|
929
|
+
- Based on concepts from combinatory logic and type theory
|
|
930
|
+
- Inspired by minimal implicational logic models
|
|
931
|
+
- Built with [Pydantic](https://pydantic-docs.helpmanual.io/) for data validation
|
|
932
|
+
|
|
933
|
+
## Citation
|
|
934
|
+
|
|
935
|
+
If you use this library in your research, please cite:
|
|
936
|
+
|
|
937
|
+
```bibtex
|
|
938
|
+
@software{implica,
|
|
939
|
+
author = {Carlos Fernandez},
|
|
940
|
+
title = {Implica: Implicational Logic Graph Library},
|
|
941
|
+
year = {2025},
|
|
942
|
+
url = {https://github.com/CarlosFerLo/implicational-logic-graph}
|
|
943
|
+
}
|
|
944
|
+
```
|
|
945
|
+
|