mdpsolver 0.11.0__cp315-cp315-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.
- mdpsolver/__init__.py +12 -0
- mdpsolver/model.py +398 -0
- mdpsolver/solvermodule.cp315-win_amd64.pyd +0 -0
- mdpsolver-0.11.0.dist-info/DELVEWHEEL +2 -0
- mdpsolver-0.11.0.dist-info/METADATA +194 -0
- mdpsolver-0.11.0.dist-info/RECORD +10 -0
- mdpsolver-0.11.0.dist-info/WHEEL +5 -0
- mdpsolver-0.11.0.dist-info/licenses/LICENSE +21 -0
- mdpsolver.libs/libomp140.x86_64-4a20c1e5c115c29771a12324513eb109.dll +0 -0
- mdpsolver.libs/msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll +0 -0
mdpsolver/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""""" # start delvewheel patch
|
|
2
|
+
def _delvewheel_patch_1_13_0():
|
|
3
|
+
import os
|
|
4
|
+
if os.path.isdir(libs_dir := os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'mdpsolver.libs'))):
|
|
5
|
+
os.add_dll_directory(libs_dir)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_delvewheel_patch_1_13_0()
|
|
9
|
+
del _delvewheel_patch_1_13_0
|
|
10
|
+
# end delvewheel patch
|
|
11
|
+
|
|
12
|
+
from mdpsolver.model import model, ConvergenceWarning
|
mdpsolver/model.py
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MDPSolver
|
|
3
|
+
|
|
4
|
+
This file defines a `model` class that wraps around the `solvermodule` to create
|
|
5
|
+
and solve Markov Decision Processes (MDPs). It provides methods to configure the model,
|
|
6
|
+
solve it, and extract the resulting policy and value vectors.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numbers
|
|
10
|
+
import warnings
|
|
11
|
+
from mdpsolver import solvermodule
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ConvergenceWarning(UserWarning):
|
|
15
|
+
"""
|
|
16
|
+
Issued by solve() when a run has not converged.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def _openError(fileName, mode="r"):
|
|
20
|
+
"""The OSError raised when the file is opened in the given mode, or None if it can be opened."""
|
|
21
|
+
try:
|
|
22
|
+
with open(fileName, mode):
|
|
23
|
+
pass
|
|
24
|
+
except OSError as error:
|
|
25
|
+
return error
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class model:
|
|
30
|
+
"""
|
|
31
|
+
Creates an MDPSolver object.
|
|
32
|
+
|
|
33
|
+
This class provides methods to initialize, solve, and return results
|
|
34
|
+
from an MDP model.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
"""Initialize the model by creating a solver module object."""
|
|
39
|
+
self.initialize()
|
|
40
|
+
|
|
41
|
+
def initialize(self):
|
|
42
|
+
"""Create and initialize the solver module object."""
|
|
43
|
+
self.mdl = solvermodule.Model()
|
|
44
|
+
# A model object contains only one model
|
|
45
|
+
self._modelDefined = False
|
|
46
|
+
self._solved = False
|
|
47
|
+
|
|
48
|
+
def _requireSolution(self, method):
|
|
49
|
+
"""
|
|
50
|
+
Raise RuntimeError unless solve() has run on this model object.
|
|
51
|
+
"""
|
|
52
|
+
if not self._modelDefined:
|
|
53
|
+
raise RuntimeError("No model has been defined and solved for this model object, so %s() has no solution to use. Call mdp() and then solve() first." % method)
|
|
54
|
+
if not self._solved:
|
|
55
|
+
raise RuntimeError("The model has not been solved yet, so %s() has no solution to use. Call solve() first." % method)
|
|
56
|
+
|
|
57
|
+
def solve(
|
|
58
|
+
self,
|
|
59
|
+
algorithm: str = "mpi",
|
|
60
|
+
tolerance: float = 1e-3,
|
|
61
|
+
update: str = "standard",
|
|
62
|
+
criterion: str = "discounted",
|
|
63
|
+
parIterLim: int = 100,
|
|
64
|
+
SORrelaxation: float = 1.0,
|
|
65
|
+
initPolicy: list = list(),
|
|
66
|
+
initValueVector: list = list(),
|
|
67
|
+
verbose: bool = False,
|
|
68
|
+
postProcessing: bool = True,
|
|
69
|
+
makeFinalCheck: bool = True,
|
|
70
|
+
parallel: bool = True,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Derive an epsilon-optimal policy for the selected MDP model.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
algorithm (str): Algorithm to use.
|
|
77
|
+
tolerance (float): Convergence threshold for the algorithm (0 < tolerance < 1).
|
|
78
|
+
update (str): The value-update method ('standard', 'gs' or 'sor'; only 'standard' with the average reward criterion).
|
|
79
|
+
criterion (str): The optimality criterion.
|
|
80
|
+
parIterLim (int): The partial evaluation limit employed in the modified policy iteration algorithm.
|
|
81
|
+
SORrelaxation (float): Relaxation parameter for the Successive Over-Relaxation method.
|
|
82
|
+
initPolicy (list): A 1D-list defining the initial policy, one valid action index per state. This option can be used for "warm starting" the optimization procedure.
|
|
83
|
+
initValueVector (list): A 1D-list defining the initial value vector, one value per state. This option can be used for "warm starting" the optimization procedure.
|
|
84
|
+
verbose (bool): If True, prints solver progress to console.
|
|
85
|
+
postProcessing (bool): If True, performs post-processing after solving.
|
|
86
|
+
makeFinalCheck (bool): If True, makes a final check of the value vector. This process checks if the resulting values are reasonable.
|
|
87
|
+
parallel (bool): If True, enables parallel computation for faster solving. This option is only available for the custom MDP model with standard updates.
|
|
88
|
+
|
|
89
|
+
Call mdp() to define the model first. Solving the same model object
|
|
90
|
+
again starts from the policy and the value vector of the previous run,
|
|
91
|
+
unless initPolicy or initValueVector replace them. Create a new model
|
|
92
|
+
object to start over.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
None
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
if not self._modelDefined:
|
|
99
|
+
raise RuntimeError("No model has been defined for this model object. Call mdp() before solve().")
|
|
100
|
+
if not (algorithm == "mpi" or algorithm == "pi" or algorithm == "vi"):
|
|
101
|
+
raise ValueError("Algorithm type not recognized. Select either: 'mpi', 'pi', or 'vi'.")
|
|
102
|
+
if tolerance <= 0.0 or tolerance >= 1.0:
|
|
103
|
+
raise ValueError("The tolerance needs to be between 0.0 and 1.0 (i.e. 0 < tolerance < 1).")
|
|
104
|
+
if not (update == "standard" or update == "gs" or update == "sor"):
|
|
105
|
+
raise ValueError("Update method not recognized. Select either: 'standard', 'gs', or 'sor'.")
|
|
106
|
+
if not (criterion == "discounted" or criterion == "average"):
|
|
107
|
+
raise ValueError("Optimality criterion not recognized. Select either: 'discounted' or 'average'.")
|
|
108
|
+
if criterion == "average" and (update == "gs" or update == "sor"):
|
|
109
|
+
raise ValueError("Gauss-Seidel and SOR updates are not available with the average reward criterion. Select update='standard'.")
|
|
110
|
+
if parIterLim <= 0 and algorithm == "mpi":
|
|
111
|
+
raise ValueError("The partial evaluation limit needs to be a positive number.")
|
|
112
|
+
if (SORrelaxation <= 0.0 or SORrelaxation >= 2.0) and update == "sor":
|
|
113
|
+
raise ValueError("The relaxation parameter needs to be between 0.0 and 2.0 (i.e. 0 < SORrelaxation < 2).")
|
|
114
|
+
if len(initPolicy) > 0 or len(initValueVector) > 0:
|
|
115
|
+
numberOfStates = self.mdl.getNumberOfStates()
|
|
116
|
+
if numberOfStates >= 0 and len(initPolicy) > 0 and len(initPolicy) != numberOfStates:
|
|
117
|
+
raise ValueError("The initial policy needs one action index per state (the model has %d states, initPolicy has %d elements)." % (numberOfStates, len(initPolicy)))
|
|
118
|
+
if numberOfStates >= 0 and len(initValueVector) > 0 and len(initValueVector) != numberOfStates:
|
|
119
|
+
raise ValueError("The initial value vector needs one value per state (the model has %d states, initValueVector has %d elements)." % (numberOfStates, len(initValueVector)))
|
|
120
|
+
for stateIndex, value in enumerate(initValueVector):
|
|
121
|
+
if not isinstance(value, numbers.Real):
|
|
122
|
+
raise TypeError("The initial value vector needs a number for every state (state %d has %r)." % (stateIndex, value))
|
|
123
|
+
if numberOfStates >= 0 and len(initPolicy) > 0:
|
|
124
|
+
for stateIndex, action in enumerate(initPolicy):
|
|
125
|
+
if not isinstance(action, numbers.Integral):
|
|
126
|
+
raise TypeError("The initial policy needs an integer action index for every state (state %d has %r)." % (stateIndex, action))
|
|
127
|
+
numberOfActions = self.mdl.getNumberOfActions(stateIndex=stateIndex)
|
|
128
|
+
if not 0 <= action < numberOfActions:
|
|
129
|
+
raise ValueError("The initial policy assigns action %s to state %d, which has %d actions (indices 0 to %d)." % (action, stateIndex, numberOfActions, numberOfActions - 1))
|
|
130
|
+
|
|
131
|
+
# The solver keeps the policy and the value vector of the previous run,
|
|
132
|
+
# unless initPolicy or initValueVector replace them.
|
|
133
|
+
|
|
134
|
+
if self._solved:
|
|
135
|
+
# Value iteration never uses the policy it starts from
|
|
136
|
+
reusesPolicy = len(initPolicy) == 0 and algorithm != "vi"
|
|
137
|
+
reusesValueVector = len(initValueVector) == 0
|
|
138
|
+
if reusesPolicy or reusesValueVector:
|
|
139
|
+
print("Note: This model has been solved before. Create a new model object to run solver with initial values.", flush=True)
|
|
140
|
+
|
|
141
|
+
self.mdl.solve(
|
|
142
|
+
algorithm=algorithm,
|
|
143
|
+
tolerance=tolerance,
|
|
144
|
+
update=update,
|
|
145
|
+
criterion=criterion,
|
|
146
|
+
parIterLim=parIterLim,
|
|
147
|
+
SORrelaxation=SORrelaxation,
|
|
148
|
+
initPolicy=initPolicy,
|
|
149
|
+
initValueVector=initValueVector,
|
|
150
|
+
verbose=verbose,
|
|
151
|
+
postProcessing=postProcessing,
|
|
152
|
+
makeFinalCheck=makeFinalCheck,
|
|
153
|
+
parallel=parallel,
|
|
154
|
+
)
|
|
155
|
+
# Set after the run, so that a run that did not finish leaves the
|
|
156
|
+
# object without a solution
|
|
157
|
+
self._solved = True
|
|
158
|
+
|
|
159
|
+
convergence = self.mdl.getConvergence()
|
|
160
|
+
hint = " Under the average reward criterion, this happens with periodic models." if criterion == "average" else ""
|
|
161
|
+
if convergence["iterationLimitReached"]:
|
|
162
|
+
warnings.warn("solve() stopped at the iteration limit of %d iterations before reaching the tolerance, so the results have not converged.%s" % (convergence["iterationLimit"], hint), ConvergenceWarning, stacklevel=2)
|
|
163
|
+
elif convergence["evaluationLimitReached"]:
|
|
164
|
+
warnings.warn("A policy evaluation of policy iteration stopped at its limit of %d iterations before reaching the tolerance, so the results have not converged.%s" % (convergence["iterationLimit"], hint), ConvergenceWarning, stacklevel=2)
|
|
165
|
+
if convergence["valuesOutOfRange"] and not postProcessing and update == "standard":
|
|
166
|
+
# The values are left uncorrected, and may lie outside the range
|
|
167
|
+
# even when the policy is right
|
|
168
|
+
warnings.warn("The final check found the value %r in state %d, outside the range that the rewards and the discount allow. With postProcessing=False, the values are not corrected and may lie outside it; use postProcessing=True for correct values, or makeFinalCheck=False to skip the check." % (convergence["outOfRangeValue"], convergence["outOfRangeState"]), ConvergenceWarning, stacklevel=2)
|
|
169
|
+
elif convergence["valuesOutOfRange"]:
|
|
170
|
+
warnings.warn("The final check found the value %r in state %d, outside the range that the rewards and the discount allow, so the results are wrong. checkModel() can look for errors in the model." % (convergence["outOfRangeValue"], convergence["outOfRangeState"]), ConvergenceWarning, stacklevel=2)
|
|
171
|
+
elif convergence["diverged"]:
|
|
172
|
+
warnings.warn("solve() stopped because the values became infinite or NaN, so the results are wrong. checkModel() can look for errors in the model.", ConvergenceWarning, stacklevel=2)
|
|
173
|
+
|
|
174
|
+
def hasConverged(self) -> bool:
|
|
175
|
+
"""
|
|
176
|
+
Whether the last run of solve() converged.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
bool: True if the last run converged.
|
|
180
|
+
"""
|
|
181
|
+
self._requireSolution("hasConverged")
|
|
182
|
+
return bool(self.mdl.getConvergence()["converged"])
|
|
183
|
+
|
|
184
|
+
def checkModel(self, tolerance: float = 1e-9) -> list:
|
|
185
|
+
"""
|
|
186
|
+
Check the model defined by mdp() for errors, and describe each kind of
|
|
187
|
+
problem found. The check reads the whole model, so it is never run by
|
|
188
|
+
mdp() or solve(); call it when the results look wrong or the model may
|
|
189
|
+
be malformed.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
tolerance (float): How far the probabilities of a state-action pair may sum from 1.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
list: One message per kind of problem, with how many were found and the first examples. Empty if none was found.
|
|
196
|
+
"""
|
|
197
|
+
if not self._modelDefined:
|
|
198
|
+
raise RuntimeError("No model has been defined for this model object. Call mdp() before checkModel().")
|
|
199
|
+
if not isinstance(tolerance, numbers.Real) or not tolerance >= 0.0:
|
|
200
|
+
raise ValueError("The tolerance of checkModel() needs to be zero or a positive number.")
|
|
201
|
+
return list(self.mdl.checkModel(tolerance=tolerance))
|
|
202
|
+
|
|
203
|
+
def getRuntime(self) -> float:
|
|
204
|
+
"""
|
|
205
|
+
Get the runtime of the last solver execution.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
float: Runtime in milliseconds.
|
|
209
|
+
"""
|
|
210
|
+
self._requireSolution("getRuntime")
|
|
211
|
+
return self.mdl.getRuntime()
|
|
212
|
+
|
|
213
|
+
def getIterations(self) -> int:
|
|
214
|
+
"""
|
|
215
|
+
Get the number of iterations of the last solver execution.
|
|
216
|
+
An iteration is one policy improvement step, i.e. one update that
|
|
217
|
+
chooses the best action in every state.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
int: Number of iterations.
|
|
221
|
+
"""
|
|
222
|
+
self._requireSolution("getIterations")
|
|
223
|
+
return self.mdl.getIterations()
|
|
224
|
+
|
|
225
|
+
def printPolicy(self) -> None:
|
|
226
|
+
"""Print the entire policy to the terminal."""
|
|
227
|
+
self._requireSolution("printPolicy")
|
|
228
|
+
self.mdl.printPolicy()
|
|
229
|
+
|
|
230
|
+
def printValueVector(self) -> None:
|
|
231
|
+
"""Print the entire value vector to the terminal."""
|
|
232
|
+
self._requireSolution("printValueVector")
|
|
233
|
+
self.mdl.printValueVector()
|
|
234
|
+
|
|
235
|
+
def getAction(self, stateIndex: int = 0) -> int:
|
|
236
|
+
"""
|
|
237
|
+
Get the action from the optimized policy for a specific state.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
stateIndex (int): Index of the state.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
int: Action for the given state.
|
|
244
|
+
|
|
245
|
+
Raises IndexError for a state outside the model.
|
|
246
|
+
"""
|
|
247
|
+
self._requireSolution("getAction")
|
|
248
|
+
return self.mdl.getAction(stateIndex=stateIndex)
|
|
249
|
+
|
|
250
|
+
def getValue(self, stateIndex: int = 0) -> float:
|
|
251
|
+
"""
|
|
252
|
+
Get the value from the optimized value vector for a specific state.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
stateIndex (int): Index of the state.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
float: Value for the given state.
|
|
259
|
+
|
|
260
|
+
Raises IndexError for a state outside the model.
|
|
261
|
+
"""
|
|
262
|
+
self._requireSolution("getValue")
|
|
263
|
+
return self.mdl.getValue(stateIndex=stateIndex)
|
|
264
|
+
|
|
265
|
+
def getPolicy(self) -> list:
|
|
266
|
+
"""
|
|
267
|
+
Get the entire optimized policy.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
list: Optimized policy.
|
|
271
|
+
"""
|
|
272
|
+
self._requireSolution("getPolicy")
|
|
273
|
+
return self.mdl.getPolicy()
|
|
274
|
+
|
|
275
|
+
def getValueVector(self):
|
|
276
|
+
"""
|
|
277
|
+
Get the entire optimized value vector.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
list: Optimized value vector.
|
|
281
|
+
"""
|
|
282
|
+
self._requireSolution("getValueVector")
|
|
283
|
+
return self.mdl.getValueVector()
|
|
284
|
+
|
|
285
|
+
def saveToFile(self, fileName: str = "result.csv", type: str = "policy") -> None:
|
|
286
|
+
"""
|
|
287
|
+
Save the optimized policy or value vector to a file.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
fileName (str): Name of the output file.
|
|
291
|
+
type (str): Type of result to save ('policy' or 'value').
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
None
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
self._requireSolution("saveToFile")
|
|
298
|
+
if not (type == "policy" or type == "value"):
|
|
299
|
+
raise ValueError("Type not recognized. Select either: 'policy' or 'value'.")
|
|
300
|
+
error = _openError(fileName, "w")
|
|
301
|
+
if error is not None:
|
|
302
|
+
# (the argument type hides the built-in type())
|
|
303
|
+
raise error.__class__("The file %s could not be opened for writing (%s)." % (fileName, error.strerror or error))
|
|
304
|
+
|
|
305
|
+
return self.mdl.saveToFile(fileName=fileName, type=type)
|
|
306
|
+
|
|
307
|
+
def mdp(
|
|
308
|
+
self,
|
|
309
|
+
discount: float = 0.99,
|
|
310
|
+
rewards: list = list(),
|
|
311
|
+
rewardsElementwise: list = list(),
|
|
312
|
+
rewardsFromFile: str = "rewards.csv",
|
|
313
|
+
tranMatWithZeros: list = list(),
|
|
314
|
+
tranMatElementwise: list = list(),
|
|
315
|
+
tranMatProbs: list = list(),
|
|
316
|
+
tranMatColumns: list = list(),
|
|
317
|
+
tranMatFromFile: str = "transitions.csv",
|
|
318
|
+
) -> None:
|
|
319
|
+
"""
|
|
320
|
+
Define the generic MDP model.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
discount (float): Discount factor.
|
|
324
|
+
rewards (list): A 2D-list containing the reward (float) of a particular action in a particular state.
|
|
325
|
+
rewardsElementwise (list): Alternative reward format. A 2D-list where each row corresponds to a combination of a state and an action.
|
|
326
|
+
rewardsFromFile (str): Load the rewards from a comma-separated (,) file.
|
|
327
|
+
tranMatWithZeros (list): A 3D-list containing the transition probabilities.
|
|
328
|
+
tranMatElementwise (list): Sparse transition probabilities (option 1). A 2D-list where each row corresponds to a combination of a current state, an action, and a next state.
|
|
329
|
+
tranMatProbs (list): Sparse transition probabilities (option 2, part 1). A 3D-list containing the non-zero transition probabilities.
|
|
330
|
+
tranMatColumns (list): Sparse transition probabilities (option 2, part 2). A 3D-list containing the columns of the non-zero transition probabilities.
|
|
331
|
+
tranMatFromFile (str): Load the transition probabilities from a comma-separated (,) file.
|
|
332
|
+
|
|
333
|
+
Give the rewards in one format and the transition probabilities in one
|
|
334
|
+
format. When no list is given for either, it is read from the file
|
|
335
|
+
named by rewardsFromFile or tranMatFromFile, by default rewards.csv or
|
|
336
|
+
transitions.csv in the working directory.
|
|
337
|
+
|
|
338
|
+
A model object holds one model, so mdp() can be called only once per
|
|
339
|
+
model object. Create a new model object to define another model.
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
None
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
if self._modelDefined:
|
|
346
|
+
raise RuntimeError("A model has already been defined for this model object, and mdp() can only be called once per model object. Create a new model object (e.g. mdl = mdpsolver.model()) to define another model.")
|
|
347
|
+
if discount <= 0.0 or discount >= 1.0:
|
|
348
|
+
raise ValueError("The discount needs to be in the interval between 0.0 and 1.0 (i.e. 0 < discount < 1).")
|
|
349
|
+
|
|
350
|
+
# The formats given. A file name other than the default counts as
|
|
351
|
+
# given; the default file is read only when no list is given.
|
|
352
|
+
if (len(tranMatProbs) > 0) != (len(tranMatColumns) > 0):
|
|
353
|
+
given, missing = ("tranMatProbs", "tranMatColumns") if len(tranMatProbs) > 0 else ("tranMatColumns", "tranMatProbs")
|
|
354
|
+
raise ValueError("%s was given without %s. This format needs both: the non-zero transition probabilities in tranMatProbs and their next states in tranMatColumns." % (given, missing))
|
|
355
|
+
rewardFormats = [name for name, isGiven in (
|
|
356
|
+
("rewards", len(rewards) > 0),
|
|
357
|
+
("rewardsElementwise", len(rewardsElementwise) > 0),
|
|
358
|
+
("rewardsFromFile", rewardsFromFile != "rewards.csv")) if isGiven]
|
|
359
|
+
tranMatFormats = [name for name, isGiven in (
|
|
360
|
+
("tranMatWithZeros", len(tranMatWithZeros) > 0),
|
|
361
|
+
("tranMatElementwise", len(tranMatElementwise) > 0),
|
|
362
|
+
("tranMatProbs and tranMatColumns", len(tranMatProbs) > 0),
|
|
363
|
+
("tranMatFromFile", tranMatFromFile != "transitions.csv")) if isGiven]
|
|
364
|
+
if len(rewardFormats) > 1:
|
|
365
|
+
raise ValueError("The rewards were given in more than one format (%s). Give them in one format only." % ", ".join(rewardFormats))
|
|
366
|
+
if len(tranMatFormats) > 1:
|
|
367
|
+
raise ValueError("The transition probabilities were given in more than one format (%s). Give them in one format only." % ", ".join(tranMatFormats))
|
|
368
|
+
|
|
369
|
+
# A file that cannot be opened would leave the model without rewards
|
|
370
|
+
# or transition probabilities
|
|
371
|
+
# (raised as the OSError that opening the file gave, e.g. FileNotFoundError)
|
|
372
|
+
if rewardFormats in ([], ["rewardsFromFile"]):
|
|
373
|
+
error = _openError(rewardsFromFile)
|
|
374
|
+
if error is not None and rewardFormats:
|
|
375
|
+
raise type(error)("The rewards file %s could not be opened (%s)." % (rewardsFromFile, error.strerror or error))
|
|
376
|
+
if error is not None:
|
|
377
|
+
raise type(error)("No rewards were given, and the file %s, which is read when none are given, could not be opened (%s). Give the rewards with rewards, rewardsElementwise or rewardsFromFile." % (rewardsFromFile, error.strerror or error))
|
|
378
|
+
if tranMatFormats in ([], ["tranMatFromFile"]):
|
|
379
|
+
error = _openError(tranMatFromFile)
|
|
380
|
+
if error is not None and tranMatFormats:
|
|
381
|
+
raise type(error)("The transition probabilities file %s could not be opened (%s)." % (tranMatFromFile, error.strerror or error))
|
|
382
|
+
if error is not None:
|
|
383
|
+
raise type(error)("No transition probabilities were given, and the file %s, which is read when none are given, could not be opened (%s). Give the transition probabilities with tranMatWithZeros, tranMatElementwise, tranMatProbs and tranMatColumns, or tranMatFromFile." % (tranMatFromFile, error.strerror or error))
|
|
384
|
+
|
|
385
|
+
# Set before loading, so that a load that fails halfway cannot be
|
|
386
|
+
# followed by another into the same object
|
|
387
|
+
self._modelDefined = True
|
|
388
|
+
self.mdl.mdp(
|
|
389
|
+
discount=discount,
|
|
390
|
+
rewards=rewards,
|
|
391
|
+
rewardsElementwise=rewardsElementwise,
|
|
392
|
+
rewardsFromFile=rewardsFromFile,
|
|
393
|
+
tranMatWithZeros=tranMatWithZeros,
|
|
394
|
+
tranMatElementwise=tranMatElementwise,
|
|
395
|
+
tranMatProbs=tranMatProbs,
|
|
396
|
+
tranMatColumns=tranMatColumns,
|
|
397
|
+
tranMatFromFile=tranMatFromFile,
|
|
398
|
+
)
|
|
Binary file
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Version: 1.13.0
|
|
2
|
+
Arguments: ['C:\\Users\\runneradmin\\AppData\\Local\\Temp\\cibw-run-95fte76u\\cp315-win_amd64\\build\\venv\\Scripts\\delvewheel', 'repair', '-w', 'C:\\Users\\runneradmin\\AppData\\Local\\Temp\\cibw-run-95fte76u\\cp315-win_amd64\\repaired_wheel', '-v', 'C:\\Users\\runneradmin\\AppData\\Local\\Temp\\cibw-run-95fte76u\\cp315-win_amd64\\built_wheel\\mdpsolver-0.11.0-cp315-cp315-win_amd64.whl']
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: mdpsolver
|
|
3
|
+
Version: 0.11.0
|
|
4
|
+
Summary: A fast Python solver for Markov Decision Processes
|
|
5
|
+
Author-Email: Anders Reenberg Andersen <andersra@live.dk>, Jesper Fink Andersen <jesperfinkandersen@gmail.com>
|
|
6
|
+
Classifier: Development Status :: 4 - Beta
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
10
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
11
|
+
Classifier: Operating System :: MacOS
|
|
12
|
+
Project-URL: Homepage, https://github.com/areenberg/MDPSolver
|
|
13
|
+
Project-URL: Issues, https://github.com/areenberg/MDPSolver/issues
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: numpy; extra == "test"
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# MDPSolver
|
|
20
|
+
|
|
21
|
+
<img src="https://raw.githubusercontent.com/areenberg/MDPSolver/master/Images/MDPSolver_logo_3.png" alt="Logo" width="180">
|
|
22
|
+
|
|
23
|
+
[](https://pypi.org/project/mdpsolver/)
|
|
24
|
+
[](https://github.com/areenberg/MDPSolver/blob/master/LICENSE)
|
|
25
|
+
[](https://doi.org/10.21105/joss.07544)
|
|
26
|
+
|
|
27
|
+
MDPSolver is a Python package for large Markov Decision Processes (MDPs) with infinite-horizons.
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
* Fast solver: Our C++-based solver is substantially faster than other CPU-based MDP packages available for Python. See details in the documentation.
|
|
32
|
+
* Two optimality criteria: *Discounted* and *Average* reward.
|
|
33
|
+
* Three optimization algorithms: *Value iteration*, *Policy iteration*, and *Modified policy iteration*.
|
|
34
|
+
* Three value-update methods: *Standard*, *Gauss–Seidel*, and *Successive over-relaxation*.
|
|
35
|
+
* Supports sparse matrices.
|
|
36
|
+
* Employs parallel computing.
|
|
37
|
+
|
|
38
|
+
# Installation
|
|
39
|
+
|
|
40
|
+
Install directly from PyPI with:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
pip install mdpsolver
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
# Quick start guide
|
|
47
|
+
|
|
48
|
+
The following shows how to get quickly started with `mdpsolver`.
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
Start by specifying the reward function and transition probabilities as lists. The following is an example of a simple MDP containing **three states** and **two actions in each state**.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
#Import packages
|
|
56
|
+
import mdpsolver
|
|
57
|
+
|
|
58
|
+
#Rewards (3 states x 2 actions)
|
|
59
|
+
#e.g. choosing second action in first state gives reward=-1
|
|
60
|
+
rewards = [[5,-1],
|
|
61
|
+
[1,-2],
|
|
62
|
+
[50,0]]
|
|
63
|
+
|
|
64
|
+
#Transition probabilities (3 from_states x 2 actions x 3 to_states)
|
|
65
|
+
#e.g. choosing first action in third state gives a probability of 0.6 of staying in third state
|
|
66
|
+
tranMatWithZeros = [[[0.9,0.1,0.0],[0.1,0.9,0.0]],
|
|
67
|
+
[[0.4,0.5,0.1],[0.3,0.5,0.2]],
|
|
68
|
+
[[0.2,0.2,0.6],[0.5,0.5,0.0]]]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Now, create the model object and insert the problem parameters.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
#Create model object
|
|
75
|
+
mdl = mdpsolver.model()
|
|
76
|
+
|
|
77
|
+
#Insert the problem parameters
|
|
78
|
+
mdl.mdp(discount=0.8,
|
|
79
|
+
rewards=rewards,
|
|
80
|
+
tranMatWithZeros=tranMatWithZeros)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
We can now optimize the policy.
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
mdl.solve()
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The optimized policy can be returned in a variety of ways. Here, we return the policy as a list and print directly in the terminal.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
print(mdl.getPolicy())
|
|
93
|
+
#[1, 1, 0]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Sparse transition matrix?
|
|
97
|
+
|
|
98
|
+
`mdpsolver` has three alternative formats for large and highly sparse transition probability matrices.
|
|
99
|
+
|
|
100
|
+
**(1) Elementwise representation (excluding elements containing zeros):**
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
#[from_state,action,to_state,probability]
|
|
104
|
+
tranMatElementwise = [[0,0,0,0.9],
|
|
105
|
+
[0,0,1,0.1],
|
|
106
|
+
[0,1,0,0.1],
|
|
107
|
+
[0,1,1,0.9],
|
|
108
|
+
[1,0,0,0.4],
|
|
109
|
+
[1,0,1,0.5],
|
|
110
|
+
[1,0,2,0.1],
|
|
111
|
+
[1,1,0,0.3],
|
|
112
|
+
[1,1,1,0.5],
|
|
113
|
+
[1,1,2,0.2],
|
|
114
|
+
[2,0,0,0.2],
|
|
115
|
+
[2,0,1,0.2],
|
|
116
|
+
[2,0,2,0.6],
|
|
117
|
+
[2,1,0,0.5],
|
|
118
|
+
[2,1,1,0.5]]
|
|
119
|
+
|
|
120
|
+
mdl = mdpsolver.model()
|
|
121
|
+
mdl.mdp(discount=0.8,
|
|
122
|
+
rewards=rewards,
|
|
123
|
+
tranMatElementwise=tranMatElementwise)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**(2) Probabilities and column (to_state) indices in separate lists:**
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
tranMatProbs = [[[0.9,0.1],[0.1,0.9]],
|
|
130
|
+
[[0.4,0.5,0.1],[0.3,0.5,0.2]],
|
|
131
|
+
[[0.2,0.2,0.6],[0.5,0.5]]]
|
|
132
|
+
|
|
133
|
+
tranMatColumns = [[[0,1],[0,1]],
|
|
134
|
+
[[0,1,2],[0,1,2]],
|
|
135
|
+
[[0,1,2],[0,1]]]
|
|
136
|
+
|
|
137
|
+
mdl = mdpsolver.model()
|
|
138
|
+
mdl.mdp(discount=0.8,
|
|
139
|
+
rewards=rewards,
|
|
140
|
+
tranMatProbs=tranMatProbs,
|
|
141
|
+
tranMatColumns=tranMatColumns)
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**(3) Load the elementwise representation from a file:**
|
|
146
|
+
|
|
147
|
+
**transitions.csv**
|
|
148
|
+
```
|
|
149
|
+
stateFrom,action,stateTo,probability
|
|
150
|
+
0,0,0,0.9
|
|
151
|
+
0,0,1,0.1
|
|
152
|
+
0,1,0,0.1
|
|
153
|
+
0,1,1,0.9
|
|
154
|
+
1,0,0,0.4
|
|
155
|
+
1,0,1,0.5
|
|
156
|
+
1,0,2,0.1
|
|
157
|
+
1,1,0,0.3
|
|
158
|
+
1,1,1,0.5
|
|
159
|
+
1,1,2,0.2
|
|
160
|
+
2,0,0,0.2
|
|
161
|
+
2,0,1,0.2
|
|
162
|
+
2,0,2,0.6
|
|
163
|
+
2,1,0,0.5
|
|
164
|
+
2,1,1,0.5
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
mdl = mdpsolver.model()
|
|
169
|
+
mdl.mdp(discount=0.8,
|
|
170
|
+
rewards=rewards,
|
|
171
|
+
tranMatFromFile="transitions.csv")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
# Documentation
|
|
175
|
+
|
|
176
|
+
The documentation can be found in the [wiki for MDPSolver](https://github.com/areenberg/MDPSolver/wiki).
|
|
177
|
+
|
|
178
|
+
# How to cite
|
|
179
|
+
|
|
180
|
+
*Andersen et al., (2025). MDPSolver: An Efficient Solver for Markov Decision Processes. Journal of Open Source Software, 10(109), 7544, https://doi.org/10.21105/joss.07544*
|
|
181
|
+
|
|
182
|
+
**BibTeX**
|
|
183
|
+
```
|
|
184
|
+
@article{Andersen2025,
|
|
185
|
+
doi = {10.21105/joss.07544},
|
|
186
|
+
url = {https://doi.org/10.21105/joss.07544},
|
|
187
|
+
year = {2025}, publisher = {The Open Journal},
|
|
188
|
+
volume = {10},
|
|
189
|
+
number = {109},
|
|
190
|
+
pages = {7544},
|
|
191
|
+
author = {Anders Reenberg Andersen and Jesper Fink Andersen},
|
|
192
|
+
title = {MDPSolver: An Efficient Solver for Markov Decision Processes},
|
|
193
|
+
journal = {Journal of Open Source Software} }
|
|
194
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
mdpsolver/model.py,sha256=srVyrp3Yfy-PZaxw_n3OLrMjDwzcqMqlRHF2mfawqNk,20932
|
|
2
|
+
mdpsolver/solvermodule.cp315-win_amd64.pyd,sha256=S7Iwj8ML2AVAzahnlohIEM2ai-CJ58kfZusAm87LqHY,368128
|
|
3
|
+
mdpsolver/__init__.py,sha256=hqapLUmvBsbefEfsyiHfapV5v8XFKNJGTOLtD_xmoyA,386
|
|
4
|
+
mdpsolver-0.11.0.dist-info/DELVEWHEEL,sha256=CHkyZcssx9iHzmk9F_NIItKg8ZKgzjPfueLzTkFNWOU,409
|
|
5
|
+
mdpsolver-0.11.0.dist-info/METADATA,sha256=wFcoi4bBVzlyPYX-BE1Ry9-qrkZPsRJ1wdH-u8mOmIo,5723
|
|
6
|
+
mdpsolver-0.11.0.dist-info/RECORD,,
|
|
7
|
+
mdpsolver-0.11.0.dist-info/WHEEL,sha256=m_QT_bq-mMsHZpYGIpEBVccZC7Hu7h_-ha-581a-KNs,105
|
|
8
|
+
mdpsolver-0.11.0.dist-info/licenses/LICENSE,sha256=4H3KqQWsxPS05-NFzPZWIh5d1dAADF2Oud2OdjUubrA,1127
|
|
9
|
+
mdpsolver.libs/libomp140.x86_64-4a20c1e5c115c29771a12324513eb109.dll,sha256=SiDB5cEVwpdxoSMkUT6xCbraxyGA95SBUnrXnZlv-zM,661856
|
|
10
|
+
mdpsolver.libs/msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll,sha256=pMIim9wqKmMKzcCVtNhgCOXD47x3cxdDVPPaT1vrnN4,575056
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anders Reenberg Andersen and Jesper Fink Andersen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
Binary file
|
|
Binary file
|