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.
@@ -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 2
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 2
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(self.Node, incount),
2548
- outdegree_rel(self.Node, outcount),
2549
- ).define(_degree_rel(self.Node, incount + outcount))
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 is the count of neighbors.
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
- node_set(self.Node), # Necessary given the match on the following line.
2561
- _degree := where(count_neighbor_rel(self.Node, Integer)).select(Integer) | 0,
2562
- ).define(_degree_rel(self.Node, _degree))
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 unweighted graphs, all edge
2941
- weights are considered to be 1.0.
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=-1.0),
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 0.0
3002
- 1 2 1.0
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 1.0
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
- # Choose the appropriate node set
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 - use all nodes
3082
- node_set = self.Node
3102
+ node_constraint = node # No constraint on nodes.
3083
3103
  else:
3084
- # Constrained to nodes in the subset
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
- node_set(self.Node),
3090
- _weighted_degree := sum(dst, weight).per(self.Node).where(self._weight(self.Node, dst, weight)) | 0.0,
3091
- ).define(_weighted_degree_rel(self.Node, _weighted_degree))
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 unweighted graphs, all edge weights are considered to be 1.0.
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 considered to be 1.0.
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 unewighted graphs without self-loops, this value will be at most 1.0;
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.666667
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
- __version__ = "0.0.0"
1
+ """Optimization and constraint programming solver interfaces.
2
2
 
3
- # NOTE(coey) using Solver class from old solvers library to avoid code duplication
4
- from relationalai.experimental.solvers import Solver, Provider
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 .common import make_name, all_different, implies
7
- from .solvers_pb import SolverModelPB
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.internal import Relationship, Expression, Aggregate
12
+ from relationalai.semantics.internal import internal as b
13
+
14
+
15
+ # =============================================================================
16
+ # Utilities
17
+ # =============================================================================
6
18
 
7
- def make_name(*args, sep: str | None = "_"):
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
- def implies(left, right) -> Expression:
33
- return Expression(Relationship.builtins["implies"], left, right)
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 # TODO(coey) change b name or remove 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: b.Relationship | b.Fragment, populate: bool = True, **kwargs):
62
- where = []
68
+ def solve_for(self, expr, where: list = [], populate: bool = True, **kwargs):
63
69
  if isinstance(expr, b.Fragment):
64
- assert expr._select and len(expr._select) == 1 and expr._where, "Fragment input for `solve_for` must have exactly one select and a where clause"
65
- rel = expr._select[0]
66
- where.extend(expr._where)
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`; must be a Relationship or Fragment")
71
- assert rel._parent and rel._short_name, "Relationship for `solve_for` must have a parent and a short name"
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, (_Any, list)), f"Expected {key} to be a value or list, got {type(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: _Number, name: _String | list | None = None):
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: _Number, name: _String | list | None = None):
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
- # model info
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(map[name](node)) | 0).to_df()
364
- # result info
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
- subctx.define.append(sm.unordered_args_hash(node, arg_hash, sym_arg)) # TODO what if some values are data not hashes?
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,