Skip to content

Commit 36074e0

Browse files
committed
isolate cp assignment hashing
1 parent c5b3972 commit 36074e0

File tree

5 files changed

+275
-23
lines changed

5 files changed

+275
-23
lines changed

ortools/constraint_solver/assignment.cc

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
#include "absl/strings/str_join.h"
2828
#include "absl/strings/string_view.h"
2929
#include "ortools/base/file.h"
30-
#include "ortools/base/logging.h"
3130
#include "ortools/base/map_util.h"
3231
#include "ortools/base/options.h"
3332
#include "ortools/base/recordio.h"
@@ -36,11 +35,28 @@
3635

3736
namespace operations_research {
3837

39-
template class AssignmentContainer<IntVar, IntVarElement>;
40-
template class AssignmentContainer<IntervalVar, IntervalVarElement>;
41-
template class AssignmentContainer<SequenceVar, SequenceVarElement>;
38+
// ----------------- BaseAssignmentContainer ------------------------
4239

43-
// ----------------- Solutions ------------------------
40+
int BaseAssignmentContainer::FindWithDefault(const void* var,
41+
int default_value) const {
42+
auto it = var_to_index_.find(var);
43+
return it == var_to_index_.end() ? default_value : it->second;
44+
}
45+
46+
int BaseAssignmentContainer::MapSize() const { return var_to_index_.size(); }
47+
bool BaseAssignmentContainer::MapEmpty() const { return var_to_index_.empty(); }
48+
void BaseAssignmentContainer::ClearMap() { var_to_index_.clear(); }
49+
void BaseAssignmentContainer::AssignMap(const void* var, int index) const {
50+
var_to_index_[var] = index;
51+
}
52+
bool BaseAssignmentContainer::FindCopy(const void* var, int* index) const {
53+
auto it = var_to_index_.find(var);
54+
if (it == var_to_index_.end()) {
55+
return false;
56+
}
57+
*index = it->second;
58+
return true;
59+
}
4460

4561
// ----- IntVarElement -----
4662

ortools/constraint_solver/constraint_solver.h

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5326,10 +5326,24 @@ class SequenceVarElement : public AssignmentElement {
53265326
std::vector<int> unperformed_;
53275327
};
53285328

5329+
class BaseAssignmentContainer {
5330+
public:
5331+
int FindWithDefault(const void* var, int default_value) const;
5332+
5333+
int MapSize() const;
5334+
bool MapEmpty() const;
5335+
void ClearMap();
5336+
void AssignMap(const void* var, int index) const;
5337+
bool FindCopy(const void* var, int* index) const;
5338+
5339+
private:
5340+
mutable absl::flat_hash_map<const void*, int> var_to_index_;
5341+
};
5342+
53295343
template <class V, class E>
5330-
class AssignmentContainer {
5344+
class AssignmentContainer : public BaseAssignmentContainer {
53315345
public:
5332-
AssignmentContainer() {}
5346+
AssignmentContainer() = default;
53335347
E* Add(V* var) {
53345348
CHECK(var != nullptr);
53355349
int index = -1;
@@ -5353,8 +5367,8 @@ class AssignmentContainer {
53535367
}
53545368
void Clear() {
53555369
elements_.clear();
5356-
if (!elements_map_.empty()) { /// 2x speedup on OR-Tools.
5357-
elements_map_.clear();
5370+
if (!MapEmpty()) { /// 2x speedup on OR-Tools.
5371+
ClearMap();
53585372
}
53595373
}
53605374
/// Advanced usage: Resizes the container, potentially adding elements with
@@ -5458,8 +5472,7 @@ class AssignmentContainer {
54585472
/// compares both content and how the map is hashed (e.g., number of
54595473
/// buckets). This is not what we want.
54605474
for (const E& element : container.elements_) {
5461-
const int position =
5462-
gtl::FindWithDefault(elements_map_, element.Var(), -1);
5475+
const int position = FindWithDefault(element.Var(), -1);
54635476
if (position < 0 || elements_[position] != element) {
54645477
return false;
54655478
}
@@ -5472,12 +5485,11 @@ class AssignmentContainer {
54725485

54735486
private:
54745487
void EnsureMapIsUpToDate() const {
5475-
absl::flat_hash_map<const V*, int>* map =
5476-
const_cast<absl::flat_hash_map<const V*, int>*>(&elements_map_);
5477-
for (int i = map->size(); i < elements_.size(); ++i) {
5478-
(*map)[elements_[i].Var()] = i;
5488+
for (int i = MapSize(); i < elements_.size(); ++i) {
5489+
AssignMap(elements_[i].Var(), i);
54795490
}
54805491
}
5492+
54815493
bool Find(const V* var, int* index) const {
54825494
/// This threshold was determined from microbenchmarks on Nehalem platform.
54835495
const size_t kMaxSizeForLinearAccess = 11;
@@ -5494,18 +5506,17 @@ class AssignmentContainer {
54945506
return false;
54955507
} else {
54965508
EnsureMapIsUpToDate();
5497-
DCHECK_EQ(elements_map_.size(), elements_.size());
5498-
return gtl::FindCopy(elements_map_, var, index);
5509+
DCHECK_EQ(MapSize(), elements_.size());
5510+
return FindCopy(var, index);
54995511
}
55005512
}
55015513

55025514
std::vector<E> elements_;
5503-
absl::flat_hash_map<const V*, int> elements_map_;
55045515
};
55055516

5506-
extern template class AssignmentContainer<IntVar, IntVarElement>;
5507-
extern template class AssignmentContainer<IntervalVar, IntervalVarElement>;
5508-
extern template class AssignmentContainer<SequenceVar, SequenceVarElement>;
5517+
template class AssignmentContainer<IntVar, IntVarElement>;
5518+
template class AssignmentContainer<IntervalVar, IntervalVarElement>;
5519+
template class AssignmentContainer<SequenceVar, SequenceVarElement>;
55095520

55105521
/// An Assignment is a variable -> domains mapping, used
55115522
/// to report solutions to the user.

ortools/routing/docs/migration.md

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
# Migration from SWIG to pybind11 for Routing
2+
3+
This document describes how to migrate your Python code using the Operations
4+
Research Routing library from the legacy SWIG-based bindings (`pywraprouting`)
5+
to the new pybind11-based bindings (`routing`).
6+
7+
The new bindings offer better integration with Python, including:
8+
9+
* Compliance with PEP8 naming conventions (snake_case for methods and
10+
functions).
11+
* Better type hinting support.
12+
* More pythonic APIs.
13+
14+
## Imports
15+
16+
### Old (SWIG)
17+
18+
```python
19+
from google3.util.operations_research.routing import enums_pb2
20+
from google3.util.operations_research.routing.python import pywraprouting
21+
```
22+
23+
### New (pybind11)
24+
25+
```python
26+
from google3.util.operations_research.constraint_solver.python import constraint_solver
27+
from google3.util.operations_research.routing.python import routing
28+
```
29+
30+
*Note: The `constraint_solver` import is often needed for types like
31+
`Assignment` which are shared with the underlying CP solver.*
32+
33+
## Naming Conventions
34+
35+
The most significant change is the shift from CamelCase to snake_case for method
36+
names and functions. Class names remain CamelCase.
37+
38+
Feature | Legacy (SWIG) | New (pybind11)
39+
:-------------------- | :-------------------------------------------------- | :-------------
40+
**Index Manager** | `pywraprouting.IndexManager(...)` | `routing.IndexManager(...)`
41+
**Routing Model** | `pywraprouting.Model(manager)` | `routing.Model(manager)`
42+
**Search Parameters** | `pywraprouting.DefaultRoutingSearchParameters()` | `routing.default_routing_search_parameters()`
43+
**Enums** | `enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC` | `routing.FirstSolutionStrategy.PATH_CHEAPEST_ARC`
44+
45+
### Common Method Mappings
46+
47+
Legacy Method | New Method
48+
:-------------------------------------- | :---------
49+
`IndexToNode(index)` | `index_to_node(index)`
50+
`NodeToIndex(node)` | `node_to_index(node)`
51+
`RegisterTransitCallback(callback)` | `register_transit_callback(callback)`
52+
`SetArcCostEvaluatorOfAllVehicles(idx)` | `set_arc_cost_evaluator_of_all_vehicles(idx)`
53+
`SolveWithParameters(params)` | `solve_with_parameters(params)`
54+
`Start(vehicle)` | `start(vehicle)`
55+
`IsEnd(index)` | `is_end(index)`
56+
`NextVar(index)` | `next_var(index)`
57+
`GetDimensionOrDie(name)` | `get_dimension_or_die(name)`
58+
`AddDimension(...)` | `add_dimension(...)`
59+
`AddVariableMinimizedByFinalizer(var)` | `add_variable_minimized_by_finalizer(var)`
60+
`GetNumberOfVehicles()` | `num_vehicles()`
61+
62+
### Assignment / Solution
63+
64+
Legacy Method | New Method
65+
:-------------------------- | :---------------------------
66+
`solution.ObjectiveValue()` | `solution.objective_value()`
67+
`solution.Value(var)` | `solution.value(var)`
68+
`solution.Min(var)` | `solution.min(var)`
69+
`solution.Max(var)` | `solution.max(var)`
70+
71+
## Solver, Variables, and Constraints
72+
73+
The `constraint_solver` module (often imported as `from
74+
google3.util.operations_research.constraint_solver.python import
75+
constraint_solver`) also sees significant API changes, particularly in how
76+
variables are created and constraints are added.
77+
78+
### Variable Creation
79+
80+
In the legacy API, variables were created using methods like `IntVar`,
81+
`BoolVar`, etc. In the new API, these are prefixed with `new_` and snake_cased.
82+
83+
Legacy Method | New Method
84+
:------------------------------------- | :---------
85+
`solver.IntVar(...)` | `solver.new_int_var(...)`
86+
`solver.BoolVar(...)` | `solver.new_bool_var(...)`
87+
`solver.IntervalVar(...)` | `solver.new_interval_var(...)`
88+
`solver.FixedDurationIntervalVar(...)` | `solver.new_fixed_duration_interval_var(...)`
89+
90+
### Adding Constraints
91+
92+
The legacy API typically involved a two-step process: creating the constraint
93+
object (often using methods named after the constraint, e.g.,
94+
`solver.AllDifferent(...)`) and then adding it to the solver using
95+
`solver.Add(...)`.
96+
97+
The new API provides dedicated `add_` methods that create and add the constraint
98+
in a single step.
99+
100+
Legacy Pattern | New Method
101+
:---------------------------------------- | :---------------------------------
102+
`solver.Add(solver.AllDifferent(...))` | `solver.add_all_different(...)`
103+
`solver.Add(solver.Cumulative(...))` | `solver.add_cumulative(...)`
104+
`solver.Add(solver.SumEquality(...))` | `solver.add_sum_equality(...)`
105+
`solver.Add(solver.MinEquality(...))` | `solver.add_min_equality(...)`
106+
`solver.Add(solver.MaxEquality(...))` | `solver.add_max_equality(...)`
107+
`solver.Add(solver.ElementEquality(...))` | `solver.add_element_equality(...)`
108+
`solver.Add(solver.AbsEquality(...))` | `solver.add_abs_equality(...)`
109+
`solver.Add(solver.BetweenCt(...))` | `solver.add_between_ct(...)`
110+
`solver.Add(solver.MemberCt(...))` | `solver.add_member_ct(...)`
111+
`solver.Add(solver.NotMemberCt(...))` | `solver.add_not_member_ct(...)`
112+
`solver.Add(solver.Count(...))` | `solver.add_count(...)`
113+
`solver.Add(solver.Distribute(...))` | `solver.add_distribute(...)`
114+
`solver.Add(solver.Deviation(...))` | `solver.add_deviation(...)`
115+
`solver.Add(solver.Circuit(...))` | `solver.add_circuit(...)`
116+
`solver.Add(solver.SubCircuit(...))` | `solver.add_sub_circuit(...)`
117+
`solver.Add(solver.Pack(...))` | `solver.add_pack(...)`
118+
119+
**Note:** For general constraints not covered by specific `add_` methods, you
120+
can still use `solver.add(...)` (snake_case version of `solver.Add(...)`).
121+
122+
## Enums and Status
123+
124+
In the legacy API, enums were typically accessed via `enums_pb2` and treated as
125+
integers or protobuf enum descriptors. In the new API, they are exposed as
126+
nested classes within the `routing` module, behaving like standard Python Enums.
127+
128+
### Legacy
129+
130+
```python
131+
from google3.util.operations_research.routing import enums_pb2
132+
133+
status = routing.Status()
134+
print(f"Status: {enums_pb2.RoutingSearchStatus.Value.Name(status)}")
135+
if status == enums_pb2.RoutingSearchStatus.ROUTING_OPTIMAL:
136+
pass
137+
```
138+
139+
### New
140+
141+
```python
142+
from google3.util.operations_research.routing.python import routing
143+
144+
status = routing_model.status()
145+
# You can access the name directly on the enum member
146+
print(f"Status: {status.name}")
147+
if status == routing.RoutingSearchStatus.ROUTING_OPTIMAL:
148+
pass
149+
```
150+
151+
## Example: Simple Routing Program
152+
153+
### Legacy Code
154+
155+
```python
156+
from google3.util.operations_research.routing import enums_pb2
157+
from google3.util.operations_research.routing.python import pywraprouting
158+
159+
def main():
160+
manager = pywraprouting.IndexManager(num_locations, num_vehicles, depot)
161+
routing = pywraprouting.Model(manager)
162+
163+
def distance_callback(from_index, to_index):
164+
from_node = manager.IndexToNode(from_index)
165+
to_node = manager.IndexToNode(to_index)
166+
return abs(to_node - from_node)
167+
168+
transit_callback_index = routing.RegisterTransitCallback(distance_callback)
169+
routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
170+
171+
search_parameters = pywraprouting.DefaultRoutingSearchParameters()
172+
search_parameters.first_solution_strategy = (
173+
enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC
174+
)
175+
176+
assignment = routing.SolveWithParameters(search_parameters)
177+
print(f"Objective: {assignment.ObjectiveValue()}")
178+
```
179+
180+
### New Code
181+
182+
```python
183+
from google3.util.operations_research.routing.python import routing
184+
185+
def main():
186+
manager = routing.IndexManager(num_locations, num_vehicles, depot)
187+
routing_model = routing.Model(manager)
188+
189+
def distance_callback(from_index, to_index):
190+
from_node = manager.index_to_node(from_index)
191+
to_node = manager.index_to_node(to_index)
192+
return abs(to_node - from_node)
193+
194+
transit_callback_index = routing_model.register_transit_callback(distance_callback)
195+
routing_model.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)
196+
197+
search_parameters = routing.default_routing_search_parameters()
198+
search_parameters.first_solution_strategy = (
199+
routing.FirstSolutionStrategy.PATH_CHEAPEST_ARC
200+
)
201+
202+
assignment = routing_model.solve_with_parameters(search_parameters)
203+
print(f"Objective: {assignment.objective_value()}")
204+
```
205+
206+
## Dimensions and Variables
207+
208+
When working with dimensions, the variable accessors also change to snake_case.
209+
210+
### Legacy
211+
212+
```python
213+
time_dimension = routing.GetDimensionOrDie("Time")
214+
index = routing.Start(0)
215+
time_var = time_dimension.CumulVar(index)
216+
time_var.SetRange(0, 10)
217+
```
218+
219+
### New
220+
221+
```python
222+
time_dimension = routing_model.get_dimension_or_die("Time")
223+
index = routing_model.start(0)
224+
time_var = time_dimension.cumul_var(index)
225+
time_var.set_range(0, 10)
226+
```

ortools/routing/python/routing_test.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import functools
1818

1919
from absl.testing import absltest
20-
from google.protobuf import text_format
2120

2221
from ortools.constraint_solver.python import constraint_solver
2322
from ortools.routing import enums_pb2

ortools/routing/samples/vrp_breaks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
from typing import Any, Dict
2828

2929
from ortools.constraint_solver.python import constraint_solver
30-
from ortools.routing.python import routing
3130
from ortools.routing import enums_pb2
31+
from ortools.routing.python import routing
3232

3333
# [END import]
3434

0 commit comments

Comments
 (0)