nextroute 1.11.1.dev0__cp39-cp39-win_amd64.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.
nextroute/__about__.py ADDED
@@ -0,0 +1,3 @@
1
+ # © 2019-present nextmv.io inc
2
+
3
+ __version__ = "v1.11.1.dev0"
nextroute/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ # © 2019-present nextmv.io inc
2
+
3
+ """
4
+ The Nextroute Python interface.
5
+
6
+ Nextroute is a flexible engine for solving Vehicle Routing Problems (VRPs). The
7
+ core of Nextroute is written in Go and this package provides a Python interface
8
+ to it.
9
+ """
10
+
11
+ from .__about__ import __version__
12
+ from .options import Options as Options
13
+ from .options import Verbosity as Verbosity
14
+ from .solve import solve as solve
15
+ from .version import nextroute_version as nextroute_version
16
+
17
+ VERSION = __version__
18
+ """The version of the Nextroute Python package."""
@@ -0,0 +1,24 @@
1
+ # © 2019-present nextmv.io inc
2
+
3
+ """
4
+ JSON class for data wrangling JSON objects.
5
+ """
6
+
7
+ from typing import Any, Dict
8
+
9
+ from pydantic import BaseModel
10
+
11
+
12
+ class BaseModel(BaseModel):
13
+ """Base class for data wrangling tasks with JSON."""
14
+
15
+ @classmethod
16
+ def from_dict(cls, data: Dict[str, Any]):
17
+ """Instantiates the class from a dict."""
18
+
19
+ return cls(**data)
20
+
21
+ def to_dict(self) -> Dict[str, Any]:
22
+ """Converts the class to a dict."""
23
+
24
+ return self.model_dump(mode="json", exclude_none=True, by_alias=True)
Binary file
@@ -0,0 +1,28 @@
1
+ # © 2019-present nextmv.io inc
2
+
3
+ """
4
+ Check provides a plugin that allows you to check models and solutions.
5
+
6
+ Checking a model or a solution checks the unplanned plan units. It checks each
7
+ individual plan unit if it can be added to the solution. If the plan unit can
8
+ be added to the solution, the report will include on how many vehicles and
9
+ what the impact would be on the objective value. If the plan unit cannot be
10
+ added to the solution, the report will include the reason why it cannot be
11
+ added to the solution.
12
+
13
+ The check can be invoked on a nextroute.Model or a nextroute.Solution. If the
14
+ check is invoked on a model, an empty solution is created and the check is
15
+ executed on this empty solution. An empty solution is a solution with all the
16
+ initial stops that are fixed, initial stops that are not fixed are not added
17
+ to the solution. The check is executed on the unplanned plan units of the
18
+ solution. If the check is invoked on a solution, it is executed on the
19
+ unplanned plan units of the solution.
20
+ """
21
+
22
+ from .schema import Objective as Objective
23
+ from .schema import ObjectiveTerm as ObjectiveTerm
24
+ from .schema import Output as Output
25
+ from .schema import PlanUnit as PlanUnit
26
+ from .schema import Solution as Solution
27
+ from .schema import Summary as Summary
28
+ from .schema import Vehicle as Vehicle
@@ -0,0 +1,145 @@
1
+ # © 2019-present nextmv.io inc
2
+
3
+ """
4
+ This module contains definitions for the schema in the Nextroute check.
5
+ """
6
+
7
+ from typing import Dict, List, Optional
8
+
9
+ from nextroute.base_model import BaseModel
10
+
11
+
12
+ class ObjectiveTerm(BaseModel):
13
+ """Check of the individual terms of the objective for a move."""
14
+
15
+ base: Optional[float] = None
16
+ """Base of the objective term."""
17
+ factor: Optional[float] = None
18
+ """Factor of the objective term."""
19
+ name: Optional[str] = None
20
+ """Name of the objective term."""
21
+ value: Optional[float] = None
22
+ """Value of the objective term, which is equivalent to `self.base *
23
+ self.factor`."""
24
+
25
+
26
+ class Objective(BaseModel):
27
+ """Estimate of an objective of a move."""
28
+
29
+ terms: Optional[List[ObjectiveTerm]] = None
30
+ """Check of the individual terms of the objective."""
31
+ value: Optional[float] = None
32
+ """Value of the objective."""
33
+ vehicle: Optional[str] = None
34
+ """ID of the vehicle for which it reports the objective."""
35
+
36
+
37
+ class Solution(BaseModel):
38
+ """Solution that the check has been executed on."""
39
+
40
+ objective: Optional[Objective] = None
41
+ """Objective of the start solution."""
42
+ plan_units_planned: Optional[int] = None
43
+ """Number of plan units planned in the start solution."""
44
+ plan_units_unplanned: Optional[int] = None
45
+ """Number of plan units unplanned in the start solution."""
46
+ stops_planned: Optional[int] = None
47
+ """Number of stops planned in the start solution."""
48
+ vehicles_not_used: Optional[int] = None
49
+ """Number of vehicles not used in the start solution."""
50
+ vehicles_used: Optional[int] = None
51
+ """Number of vehicles used in the start solution."""
52
+
53
+
54
+ class Summary(BaseModel):
55
+ """Summary of the check."""
56
+
57
+ moves_failed: Optional[int] = None
58
+ """number of moves that failed. A move can fail if the estimate of a
59
+ constraint is incorrect. A constraint is incorrect if `ModelConstraint.
60
+ EstimateIsViolated` returns true and one of the violation checks returns
61
+ false. Violation checks are implementations of one or more of the
62
+ interfaces [SolutionStopViolationCheck], [SolutionVehicleViolationCheck] or
63
+ [SolutionViolationCheck] on the same constraint. Most constraints do not
64
+ need and do not have violation checks as the estimate is perfect. The
65
+ number of moves failed can be more than one per plan unit as we continue to
66
+ try moves on different vehicles until we find a move that is executable or
67
+ all vehicles have been visited."""
68
+ plan_units_best_move_failed: Optional[int] = None
69
+ """Number of plan units for which the best move can not be planned. This
70
+ should not happen if all the constraints are implemented correct."""
71
+ plan_units_best_move_found: Optional[int] = None
72
+ """Number of plan units for which at least one move has been found and the
73
+ move is executable."""
74
+ plan_units_best_move_increases_objective: Optional[int] = None
75
+ """Number of plan units for which the best move is executable but would
76
+ increase the objective value instead of decreasing it."""
77
+ plan_units_checked: Optional[int] = None
78
+ """Number of plan units that have been checked. If this is less than
79
+ `self.plan_units_to_be_checked` the check timed out."""
80
+ plan_units_have_no_move: Optional[int] = None
81
+ """Number of plan units for which no feasible move has been found. This
82
+ implies there is no move that can be executed without violating a
83
+ constraint."""
84
+ plan_units_to_be_checked: Optional[int] = None
85
+ """Number of plan units to be checked."""
86
+
87
+
88
+ class PlanUnit(BaseModel):
89
+ """Check of a plan unit."""
90
+
91
+ best_move_failed: Optional[bool] = None
92
+ """True if the plan unit's best move failed to execute."""
93
+ best_move_increases_objective: Optional[bool] = None
94
+ """True if the best move for the plan unit increases the objective."""
95
+ best_move_objective: Optional[Objective] = None
96
+ """Estimate of the objective of the best move if the plan unit has a best
97
+ move."""
98
+ constraints: Optional[Dict[str, int]] = None
99
+ """Constraints that are violated for the plan unit."""
100
+ has_best_move: Optional[bool] = None
101
+ """True if a move is found for the plan unit. A plan unit has no move found
102
+ if the plan unit is over-constrained or the move found is too expensive."""
103
+ stops: Optional[List[str]] = None
104
+ """IDs of the sops in the plan unit."""
105
+ vehicles_have_moves: Optional[int] = None
106
+ """Number of vehicles that have moves for the plan unit. Only calculated if
107
+ the verbosity is very high."""
108
+ vehicles_with_moves: Optional[List[str]] = None
109
+ """IDs of the vehicles that have moves for the plan unit. Only calculated
110
+ if the verbosity is very high."""
111
+
112
+
113
+ class Vehicle(BaseModel):
114
+ """Check of a vehicle."""
115
+
116
+ id: str
117
+ """ID of the vehicle."""
118
+
119
+ plan_units_have_moves: Optional[int] = None
120
+ """Number of plan units that have moves for the vehicle. Only calculated if
121
+ the depth is medium."""
122
+
123
+
124
+ class Output(BaseModel):
125
+ """Output of a feasibility check."""
126
+
127
+ duration_maximum: Optional[float] = None
128
+ """Maximum duration of the check, in seconds."""
129
+ duration_used: Optional[float] = None
130
+ """Duration used by the check, in seconds."""
131
+ error: Optional[str] = None
132
+ """Error raised during the check."""
133
+ plan_units: Optional[List[PlanUnit]] = None
134
+ """Check of the individual plan units."""
135
+ remark: Optional[str] = None
136
+ """Remark of the check. It can be "ok", "timeout" or anything else that
137
+ should explain itself."""
138
+ solution: Optional[Solution] = None
139
+ """Start soltuion of the check."""
140
+ summary: Optional[Summary] = None
141
+ """Summary of the check."""
142
+ vehicles: Optional[List[Vehicle]] = None
143
+ """Check of the vehicles."""
144
+ verbosity: Optional[str] = None
145
+ """Verbosity level of the check."""
nextroute/options.py ADDED
@@ -0,0 +1,225 @@
1
+ # © 2019-present nextmv.io inc
2
+
3
+ """
4
+ Options for working with the Nextroute engine.
5
+ """
6
+
7
+ import json
8
+ from enum import Enum
9
+ from typing import Any, Dict, List
10
+
11
+ from pydantic import Field
12
+
13
+ from nextroute.base_model import BaseModel
14
+
15
+ # Arguments that require a duration suffix.
16
+ _DURATIONS_ARGS = [
17
+ "-check.duration",
18
+ "-solve.duration",
19
+ "-solve.plateau.duration",
20
+ ]
21
+
22
+ # Arguments that require a string enum.
23
+ _STR_ENUM_ARGS = [
24
+ "CHECK_VERBOSITY",
25
+ ]
26
+
27
+
28
+ class Verbosity(str, Enum):
29
+ """Format of an `Input`."""
30
+
31
+ OFF = "off"
32
+ """The check engine is not run."""
33
+ LOW = "low"
34
+ """Low verbosity for the check engine."""
35
+ MEDIUM = "medium"
36
+ """Medium verbosity for the check engine."""
37
+ HIGH = "high"
38
+ """High verbosity for the check engine."""
39
+
40
+
41
+ class Options(BaseModel):
42
+ """Options for using Nextroute."""
43
+
44
+ CHECK_DURATION: float = 30
45
+ """Maximum duration of the check, in seconds."""
46
+ CHECK_VERBOSITY: Verbosity = Verbosity.OFF
47
+ """Verbosity of the check engine."""
48
+ FORMAT_DISABLE_PROGRESSION: bool = False
49
+ """Whether to disable the progression series."""
50
+ MODEL_CONSTRAINTS_DISABLE_ATTRIBUTES: bool = False
51
+ """Ignore the compatibility attributes constraint."""
52
+ MODEL_CONSTRAINTS_DISABLE_CAPACITIES: List[str] = Field(default_factory=list)
53
+ """Ignore the capacity constraint for the given resource names."""
54
+ MODEL_CONSTRAINTS_DISABLE_CAPACITY: bool = False
55
+ """Ignore the capacity constraint for all resources."""
56
+ MODEL_CONSTRAINTS_DISABLE_DISTANCELIMIT: bool = False
57
+ """Ignore the distance limit constraint."""
58
+ MODEL_CONSTRAINTS_DISABLE_GROUPS: bool = False
59
+ """Ignore the groups constraint."""
60
+ MODEL_CONSTRAINTS_DISABLE_MAXIMUMDURATION: bool = False
61
+ """Ignore the maximum duration constraint."""
62
+ MODEL_CONSTRAINTS_DISABLE_MAXIMUMSTOPS: bool = False
63
+ """Ignore the maximum stops constraint."""
64
+ MODEL_CONSTRAINTS_DISABLE_MAXIMUMWAITSTOP: bool = False
65
+ """Ignore the maximum stop wait constraint."""
66
+ MODEL_CONSTRAINTS_DISABLE_MAXIMUMWAITVEHICLE: bool = False
67
+ """Ignore the maximum vehicle wait constraint."""
68
+ MODEL_CONSTRAINTS_DISABLE_MIXINGITEMS: bool = False
69
+ """Ignore the do not mix items constraint."""
70
+ MODEL_CONSTRAINTS_DISABLE_PRECEDENCE: bool = False
71
+ """Ignore the precedence (pickups & deliveries) constraint."""
72
+ MODEL_CONSTRAINTS_DISABLE_STARTTIMEWINDOWS: bool = False
73
+ """Ignore the start time windows constraint."""
74
+ MODEL_CONSTRAINTS_DISABLE_VEHICLEENDTIME: bool = False
75
+ """Ignore the vehicle end time constraint."""
76
+ MODEL_CONSTRAINTS_DISABLE_VEHICLESTARTTIME: bool = False
77
+ """Ignore the vehicle start time constraint."""
78
+ MODEL_CONSTRAINTS_ENABLE_CLUSTER: bool = False
79
+ """Enable the cluster constraint."""
80
+ MODEL_OBJECTIVES_CAPACITIES: str = ""
81
+ """
82
+ Capacity objective, provide triple for each resource
83
+ `name:default;factor:1.0;offset;0.0`.
84
+ """
85
+ MODEL_OBJECTIVES_CLUSTER: float = 0.0
86
+ """Factor to weigh the cluster objective."""
87
+ MODEL_OBJECTIVES_EARLYARRIVALPENALTY: float = 1.0
88
+ """Factor to weigh the early arrival objective."""
89
+ MODEL_OBJECTIVES_LATEARRIVALPENALTY: float = 1.0
90
+ """Factor to weigh the late arrival objective."""
91
+ MODEL_OBJECTIVES_MINSTOPS: float = 1.0
92
+ """Factor to weigh the min stops objective."""
93
+ MODEL_OBJECTIVES_STOPBALANCE: float = 0.0
94
+ """Factor to weigh the stop balance objective."""
95
+ MODEL_OBJECTIVES_TRAVELDURATION: float = 0.0
96
+ """Factor to weigh the travel duration objective."""
97
+ MODEL_OBJECTIVES_UNPLANNEDPENALTY: float = 1.0
98
+ """Factor to weigh the unplanned objective."""
99
+ MODEL_OBJECTIVES_VEHICLEACTIVATIONPENALTY: float = 1.0
100
+ """Factor to weigh the vehicle activation objective."""
101
+ MODEL_OBJECTIVES_VEHICLESDURATION: float = 1.0
102
+ """Factor to weigh the vehicles duration objective."""
103
+ MODEL_PROPERTIES_DISABLE_DURATIONGROUPS: bool = False
104
+ """Ignore the durations groups of stops."""
105
+ MODEL_PROPERTIES_DISABLE_DURATIONS: bool = False
106
+ """Ignore the durations of stops."""
107
+ MODEL_PROPERTIES_DISABLE_INITIALSOLUTION: bool = False
108
+ """Ignore the initial solution."""
109
+ MODEL_PROPERTIES_DISABLE_STOPDURATIONMULTIPLIERS: bool = False
110
+ """Ignore the stop duration multipliers defined on vehicles."""
111
+ MODEL_PROPERTIES_MAXIMUMTIMEHORIZON: int = 15552000
112
+ """Maximum time horizon for the model in seconds."""
113
+ MODEL_VALIDATE_DISABLE_RESOURCES: bool = False
114
+ """Disable the resources validation."""
115
+ MODEL_VALIDATE_DISABLE_STARTTIME: bool = False
116
+ """Disable the start time validation."""
117
+ MODEL_VALIDATE_ENABLE_MATRIX: bool = False
118
+ """Enable matrix validation."""
119
+ MODEL_VALIDATE_ENABLE_MATRIXASYMMETRYTOLERANCE: int = 20
120
+ """Percentage of acceptable matrix asymmetry, requires matrix validation enabled."""
121
+ SOLVE_DURATION: float = 5
122
+ """Maximum duration, in seconds, of the solver."""
123
+ SOLVE_ITERATIONS: int = -1
124
+ """
125
+ Maximum number of iterations, -1 assumes no limit; iterations are counted
126
+ after start solutions are generated.
127
+ """
128
+ SOLVE_PARALLELRUNS: int = -1
129
+ """
130
+ Maximum number of parallel runs, -1 results in using all available
131
+ resources.
132
+ """
133
+ SOLVE_PLATEAU_ABSOLUTETHRESHOLD: float = -1
134
+ """Absolute threshold for significant improvement."""
135
+ SOLVE_PLATEAU_DURATION: float = 0.0
136
+ """Maximum duration, in seconds, without (significant) improvement."""
137
+ SOLVE_PLATEAU_ITERATIONS: int = 0
138
+ """Maximum number of iterations without (significant) improvement."""
139
+ SOLVE_PLATEAU_RELATIVETHRESHOLD: float = 0.0
140
+ """Relative threshold for significant improvement."""
141
+ SOLVE_RUNDETERMINISTICALLY: bool = False
142
+ """Run the parallel solver deterministically."""
143
+ SOLVE_STARTSOLUTIONS: int = -1
144
+ """
145
+ Number of solutions to generate on top of those passed in; one solution
146
+ generated with sweep algorithm, the rest generated randomly.
147
+ """
148
+
149
+ def to_args(self) -> List[str]:
150
+ """
151
+ Convert the options to command-line arguments.
152
+
153
+ Returns
154
+ ----------
155
+ List[str]
156
+ The flattened options as a list of strings.
157
+ """
158
+
159
+ opt_dict = self.to_dict()
160
+
161
+ default_options = Options()
162
+ default_options_dict = default_options.to_dict()
163
+
164
+ args = []
165
+ for key, value in opt_dict.items():
166
+ # We only care about custom options, so we skip the default ones.
167
+ default_value = default_options_dict.get(key)
168
+ if value == default_value:
169
+ continue
170
+
171
+ key = f"-{key.replace('_', '.').lower()}"
172
+
173
+ str_value = json.dumps(value)
174
+ if key in _DURATIONS_ARGS:
175
+ str_value = str_value + "s" # Transforms into seconds.
176
+
177
+ if str_value.startswith('"') and str_value.endswith('"'):
178
+ str_value = str_value[1:-1]
179
+
180
+ # Nextroute’s Go implementation does not support boolean flags with
181
+ # values. If the value is a boolean, then we only append the key if
182
+ # the value is True.
183
+ should_append_value = True
184
+ if isinstance(value, bool):
185
+ if not value:
186
+ continue
187
+
188
+ should_append_value = False
189
+
190
+ args.append(key)
191
+ if should_append_value:
192
+ args.append(str_value)
193
+
194
+ return args
195
+
196
+ @classmethod
197
+ def extract_from_dict(cls, data: Dict[str, Any]) -> "Options":
198
+ """
199
+ Extracts options from a dictionary. This dictionary may contain more
200
+ keys that are not part of the Nextroute options.
201
+
202
+ Parameters
203
+ ----------
204
+ data : Dict[str, Any]
205
+ The dictionary to extract options from.
206
+
207
+ Returns
208
+ ----------
209
+ Options
210
+ The Nextroute options.
211
+ """
212
+
213
+ options = cls()
214
+ for key, value in data.items():
215
+ key = key.upper()
216
+ if not hasattr(options, key):
217
+ continue
218
+
219
+ # Enums need to be handled manually.
220
+ if key == "CHECK_VERBOSITY":
221
+ value = Verbosity(value)
222
+
223
+ setattr(options, key, value)
224
+
225
+ return options
@@ -0,0 +1,29 @@
1
+ # © 2019-present nextmv.io inc
2
+
3
+ """
4
+ Schema (class) definitions for the entities in Nextroute.
5
+ """
6
+
7
+ from .input import Defaults as Defaults
8
+ from .input import DurationGroup as DurationGroup
9
+ from .input import Input as Input
10
+ from .location import Location as Location
11
+ from .output import ObjectiveOutput as ObjectiveOutput
12
+ from .output import Output as Output
13
+ from .output import PlannedStopOutput as PlannedStopOutput
14
+ from .output import Solution as Solution
15
+ from .output import StopOutput as StopOutput
16
+ from .output import VehicleOutput as VehicleOutput
17
+ from .output import Version as Version
18
+ from .statistics import DataPoint as DataPoint
19
+ from .statistics import ResultStatistics as ResultStatistics
20
+ from .statistics import RunStatistics as RunStatistics
21
+ from .statistics import Series as Series
22
+ from .statistics import SeriesData as SeriesData
23
+ from .statistics import Statistics as Statistics
24
+ from .stop import AlternateStop as AlternateStop
25
+ from .stop import Stop as Stop
26
+ from .stop import StopDefaults as StopDefaults
27
+ from .vehicle import InitialStop as InitialStop
28
+ from .vehicle import Vehicle as Vehicle
29
+ from .vehicle import VehicleDefaults as VehicleDefaults
@@ -0,0 +1,87 @@
1
+ # © 2019-present nextmv.io inc
2
+
3
+ """
4
+ Defines the input class.
5
+ """
6
+
7
+ from datetime import datetime
8
+ from typing import Any, List, Optional, Union
9
+
10
+ from nextroute.base_model import BaseModel
11
+ from nextroute.schema.stop import AlternateStop, Stop, StopDefaults
12
+ from nextroute.schema.vehicle import Vehicle, VehicleDefaults
13
+
14
+
15
+ class Defaults(BaseModel):
16
+ """Default values for vehicles and stops."""
17
+
18
+ stops: Optional[StopDefaults] = None
19
+ """Default values for stops."""
20
+ vehicles: Optional[VehicleDefaults] = None
21
+ """Default values for vehicles."""
22
+
23
+
24
+ class DurationGroup(BaseModel):
25
+ """Represents a group of stops that get additional duration whenever a stop
26
+ of the group is approached for the first time."""
27
+
28
+ duration: int
29
+ """Duration to add when visiting the group."""
30
+ group: List[str]
31
+ """Stop IDs contained in the group."""
32
+
33
+
34
+ class MatrixTimeFrame(BaseModel):
35
+ """Represents a time-dependent duration matrix or scaling factor."""
36
+
37
+ start_time: datetime
38
+ """Start time of the time frame."""
39
+ end_time: datetime
40
+ """End time of the time frame."""
41
+ matrix: Optional[List[List[float]]] = None
42
+ """Duration matrix for the time frame."""
43
+ scaling_factor: Optional[float] = None
44
+ """Scaling factor for the time frame."""
45
+
46
+
47
+ class TimeDependentMatrix(BaseModel):
48
+ """Represents time-dependent duration matrices."""
49
+
50
+ vehicle_ids: Optional[List[str]] = None
51
+ """Vehicle IDs for which the duration matrix is defined."""
52
+ default_matrix: List[List[float]]
53
+ """Default duration matrix."""
54
+ matrix_time_frames: Optional[List[MatrixTimeFrame]] = None
55
+ """Time-dependent duration matrices."""
56
+
57
+
58
+ class Input(BaseModel):
59
+ """Input schema for Nextroute."""
60
+
61
+ stops: List[Stop]
62
+ """Stops that must be visited by the vehicles."""
63
+ vehicles: List[Vehicle]
64
+ """Vehicles that service the stops."""
65
+
66
+ alternate_stops: Optional[List[AlternateStop]] = None
67
+ """A set of alternate stops for the vehicles."""
68
+ custom_data: Optional[Any] = None
69
+ """Arbitrary data associated with the input."""
70
+ defaults: Optional[Defaults] = None
71
+ """Default values for vehicles and stops."""
72
+ distance_matrix: Optional[List[List[float]]] = None
73
+ """Matrix of travel distances in meters between stops."""
74
+ duration_groups: Optional[List[DurationGroup]] = None
75
+ """Duration in seconds added when approaching the group."""
76
+ duration_matrix: Optional[
77
+ Union[
78
+ List[List[float]],
79
+ TimeDependentMatrix,
80
+ List[TimeDependentMatrix],
81
+ ]
82
+ ] = None
83
+ """Matrix of travel durations in seconds between stops as a single matrix or duration matrices."""
84
+ options: Optional[Any] = None
85
+ """Arbitrary options."""
86
+ stop_groups: Optional[List[List[str]]] = None
87
+ """Groups of stops that must be part of the same route."""
@@ -0,0 +1,16 @@
1
+ # © 2019-present nextmv.io inc
2
+
3
+ """
4
+ Defines the location class.
5
+ """
6
+
7
+ from nextroute.base_model import BaseModel
8
+
9
+
10
+ class Location(BaseModel):
11
+ """Location represents a geographical location."""
12
+
13
+ lat: float
14
+ """Latitude of the location."""
15
+ lon: float
16
+ """Longitude of the location."""