Right here we received’t begin from scratch. As acknowledged earlier, we already developed the code that builds a Pyomo mannequin of the TSP and solves it in dash 3. And belief me, that was the toughest half. Now, we now have the better job of organizing what we did in a manner that makes it normal, hiding the small print whereas conserving the important components seen. In a way, we wish the optimizer to appear to be a “magic field” that even customers not conversant in math modeling are in a position to make use of to resolve their TSP issues intuitively.
4.1. TravelingSalesmanOptimizer design
Our optimizer class could have “core” strategies, doing the majority of the work, and “superficial” strategies, serving because the high-level interface of the category, which invoke the core strategies beneath.
These are the steps that may lie on the core of the optimizer’s logic:
Create a Pyomo mannequin out of a distance matrix. That is executed by the _create_model technique, which mainly wraps the code of the proof-of-concept we already did. It accepts a dataframe of a distance matrix and builds a Pyomo mannequin out of it. The one necessary distinction between what we did and what we’re doing is that, now, the preliminary website is just not hard-coded as merely “lodge”, however is assumed to be the location of the primary row in df_distances. Within the normal case, thus, the preliminary website is taken to be the primary one within the coordinates dataframe⁴ df_sites. This generalization permits the optimizer to resolve any occasion.(Try and) Resolve the mannequin. That is carried out within the _optimize technique inherited from BaseOptimizer, which returns True provided that an answer is discovered.Extract the answer from the mannequin and parse it in a manner that’s simple to interpret and use. This occurs inside _store_solution_from_model, which is a technique that inspects the solved mannequin and extracts the values of the choice variables, and the worth of the target operate, to create the attributes tour_ and tour_distance_, respectively. This technique will get invoked provided that an answer exists, so if no answer is discovered, the “answer attributes” tour_ and tour_distance_ by no means get created. The good thing about that is that the presence of those two “answer attributes”, after becoming, will inform the person of the existence of an answer. As a plus, the optimum values of each the variables and goal will be conveniently retrieved at any level, not essentially in the mean time of becoming.
The final 2 steps — discovering and extracting the answer — are wrapped contained in the final “core” technique, _fit_to_distances.
“However wait” — you may assume — “Because the title implies, _fit_to_distances requires distances as enter; is not our purpose to resolve TSP issues utilizing solely coordinates, not distances?”. Sure, that is the place the match technique matches in. We cross coordinates to it, and we benefit from GeoAnalyzer to assemble the space matrix, which is then processed usually by _fit_to_distances. On this manner, if the person doesn’t wish to gather the distances himself, he can delegate the duty by utilizing match. If, nonetheless, he prefers to make use of customized knowledge, he can assemble it in a df_distances and cross it to _fit_to_distances as a substitute.
4.2. TravelingSalesmanOptimizer implementation
Let’s comply with the design outlined above to incrementally construct the optimizer. First, a minimalist model that simply builds a mannequin and solves it — with none answer parsing but. Discover how the __repr__ technique permits us to know the title and variety of websites the optimizer accommodates each time we print it.
from typing import Tuple, Record
class TravelingSalesmanOptimizer(BaseOptimizer):”””Implements the Miller–Tucker–Zemlin formulation [1] of the Touring Salesman Downside (TSP) as a linear integer program. The TSP will be acknowledged like: “Given a set of places (and normally their pair-wise distances), discover the tour of minimal distance that traverses all of them precisely as soon as and ends on the identical location it began from. For a derivation of the mathematical mannequin, see [2].
Parameters———-name : strOptional title to offer to a selected TSP occasion
Attributes———-tour_ : listList of places sorted in go to order, obtained after becoming.To keep away from duplicity, the final website within the listing is just not the preliminary one, however the final one earlier than closing the tour.
tour_distance_ : floatTotal distance of the optimum tour, obtained after becoming.
Instance——–>>> tsp = TravelingSalesmanOptimizer()>>> tsp.match(df_sites) # match to a dataframe of geo-coordinates>>> tsp.tour_ # listing ofsites sorted by go to order
References———-[1] https://en.wikipedia.org/wiki/Travelling_salesman_problem[2] https://towardsdatascience.com/plan-optimal-trips-automatically-with-python-and-operations-research-models-part-2-fc7ee8198b6c”””def __init__(self, title=””):tremendous().__init__()self.title = title
def _create_model(self, df_distances: pd.DataFrame) -> pyo.ConcreteModel:””” Given a pandas dataframe of a distance matrix, create a Pyomo mannequin of the TSP and populate it with that distance knowledge “””mannequin = pyo.ConcreteModel(self.title)
# a website must be picked because the “preliminary” one, would not matter which # actually; by lack of higher standards, take first website in dataframe # because the preliminary onemodel.initial_site = df_distances.iloc[0].title
#=========== units declaration ===========#list_of_sites = df_distances.index.tolist()
mannequin.websites = pyo.Set(initialize=list_of_sites, area=pyo.Any, doc=”set of all websites to be visited (𝕊)”)
def _rule_domain_arcs(mannequin, i, j):””” All doable arcs connecting the websites (𝔸) “””# solely create pair (i, j) if website i and website j are differentreturn (i, j) if i != j else None
rule = _rule_domain_arcsmodel.valid_arcs = pyo.Set(initialize=mannequin.websites * mannequin.websites, # 𝕊 × 𝕊filter=rule, doc=rule.__doc__)
mannequin.sites_except_initial = pyo.Set(initialize=mannequin.websites – {mannequin.initial_site}, area=mannequin.websites,doc=”All websites besides the preliminary website”)#=========== parameters declaration ===========#def _rule_distance_between_sites(mannequin, i, j):””” Distance between website i and website j (𝐷𝑖𝑗) “””return df_distances.at[i, j] # fetch the space from dataframe
rule = _rule_distance_between_sitesmodel.distance_ij = pyo.Param(mannequin.valid_arcs, initialize=rule, doc=rule.__doc__)
mannequin.M = pyo.Param(initialize=1 – len(mannequin.sites_except_initial),doc=”massive M to make some constraints redundant”)
#=========== variables declaration ===========#mannequin.delta_ij = pyo.Var(mannequin.valid_arcs, inside=pyo.Binary, doc=”Whether or not to go from website i to website j (𝛿𝑖𝑗)”)
mannequin.rank_i = pyo.Var(mannequin.sites_except_initial, inside=pyo.NonNegativeReals, bounds=(1, len(mannequin.sites_except_initial)), doc=(“Rank of every website to trace go to order”))
#=========== goal declaration ===========#def _rule_total_distance_traveled(mannequin):””” whole distance traveled “””return pyo.summation(mannequin.distance_ij, mannequin.delta_ij)
rule = _rule_total_distance_traveledmodel.goal = pyo.Goal(rule=rule, sense=pyo.decrease, doc=rule.__doc__)
#=========== constraints declaration ===========#def _rule_site_is_entered_once(mannequin, j):””” every website j should be visited from precisely one different website “””return sum(mannequin.delta_ij[i, j] for i in mannequin.websites if i != j) == 1
rule = _rule_site_is_entered_oncemodel.constr_each_site_is_entered_once = pyo.Constraint(mannequin.websites, rule=rule, doc=rule.__doc__)
def _rule_site_is_exited_once(mannequin, i):””” every website i need to departure to precisely one different website “””return sum(mannequin.delta_ij[i, j] for j in mannequin.websites if j != i) == 1
rule = _rule_site_is_exited_oncemodel.constr_each_site_is_exited_once = pyo.Constraint(mannequin.websites, rule=rule, doc=rule.__doc__)
def _rule_path_is_single_tour(mannequin, i, j):””” For every pair of non-initial websites (i, j), if website j is visited from website i, the rank of j should be strictly better than the rank of i.”””if i == j: # if websites coincide, skip making a constraintreturn pyo.Constraint.Skip
r_i = mannequin.rank_i[i]r_j = mannequin.rank_i[j]delta_ij = mannequin.delta_ij[i, j]return r_j >= r_i + delta_ij + (1 – delta_ij) * mannequin.M
# cross product of non-initial websites, to index the constraintnon_initial_site_pairs = (mannequin.sites_except_initial * mannequin.sites_except_initial)
rule = _rule_path_is_single_tourmodel.constr_path_is_single_tour = pyo.Constraint(non_initial_site_pairs,rule=rule, doc=rule.__doc__)
self._store_model(mannequin) # technique inherited from BaseOptimizerreturn mannequin
def _fit_to_distances(self, df_distances: pd.DataFrame) -> None:self._name_index = df_distances.index.namemodel = self._create_model(df_distances)solution_exists = self._optimize(mannequin)return self
@propertydef websites(self) -> Tuple[str]:””” Return tuple of website names the optimizer considers “””return self.mannequin.websites.knowledge() if self.is_model_created else ()
@propertydef num_sites(self) -> int:””” Variety of places to go to “””return len(self.websites)
@propertydef initial_site(self):return self.mannequin.initial_site if self.is_fitted else None
def __repr__(self) -> str:title = f”{self.title}, ” if self.title else ”return f”{self.__class__.__name__}({title}n={self.num_sites})”
Let’s rapidly verify how the optimizer behaves. Upon instantiation, the optimizer doesn’t include any variety of websites, because the illustration string reveals, or an inside mannequin, and it’s after all not fitted:
tsp = TravelingSalesmanOptimizer(“trial 1”)
print(tsp)#[Out]: TravelingSalesmanOptimizer(trial 1, n=0)print(tsp.is_model_created, tsp.is_fitted)#[Out]: (False, False)
We now match it to the space knowledge, and if we don’t get a warning, it implies that all of it went effectively. We are able to see that now the illustration string tells us we supplied 9 websites, there’s an inside mannequin, and that the optimizer was fitted to the space knowledge:
tsp._fit_to_distances(df_distances)
print(tsp)#[Out]: TravelingSalesmanOptimizer(trial 1, n=9)print(tsp.is_model_created, tsp.is_fitted)#[Out]: (True, True)
That the optimum answer was discovered is corroborated by the presence of particular values within the rank resolution variables of the mannequin:
tsp.mannequin.rank_i.get_values(){‘Sacre Coeur’: 8.0,’Louvre’: 2.0,’Montmartre’: 7.0,’Port de Suffren’: 4.0,’Arc de Triomphe’: 5.0,’Av. Champs Élysées’: 6.0,’Notre Dame’: 1.0,’Tour Eiffel’: 3.0}
These rank variables signify the chronological order of the stops within the optimum tour. In case you recall from their definition, they’re outlined over all websites besides the preliminary one⁵, and that’s why the lodge doesn’t seem in them. Simple, we might add the lodge with rank 0, and there we might have the reply to our downside. We don’t have to extract 𝛿ᵢⱼ, the choice variables for the person arcs of the tour, to know through which order we should always go to the websites. Though that’s true, we’re nonetheless going to make use of the arc variables 𝛿ᵢⱼ to extract the precise sequence of stops from the solved mannequin.
💡 Agile doesn’t have to be fragile
If our solely goal have been to resolve the TSP, with out trying to prolong the mannequin to embody extra particulars of our real-life downside, it might be sufficient to make use of the rank variables to extract the optimum tour. Nevertheless, because the TSP is simply the preliminary prototype of what is going to grow to be a extra subtle mannequin, we’re higher off extracting the answer from the arc resolution variables 𝛿ᵢⱼ, as they are going to be current in any mannequin that includes routing selections. All different resolution variables are auxiliary, and, when wanted, their job is to signify states or point out circumstances dependant on the true resolution variables, 𝛿ᵢⱼ. As you’ll see within the subsequent articles, selecting the rank variables to extract the tour works for a pure TSP mannequin, however received’t work for extensions of it that make it elective to go to some websites. Therefore, if we extract the answer from 𝛿ᵢⱼ, our strategy might be normal and re-usable, regardless of how complicated the mannequin we’re utilizing.
The advantages of this strategy will grow to be obvious within the following articles, the place new necessities are added, and thus extra variables are wanted contained in the mannequin. With the why coated, let’s soar into the how.
4.2.1 Extracting the optimum tour from the mannequin
We’ve got the variable 𝛿ᵢⱼ, listed by doable arcs (i, j), the place 𝛿ᵢⱼ=0 means the arc is just not chosen and 𝛿ᵢⱼ=1 means the arc is chosen.We would like a dataframe the place the websites are within the index (as in our enter df_sites), and the place the cease quantity is indicated within the column “visit_order”.We write a way to extract such dataframe from the fitted optimizer. These are the steps we’ll comply with, with every step encapsulated in its personal helper technique(s):Extract the chosen arcs from 𝛿ᵢⱼ, which represents the tour. Performed in _get_selected_arcs_from_model.Convert the listing of arcs (edges) into a listing of stops (nodes). Performed in _get_stops_order_list.Convert the listing of stops right into a dataframe with a constant construction. Performed in _get_tour_stops_dataframe.
As the chosen arcs are combined (i.e., not in “traversing order”), getting a listing of ordered stops is just not that straight-forward. To keep away from convoluted code, we exploit the truth that the arcs signify a graph, and we use the graph object G_tour to traverse the tour nodes so as, arriving on the listing simply.
import networkx as nx
# class TravelingSalesmanOptimizer(BaseOptimizer):# def __init__()# def _create_model()# def _fit_to_distances()# def websites()# def num_sites()# def initial_site()
_Arc = Tuple[str, str]
def _get_selected_arcs_from_model(self, mannequin: pyo.ConcreteModel) -> Record[_Arc]:”””Return the optimum arcs from the choice variable delta_{ij}as an unordered listing of arcs. Assumes the mannequin has been solved”””selected_arcs = [arc for arc, selected in model.delta_ij.get_values().items()if selected]return selected_arcs
def _extract_solution_as_graph(self, mannequin: pyo.ConcreteModel) -> nx.Graph:”””Extracts the chosen arcs from the choice variables of the mannequin, shops them in a networkX graph and returns such a graph”””selected_arcs = self._get_selected_arcs_from_model(mannequin)self._G_tour = nx.DiGraph(title=mannequin.title)self._G_tour.add_edges_from(selected_arcs)return self._G_tour
def _get_stops_order_list(self) -> Record[str]:”””Return the stops of the tour in a listing **ordered** by go to order”””visit_order = []next_stop = self.initial_site # by conference…visit_order.append(next_stop) # …tour begins at preliminary website
G_tour = self._extract_solution_as_graph(self.mannequin)# beginning at first cease, traverse the directed graph one arc at a timefor _ in G_tour.nodes:# get consecutive cease and retailer itnext_stop = listing(G_tour[next_stop])[0]visit_order.append(next_stop)# discard final cease in listing, because it’s repeated (the preliminary website) return visit_order[:-1]
def get_tour_stops_dataframe(self) -> pd.DataFrame:”””Return a dataframe of the stops alongside the optimum tour”””if self.is_fitted:ordered_stops = self._get_stops_order_list()df_stops = (pd.DataFrame(ordered_stops, columns=[self._name_index]).reset_index(names=’visit_order’) # from 0 to N.set_index(self._name_index) # preserve index constant)return df_stops
print(“No answer discovered. Match me to some knowledge and take a look at once more”)
Let’s see what this new technique provides us:
tsp = TravelingSalesmanOptimizer(“trial 2”)tsp._fit_to_distances(df_distances)tsp.get_tour_stops_dataframe()
The visit_order column signifies we should always go from the lodge to Notre Dame, then to the Louvre, and so forth, till the final cease earlier than closing the tour, Sacre Coeur. After that, it is trivial that one should return to the lodge. Good, now we now have the answer in a format simple to interpret and work with. However the sequence of stops is just not all we care about. The worth of the target operate can also be an necessary metric to maintain monitor of, as it is the criterion guiding our selections. For our specific case of the TSP, this implies getting the whole distance of the optimum tour.
4.2.2 Extracting the optimum goal from the mannequin
In the identical method that we didn’t use the rank variables to extract the sequence of stops as a result of in additional complicated fashions their values wouldn’t coincide with the tour stops, we received’t use the target operate on to get hold of the whole distance of the tour, though, right here too, each measures are equal. In additional complicated fashions, the target operate will even incorporate different targets, so this equivalence will now not maintain.
For now, we’ll preserve it easy and create a personal technique, _get_tour_total_distance, which clearly signifies the intent. The small print of the place this distance comes from are hidden, and can depend upon the actual targets that extra superior fashions care about. For now, the small print are easy: get the target worth of the solved mannequin.
# class TravelingSalesmanOptimizer(BaseOptimizer):# def __init__()# def _create_model()# def _fit_to_distances()# def websites()# def num_sites()# def initial_site()# def _get_selected_arcs_from_model()# def _extract_solution_as_graph()# def _get_stops_order_list()# def get_tour_stops_dataframe()
def _get_tour_total_distance(self) -> float:”””Return the whole distance of the optimum tour”””if self.is_fitted:# as the target is an expression for the whole distance, distance_tour = self.mannequin.goal() # we simply get its valuereturn distance_tour
print(“Optimizer is just not fitted to any knowledge, no optimum goal exists.”)return None
It could look superfluous now, however it’ll function a reminder to our future selves that there’s a design for grabbing goal values we’d higher comply with. Let’s verify it:
tsp = TravelingSalesmanOptimizer(“trial 3″)tsp._fit_to_distances(df_distances)print(f”Complete distance: {tsp._get_tour_total_distance()} m”)# [Out]: Complete distance: 14931.0 m
It’s round 14.9 km. As each the optimum tour and its distance are necessary, let’s make the optimizer retailer them collectively each time the _fit_to_distances technique will get known as, and solely when an optimum answer is discovered.
4.2.3 Storing the answer in attributes
Within the implementation of _fit_to_distances above, we simply created a mannequin and solved it, we did not do any parsing of the answer saved contained in the mannequin. Now, we’ll modify _fit_to_distances in order that when the mannequin answer succeeds, two new attributes are created and made out there with the 2 related elements of the answer: the tour_ and the tour_distance_. To make it easy, the tour_ attribute will not return the dataframe we did earlier, it’s going to return the listing with ordered stops. The brand new technique _store_solution_from_model takes care of this.
# class TravelingSalesmanOptimizer(BaseOptimizer):# def __init__()# def _create_model()# def websites()# def num_sites()# def initial_site()# def _get_selected_arcs_from_model()# def _extract_solution_as_graph()# def _get_stops_order_list()# def get_tour_stops_dataframe()# def _get_tour_total_distance()
def _fit_to_distances(self, df_distances: pd.DataFrame):”””Creates a mannequin of the TSP utilizing the space matrix supplied in `df_distances`, after which optimizes it. If the mannequin has an optimum answer, it’s extracted, parsed and saved internally so it may be retrieved.
Parameters———-df_distances : pd.DataFramePandas dataframe the place the indices and columns are the “cities” (or any website of curiosity) of the issue, and the cells of the dataframe include the pair-wise distances between the cities, i.e.,df_distances.at[i, j] accommodates the space between i and j.
Returns——-self : objectInstance of the optimizer.”””mannequin = self._create_model(df_distances)solution_exists = self._optimize(mannequin)
if solution_exists:# if an answer wasn’t discovered, the attributes will not existself._store_solution_from_model()
return self
#==================== answer dealing with ====================def _store_solution_from_model(self) -> None:”””Extract the optimum answer from the mannequin and create the “fitted attributes” `tour_` and `tour_distance_`”””self.tour_ = self._get_stops_order_list()self.tour_distance_ = self._get_tour_total_distance()
Let’s match the optimizer once more to the space knowledge and see how simple it’s to get the answer now:
tsp = TravelingSalesmanOptimizer(“trial 4”)._fit_to_distances(df_distances)
print(f”Complete distance: {tsp.tour_distance_} m”)print(f”Greatest tour:n”, tsp.tour_)# [Out]:# Complete distance: 14931.0 m# Greatest tour:# [‘hotel’, ‘Notre Dame’, ‘Louvre’, ‘Tour Eiffel’, ‘Port de Suffren’, ‘Arc de Triomphe’, ‘Av. Champs Élysées’, ‘Montmartre’, ‘Sacre Coeur’]
Good. However we are able to do even higher. To additional enhance the usability of this class, let’s permit the person to resolve the issue by solely offering the dataframe of web sites coordinates. As not everybody will have the ability to gather a distance matrix for his or her websites of curiosity, the category can handle it and supply an approximate distance matrix. This was executed above in part 3.2 with the GeoAnalyzer, right here we simply put it beneath the brand new match technique:
# class TravelingSalesmanOptimizer(BaseOptimizer):# def __init__()# def _create_model()# def _fit_to_distances()# def websites()# def num_sites()# def initial_site()# def _get_selected_arcs_from_model()# def _extract_solution_as_graph()# def _get_stops_order_list()# def get_tour_stops_dataframe()# def _get_tour_total_distance()# def _store_solution_from_model()
def match(self, df_sites: pd.DataFrame):”””Creates a mannequin occasion of the TSP downside utilizing a distance matrix derived (see notes) from the coordinates supplied in `df_sites`.
Parameters———-df_sites : pd.DataFrameDataframe of places “the salesperson” desires to go to, having the names of the places within the index and not less than one column named ‘latitude’ and one column named ‘longitude’.
Returns——-self : objectInstance of the optimizer.
Notes—–The space matrix used is derived from the coordinates of `df_sites`utilizing the ellipsoidal distance between any pair of coordinates, as supplied by `geopy.distance.distance`.”””self._validate_data(df_sites)
self._name_index = df_sites.index.nameself._geo_analyzer = GeoAnalyzer()self._geo_analyzer.add_locations(df_sites)df_distances = self._geo_analyzer.get_distance_matrix(precision=0)self._fit_to_distances(df_distances)return self
def _validate_data(self, df_sites):”””Raises error if the enter dataframe doesn’t have the anticipated columns”””if not (‘latitude’ in df_sites and ‘longitude’ in df_sites):increase ValueError(“dataframe should have columns ‘latitude’ and ‘longitude'”)
And now we now have achieved our purpose: discover the optimum tour from simply the websites places (and never from the distances as earlier than):
tsp = TravelingSalesmanOptimizer(“trial 5”)tsp.match(df_sites)
print(f”Complete distance: {tsp.tour_distance_} m”)tsp.tour_#[Out]:# Complete distance: 14931.0 m# [‘hotel’,# ‘Notre Dame’,# ‘Louvre’,# ‘Tour Eiffel’,# ‘Port de Suffren’,# ‘Arc de Triomphe’,# ‘Av. Champs Élysées’,# ‘Montmartre’,# ‘Sacre Coeur’]
4.3. TravelingSalesmanOptimizer for dummies
Congratulations! We reached the purpose the place the optimizer may be very intuitive to make use of. For mere comfort, I’ll add one other technique that might be fairly useful afterward after we do [sensitivity analysis] and evaluate the outcomes of various fashions. The optimizer, as it’s now, tells me the optimum go to order in a listing, or in a separate dataframe returned by get_tour_stops_dataframe(), however I might prefer it to inform me the go to order by reworking the places dataframe that I give it straight—by returning the identical dataframe with a brand new column having the optimum sequence of stops. The tactic fit_prescribe might be accountable for this:
# class TravelingSalesmanOptimizer(BaseOptimizer):# def __init__()# def _create_model()# def websites()# def num_sites()# def initial_site()# def _get_selected_arcs_from_model()# def _extract_solution_as_graph()# def _get_stops_order_list()# def get_tour_stops_dataframe()# def _get_tour_total_distance()# def _fit_to_distances()# def _store_solution_from_model()# def match()# def _validate_data()
def fit_prescribe(self, df_sites: pd.DataFrame, type=True) -> pd.DataFrame:”””In a single line, absorb a dataframe of places and return a duplicate of it with a brand new column specifying the optimum go to orderthat minimizes whole distance.
Parameters———-df_sites : pd.DataFrameDataframe with the websites within the index and the geolocation data in columns (first column latitude, second longitude).
type : bool (default=True)Whether or not to type the places by go to order.
Returns——-df_sites_ranked : pd.DataFrame Copy of enter dataframe `df_sites` with a brand new column, ‘visit_order’, containing the cease sequence of the optimum tour.
See Additionally——–fit : Resolve a TSP from simply website places.
Examples——–>>> tsp = TravelingSalesmanOptimizer()>>> df_sites_tour = tsp.fit_prescribe(df_sites) # answer appended”””self.match(df_sites) # discover optimum tour for the websites
if not self.is_fitted: # unlikely to occur, however stillraise Exception(“An answer couldn’t be discovered. “”Overview knowledge or examine attribute `_results` for particulars.”)# be part of enter dataframe with column of solutiondf_sites_ranked = df_sites.copy().be part of(self.get_tour_stops_dataframe()) if type:df_sites_ranked.sort_values(by=”visit_order”, inplace=True)return df_sites_ranked
Now we are able to clear up any TSP in only one line:
tsp = TravelingSalesmanOptimizer(“Paris”)
tsp.fit_prescribe(df_sites)
If we’d prefer to preserve the unique order of places as they have been in df_sites, we are able to do it by specifying type=False:
tsp.fit_prescribe(df_sites, type=False)
And if we’re curious we are able to additionally verify the variety of variables and constraints the inner mannequin wanted to resolve our specific occasion of the TSP. This might be helpful when doing debugging or efficiency evaluation.
tsp.print_model_info()#[Out]:# Identify: Paris# – Num variables: 80# – Num constraints: 74# – Num aims: 1