relationalai 0.12.7__py3-none-any.whl → 0.12.8__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.
- relationalai/clients/snowflake.py +37 -5
- relationalai/clients/use_index_poller.py +11 -1
- relationalai/semantics/lqp/passes.py +2 -1
- relationalai/semantics/metamodel/builtins.py +1 -0
- relationalai/semantics/metamodel/rewrite/__init__.py +2 -1
- relationalai/semantics/metamodel/rewrite/dnf_union_splitter.py +1 -1
- relationalai/semantics/metamodel/rewrite/extract_nested_logicals.py +5 -6
- relationalai/semantics/metamodel/rewrite/flatten.py +18 -149
- relationalai/semantics/metamodel/rewrite/format_outputs.py +165 -0
- relationalai/semantics/reasoners/graph/core.py +98 -70
- relationalai/semantics/reasoners/optimization/__init__.py +55 -10
- relationalai/semantics/reasoners/optimization/common.py +63 -8
- relationalai/semantics/reasoners/optimization/solvers_dev.py +39 -33
- relationalai/semantics/reasoners/optimization/solvers_pb.py +1033 -385
- relationalai/semantics/rel/compiler.py +2 -1
- relationalai/tools/cli.py +10 -0
- relationalai/tools/cli_controls.py +15 -0
- {relationalai-0.12.7.dist-info → relationalai-0.12.8.dist-info}/METADATA +1 -1
- {relationalai-0.12.7.dist-info → relationalai-0.12.8.dist-info}/RECORD +22 -21
- {relationalai-0.12.7.dist-info → relationalai-0.12.8.dist-info}/WHEEL +0 -0
- {relationalai-0.12.7.dist-info → relationalai-0.12.8.dist-info}/entry_points.txt +0 -0
- {relationalai-0.12.7.dist-info → relationalai-0.12.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -2368,7 +2368,8 @@ class Graph():
|
|
|
2368
2368
|
"""Returns a binary relationship containing the degree of each node.
|
|
2369
2369
|
|
|
2370
2370
|
For directed graphs, a node's degree is the sum of its indegree and
|
|
2371
|
-
outdegree.
|
|
2371
|
+
outdegree. For undirected graphs, a self-loop contributes +2 to the
|
|
2372
|
+
node's degree.
|
|
2372
2373
|
|
|
2373
2374
|
Parameters
|
|
2374
2375
|
----------
|
|
@@ -2392,17 +2393,17 @@ class Graph():
|
|
|
2392
2393
|
|
|
2393
2394
|
Supported Graph Types
|
|
2394
2395
|
---------------------
|
|
2395
|
-
| Graph Type | Supported | Notes
|
|
2396
|
-
| :--------- | :-------- |
|
|
2397
|
-
| Undirected | Yes |
|
|
2398
|
-
| Directed | Yes |
|
|
2399
|
-
| Weighted | Yes | Weights are ignored.
|
|
2396
|
+
| Graph Type | Supported | Notes |
|
|
2397
|
+
| :--------- | :-------- | :----------------------------- |
|
|
2398
|
+
| Undirected | Yes | A self-loop contributes +2. |
|
|
2399
|
+
| Directed | Yes | |
|
|
2400
|
+
| Weighted | Yes | Weights are ignored. |
|
|
2400
2401
|
|
|
2401
2402
|
Examples
|
|
2402
2403
|
--------
|
|
2403
2404
|
**Undirected Graph Example**
|
|
2404
2405
|
|
|
2405
|
-
>>> from relationalai.semantics import Model, define, select, where
|
|
2406
|
+
>>> from relationalai.semantics import Model, define, select, where, Integer, union
|
|
2406
2407
|
>>> from relationalai.semantics.reasoners.graph import Graph
|
|
2407
2408
|
>>>
|
|
2408
2409
|
>>> # 1. Set up an undirected graph
|
|
@@ -2428,7 +2429,7 @@ class Graph():
|
|
|
2428
2429
|
id node_degree
|
|
2429
2430
|
0 1 1
|
|
2430
2431
|
1 2 3
|
|
2431
|
-
2 3
|
|
2432
|
+
2 3 3
|
|
2432
2433
|
3 4 1
|
|
2433
2434
|
|
|
2434
2435
|
>>> # 4. Use 'of' parameter to constrain the set of nodes to compute degree of
|
|
@@ -2443,7 +2444,7 @@ class Graph():
|
|
|
2443
2444
|
▰▰▰▰ Setup complete
|
|
2444
2445
|
id node_degree
|
|
2445
2446
|
0 2 3
|
|
2446
|
-
1 3
|
|
2447
|
+
1 3 3
|
|
2447
2448
|
|
|
2448
2449
|
**Directed Graph Example**
|
|
2449
2450
|
|
|
@@ -2533,6 +2534,7 @@ class Graph():
|
|
|
2533
2534
|
def _create_degree_relationship(self, *, node_subset: Optional[Relationship]):
|
|
2534
2535
|
_degree_rel = self._model.Relationship(f"{{node:{self._NodeConceptStr}}} has degree {{count:Integer}}")
|
|
2535
2536
|
|
|
2537
|
+
node = self.Node.ref()
|
|
2536
2538
|
if self.directed:
|
|
2537
2539
|
# For directed graphs, degree is the sum of indegree and outdegree.
|
|
2538
2540
|
if node_subset is None:
|
|
@@ -2544,22 +2546,35 @@ class Graph():
|
|
|
2544
2546
|
|
|
2545
2547
|
incount, outcount = Integer.ref(), Integer.ref()
|
|
2546
2548
|
where(
|
|
2547
|
-
indegree_rel(
|
|
2548
|
-
outdegree_rel(
|
|
2549
|
-
).define(_degree_rel(
|
|
2549
|
+
indegree_rel(node, incount),
|
|
2550
|
+
outdegree_rel(node, outcount),
|
|
2551
|
+
).define(_degree_rel(node, incount + outcount))
|
|
2550
2552
|
else:
|
|
2551
|
-
# For undirected graphs, degree
|
|
2553
|
+
# For undirected graphs, degree counts each non-loop edge once and each
|
|
2554
|
+
# self-loop twice. Self-loops can be computed as the difference between
|
|
2555
|
+
# count_neighbor and degree_no_self, where degree_no_self counts neighbors
|
|
2556
|
+
# excluding self-loops.
|
|
2557
|
+
|
|
2558
|
+
# _self_loop_count := _neighbor_count - _degree_no_self
|
|
2559
|
+
# _degree := _degree_no_self + 2 * _self_loop_count
|
|
2560
|
+
# Therefore:
|
|
2561
|
+
# _degree := 2 * _neighbor_count - _degree_no_self
|
|
2562
|
+
|
|
2552
2563
|
if node_subset is None:
|
|
2553
|
-
node_set = self.Node
|
|
2554
2564
|
count_neighbor_rel = self._count_neighbor
|
|
2565
|
+
degree_no_self_rel = self._degree_no_self
|
|
2555
2566
|
else:
|
|
2556
|
-
node_set = node_subset
|
|
2557
2567
|
count_neighbor_rel = self._count_neighbor_of(node_subset)
|
|
2568
|
+
degree_no_self_rel = self._degree_no_self_of(node_subset)
|
|
2558
2569
|
|
|
2570
|
+
_degree_no_self = Integer.ref()
|
|
2559
2571
|
where(
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2572
|
+
# No explicit node constraint needed here, as `_degree_no_self_of`
|
|
2573
|
+
# relation is constrained to `node_subset`.
|
|
2574
|
+
_neighbor_count := where(count_neighbor_rel(node, Integer)).select(Integer) | 0,
|
|
2575
|
+
degree_no_self_rel(node, _degree_no_self),
|
|
2576
|
+
_degree := 2 * _neighbor_count - _degree_no_self
|
|
2577
|
+
).define(_degree_rel(node, _degree))
|
|
2563
2578
|
|
|
2564
2579
|
return _degree_rel
|
|
2565
2580
|
|
|
@@ -2569,7 +2584,8 @@ class Graph():
|
|
|
2569
2584
|
"""Returns a binary relationship containing the indegree of each node.
|
|
2570
2585
|
|
|
2571
2586
|
A node's indegree is the number of incoming edges. For undirected
|
|
2572
|
-
graphs, a node's indegree is identical to its degree
|
|
2587
|
+
graphs, a node's indegree is identical to its degree, except self-loops
|
|
2588
|
+
contribute only +1 to a node's indegree.
|
|
2573
2589
|
|
|
2574
2590
|
Parameters
|
|
2575
2591
|
----------
|
|
@@ -2593,11 +2609,11 @@ class Graph():
|
|
|
2593
2609
|
|
|
2594
2610
|
Supported Graph Types
|
|
2595
2611
|
---------------------
|
|
2596
|
-
| Graph Type | Supported | Notes
|
|
2597
|
-
| :--------- | :-------- |
|
|
2598
|
-
| Undirected | Yes | Identical to `degree
|
|
2599
|
-
| Directed | Yes |
|
|
2600
|
-
| Weighted | Yes | Weights are ignored.
|
|
2612
|
+
| Graph Type | Supported | Notes |
|
|
2613
|
+
| :--------- | :-------- | :-------------------------------------------- |
|
|
2614
|
+
| Undirected | Yes | Identical to `degree`, except for self-loops. |
|
|
2615
|
+
| Directed | Yes | |
|
|
2616
|
+
| Weighted | Yes | Weights are ignored. |
|
|
2601
2617
|
|
|
2602
2618
|
Examples
|
|
2603
2619
|
--------
|
|
@@ -2752,7 +2768,8 @@ class Graph():
|
|
|
2752
2768
|
"""Returns a binary relationship containing the outdegree of each node.
|
|
2753
2769
|
|
|
2754
2770
|
A node's outdegree is the number of outgoing edges. For undirected
|
|
2755
|
-
graphs, a node's outdegree is identical to its degree
|
|
2771
|
+
graphs, a node's outdegree is identical to its degree, except self-loops
|
|
2772
|
+
contribute only +1 to a node's outdegree.
|
|
2756
2773
|
|
|
2757
2774
|
Parameters
|
|
2758
2775
|
----------
|
|
@@ -2776,11 +2793,11 @@ class Graph():
|
|
|
2776
2793
|
|
|
2777
2794
|
Supported Graph Types
|
|
2778
2795
|
---------------------
|
|
2779
|
-
| Graph Type | Supported | Notes
|
|
2780
|
-
| :--------- | :-------- |
|
|
2781
|
-
| Undirected | Yes | Identical to `degree
|
|
2782
|
-
| Directed | Yes |
|
|
2783
|
-
| Weighted | Yes | Weights are ignored.
|
|
2796
|
+
| Graph Type | Supported | Notes |
|
|
2797
|
+
| :--------- | :-------- | :-------------------------------------------- |
|
|
2798
|
+
| Undirected | Yes | Identical to `degree`, except for self-loops. |
|
|
2799
|
+
| Directed | Yes | |
|
|
2800
|
+
| Weighted | Yes | Weights are ignored. |
|
|
2784
2801
|
|
|
2785
2802
|
Examples
|
|
2786
2803
|
--------
|
|
@@ -2937,8 +2954,9 @@ class Graph():
|
|
|
2937
2954
|
|
|
2938
2955
|
A node's weighted degree is the sum of the weights of all edges
|
|
2939
2956
|
connected to it. For directed graphs, this is the sum of the weights
|
|
2940
|
-
of both incoming and outgoing edges. For
|
|
2941
|
-
|
|
2957
|
+
of both incoming and outgoing edges. For undirected graphs, the weights
|
|
2958
|
+
of self-loops contribute twice to the node's weighted degree. For
|
|
2959
|
+
unweighted graphs, all edge weights are considered to be 1.0.
|
|
2942
2960
|
|
|
2943
2961
|
Parameters
|
|
2944
2962
|
----------
|
|
@@ -2962,11 +2980,11 @@ class Graph():
|
|
|
2962
2980
|
|
|
2963
2981
|
Supported Graph Types
|
|
2964
2982
|
---------------------
|
|
2965
|
-
| Graph Type | Supported | Notes
|
|
2966
|
-
| :----------- | :-------- |
|
|
2967
|
-
| Undirected | Yes |
|
|
2968
|
-
| Directed | Yes |
|
|
2969
|
-
| Weighted | Yes |
|
|
2983
|
+
| Graph Type | Supported | Notes |
|
|
2984
|
+
| :----------- | :-------- | :------------------------------------- |
|
|
2985
|
+
| Undirected | Yes | A self-loop contributes twice. |
|
|
2986
|
+
| Directed | Yes | |
|
|
2987
|
+
| Weighted | Yes | |
|
|
2970
2988
|
| Unweighted | Yes | Edge weights are considered to be 1.0. |
|
|
2971
2989
|
|
|
2972
2990
|
Examples
|
|
@@ -2984,7 +3002,7 @@ class Graph():
|
|
|
2984
3002
|
>>> define(n1, n2, n3)
|
|
2985
3003
|
>>> define(
|
|
2986
3004
|
... Edge.new(src=n1, dst=n2, weight=1.0),
|
|
2987
|
-
... Edge.new(src=n2, dst=n1, weight
|
|
3005
|
+
... Edge.new(src=n2, dst=n1, weight=0.0),
|
|
2988
3006
|
... Edge.new(src=n2, dst=n3, weight=1.0),
|
|
2989
3007
|
... )
|
|
2990
3008
|
>>>
|
|
@@ -2998,8 +3016,8 @@ class Graph():
|
|
|
2998
3016
|
... ).inspect()
|
|
2999
3017
|
▰▰▰▰ Setup complete
|
|
3000
3018
|
id node_weighted_degree
|
|
3001
|
-
0 1
|
|
3002
|
-
1 2
|
|
3019
|
+
0 1 1.0
|
|
3020
|
+
1 2 2.0
|
|
3003
3021
|
2 3 1.0
|
|
3004
3022
|
>>>
|
|
3005
3023
|
>>> # 4. Use 'of' parameter to constrain the set of nodes to compute weighted degree of
|
|
@@ -3014,7 +3032,7 @@ class Graph():
|
|
|
3014
3032
|
... ).inspect()
|
|
3015
3033
|
▰▰▰▰ Setup complete
|
|
3016
3034
|
id node_weighted_degree
|
|
3017
|
-
0 2
|
|
3035
|
+
0 2 2.0
|
|
3018
3036
|
1 3 1.0
|
|
3019
3037
|
|
|
3020
3038
|
Notes
|
|
@@ -3076,19 +3094,26 @@ class Graph():
|
|
|
3076
3094
|
weighted_outdegree_rel(self.Node, outweight),
|
|
3077
3095
|
).define(_weighted_degree_rel(self.Node, inweight + outweight))
|
|
3078
3096
|
elif not self.directed:
|
|
3079
|
-
#
|
|
3097
|
+
# For undirected graphs, weighted degree counts each non-loop edge weight once
|
|
3098
|
+
# and each self-loop edge weight twice.
|
|
3099
|
+
node, neighbor, weight = self.Node.ref(), self.Node.ref(), Float.ref()
|
|
3100
|
+
|
|
3080
3101
|
if node_subset is None:
|
|
3081
|
-
# No constraint
|
|
3082
|
-
node_set = self.Node
|
|
3102
|
+
node_constraint = node # No constraint on nodes.
|
|
3083
3103
|
else:
|
|
3084
|
-
#
|
|
3085
|
-
node_set = node_subset
|
|
3104
|
+
node_constraint = node_subset(node) # Nodes constrained to given subset.
|
|
3086
3105
|
|
|
3087
|
-
dst, weight = self.Node.ref(), Float.ref()
|
|
3088
3106
|
where(
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3107
|
+
node_constraint,
|
|
3108
|
+
weighted_degree_no_loops := sum(neighbor, weight).per(node).where(
|
|
3109
|
+
self._weight(node, neighbor, weight),
|
|
3110
|
+
node != neighbor,
|
|
3111
|
+
) | 0.0,
|
|
3112
|
+
weighted_degree_self_loops := sum(neighbor, weight).per(node).where(
|
|
3113
|
+
self._weight(node, neighbor, weight),
|
|
3114
|
+
node == neighbor,
|
|
3115
|
+
) | 0.0,
|
|
3116
|
+
).define(_weighted_degree_rel(node, weighted_degree_no_loops + 2 * weighted_degree_self_loops))
|
|
3092
3117
|
|
|
3093
3118
|
return _weighted_degree_rel
|
|
3094
3119
|
|
|
@@ -3098,8 +3123,10 @@ class Graph():
|
|
|
3098
3123
|
"""Returns a binary relationship containing the weighted indegree of each node.
|
|
3099
3124
|
|
|
3100
3125
|
A node's weighted indegree is the sum of the weights of all incoming
|
|
3101
|
-
edges. For undirected graphs, this is identical to `weighted_degree
|
|
3102
|
-
For
|
|
3126
|
+
edges. For undirected graphs, this is identical to `weighted_degree`,
|
|
3127
|
+
except for self-loops. For weighted graphs, we assume edge weights are
|
|
3128
|
+
non-negative. For unweighted graphs, all edge weights are considered
|
|
3129
|
+
to be 1.0.
|
|
3103
3130
|
|
|
3104
3131
|
Parameters
|
|
3105
3132
|
----------
|
|
@@ -3123,12 +3150,12 @@ class Graph():
|
|
|
3123
3150
|
|
|
3124
3151
|
Supported Graph Types
|
|
3125
3152
|
---------------------
|
|
3126
|
-
| Graph Type | Supported | Notes
|
|
3127
|
-
| :----------- | :-------- |
|
|
3128
|
-
| Undirected | Yes | Identical to `weighted_degree
|
|
3129
|
-
| Directed | Yes |
|
|
3130
|
-
| Weighted | Yes |
|
|
3131
|
-
| Unweighted | Yes | Edge weights are considered to be 1.0.
|
|
3153
|
+
| Graph Type | Supported | Notes |
|
|
3154
|
+
| :----------- | :-------- | :----------------------------------------------------- |
|
|
3155
|
+
| Undirected | Yes | Identical to `weighted_degree`, except for self-loops. |
|
|
3156
|
+
| Directed | Yes | |
|
|
3157
|
+
| Weighted | Yes | Assumes non-negative weights. |
|
|
3158
|
+
| Unweighted | Yes | Edge weights are considered to be 1.0. |
|
|
3132
3159
|
|
|
3133
3160
|
Examples
|
|
3134
3161
|
--------
|
|
@@ -3249,8 +3276,9 @@ class Graph():
|
|
|
3249
3276
|
"""Returns a binary relationship containing the weighted outdegree of each node.
|
|
3250
3277
|
|
|
3251
3278
|
A node's weighted outdegree is the sum of the weights of all outgoing
|
|
3252
|
-
edges. For undirected graphs, this is identical to `weighted_degree
|
|
3253
|
-
For unweighted graphs, all edge weights are
|
|
3279
|
+
edges. For undirected graphs, this is identical to `weighted_degree`,
|
|
3280
|
+
except for self-loops. For unweighted graphs, all edge weights are
|
|
3281
|
+
considered to be 1.0.
|
|
3254
3282
|
|
|
3255
3283
|
Parameters
|
|
3256
3284
|
----------
|
|
@@ -3274,12 +3302,12 @@ class Graph():
|
|
|
3274
3302
|
|
|
3275
3303
|
Supported Graph Types
|
|
3276
3304
|
---------------------
|
|
3277
|
-
| Graph Type | Supported | Notes
|
|
3278
|
-
| :----------- | :-------- |
|
|
3279
|
-
| Undirected | Yes | Identical to `weighted_degree
|
|
3280
|
-
| Directed | Yes |
|
|
3281
|
-
| Weighted | Yes |
|
|
3282
|
-
| Unweighted | Yes | Edge weights are considered to be 1.0.
|
|
3305
|
+
| Graph Type | Supported | Notes |
|
|
3306
|
+
| :----------- | :-------- | :----------------------------------------------------- |
|
|
3307
|
+
| Undirected | Yes | Identical to `weighted_degree`, except for self-loops. |
|
|
3308
|
+
| Directed | Yes | |
|
|
3309
|
+
| Weighted | Yes | |
|
|
3310
|
+
| Unweighted | Yes | Edge weights are considered to be 1.0. |
|
|
3283
3311
|
|
|
3284
3312
|
Examples
|
|
3285
3313
|
--------
|
|
@@ -3399,7 +3427,7 @@ class Graph():
|
|
|
3399
3427
|
degree (or weighted degree for weighted graphs) divided by the number
|
|
3400
3428
|
of other nodes in the graph.
|
|
3401
3429
|
|
|
3402
|
-
For
|
|
3430
|
+
For unweighted graphs without self-loops, this value will be at most 1.0;
|
|
3403
3431
|
unweighted graphs with self-loops might have nodes with a degree centrality
|
|
3404
3432
|
greater than 1.0. Weighted graphs may have degree centralities
|
|
3405
3433
|
greater than 1.0 as well.
|
|
@@ -3436,7 +3464,7 @@ class Graph():
|
|
|
3436
3464
|
--------
|
|
3437
3465
|
**Unweighted Graph Example**
|
|
3438
3466
|
|
|
3439
|
-
>>> from relationalai.semantics import Model, define, select, Float
|
|
3467
|
+
>>> from relationalai.semantics import Model, define, select, Float, where, union
|
|
3440
3468
|
>>> from relationalai.semantics.reasoners.graph import Graph
|
|
3441
3469
|
>>>
|
|
3442
3470
|
>>> # 1. Set up an unweighted graph
|
|
@@ -3463,7 +3491,7 @@ class Graph():
|
|
|
3463
3491
|
0 1 0.333333
|
|
3464
3492
|
1 2 1.000000
|
|
3465
3493
|
2 3 1.000000
|
|
3466
|
-
3 4 0.
|
|
3494
|
+
3 4 0.333333
|
|
3467
3495
|
|
|
3468
3496
|
>>> # 4. Use 'of' parameter to constrain the set of nodes to compute degree centrality of
|
|
3469
3497
|
>>> # Define a subset containing only nodes 2 and 3
|
|
@@ -4816,7 +4844,7 @@ class Graph():
|
|
|
4816
4844
|
def _degree_no_self(self):
|
|
4817
4845
|
"""
|
|
4818
4846
|
Lazily define and cache the self._degree_no_self relationship,
|
|
4819
|
-
a non-public helper for local_clustering_coefficient.
|
|
4847
|
+
a non-public helper for degree and local_clustering_coefficient.
|
|
4820
4848
|
"""
|
|
4821
4849
|
return self._create_degree_no_self_relationship(node_subset=None)
|
|
4822
4850
|
|
|
@@ -1,23 +1,68 @@
|
|
|
1
|
-
|
|
1
|
+
"""Optimization and constraint programming solver interfaces.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
This package provides solver model interfaces for defining and solving
|
|
4
|
+
mathematical optimization and constraint programming problems using
|
|
5
|
+
RelationalAI's solver infrastructure.
|
|
6
|
+
"""
|
|
5
7
|
|
|
6
|
-
from
|
|
7
|
-
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Union
|
|
11
|
+
import warnings
|
|
12
|
+
|
|
13
|
+
from relationalai.experimental.solvers import Provider, Solver
|
|
14
|
+
from relationalai.semantics.internal import Model
|
|
15
|
+
|
|
16
|
+
from .common import all_different, implies, make_name, special_ordered_set_type_2
|
|
8
17
|
from .solvers_dev import SolverModelDev
|
|
18
|
+
from .solvers_pb import SolverModelPB
|
|
19
|
+
|
|
20
|
+
__version__ = "0.0.0"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Copied from graphs library:
|
|
24
|
+
# warn on import that this package is at an early stage of development.
|
|
25
|
+
warnings.warn(
|
|
26
|
+
(
|
|
27
|
+
"\n\nThis library is still in early stages of development and is intended "
|
|
28
|
+
"for internal use only. Among other considerations, interfaces will change, "
|
|
29
|
+
"and performance is appropriate only for relatively small problems. Please "
|
|
30
|
+
"see this package's README for additional information.\n\n"
|
|
31
|
+
"If you are an internal user seeing this, please also contact "
|
|
32
|
+
"the symbolic reasoning team such that we can track usage, get "
|
|
33
|
+
"feedback, and help you through breaking changes.\n"
|
|
34
|
+
),
|
|
35
|
+
FutureWarning,
|
|
36
|
+
stacklevel=2
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def SolverModel(
|
|
41
|
+
model: Model, num_type: str, use_pb: bool = True
|
|
42
|
+
) -> Union[SolverModelPB, SolverModelDev]:
|
|
43
|
+
"""Create a solver model for an optimization or constraint programming problem.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
model: The RelationalAI model to attach the solver to.
|
|
47
|
+
num_type: Numeric type for decision variables ('cont' or 'int').
|
|
48
|
+
use_pb: Whether to use protobuf format (True) or development format (False).
|
|
49
|
+
Defaults to True. Note: protobuf format will be deprecated in favor
|
|
50
|
+
of the development format.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
A SolverModelPB or SolverModelDev instance.
|
|
54
|
+
"""
|
|
55
|
+
return (SolverModelPB if use_pb else SolverModelDev)(model, num_type)
|
|
9
56
|
|
|
10
|
-
def SolverModel(model, num_type: str, use_pb: bool = True):
|
|
11
|
-
if use_pb:
|
|
12
|
-
return SolverModelPB(model, num_type)
|
|
13
|
-
else:
|
|
14
|
-
return SolverModelDev(model, num_type)
|
|
15
57
|
|
|
16
58
|
__all__ = [
|
|
17
59
|
"Solver",
|
|
18
60
|
"Provider",
|
|
19
61
|
"SolverModel",
|
|
62
|
+
"SolverModelPB",
|
|
63
|
+
"SolverModelDev",
|
|
20
64
|
"make_name",
|
|
21
65
|
"all_different",
|
|
22
66
|
"implies",
|
|
67
|
+
"special_ordered_set_type_2",
|
|
23
68
|
]
|
|
@@ -1,10 +1,32 @@
|
|
|
1
|
+
"""Common utilities for solver models.
|
|
2
|
+
|
|
3
|
+
This module provides shared helper functions and constraint constructors used
|
|
4
|
+
across solver model implementations (both protobuf and development versions).
|
|
5
|
+
"""
|
|
6
|
+
|
|
1
7
|
from __future__ import annotations
|
|
2
8
|
|
|
3
|
-
from typing import Any
|
|
9
|
+
from typing import Any, Optional, Union
|
|
10
|
+
|
|
4
11
|
from relationalai.semantics import std
|
|
5
|
-
from relationalai.semantics.internal
|
|
12
|
+
from relationalai.semantics.internal import internal as b
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# =============================================================================
|
|
16
|
+
# Utilities
|
|
17
|
+
# =============================================================================
|
|
6
18
|
|
|
7
|
-
|
|
19
|
+
|
|
20
|
+
def make_name(*args, sep: Optional[str] = "_") -> Union[str, b.Expression]:
|
|
21
|
+
"""Construct a name by concatenating arguments into a string expression.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
*args: Arguments to concatenate (strings, numbers, or lists).
|
|
25
|
+
sep: Separator between arguments. Defaults to "_", use None for no separator.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
A string or expression representing the concatenated name.
|
|
29
|
+
"""
|
|
8
30
|
if not args:
|
|
9
31
|
raise ValueError("No arguments provided to `make_name`")
|
|
10
32
|
elif len(args) == 1:
|
|
@@ -25,9 +47,42 @@ def make_name(*args, sep: str | None = "_"):
|
|
|
25
47
|
str_args = map(std.strings.string, args)
|
|
26
48
|
return std.strings.concat(*str_args)
|
|
27
49
|
|
|
28
|
-
# TODO move to std? need to support normal logical evaluation.
|
|
29
|
-
def all_different(*args: Any) -> Aggregate:
|
|
30
|
-
return Aggregate(Relationship.builtins["all_different"], *args)
|
|
31
50
|
|
|
32
|
-
|
|
33
|
-
|
|
51
|
+
# =============================================================================
|
|
52
|
+
# Constraint Constructors
|
|
53
|
+
# =============================================================================
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def all_different(*args: Any) -> b.Aggregate:
|
|
57
|
+
"""Create an all_different constraint requiring all arguments to have distinct values.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
*args: Variables or expressions that must all have different values.
|
|
61
|
+
"""
|
|
62
|
+
return b.Aggregate(b.Relationship.builtins["all_different"], *args)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def implies(left: Any, right: Any) -> b.Expression:
|
|
66
|
+
"""Create a logical implication constraint (left => right).
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
left: The antecedent (condition) of the implication.
|
|
70
|
+
right: The consequent (result) of the implication.
|
|
71
|
+
"""
|
|
72
|
+
return b.Expression(b.Relationship.builtins["implies"], left, right)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def special_ordered_set_type_2(rank: Any, variables: Any) -> b.Aggregate:
|
|
76
|
+
"""Create a special ordered set type 2 (SOS2) constraint.
|
|
77
|
+
|
|
78
|
+
In an SOS2 constraint, at most two variables can be non-zero, and they
|
|
79
|
+
must be consecutive in the given order. This is useful for piecewise-linear
|
|
80
|
+
approximations where the variables represent weights on ordered breakpoints.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
rank: An expression that specifies the order/rank of each variable.
|
|
84
|
+
Variables are ordered by their rank values to determine which pairs
|
|
85
|
+
are consecutive.
|
|
86
|
+
variables: The decision variables that form the special ordered set.
|
|
87
|
+
"""
|
|
88
|
+
return b.Aggregate(b.Relationship.builtins["special_ordered_set_type_2"], rank, variables)
|
|
@@ -6,7 +6,7 @@ import time
|
|
|
6
6
|
|
|
7
7
|
from relationalai.semantics.snowflake import Table
|
|
8
8
|
from relationalai.semantics import std
|
|
9
|
-
from relationalai.semantics.internal import internal as b
|
|
9
|
+
from relationalai.semantics.internal import internal as b
|
|
10
10
|
from relationalai.semantics.rel.executor import RelExecutor
|
|
11
11
|
from relationalai.semantics.lqp.executor import LQPExecutor
|
|
12
12
|
from relationalai.tools.constants import DEFAULT_QUERY_TIMEOUT_MINS
|
|
@@ -57,18 +57,29 @@ class SolverModelDev:
|
|
|
57
57
|
# self.objective_values = model.Relationship(f"point {{i:int}} has objective value {{val:{data_type}}}", short_name=_name("objective_values"))
|
|
58
58
|
# self.primal_statuses = model.Relationship("point {i:int} has primal status {status:str}", short_name=_name("primal_statuses"))
|
|
59
59
|
|
|
60
|
+
self._model_info = {
|
|
61
|
+
"num_variables": self.variables,
|
|
62
|
+
"num_constraints": self.constraints,
|
|
63
|
+
"num_min_objectives": self.min_objectives,
|
|
64
|
+
"num_max_objectives": self.max_objectives,
|
|
65
|
+
}
|
|
66
|
+
|
|
60
67
|
# TODO(coey) assert that it is a property? not just a relationship.
|
|
61
|
-
def solve_for(self, expr:
|
|
62
|
-
where = []
|
|
68
|
+
def solve_for(self, expr, where: list = [], populate: bool = True, **kwargs):
|
|
63
69
|
if isinstance(expr, b.Fragment):
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
# TODO(coey) remove in future
|
|
71
|
+
raise ValueError("select fragment argument to `solve_for` is deprecated; instead use `where = [conditions...]` kwarg to specify optional grounding conditions")
|
|
72
|
+
elif isinstance(expr, b.Expression):
|
|
73
|
+
# must be of the form rel(a, ..., x) where the last element is the decision variable
|
|
74
|
+
rel = expr._op
|
|
75
|
+
assert isinstance(rel, b.Relationship)
|
|
76
|
+
params = expr._params
|
|
67
77
|
elif isinstance(expr, b.Relationship):
|
|
68
78
|
rel = expr
|
|
79
|
+
params = [b.field_to_type(self._model, f) for f in rel._fields]
|
|
69
80
|
else:
|
|
70
|
-
raise ValueError(f"Invalid expression type {type(expr)} for `solve_for
|
|
71
|
-
assert
|
|
81
|
+
raise ValueError(f"Invalid expression type {type(expr)} for `solve_for`")
|
|
82
|
+
assert len(params) == len(rel._fields)
|
|
72
83
|
assert rel not in self._variable_relationships
|
|
73
84
|
|
|
74
85
|
self._variable_relationships.add(rel)
|
|
@@ -81,7 +92,7 @@ class SolverModelDev:
|
|
|
81
92
|
new_kwargs["type"] = "cont" if self._data_type == "float" else "int"
|
|
82
93
|
for (key, val) in new_kwargs.items():
|
|
83
94
|
if key == "name":
|
|
84
|
-
assert isinstance(val, (
|
|
95
|
+
assert isinstance(val, _Any) or isinstance(val, list), f"Expected {key} to be a value or list, got {type(val)}"
|
|
85
96
|
defs.append(self.variable_name(node, make_name(val)))
|
|
86
97
|
elif key == "type":
|
|
87
98
|
assert val in ("cont", "int", "bin"), f"Unsupported variable type {val} for `solve_for`; must be cont, int, or bin"
|
|
@@ -96,9 +107,7 @@ class SolverModelDev:
|
|
|
96
107
|
self.satisfy(b.require(b.Expression(b.Relationship.builtins[op], rel, val)).where(*where))
|
|
97
108
|
else:
|
|
98
109
|
raise ValueError(f"Invalid keyword argument {key} for `solve_for`")
|
|
99
|
-
|
|
100
|
-
where.append(_make_hash((rel._short_name, rel._parent), node))
|
|
101
|
-
b.define(*defs).where(*where)
|
|
110
|
+
b.define(*defs).where(*where, _make_hash((rel._short_name, rel._parent or 0), node))
|
|
102
111
|
|
|
103
112
|
if populate:
|
|
104
113
|
# get variable values from the result point (populated by the solver)
|
|
@@ -108,12 +117,10 @@ class SolverModelDev:
|
|
|
108
117
|
|
|
109
118
|
return None
|
|
110
119
|
|
|
111
|
-
def minimize(self, expr
|
|
112
|
-
assert isinstance(expr, _Number)
|
|
120
|
+
def minimize(self, expr, name: _String | list | None = None):
|
|
113
121
|
return self._handle_expr(self.min_objectives, expr, name)
|
|
114
122
|
|
|
115
|
-
def maximize(self, expr
|
|
116
|
-
assert isinstance(expr, _Number)
|
|
123
|
+
def maximize(self, expr, name: _String | list | None = None):
|
|
117
124
|
return self._handle_expr(self.max_objectives, expr, name)
|
|
118
125
|
|
|
119
126
|
def satisfy(self, expr: b.Fragment, check: bool = False, name: _String | list | None = None):
|
|
@@ -351,18 +358,10 @@ class SolverModelDev:
|
|
|
351
358
|
# get scalar information
|
|
352
359
|
def __getattr__(self, name: str):
|
|
353
360
|
df = None
|
|
354
|
-
|
|
355
|
-
if name in {"num_variables", "num_constraints", "num_min_objectives", "num_max_objectives"}:
|
|
356
|
-
map = {
|
|
357
|
-
"num_variables": self.variables,
|
|
358
|
-
"num_constraints": self.constraints,
|
|
359
|
-
"num_min_objectives": self.min_objectives,
|
|
360
|
-
"num_max_objectives": self.max_objectives,
|
|
361
|
-
}
|
|
361
|
+
if name in self._model_info:
|
|
362
362
|
node = b.Hash.ref()
|
|
363
|
-
df = b.select(b.count(node).where(
|
|
364
|
-
|
|
365
|
-
if name in {"error", "termination_status", "solver_version", "printed_model", "solve_time_sec", "objective_value", "result_count"}:
|
|
363
|
+
df = b.select(b.count(node).where(self._model_info[name](node)) | 0).to_df()
|
|
364
|
+
elif name in {"error", "termination_status", "solver_version", "printed_model", "solve_time_sec", "objective_value", "result_count"}:
|
|
366
365
|
val = b.String.ref()
|
|
367
366
|
df = b.select(val).where(self.result_info(name, val)).to_df()
|
|
368
367
|
if df is not None:
|
|
@@ -404,7 +403,7 @@ def _rewrite(expr: b.Producer | b.Fragment, ctx: ExprContext):
|
|
|
404
403
|
elif isinstance(expr, (b.Relationship, b.RelationshipRef, b.RelationshipFieldRef)):
|
|
405
404
|
rel = expr if isinstance(expr, b.Relationship) else expr._relationship
|
|
406
405
|
if rel in ctx.solver_model._variable_relationships:
|
|
407
|
-
return std.hash(rel._short_name, expr._parent)
|
|
406
|
+
return std.hash(rel._short_name, expr._parent or 0)
|
|
408
407
|
return None
|
|
409
408
|
|
|
410
409
|
elif isinstance(expr, b.Union):
|
|
@@ -465,11 +464,18 @@ def _rewrite(expr: b.Producer | b.Fragment, ctx: ExprContext):
|
|
|
465
464
|
subctx = ExprContext(sm)
|
|
466
465
|
ctx.subcontext.append(subctx)
|
|
467
466
|
subctx.where.extend(expr._where._where)
|
|
468
|
-
arg_hash = b.Hash.ref()
|
|
469
|
-
subctx.where.append(_make_hash((sm._expr_id, *pre_args), arg_hash)) # TODO also add sym_arg here?
|
|
470
467
|
sm._expr_id += 1
|
|
471
468
|
subctx.define.append(sm.operator(node, op))
|
|
472
|
-
|
|
469
|
+
|
|
470
|
+
# special_ordered_set_type_2 has two ordered arguments: rank and variables
|
|
471
|
+
if op == "special_ordered_set_type_2":
|
|
472
|
+
assert len(pre_args) == 1, "special_ordered_set_type_2 expects exactly 2 arguments (rank, variables)"
|
|
473
|
+
subctx.define.append(sm.ordered_args_hash(node, pre_args[0], sym_arg))
|
|
474
|
+
else:
|
|
475
|
+
# other aggregate operators use unordered args
|
|
476
|
+
arg_hash = b.Hash.ref()
|
|
477
|
+
subctx.where.append(_make_hash((sm._expr_id, *pre_args), arg_hash))
|
|
478
|
+
subctx.define.append(sm.unordered_args_hash(node, arg_hash, sym_arg))
|
|
473
479
|
return node
|
|
474
480
|
|
|
475
481
|
elif isinstance(expr, b.Fragment):
|
|
@@ -502,7 +508,7 @@ def _expr_strings_rec(x, names_dict, ops_dict, args_dict):
|
|
|
502
508
|
s = f"({s})"
|
|
503
509
|
arg_strs.append(s)
|
|
504
510
|
|
|
505
|
-
if op in agg_ops:
|
|
511
|
+
if op in agg_ops and not op == "special_ordered_set_type_2":
|
|
506
512
|
# sort unordered args to improve determinism
|
|
507
513
|
arg_strs.sort()
|
|
508
514
|
|
|
@@ -522,7 +528,7 @@ infix_ops = set(["+", "-", "*", "/", "^"])
|
|
|
522
528
|
infix_comps = set(["=", "!=", "<", "<=", ">", ">=", "implies"])
|
|
523
529
|
infixs = infix_ops.union(infix_comps)
|
|
524
530
|
prefix_ops = set(["abs", "log", "exp"])
|
|
525
|
-
agg_ops = set(["sum", "count", "min", "max", "all_different"])
|
|
531
|
+
agg_ops = set(["sum", "count", "min", "max", "all_different", "special_ordered_set_type_2"])
|
|
526
532
|
|
|
527
533
|
# _variable_types = {
|
|
528
534
|
# "cont": 40,
|