mdpsolver 0.12.0__tar.gz → 0.12.1__tar.gz
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-0.12.0 → mdpsolver-0.12.1}/CPP_Source_Code/ModifiedPolicyIteration.cpp +15 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/CPP_Source_Code/ModifiedPolicyIteration.h +2 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/CPP_Source_Code/ModuleInterface.cpp +113 -6
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/CPP_Source_Code/ModuleInterface.h +9 -1
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/CPP_Source_Code/PythonModule.cpp +2 -1
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/PKG-INFO +2 -2
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/Python/src/mdpsolver/model.py +86 -4
- mdpsolver-0.12.1/Python/tests/test_average.py +313 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/Python/tests/test_order.py +19 -9
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/Python/tests/test_output.py +93 -21
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/README.md +1 -1
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/pyproject.toml +1 -1
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/.gitignore +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/CPP_Source_Code/CBMmodel.cpp +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/CPP_Source_Code/CBMmodel.h +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/CPP_Source_Code/CMakeLists.txt +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/CPP_Source_Code/GeneralMDPmodel.cpp +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/CPP_Source_Code/GeneralMDPmodel.h +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/CPP_Source_Code/ModelType.cpp +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/CPP_Source_Code/ModelType.h +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/CPP_Source_Code/Policy.cpp +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/CPP_Source_Code/Policy.h +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/CPP_Source_Code/Rewards.cpp +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/CPP_Source_Code/Rewards.h +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/CPP_Source_Code/TBMmodel.cpp +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/CPP_Source_Code/TBMmodel.h +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/CPP_Source_Code/TransitionMatrix.cpp +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/CPP_Source_Code/TransitionMatrix.h +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/CPP_Source_Code/ValueVector.cpp +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/CPP_Source_Code/ValueVector.h +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/LICENSE +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/Python/src/mdpsolver/__init__.py +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/Python/tests/knownsolutions.py +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/Python/tests/loadmdpsolver.py +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/Python/tests/test_arrays.py +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/Python/tests/test_checkmodel.py +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/Python/tests/test_imports.py +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/Python/tests/test_large.py +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/Python/tests/test_mpi.py +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/Python/tests/test_pi.py +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/Python/tests/test_random.py +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/Python/tests/test_reuse.py +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/Python/tests/test_threads.py +0 -0
- {mdpsolver-0.12.0 → mdpsolver-0.12.1}/Python/tests/test_vi.py +0 -0
|
@@ -73,6 +73,8 @@ ModifiedPolicyIteration::ModifiedPolicyIteration(double epsilon, string algorith
|
|
|
73
73
|
diverged(false),
|
|
74
74
|
outOfRangeState(-1),
|
|
75
75
|
outOfRangeValue(0.0),
|
|
76
|
+
gainLower(numeric_limits<double>::quiet_NaN()),
|
|
77
|
+
gainUpper(numeric_limits<double>::quiet_NaN()),
|
|
76
78
|
parIter(0)
|
|
77
79
|
{
|
|
78
80
|
//check valid string input
|
|
@@ -194,6 +196,19 @@ void ModifiedPolicyIteration::solve(ModelType * mdl, Policy * ply, ValueVector *
|
|
|
194
196
|
auto t2 = chrono::high_resolution_clock::now(); //stop time
|
|
195
197
|
duration = (double) chrono::duration_cast<chrono::nanoseconds>( t2 - t1 ).count() / 1e6;
|
|
196
198
|
|
|
199
|
+
//BOUNDS ON THE GAIN
|
|
200
|
+
|
|
201
|
+
//every loop ends with an improvement step, v' = Lv, whose smallest and largest change
|
|
202
|
+
//(diffMin and diffMax) bound the gain under the average reward criterion. The policy
|
|
203
|
+
//found chose its actions in that step, so its gain is an average of v'-v, weighted by
|
|
204
|
+
//its limiting distribution, and lies between them; the optimal gain is at least the
|
|
205
|
+
//policy's, and at most the same average for an optimal policy, which is at most diffMax.
|
|
206
|
+
//They hold for any v, so also when a limit stopped the loops.
|
|
207
|
+
if (useAvg) {
|
|
208
|
+
gainLower = diffMin;
|
|
209
|
+
gainUpper = diffMax;
|
|
210
|
+
}
|
|
211
|
+
|
|
197
212
|
//POST PROCESSING
|
|
198
213
|
|
|
199
214
|
//make sure v is the last updated vector if we use standard updates
|
|
@@ -57,6 +57,8 @@ public:
|
|
|
57
57
|
bool diverged; //the values became infinite or NaN, which ends the loops (a NaN norm passes their test)
|
|
58
58
|
int outOfRangeState; //the first state with such a value (-1 if none)
|
|
59
59
|
double outOfRangeValue; //and its value
|
|
60
|
+
double gainLower; //under the average reward criterion, a lower bound on the gain (NaN otherwise)
|
|
61
|
+
double gainUpper; //and an upper bound
|
|
60
62
|
int getIterationLimit() { return iterLim; }
|
|
61
63
|
|
|
62
64
|
//methods
|
|
@@ -342,7 +342,10 @@ void ModuleInterface::solve(string algorithm,
|
|
|
342
342
|
results.outOfRangeState=solver.outOfRangeState;
|
|
343
343
|
results.outOfRangeValue=solver.outOfRangeValue;
|
|
344
344
|
results.iterationLimit=solver.getIterationLimit();
|
|
345
|
-
|
|
345
|
+
//save the bounds on the gain (NaN under the discounted criterion)
|
|
346
|
+
results.gainLower=solver.gainLower;
|
|
347
|
+
results.gainUpper=solver.gainUpper;
|
|
348
|
+
|
|
346
349
|
}
|
|
347
350
|
|
|
348
351
|
void ModuleInterface::setInitPolicy(py::list initPolicy){
|
|
@@ -418,14 +421,114 @@ void ModuleInterface::printValueVector(){
|
|
|
418
421
|
}
|
|
419
422
|
}
|
|
420
423
|
|
|
421
|
-
|
|
424
|
+
//---------------------------------------
|
|
425
|
+
// SAVING: the policy or the value vector
|
|
426
|
+
//---------------------------------------
|
|
427
|
+
//saveToFile() writes the policy or the value vector to a NumPy .npy file (filetype "npy", the
|
|
428
|
+
//default) or to a comma-separated file (filetype "csv"). The .npy file holds a 1D array with an
|
|
429
|
+
//entry per state, as np.save writes it, so that np.load gives what getPolicy() (int64) and
|
|
430
|
+
//getValueVector() (float64) return. The comma-separated file has a header line and a row per
|
|
431
|
+
//state: the state index, then the action index or the value.
|
|
432
|
+
|
|
433
|
+
namespace {
|
|
434
|
+
|
|
435
|
+
//the start of an .npy file (version 1.0) of a 1D array of n numbers of the NumPy type (e.g.
|
|
436
|
+
//"i8"), in the computer's byte order: the magic string, the version, the length of the
|
|
437
|
+
//header, and the header, padded with spaces so that the numbers start at a multiple of 64
|
|
438
|
+
//bytes, as np.save aligns them (see "LOADING: NumPy .npy files" for the format)
|
|
439
|
+
string npyStart(const string& type, size_t n){
|
|
440
|
+
const uint16_t one = 1;
|
|
441
|
+
const char order = (*(const unsigned char*)&one == 1) ? '<' : '>';
|
|
442
|
+
string header = "{'descr': '" + string(1, order) + type + "', 'fortran_order': False, 'shape': (" + to_string(n) + ",), }";
|
|
443
|
+
header.append((64 - (10 + header.size() + 1) % 64) % 64, ' ');
|
|
444
|
+
header += '\n';
|
|
445
|
+
string start = "\x93NUMPY";
|
|
446
|
+
start += (char)1;
|
|
447
|
+
start += (char)0;
|
|
448
|
+
start += (char)(header.size() & 0xff);
|
|
449
|
+
start += (char)(header.size() >> 8);
|
|
450
|
+
return start + header;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
//closes a file that has been written, and throws if any of it could not be written (e.g. on a
|
|
454
|
+
//full disk)
|
|
455
|
+
void closeWritten(ofstream& outFile, const string& fileName){
|
|
456
|
+
outFile.close();
|
|
457
|
+
if (outFile.fail()){
|
|
458
|
+
throw runtime_error("saveToFile(): failed to write the file " + fileName + ".");
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
void ModuleInterface::saveToFile(string fileName, string type, string filetype){
|
|
465
|
+
bool policy;
|
|
422
466
|
if (type.compare("policy")==0 || type.compare("p")==0){
|
|
423
|
-
|
|
467
|
+
policy = true;
|
|
424
468
|
}else if(type.compare("value")==0 || type.compare("values")==0 || type.compare("v")==0){
|
|
425
|
-
|
|
469
|
+
policy = false;
|
|
426
470
|
}else{
|
|
427
471
|
throw invalid_argument("saveToFile(): the type '" + type + "' is not recognized. Select either 'policy' or 'value'.");
|
|
428
472
|
}
|
|
473
|
+
//the filetype in any case, e.g. "NPY"
|
|
474
|
+
const string given = filetype;
|
|
475
|
+
transform(filetype.begin(), filetype.end(), filetype.begin(), [](unsigned char c){ return (char)tolower(c); });
|
|
476
|
+
if (filetype.compare("npy")!=0 && filetype.compare("csv")!=0){
|
|
477
|
+
throw invalid_argument("saveToFile(): the filetype '" + given + "' is not recognized. Select either 'npy' or 'csv'.");
|
|
478
|
+
}
|
|
479
|
+
const bool npy = (filetype.compare("npy")==0);
|
|
480
|
+
|
|
481
|
+
//the file name, by default result.npy or result.csv. A name ending in .csv or .npy must
|
|
482
|
+
//agree with the filetype, as mdp() reads a file by its extension.
|
|
483
|
+
if (fileName.empty()){
|
|
484
|
+
fileName = "result." + filetype;
|
|
485
|
+
}
|
|
486
|
+
const string other = npy ? ".csv" : ".npy";
|
|
487
|
+
if (hasExtension(fileName, other)){
|
|
488
|
+
throw invalid_argument("saveToFile(): the file name " + fileName + " ends in " + other + ", but the filetype is '" + filetype + "'. Use filetype='" + other.substr(1) + "' to save a " + (npy ? "comma-separated" : "NumPy .npy") + " file.");
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (npy && policy){
|
|
492
|
+
savePolicyToNpy(fileName);
|
|
493
|
+
}else if (npy){
|
|
494
|
+
saveValueVectorToNpy(fileName);
|
|
495
|
+
}else if (policy){
|
|
496
|
+
savePolicyToFile(fileName,',');
|
|
497
|
+
}else{
|
|
498
|
+
saveValueVectorToFile(fileName,',');
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
void ModuleInterface::savePolicyToNpy(string fileName){
|
|
503
|
+
ofstream outFile(fileName, ios::binary);
|
|
504
|
+
if (!outFile.is_open()) {
|
|
505
|
+
throw runtime_error("saveToFile(): failed to open the file " + fileName + " for writing.");
|
|
506
|
+
}
|
|
507
|
+
//as int64, as getPolicy() returns it, converted a block of states at a time
|
|
508
|
+
const vector<int>& policy = problem.policy.policy;
|
|
509
|
+
const string start = npyStart("i8", policy.size());
|
|
510
|
+
outFile.write(start.data(), (streamsize)start.size());
|
|
511
|
+
vector<int64_t> block(min(policy.size(), (size_t)65536));
|
|
512
|
+
for (size_t first = 0; first < policy.size(); first += block.size()) {
|
|
513
|
+
size_t n = min(block.size(), policy.size() - first);
|
|
514
|
+
for (size_t k = 0; k < n; k++) {
|
|
515
|
+
block[k] = policy[first + k];
|
|
516
|
+
}
|
|
517
|
+
outFile.write((const char*)block.data(), (streamsize)(n * sizeof(int64_t)));
|
|
518
|
+
}
|
|
519
|
+
closeWritten(outFile, fileName);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
void ModuleInterface::saveValueVectorToNpy(string fileName){
|
|
523
|
+
ofstream outFile(fileName, ios::binary);
|
|
524
|
+
if (!outFile.is_open()) {
|
|
525
|
+
throw runtime_error("saveToFile(): failed to open the file " + fileName + " for writing.");
|
|
526
|
+
}
|
|
527
|
+
const vector<double>& values = problem.valueVector.valueVector;
|
|
528
|
+
const string start = npyStart("f8", values.size());
|
|
529
|
+
outFile.write(start.data(), (streamsize)start.size());
|
|
530
|
+
outFile.write((const char*)values.data(), (streamsize)(values.size() * sizeof(double)));
|
|
531
|
+
closeWritten(outFile, fileName);
|
|
429
532
|
}
|
|
430
533
|
|
|
431
534
|
void ModuleInterface::savePolicyToFile(string fileName, char sep){
|
|
@@ -437,7 +540,7 @@ void ModuleInterface::savePolicyToFile(string fileName, char sep){
|
|
|
437
540
|
for (int sidx = 0; sidx < problem.policy.policy.size(); sidx++) {
|
|
438
541
|
outFile << sidx << sep << problem.policy.policy[sidx] << endl;
|
|
439
542
|
}
|
|
440
|
-
outFile
|
|
543
|
+
closeWritten(outFile, fileName);
|
|
441
544
|
}
|
|
442
545
|
|
|
443
546
|
void ModuleInterface::saveValueVectorToFile(string fileName, char sep){
|
|
@@ -449,7 +552,7 @@ void ModuleInterface::saveValueVectorToFile(string fileName, char sep){
|
|
|
449
552
|
for (int sidx = 0; sidx < problem.valueVector.valueVector.size(); sidx++) {
|
|
450
553
|
outFile << sidx << sep << problem.valueVector.valueVector[sidx] << endl;
|
|
451
554
|
}
|
|
452
|
-
outFile
|
|
555
|
+
closeWritten(outFile, fileName);
|
|
453
556
|
}
|
|
454
557
|
|
|
455
558
|
double ModuleInterface::getRuntime(){
|
|
@@ -473,6 +576,10 @@ py::dict ModuleInterface::getConvergence(){
|
|
|
473
576
|
return(convergence);
|
|
474
577
|
}
|
|
475
578
|
|
|
579
|
+
py::tuple ModuleInterface::getGainBounds(){
|
|
580
|
+
return(py::make_tuple(results.gainLower, results.gainUpper));
|
|
581
|
+
}
|
|
582
|
+
|
|
476
583
|
int ModuleInterface::getNumberOfStates(){
|
|
477
584
|
//the number of states of the general MDP model
|
|
478
585
|
if (problem.problemType.compare("mdp")==0){
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
#include <cstdint>
|
|
30
30
|
#include <string>
|
|
31
31
|
#include <iostream>
|
|
32
|
+
#include <limits>
|
|
32
33
|
|
|
33
34
|
#include "ModelType.h" //Generic model class
|
|
34
35
|
#include "ModifiedPolicyIteration.h" //The solver
|
|
@@ -121,6 +122,9 @@ public:
|
|
|
121
122
|
int outOfRangeState=-1;
|
|
122
123
|
double outOfRangeValue=0.0;
|
|
123
124
|
int iterationLimit=0;
|
|
125
|
+
//bounds on the gain under the average reward criterion (NaN after a solve under the discounted criterion)
|
|
126
|
+
double gainLower=numeric_limits<double>::quiet_NaN();
|
|
127
|
+
double gainUpper=numeric_limits<double>::quiet_NaN();
|
|
124
128
|
} results;
|
|
125
129
|
|
|
126
130
|
|
|
@@ -192,8 +196,9 @@ public:
|
|
|
192
196
|
int getNumberOfActions(int sidx); //returns the number of actions of a state of the general MDP model (-1 for the built-in models or a state outside the model)
|
|
193
197
|
py::array_t<int64_t> getPolicy(); //returns the entire policy, as a NumPy array (a copy)
|
|
194
198
|
py::array_t<double> getValueVector(); //returns the entire value vector, as a NumPy array (a copy)
|
|
195
|
-
void saveToFile(string fileName, string type); //save the policy or value vector to a file
|
|
199
|
+
void saveToFile(string fileName, string type, string filetype); //save the policy or value vector to a NumPy .npy file or a comma-separated file (by default result.npy or result.csv when fileName is empty)
|
|
196
200
|
py::dict getConvergence(); //whether the last solve converged, and if not, why
|
|
201
|
+
py::tuple getGainBounds(); //the lower and upper bound on the gain from the last solve under the average reward criterion
|
|
197
202
|
|
|
198
203
|
//------ model check ------
|
|
199
204
|
|
|
@@ -224,6 +229,9 @@ private:
|
|
|
224
229
|
//the elementwise formats from NumPy .npy files, read in blocks straight into the tables
|
|
225
230
|
void loadRewardsFromNpy(string rewardsFromFile);
|
|
226
231
|
void loadTranMatFromNpy(string tranMatFromFile);
|
|
232
|
+
//the policy or the value vector to a NumPy .npy file (a 1D array) or a comma-separated file
|
|
233
|
+
void savePolicyToNpy(string fileName);
|
|
234
|
+
void saveValueVectorToNpy(string fileName);
|
|
227
235
|
void savePolicyToFile(string fileName, char sep);
|
|
228
236
|
void saveValueVectorToFile(string fileName, char sep);
|
|
229
237
|
|
|
@@ -68,8 +68,9 @@ PYBIND11_MODULE(solvermodule, m) {
|
|
|
68
68
|
.def("getValue", &ModuleInterface::getValue,"Returns a value from the optimized policy.",py::arg("stateIndex")=0)
|
|
69
69
|
.def("getPolicy", &ModuleInterface::getPolicy,"Returns the optimized policy as a NumPy array.")
|
|
70
70
|
.def("getValueVector", &ModuleInterface::getValueVector,"Returns the optimized value vector as a NumPy array.")
|
|
71
|
-
.def("saveToFile", &ModuleInterface::saveToFile,"Saves the optimized policy or value vector to a file.",py::arg("fileName")="
|
|
71
|
+
.def("saveToFile", &ModuleInterface::saveToFile,"Saves the optimized policy or value vector to a NumPy .npy file (filetype='npy') or a comma-separated file (filetype='csv'), in any case; an empty fileName saves to result.npy or result.csv.",py::arg("fileName")="",py::arg("type")="policy",py::arg("filetype")="npy")
|
|
72
72
|
.def("getConvergence", &ModuleInterface::getConvergence,"Returns whether the last solve converged, and if not, why.")
|
|
73
|
+
.def("getGainBounds", &ModuleInterface::getGainBounds,"Returns the lower and upper bound on the gain from the last solve under the average reward criterion (NaN after a solve under the discounted criterion).")
|
|
73
74
|
.def("checkModel", &ModuleInterface::checkModel,"Checks the general MDP model for malformed input; returns a description of each kind of problem found. Never called by mdp() or solve().",py::arg("tolerance")=1e-9);
|
|
74
75
|
|
|
75
76
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: mdpsolver
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.1
|
|
4
4
|
Summary: A fast Python solver for Markov Decision Processes
|
|
5
5
|
Author-Email: Anders Reenberg Andersen <andersra@live.dk>, Jesper Fink Andersen <jesperfinkandersen@gmail.com>
|
|
6
6
|
Classifier: Development Status :: 4 - Beta
|
|
@@ -49,7 +49,7 @@ MDPSolver is a Python package for large Markov Decision Processes (MDPs) with in
|
|
|
49
49
|
| 5,000,000 | 500,000,000 | 6.3 GB | 5.03 s |
|
|
50
50
|
| 10,000,000 | 1,000,000,000 | 12.7 GB | 13.0 s |
|
|
51
51
|
|
|
52
|
-
Mean of 10 random models with 5 actions per state and 20 possible next states per state and action, loaded from NPY-files and solved by modified policy iteration under the discounted reward criterion with discount 0.9. Measured on an Apple M5 with 10 cores.
|
|
52
|
+
Mean of 10 random models with 5 actions per state and 20 possible next states per state and action, loaded from NPY-files and solved by modified policy iteration under the discounted reward criterion with discount $\lambda=0.9$ and tolerance $\epsilon = 0.001$. Measured on an Apple M5 with 10 cores.
|
|
53
53
|
|
|
54
54
|
# Installation
|
|
55
55
|
|
|
@@ -121,6 +121,8 @@ class model:
|
|
|
121
121
|
# A model object contains only one model
|
|
122
122
|
self._modelDefined = False
|
|
123
123
|
self._solved = False
|
|
124
|
+
# The optimality criterion of the last run of solve()
|
|
125
|
+
self._criterion = None
|
|
124
126
|
|
|
125
127
|
def _requireSolution(self, method):
|
|
126
128
|
"""
|
|
@@ -235,6 +237,7 @@ class model:
|
|
|
235
237
|
# Set after the run, so that a run that did not finish leaves the
|
|
236
238
|
# object without a solution
|
|
237
239
|
self._solved = True
|
|
240
|
+
self._criterion = criterion
|
|
238
241
|
|
|
239
242
|
convergence = self.mdl.getConvergence()
|
|
240
243
|
hint = " Under the average reward criterion, this happens with periodic models." if criterion == "average" else ""
|
|
@@ -373,27 +376,106 @@ class model:
|
|
|
373
376
|
return self.mdl.getValueVector()
|
|
374
377
|
|
|
375
378
|
@_oneAtATime
|
|
376
|
-
def
|
|
379
|
+
def getGain(self, bound: str | None = None) -> float:
|
|
380
|
+
"""
|
|
381
|
+
Get the gain, the long-run average reward per period, from the last
|
|
382
|
+
run of solve() under the average reward criterion.
|
|
383
|
+
|
|
384
|
+
The smallest and largest change of the values in the last iteration
|
|
385
|
+
of solve() are a lower and an upper bound on the gain: the gain of the
|
|
386
|
+
optimized policy and the optimal gain both lie between them. When
|
|
387
|
+
solve() has converged, the bounds are less than the tolerance apart,
|
|
388
|
+
so their midpoint is within half the tolerance of the gain.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
bound (str): None for the midpoint of the bounds, 'lower' for the lower bound, or 'upper' for the upper bound (in any case).
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
float: The gain, or the bound selected.
|
|
395
|
+
|
|
396
|
+
Raises RuntimeError if the last run of solve() used the discounted
|
|
397
|
+
reward criterion.
|
|
398
|
+
"""
|
|
399
|
+
self._requireAverage("getGain", "gain")
|
|
400
|
+
if not (bound is None or (isinstance(bound, str) and bound.lower() in ("lower", "upper"))):
|
|
401
|
+
raise ValueError("Bound not recognized. Select either: None, 'lower', or 'upper'.")
|
|
402
|
+
lower, upper = self.mdl.getGainBounds()
|
|
403
|
+
if bound is None:
|
|
404
|
+
return (lower + upper) / 2.0
|
|
405
|
+
return lower if bound.lower() == "lower" else upper
|
|
406
|
+
|
|
407
|
+
@_oneAtATime
|
|
408
|
+
def getBias(self, stateIndex: int, referenceIndex: int = 0) -> float:
|
|
409
|
+
"""
|
|
410
|
+
Get the bias (the relative value) of a state from the last run of
|
|
411
|
+
solve() under the average reward criterion, with the bias of a
|
|
412
|
+
reference state set to 0. Thus, the bias of a state is its value
|
|
413
|
+
minus the value of the reference state.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
stateIndex (int): Index of the state.
|
|
417
|
+
referenceIndex (int): Index of the reference state, whose bias is 0.
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
float: Bias of the given state.
|
|
421
|
+
|
|
422
|
+
Raises RuntimeError if the last run of solve() used the discounted
|
|
423
|
+
reward criterion, and IndexError for a state outside the model.
|
|
424
|
+
"""
|
|
425
|
+
self._requireAverage("getBias", "bias")
|
|
426
|
+
numberOfStates = self.mdl.getNumberOfStates()
|
|
427
|
+
for name, index in (("state", stateIndex), ("reference state", referenceIndex)):
|
|
428
|
+
if not isinstance(index, numbers.Integral) or isinstance(index, bool):
|
|
429
|
+
raise TypeError("The %s needs to be an integer state index (got %r)." % (name, index))
|
|
430
|
+
if not 0 <= index < numberOfStates:
|
|
431
|
+
raise IndexError("The %s %d is outside the model, which has %d states (indices 0 to %d)." % (name, index, numberOfStates, numberOfStates - 1))
|
|
432
|
+
return self.mdl.getValue(stateIndex=int(stateIndex)) - self.mdl.getValue(stateIndex=int(referenceIndex))
|
|
433
|
+
|
|
434
|
+
def _requireAverage(self, method, quantity):
|
|
435
|
+
"""
|
|
436
|
+
Raise RuntimeError unless the last run of solve() used the average
|
|
437
|
+
reward criterion.
|
|
438
|
+
"""
|
|
439
|
+
self._requireSolution(method)
|
|
440
|
+
if self._criterion != "average":
|
|
441
|
+
raise RuntimeError("The %s is defined under the average reward criterion, but the last run of solve() used criterion='%s'. Call solve(criterion='average') first." % (quantity, self._criterion))
|
|
442
|
+
|
|
443
|
+
@_oneAtATime
|
|
444
|
+
def saveToFile(self, fileName: str | None = None, type: str = "policy", filetype: str = "npy") -> None:
|
|
377
445
|
"""
|
|
378
446
|
Save the optimized policy or value vector to a file.
|
|
379
447
|
|
|
380
448
|
Args:
|
|
381
|
-
fileName (str): Name of the output file.
|
|
449
|
+
fileName (str): Name of the output file. By default result.npy, or result.csv when filetype is 'csv'.
|
|
382
450
|
type (str): Type of result to save ('policy' or 'value').
|
|
451
|
+
filetype (str): Type of file to save ('npy' or 'csv', in any case). 'npy' saves a NumPy .npy file of a 1D array with an entry per state, which np.load reads as the array getPolicy() (int64) or getValueVector() (float64) returns. 'csv' saves a comma-separated file with a header line and a row per state: the state index, then the action index or the value.
|
|
452
|
+
|
|
453
|
+
A file name ending in .csv with filetype 'npy', or in .npy with
|
|
454
|
+
filetype 'csv', raises a ValueError.
|
|
383
455
|
|
|
384
456
|
Returns:
|
|
385
457
|
None
|
|
386
458
|
"""
|
|
387
|
-
|
|
459
|
+
|
|
388
460
|
self._requireSolution("saveToFile")
|
|
389
461
|
if not (type == "policy" or type == "value"):
|
|
390
462
|
raise ValueError("Type not recognized. Select either: 'policy' or 'value'.")
|
|
463
|
+
# (in any case, e.g. "NPY")
|
|
464
|
+
if not (isinstance(filetype, str) and filetype.lower() in ("npy", "csv")):
|
|
465
|
+
raise ValueError("Filetype not recognized. Select either: 'npy' or 'csv'.")
|
|
466
|
+
filetype = filetype.lower()
|
|
467
|
+
if fileName is None:
|
|
468
|
+
fileName = "result." + filetype
|
|
469
|
+
# (mdp() reads a file by its extension)
|
|
470
|
+
other = ".csv" if filetype == "npy" else ".npy"
|
|
471
|
+
if str(fileName).lower().endswith(other):
|
|
472
|
+
raise ValueError("The file name %s ends in %s, but filetype is '%s'. Use filetype='%s' to save a %s file." % (fileName, other, filetype, other[1:], "comma-separated" if filetype == "npy" else "NumPy .npy"))
|
|
391
473
|
error = _openError(fileName, "w")
|
|
392
474
|
if error is not None:
|
|
393
475
|
# (the argument type hides the built-in type())
|
|
394
476
|
raise error.__class__("The file %s could not be opened for writing (%s)." % (fileName, error.strerror or error))
|
|
395
477
|
|
|
396
|
-
return self.mdl.saveToFile(fileName=fileName, type=type)
|
|
478
|
+
return self.mdl.saveToFile(fileName=fileName, type=type, filetype=filetype)
|
|
397
479
|
|
|
398
480
|
@_oneAtATime
|
|
399
481
|
def mdp(
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
'''
|
|
2
|
+
UNIT TEST FOR THE GAIN AND THE BIAS UNDER THE AVERAGE REWARD CRITERION
|
|
3
|
+
(model.getGain() and model.getBias()).
|
|
4
|
+
|
|
5
|
+
Under the average reward criterion, solve() ends every algorithm with an
|
|
6
|
+
improvement step v' = Lv. The smallest and largest change in that step, the
|
|
7
|
+
smallest and largest entry of v' - v, bound the gain: the policy found chose
|
|
8
|
+
its actions in that step, so its gain is an average of v' - v (weighted by its
|
|
9
|
+
limiting distribution), and the optimal gain is at least the policy's and at
|
|
10
|
+
most the largest entry (Puterman, 1994, Section 8.5). The bounds hold for any
|
|
11
|
+
v, so also when solve() stops at a limit. getGain() returns their midpoint, and
|
|
12
|
+
getGain(bound="lower") and getGain(bound="upper") the bounds themselves.
|
|
13
|
+
|
|
14
|
+
The values grow by about the gain in every iteration, so only their
|
|
15
|
+
differences are meaningful: they approach the differences between the biases
|
|
16
|
+
of the states. getBias(s, referenceIndex=k) returns the value of state s
|
|
17
|
+
minus the value of state k, the bias of state s with the bias of state k set
|
|
18
|
+
to 0.
|
|
19
|
+
|
|
20
|
+
Sub-tests:
|
|
21
|
+
Case 1: The hub-and-spoke problem of knownsolutions.py, whose gain is known
|
|
22
|
+
in closed form, under value iteration, policy iteration and
|
|
23
|
+
modified policy iteration, with serial and parallel computation, and
|
|
24
|
+
at a loose and a tight tolerance: the bounds contain the gain and
|
|
25
|
+
are less than the tolerance apart, getGain() is their midpoint and
|
|
26
|
+
lies within half the tolerance of the gain, and the run converged.
|
|
27
|
+
Case 2: Random unichain models, at a loose tolerance: the bounds contain
|
|
28
|
+
both the gain of the policy found and the optimal gain, each
|
|
29
|
+
computed exactly by solving linear equations.
|
|
30
|
+
Case 3: A periodic model, a cycle of three states with one action each,
|
|
31
|
+
where value iteration and the evaluation of policy iteration never
|
|
32
|
+
converge: solve() stops at its limit with a ConvergenceWarning, and
|
|
33
|
+
the bounds still contain the gain, while their midpoint does not
|
|
34
|
+
equal it.
|
|
35
|
+
Case 4: The hub-and-spoke problem and the random models of case 2, whose
|
|
36
|
+
biases are known (in closed form, and by solving linear
|
|
37
|
+
equations), at a tight tolerance, under every algorithm: for
|
|
38
|
+
every state and every reference state, the bias is a float, the
|
|
39
|
+
value of the state minus that of the reference state (so exactly
|
|
40
|
+
0 for the reference state itself), and lies within BIASTOL of the
|
|
41
|
+
known bias with that of the reference state subtracted. The
|
|
42
|
+
default reference state is state 0, and NumPy integers work as
|
|
43
|
+
well.
|
|
44
|
+
Case 5: getGain() and getBias() before solve() raise a RuntimeError, and
|
|
45
|
+
so do they after a run under the discounted criterion, also when
|
|
46
|
+
an earlier run on the same object was under the average criterion.
|
|
47
|
+
A run under the average criterion after a discounted one gives the
|
|
48
|
+
gain and the bias. The bound is selected in any case ("Lower",
|
|
49
|
+
"UPPER"), and any other bound raises a ValueError. A state or a
|
|
50
|
+
reference state outside the model raises an IndexError, and one
|
|
51
|
+
that is not an integer a TypeError.
|
|
52
|
+
|
|
53
|
+
References:
|
|
54
|
+
Puterman, M. L. (1994). Markov Decision Processes: Discrete Stochastic
|
|
55
|
+
Dynamic Programming. Wiley. https://doi.org/10.1002/9780470316887
|
|
56
|
+
|
|
57
|
+
mdpsolver is loaded by loadmdpsolver.py, which explains where the compiled
|
|
58
|
+
solvermodule is taken from.
|
|
59
|
+
'''
|
|
60
|
+
|
|
61
|
+
import sys
|
|
62
|
+
import warnings
|
|
63
|
+
|
|
64
|
+
import numpy as np
|
|
65
|
+
|
|
66
|
+
from loadmdpsolver import mdpsolver, finalline, quietly
|
|
67
|
+
from knownsolutions import arrayinput, exactaverage, hubaverage, hubproblem, matrices
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ------------------------------------------------------------------
|
|
71
|
+
# INITIALIZATION
|
|
72
|
+
# ------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
DISCOUNT = 0.9 # mdp() needs one; the average reward criterion ignores it
|
|
75
|
+
TOLERANCES = (1e-3, 1e-9) # a loose and a tight epsilon handed to solve()
|
|
76
|
+
LOOSE = TOLERANCES[0]
|
|
77
|
+
TIGHT = TOLERANCES[1]
|
|
78
|
+
ROUNDING = 1e-12 # the margin for rounding when the bounds are compared with an exact gain
|
|
79
|
+
BIASTOL = 1e-8 # how far a bias may lie from the known one at the tight tolerance (the errors are about 1e-10)
|
|
80
|
+
|
|
81
|
+
ALGORITHMS = (("value iteration", "vi"), ("policy iteration", "pi"), ("modified policy iteration", "mpi"))
|
|
82
|
+
|
|
83
|
+
HUBREWARDS, HUBTRANSITIONS = hubproblem()
|
|
84
|
+
HUBGAIN, HUBBIAS = hubaverage()[1:]
|
|
85
|
+
|
|
86
|
+
# Case 2: random models in which every action leads to state 0 with a
|
|
87
|
+
# probability of at least MINPROB, so every policy is unichain (state 0 is in
|
|
88
|
+
# its only recurrent class) and aperiodic (state 0 can stay in state 0)
|
|
89
|
+
NSTATES, NACTIONS, NNEXT = 40, 3, 4
|
|
90
|
+
MINPROB = 0.05
|
|
91
|
+
SEEDS = range(5)
|
|
92
|
+
|
|
93
|
+
# Case 3: a cycle 0 -> 1 -> 2 -> 0 with the reward CYCLEREWARD in state 2 only,
|
|
94
|
+
# whose gain is CYCLEREWARD / 3, while the changes of a step are 0 and CYCLEREWARD
|
|
95
|
+
CYCLEREWARD = 3.0
|
|
96
|
+
CYCLEREWARDS = [[0.0], [0.0], [CYCLEREWARD]]
|
|
97
|
+
CYCLETRANSITIONS = [[{1: 1.0}], [{2: 1.0}], [{0: 1.0}]]
|
|
98
|
+
CYCLEGAIN = CYCLEREWARD / 3.0
|
|
99
|
+
|
|
100
|
+
if not HUBGAIN > 0.0:
|
|
101
|
+
sys.exit("Validation test failed at initialization: the gain of the hub-and-spoke problem is %r, where it should be positive." % HUBGAIN)
|
|
102
|
+
# the biases of the hub-and-spoke problem differ between the states by far
|
|
103
|
+
# more than BIASTOL, so a wrong reference state cannot pass case 4
|
|
104
|
+
if HUBBIAS[0] != 0.0 or np.min(np.abs(np.subtract.outer(HUBBIAS, HUBBIAS))[~np.eye(len(HUBBIAS), dtype=bool)]) < 100 * BIASTOL:
|
|
105
|
+
sys.exit("Validation test failed at initialization: the biases of the hub-and-spoke problem, %s, are not 0 in the hub and far apart." % HUBBIAS)
|
|
106
|
+
if np.isclose(CYCLEGAIN, CYCLEREWARD / 2.0):
|
|
107
|
+
sys.exit("Validation test failed at initialization: the gain of the cycle equals the midpoint of its bounds, so case 3 could not tell them apart.")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ------------------------------------------------------------------
|
|
111
|
+
# HELPER FUNCTIONS
|
|
112
|
+
# ------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
def solve(rewards, transitions, **settings):
|
|
115
|
+
'''A new model object, solved under the average reward criterion; anything solve() prints ends the test.'''
|
|
116
|
+
def run():
|
|
117
|
+
mdl = mdpsolver.model()
|
|
118
|
+
mdl.mdp(discount=DISCOUNT, **arrayinput(rewards, transitions, bycolumn=True))
|
|
119
|
+
mdl.solve(criterion="average", **settings)
|
|
120
|
+
return mdl
|
|
121
|
+
|
|
122
|
+
mdl, output = quietly(run)
|
|
123
|
+
if output:
|
|
124
|
+
sys.exit("Validation test failed: solve() with %s printed %r." % (settings, output.strip()))
|
|
125
|
+
return mdl
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def bounds(mdl):
|
|
129
|
+
'''The lower bound, the midpoint and the upper bound that getGain() returns.'''
|
|
130
|
+
return mdl.getGain(bound="lower"), mdl.getGain(), mdl.getGain(bound="upper")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def checkbounds(case, label, lower, upper, gain, what="the gain"):
|
|
134
|
+
'''Ends the test unless the bounds contain the gain.'''
|
|
135
|
+
if not (np.isfinite(lower) and np.isfinite(upper)):
|
|
136
|
+
sys.exit("Validation test failed at case %d: %s gives the bounds [%r, %r] on the gain." % (case, label, lower, upper))
|
|
137
|
+
if not (lower - ROUNDING <= gain <= upper + ROUNDING):
|
|
138
|
+
sys.exit("Validation test failed at case %d: %s gives the bounds [%.15g, %.15g], which do not contain %s, %.15g." % (case, label, lower, upper, what, gain))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def randommodel(seed):
|
|
142
|
+
'''A random unichain and aperiodic model, as rewards[s][a] and transitions[s][a] = {next state: probability}.'''
|
|
143
|
+
rng = np.random.default_rng(seed)
|
|
144
|
+
rewards, transitions = [], []
|
|
145
|
+
for s in range(NSTATES):
|
|
146
|
+
rewards.append(list(rng.uniform(0.0, 10.0, NACTIONS)))
|
|
147
|
+
state = []
|
|
148
|
+
for a in range(NACTIONS):
|
|
149
|
+
others = rng.choice(np.arange(1, NSTATES), size=NNEXT - 1, replace=False)
|
|
150
|
+
probs = MINPROB + (1.0 - NNEXT * MINPROB) * rng.dirichlet(np.ones(NNEXT))
|
|
151
|
+
state.append({0: float(probs[0]), **{int(j): float(p) for j, p in zip(others, probs[1:])}})
|
|
152
|
+
transitions.append(state)
|
|
153
|
+
return rewards, transitions
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def policygain(rewards, transitions, policy):
|
|
157
|
+
'''The gain of a unichain policy, from h + g = r + P h with h(0) = 0.'''
|
|
158
|
+
R, P = matrices(rewards, transitions)
|
|
159
|
+
n = len(R)
|
|
160
|
+
A = np.zeros((n + 1, n + 1))
|
|
161
|
+
A[:n, :n] = np.eye(n) - np.array([P[s][policy[s]] for s in range(n)])
|
|
162
|
+
A[:n, n] = 1.0
|
|
163
|
+
A[n, 0] = 1.0
|
|
164
|
+
return np.linalg.solve(A, np.r_[[R[s][policy[s]] for s in range(n)], 0.0])[n]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def raises(error, function):
|
|
168
|
+
'''Whether the function raises the error; returns the message, or None.'''
|
|
169
|
+
try:
|
|
170
|
+
function()
|
|
171
|
+
except error as raised:
|
|
172
|
+
return str(raised)
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ------------------------------------------------------------------
|
|
177
|
+
# CASE 1: The hub-and-spoke problem, whose gain is known
|
|
178
|
+
# ------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
for algorithmlabel, algorithm in ALGORITHMS:
|
|
181
|
+
for parallel in (False, True):
|
|
182
|
+
for tolerance in TOLERANCES:
|
|
183
|
+
label = "%s (parallel=%s, tolerance %g) on the hub-and-spoke problem" % (algorithmlabel, parallel, tolerance)
|
|
184
|
+
mdl = solve(HUBREWARDS, HUBTRANSITIONS, algorithm=algorithm, tolerance=tolerance, parallel=parallel)
|
|
185
|
+
if not mdl.hasConverged():
|
|
186
|
+
sys.exit("Validation test failed at case 1: %s did not converge." % label)
|
|
187
|
+
lower, middle, upper = bounds(mdl)
|
|
188
|
+
checkbounds(1, label, lower, upper, HUBGAIN)
|
|
189
|
+
if not upper - lower < tolerance:
|
|
190
|
+
sys.exit("Validation test failed at case 1: %s gives bounds %.3e apart, which is not less than the tolerance." % (label, upper - lower))
|
|
191
|
+
if middle != (lower + upper) / 2.0:
|
|
192
|
+
sys.exit("Validation test failed at case 1: %s gives getGain() = %.15g, where the midpoint of the bounds is %.15g." % (label, middle, (lower + upper) / 2.0))
|
|
193
|
+
if abs(middle - HUBGAIN) > tolerance / 2.0:
|
|
194
|
+
sys.exit("Validation test failed at case 1: %s gives getGain() = %.15g, which is %.3e from the gain %.15g, more than half the tolerance." % (label, middle, abs(middle - HUBGAIN), HUBGAIN))
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ------------------------------------------------------------------
|
|
198
|
+
# CASE 2: Random unichain models, against exact gains
|
|
199
|
+
# ------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
for seed in SEEDS:
|
|
202
|
+
rewards, transitions = randommodel(seed)
|
|
203
|
+
optimalgain = exactaverage(rewards, transitions)[1]
|
|
204
|
+
for algorithmlabel, algorithm in ALGORITHMS:
|
|
205
|
+
label = "%s (tolerance %g) on random model %d" % (algorithmlabel, LOOSE, seed)
|
|
206
|
+
mdl = solve(rewards, transitions, algorithm=algorithm, tolerance=LOOSE, parallel=False)
|
|
207
|
+
if not mdl.hasConverged():
|
|
208
|
+
sys.exit("Validation test failed at case 2: %s did not converge." % label)
|
|
209
|
+
lower, middle, upper = bounds(mdl)
|
|
210
|
+
checkbounds(2, label, lower, upper, policygain(rewards, transitions, [int(a) for a in mdl.getPolicy()]), "the gain of the policy found")
|
|
211
|
+
checkbounds(2, label, lower, upper, optimalgain, "the optimal gain")
|
|
212
|
+
if not upper - lower < LOOSE:
|
|
213
|
+
sys.exit("Validation test failed at case 2: %s gives bounds %.3e apart, which is not less than the tolerance." % (label, upper - lower))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ------------------------------------------------------------------
|
|
217
|
+
# CASE 3: A periodic model, where solve() stops at a limit
|
|
218
|
+
# ------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
for algorithmlabel, algorithm in ALGORITHMS[:2]: # (modified policy iteration would take 100 sweeps per iteration to its limit)
|
|
221
|
+
label = "%s on the three-state cycle" % algorithmlabel
|
|
222
|
+
with warnings.catch_warnings(record=True) as caught:
|
|
223
|
+
warnings.simplefilter("always")
|
|
224
|
+
mdl = solve(CYCLEREWARDS, CYCLETRANSITIONS, algorithm=algorithm, tolerance=LOOSE, parallel=False)
|
|
225
|
+
if mdl.hasConverged() or not any(issubclass(w.category, mdpsolver.ConvergenceWarning) for w in caught):
|
|
226
|
+
sys.exit("Validation test failed at case 3: %s converged or gave no ConvergenceWarning, where the cycle should stop it at its limit." % label)
|
|
227
|
+
lower, middle, upper = bounds(mdl)
|
|
228
|
+
checkbounds(3, label, lower, upper, CYCLEGAIN)
|
|
229
|
+
if abs(middle - CYCLEGAIN) < LOOSE:
|
|
230
|
+
sys.exit("Validation test failed at case 3: %s gives the midpoint %.15g, which equals the gain, where the bounds [%.15g, %.15g] should be too far apart for that." % (label, middle, lower, upper))
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ------------------------------------------------------------------
|
|
234
|
+
# CASE 4: The bias, against known biases
|
|
235
|
+
# ------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
problems = [("the hub-and-spoke problem", HUBREWARDS, HUBTRANSITIONS, HUBBIAS)]
|
|
238
|
+
for seed in SEEDS:
|
|
239
|
+
rewards, transitions = randommodel(seed)
|
|
240
|
+
problems.append(("random model %d" % seed, rewards, transitions, exactaverage(rewards, transitions)[2]))
|
|
241
|
+
|
|
242
|
+
for problemlabel, rewards, transitions, knownbias in problems:
|
|
243
|
+
for algorithmlabel, algorithm in ALGORITHMS:
|
|
244
|
+
label = "%s (tolerance %g) on %s" % (algorithmlabel, TIGHT, problemlabel)
|
|
245
|
+
mdl = solve(rewards, transitions, algorithm=algorithm, tolerance=TIGHT, parallel=False)
|
|
246
|
+
if not mdl.hasConverged():
|
|
247
|
+
sys.exit("Validation test failed at case 4: %s did not converge." % label)
|
|
248
|
+
values = mdl.getValueVector()
|
|
249
|
+
for reference in range(len(rewards)):
|
|
250
|
+
for state in range(len(rewards)):
|
|
251
|
+
bias = mdl.getBias(state, referenceIndex=reference)
|
|
252
|
+
if not isinstance(bias, float) or bias != values[state] - values[reference]:
|
|
253
|
+
sys.exit("Validation test failed at case 4: getBias(%d, referenceIndex=%d) after %s gives %r, where it should be the float value of state %d minus that of state %d, %r." % (state, reference, label, bias, state, reference, values[state] - values[reference]))
|
|
254
|
+
difference = abs(bias - (knownbias[state] - knownbias[reference]))
|
|
255
|
+
if difference > BIASTOL:
|
|
256
|
+
sys.exit("Validation test failed at case 4: getBias(%d, referenceIndex=%d) after %s differs by %.3e from the known bias, which is more than %.1e." % (state, reference, label, difference, BIASTOL))
|
|
257
|
+
for state in range(len(rewards)):
|
|
258
|
+
if mdl.getBias(state) != mdl.getBias(state, referenceIndex=0):
|
|
259
|
+
sys.exit("Validation test failed at case 4: getBias(%d) after %s differs from getBias(%d, referenceIndex=0)." % (state, label, state))
|
|
260
|
+
if mdl.getBias(np.int64(2), referenceIndex=np.int64(1)) != mdl.getBias(2, referenceIndex=1):
|
|
261
|
+
sys.exit("Validation test failed at case 4: getBias() after %s gives another result with the NumPy integers 2 and 1 than with the integers." % label)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# ------------------------------------------------------------------
|
|
265
|
+
# CASE 5: Errors, the bound and the reference state
|
|
266
|
+
# ------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
mdl = mdpsolver.model()
|
|
269
|
+
mdl.mdp(discount=DISCOUNT, **arrayinput(HUBREWARDS, HUBTRANSITIONS, bycolumn=True))
|
|
270
|
+
CALLS = (("getGain()", lambda: mdl.getGain()), ("getBias(1)", lambda: mdl.getBias(1)))
|
|
271
|
+
for call, function in CALLS:
|
|
272
|
+
message = raises(RuntimeError, function)
|
|
273
|
+
if message is None or "solve()" not in message:
|
|
274
|
+
sys.exit("Validation test failed at case 5: %s before solve() gives %r, where it should raise a RuntimeError naming solve()." % (call, message))
|
|
275
|
+
|
|
276
|
+
quietly(lambda: mdl.solve(tolerance=LOOSE))
|
|
277
|
+
for call, function in CALLS:
|
|
278
|
+
message = raises(RuntimeError, function)
|
|
279
|
+
if message is None or "discounted" not in message:
|
|
280
|
+
sys.exit("Validation test failed at case 5: %s after a run under the discounted criterion gives %r, where it should raise a RuntimeError naming the criterion." % (call, message))
|
|
281
|
+
|
|
282
|
+
# a run under the average criterion after the discounted one (the note that
|
|
283
|
+
# the model has been solved before is printed)
|
|
284
|
+
quietly(lambda: mdl.solve(tolerance=TIGHT, criterion="average"))
|
|
285
|
+
lower, middle, upper = bounds(mdl)
|
|
286
|
+
checkbounds(5, "a run under the average criterion after a discounted one", lower, upper, HUBGAIN)
|
|
287
|
+
biases = np.array([mdl.getBias(s) for s in range(len(HUBREWARDS))])
|
|
288
|
+
if float(np.max(np.abs(biases - HUBBIAS))) > BIASTOL:
|
|
289
|
+
sys.exit("Validation test failed at case 5: getBias() after a run under the average criterion that followed a discounted one gives %s, where the known bias is %s." % (biases, HUBBIAS))
|
|
290
|
+
for name, expected in (("Lower", lower), ("LOWER", lower), ("upper", upper), ("Upper", upper), ("UPPER", upper), (None, middle)):
|
|
291
|
+
if mdl.getGain(bound=name) != expected:
|
|
292
|
+
sys.exit("Validation test failed at case 5: getGain(bound=%r) gives %r, where it should give %r." % (name, mdl.getGain(bound=name), expected))
|
|
293
|
+
for name in ("middle", "", "low", 1, True):
|
|
294
|
+
if raises(ValueError, lambda: mdl.getGain(bound=name)) is None:
|
|
295
|
+
sys.exit("Validation test failed at case 5: getGain(bound=%r) does not raise a ValueError." % (name,))
|
|
296
|
+
for index, error in ((-1, IndexError), (len(HUBREWARDS), IndexError), (1.0, TypeError), ("0", TypeError), (True, TypeError), (None, TypeError)):
|
|
297
|
+
for call, function in (("getBias(%r)" % (index,), lambda: mdl.getBias(index)),
|
|
298
|
+
("getBias(0, referenceIndex=%r)" % (index,), lambda: mdl.getBias(0, referenceIndex=index))):
|
|
299
|
+
if raises(error, function) is None:
|
|
300
|
+
sys.exit("Validation test failed at case 5: %s does not raise %s." % (call, error.__name__))
|
|
301
|
+
|
|
302
|
+
# and a run under the discounted criterion again
|
|
303
|
+
quietly(lambda: mdl.solve(tolerance=LOOSE))
|
|
304
|
+
for call, function in CALLS:
|
|
305
|
+
if raises(RuntimeError, function) is None:
|
|
306
|
+
sys.exit("Validation test failed at case 5: %s after a discounted run that followed an average one does not raise a RuntimeError." % call)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ------------------------------------------------------------------
|
|
310
|
+
# FINAL VALIDATION
|
|
311
|
+
# ------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
print(finalline(__file__))
|
|
@@ -3,8 +3,9 @@ UNIT TEST FOR CALLING THE METHODS OF A MODEL OBJECT IN THE WRONG ORDER.
|
|
|
3
3
|
|
|
4
4
|
A model object is used in three steps: mdp() defines the model, solve() solves
|
|
5
5
|
it, and the output methods (getPolicy(), getValueVector(), getAction(),
|
|
6
|
-
getValue(),
|
|
7
|
-
printValueVector() and saveToFile()) report
|
|
6
|
+
getValue(), getGain(), getBias(), getRuntime(), getIterations(),
|
|
7
|
+
hasConverged(), printPolicy(), printValueVector() and saveToFile()) report
|
|
8
|
+
the solution. checkModel() needs
|
|
8
9
|
only mdp(). Before a solution exists, the solver
|
|
9
10
|
holds a placeholder policy and value vector, a single -1 each, which the output
|
|
10
11
|
methods used to return as if it were a solution: getPolicy() gave [-1] and
|
|
@@ -42,7 +43,10 @@ Sub-tests:
|
|
|
42
43
|
getValueVector(), which are the known solution; printPolicy(),
|
|
43
44
|
printValueVector() and the file of saveToFile() with them; and
|
|
44
45
|
getIterations() and getRuntime() are a positive count and a time,
|
|
45
|
-
and hasConverged() is True.
|
|
46
|
+
and hasConverged() is True. The run is under the discounted
|
|
47
|
+
criterion, where getGain() and getBias() raise a RuntimeError
|
|
48
|
+
naming the average reward criterion (test_average.py tests them
|
|
49
|
+
under that criterion).
|
|
46
50
|
A later run of solve() rejected by its own checks leaves the
|
|
47
51
|
solution in place.
|
|
48
52
|
|
|
@@ -81,7 +85,7 @@ if NSTATES < 2 or -1 in HUBPOLICY or np.any(HUBVALUES == -1.0):
|
|
|
81
85
|
tmpdir = tempfile.TemporaryDirectory()
|
|
82
86
|
|
|
83
87
|
# The output methods, each with the arguments of a call
|
|
84
|
-
OUTPUTFILE = os.path.join(tmpdir.name, "policy.
|
|
88
|
+
OUTPUTFILE = os.path.join(tmpdir.name, "policy.npy")
|
|
85
89
|
OUTPUTMETHODS = {
|
|
86
90
|
"getRuntime": dict(),
|
|
87
91
|
"getIterations": dict(),
|
|
@@ -90,6 +94,8 @@ OUTPUTMETHODS = {
|
|
|
90
94
|
"printValueVector": dict(),
|
|
91
95
|
"getAction": dict(stateIndex=1),
|
|
92
96
|
"getValue": dict(stateIndex=1),
|
|
97
|
+
"getGain": dict(),
|
|
98
|
+
"getBias": dict(stateIndex=1),
|
|
93
99
|
"getPolicy": dict(),
|
|
94
100
|
"getValueVector": dict(),
|
|
95
101
|
"saveToFile": dict(fileName=OUTPUTFILE, type="policy"),
|
|
@@ -231,6 +237,12 @@ for rejected in (False, True):
|
|
|
231
237
|
results, printed = {}, {}
|
|
232
238
|
for method in OUTPUTMETHODS:
|
|
233
239
|
removeoutputfile()
|
|
240
|
+
if method in ("getGain", "getBias"):
|
|
241
|
+
# (the gain and the bias exist under the average reward criterion only)
|
|
242
|
+
error, output = attempt(lambda: call(mdl, method))
|
|
243
|
+
if type(error) is not RuntimeError or "average" not in str(error) or output:
|
|
244
|
+
sys.exit("Validation test failed at case 3: %s() %s under the discounted criterion gave %r and printed %r, where it should raise a RuntimeError naming the average reward criterion and print nothing else." % (method, after, error, output.strip()))
|
|
245
|
+
continue
|
|
234
246
|
try:
|
|
235
247
|
results[method], printed[method] = quietly(lambda: call(mdl, method))
|
|
236
248
|
except Exception as error:
|
|
@@ -254,11 +266,9 @@ for rejected in (False, True):
|
|
|
254
266
|
or not np.allclose([float(line.split(": ")[1]) for line in lines], values, rtol=1e-5):
|
|
255
267
|
sys.exit("Validation test failed at case 3: printValueVector() %s printed %r, where it should print the %d values of getValueVector()." % (after, printed["printValueVector"].strip(), NSTATES))
|
|
256
268
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if rows != expected:
|
|
261
|
-
sys.exit("Validation test failed at case 3: saveToFile() %s wrote %r, where it should write %r." % (after, rows, expected))
|
|
269
|
+
saved = np.load(OUTPUTFILE, allow_pickle=False)
|
|
270
|
+
if list(saved) != policy:
|
|
271
|
+
sys.exit("Validation test failed at case 3: saveToFile() %s wrote %r, where it should write %r." % (after, list(saved), policy))
|
|
262
272
|
|
|
263
273
|
iterations, runtime = results["getIterations"], results["getRuntime"]
|
|
264
274
|
if not isinstance(iterations, int) or iterations < 1:
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
UNIT TEST FOR THE OUTPUT OF A SOLVED MODEL: saveToFile(), getAction(),
|
|
3
3
|
getValue() AND THE REPORT ON CONVERGENCE.
|
|
4
4
|
|
|
5
|
-
saveToFile() writes the policy or the value vector to a
|
|
6
|
-
|
|
5
|
+
saveToFile() writes the policy or the value vector to a NumPy .npy file
|
|
6
|
+
(filetype="npy", the default) or to a comma-separated file (filetype="csv");
|
|
7
|
+
until 0.12.0 it wrote only comma-separated files. type="value" used to write nothing, since the compiled module knew only
|
|
7
8
|
"values", and a file that could not be opened was reported on stderr only.
|
|
8
9
|
getAction() and getValue() used to return -1 and 0.0 for a state outside the
|
|
9
10
|
model, and 0.0 cannot be told from a real value; they now raise IndexError.
|
|
@@ -18,12 +19,20 @@ limit, when the values become infinite or NaN, or when the final check
|
|
|
18
19
|
discount allow.
|
|
19
20
|
|
|
20
21
|
Sub-tests:
|
|
21
|
-
Case 1: saveToFile() with type="policy" and type="value" writes
|
|
22
|
-
|
|
23
|
-
(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
file.
|
|
22
|
+
Case 1: saveToFile() with type="policy" and type="value" writes by
|
|
23
|
+
default an .npy file that np.load reads as the array of
|
|
24
|
+
getPolicy() (int64) or getValueVector() (float64), and with
|
|
25
|
+
filetype="csv" a header and one row per state, equal to them (to
|
|
26
|
+
the six significant digits written); both replace a file that is
|
|
27
|
+
there. Without a file name, it writes result.npy, or result.csv
|
|
28
|
+
with filetype="csv", in the working directory. The filetype may be
|
|
29
|
+
in any case, e.g. "NPY" or "Csv", also in the compiled module,
|
|
30
|
+
and a default name is then in lower case. An unknown type or
|
|
31
|
+
filetype, and a name ending in .csv with filetype="npy" or in .npy
|
|
32
|
+
with filetype="csv" (in any case), raise a ValueError, also in the
|
|
33
|
+
compiled module, whose message for a name names the filetype to
|
|
34
|
+
use; a file in a folder that does not exist raises a
|
|
35
|
+
FileNotFoundError. None of them writes a file.
|
|
27
36
|
Case 2: getAction() and getValue() agree with getPolicy() and
|
|
28
37
|
getValueVector() in every state, and raise an IndexError naming the
|
|
29
38
|
index for a state outside the model.
|
|
@@ -137,6 +146,14 @@ def readrows(path):
|
|
|
137
146
|
return file.read().strip().splitlines()
|
|
138
147
|
|
|
139
148
|
|
|
149
|
+
def loadednpy(path):
|
|
150
|
+
'''The array np.load reads from an .npy file, or the exception it raises.'''
|
|
151
|
+
try:
|
|
152
|
+
return np.load(path, allow_pickle=False)
|
|
153
|
+
except Exception as error:
|
|
154
|
+
return error
|
|
155
|
+
|
|
156
|
+
|
|
140
157
|
mdl, caught, output = solved(HUBMODEL)
|
|
141
158
|
if output or caught:
|
|
142
159
|
sys.exit("Validation test failed at initialization: solving the hub-and-spoke problem printed %r and warned %r." % (output.strip(), [str(w.message) for w in caught]))
|
|
@@ -149,29 +166,84 @@ if POLICY != HUBPOLICY or float(np.max(np.abs(np.array(VALUES) - HUBVALUES))) >
|
|
|
149
166
|
# CASE 1: saveToFile()
|
|
150
167
|
# ------------------------------------------------------------------
|
|
151
168
|
|
|
152
|
-
for kind, header, column in (("policy", "State_Index,Action_Index", POLICY), ("value", "State_Index,Value", VALUES)):
|
|
153
|
-
|
|
169
|
+
for kind, header, column, dtype in (("policy", "State_Index,Action_Index", POLICY, np.int64), ("value", "State_Index,Value", VALUES, np.float64)):
|
|
170
|
+
# The default filetype: the array of getPolicy() or getValueVector()
|
|
171
|
+
path = os.path.join(tmpdir.name, "%s.npy" % kind)
|
|
154
172
|
with open(path, "w") as file:
|
|
155
173
|
file.write("this file is replaced\n")
|
|
156
174
|
error, output = raises(lambda: mdl.saveToFile(fileName=path, type=kind))
|
|
157
175
|
if error is not None or output:
|
|
158
176
|
sys.exit("Validation test failed at case 1: saveToFile(type=%r) raised %r and printed %r." % (kind, error, output.strip()))
|
|
177
|
+
array = loadednpy(path)
|
|
178
|
+
if not isinstance(array, np.ndarray) or array.dtype != dtype or array.shape != (NSTATES,) or not np.array_equal(array, column):
|
|
179
|
+
sys.exit("Validation test failed at case 1: saveToFile(type=%r) wrote a file that np.load reads as %r, where it should be the %s array %r." % (kind, array, np.dtype(dtype).name, column))
|
|
180
|
+
|
|
181
|
+
# filetype="csv": a header and a row per state
|
|
182
|
+
path = os.path.join(tmpdir.name, "%s.csv" % kind)
|
|
183
|
+
with open(path, "w") as file:
|
|
184
|
+
file.write("this file is replaced\n")
|
|
185
|
+
error, output = raises(lambda: mdl.saveToFile(fileName=path, type=kind, filetype="csv"))
|
|
186
|
+
if error is not None or output:
|
|
187
|
+
sys.exit("Validation test failed at case 1: saveToFile(type=%r, filetype='csv') raised %r and printed %r." % (kind, error, output.strip()))
|
|
159
188
|
rows = readrows(path)
|
|
160
189
|
if not rows or rows[0] != header or len(rows) != NSTATES + 1:
|
|
161
|
-
sys.exit("Validation test failed at case 1: saveToFile(type=%r) wrote %r, where it should write the header %r and %d rows." % (kind, rows, header, NSTATES))
|
|
190
|
+
sys.exit("Validation test failed at case 1: saveToFile(type=%r, filetype='csv') wrote %r, where it should write the header %r and %d rows." % (kind, rows, header, NSTATES))
|
|
162
191
|
for s, row in enumerate(rows[1:]):
|
|
163
192
|
index, entry = row.split(",")
|
|
164
193
|
if int(index) != s or not np.isclose(float(entry), column[s], rtol=1e-5, atol=0.0):
|
|
165
|
-
sys.exit("Validation test failed at case 1: saveToFile(type=%r) wrote the row %r for state %d, where its %s is %r." % (kind, row, s, kind, column[s]))
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
194
|
+
sys.exit("Validation test failed at case 1: saveToFile(type=%r, filetype='csv') wrote the row %r for state %d, where its %s is %r." % (kind, row, s, kind, column[s]))
|
|
195
|
+
|
|
196
|
+
# Without a file name: result.npy or result.csv in the working directory (in
|
|
197
|
+
# lower case also for a filetype in upper case)
|
|
198
|
+
folder = os.path.join(tmpdir.name, "defaults")
|
|
199
|
+
os.mkdir(folder)
|
|
200
|
+
here = os.getcwd()
|
|
201
|
+
os.chdir(folder)
|
|
202
|
+
try:
|
|
203
|
+
results = [raises(lambda: mdl.saveToFile(type="value")), raises(lambda: mdl.saveToFile(type="value", filetype="CSV"))]
|
|
204
|
+
finally:
|
|
205
|
+
os.chdir(here)
|
|
206
|
+
for (error, output), given in zip(results, ("", ", filetype='CSV'")):
|
|
207
|
+
if error is not None or output:
|
|
208
|
+
sys.exit("Validation test failed at case 1: saveToFile(type='value'%s) without a file name raised %r and printed %r." % (given, error, output.strip()))
|
|
209
|
+
if sorted(os.listdir(folder)) != ["result.csv", "result.npy"]:
|
|
210
|
+
sys.exit("Validation test failed at case 1: saveToFile() without a file name wrote %s, where it should write result.npy, and result.csv with filetype='csv'." % sorted(os.listdir(folder)))
|
|
211
|
+
array, rows = loadednpy(os.path.join(folder, "result.npy")), readrows(os.path.join(folder, "result.csv"))
|
|
212
|
+
if not isinstance(array, np.ndarray) or not np.array_equal(array, VALUES) or rows[0] != "State_Index,Value" or len(rows) != NSTATES + 1:
|
|
213
|
+
sys.exit("Validation test failed at case 1: saveToFile(type='value') without a file name wrote %r to result.npy and %r to result.csv, where they should hold the value vector." % (array, rows))
|
|
214
|
+
|
|
215
|
+
# The filetype in any case, also in the compiled module
|
|
216
|
+
for filetype in ("NPY", "Csv"):
|
|
217
|
+
for name, method in (("saveToFile()", mdl.saveToFile), ("The compiled module's saveToFile()", mdl.mdl.saveToFile)):
|
|
218
|
+
path = os.path.join(tmpdir.name, "anycase.%s" % filetype.lower())
|
|
219
|
+
if os.path.exists(path):
|
|
220
|
+
os.remove(path)
|
|
221
|
+
error, output = raises(lambda: method(fileName=path, type="value", filetype=filetype))
|
|
222
|
+
if error is not None or output:
|
|
223
|
+
sys.exit("Validation test failed at case 1: %s with filetype=%r raised %r and printed %r." % (name, filetype, error, output.strip()))
|
|
224
|
+
saved = loadednpy(path) if filetype.lower() == "npy" else readrows(path)
|
|
225
|
+
if (filetype.lower() == "npy" and not (isinstance(saved, np.ndarray) and np.array_equal(saved, VALUES))) \
|
|
226
|
+
or (filetype.lower() == "csv" and (saved[0] != "State_Index,Value" or len(saved) != NSTATES + 1)):
|
|
227
|
+
sys.exit("Validation test failed at case 1: %s with filetype=%r wrote %r, where it should write the value vector as a %s file." % (name, filetype, saved, filetype.lower()))
|
|
228
|
+
|
|
229
|
+
# Arguments that are not used, and a file that cannot be opened (the compiled
|
|
230
|
+
# module makes the same checks of the arguments)
|
|
231
|
+
for label, given, expected, words in (
|
|
232
|
+
("an unknown type", dict(fileName="result.npy", type="values2"), ValueError, "'policy' or 'value'"),
|
|
233
|
+
("an unknown filetype", dict(fileName="result.npy", filetype="npz"), ValueError, "'npy' or 'csv'"),
|
|
234
|
+
("a name ending in .csv and the default filetype", dict(fileName="result.csv"), ValueError, "filetype='csv'"),
|
|
235
|
+
("a name ending in .CSV and filetype='npy'", dict(fileName="result.CSV", filetype="npy"), ValueError, "filetype='csv'"),
|
|
236
|
+
("a name ending in .npy and filetype='csv'", dict(fileName="result.npy", filetype="csv"), ValueError, "filetype='npy'"),
|
|
237
|
+
("a name ending in .npy and filetype='CSV'", dict(fileName="result.npy", filetype="CSV"), ValueError, "filetype='npy'"),
|
|
238
|
+
("a file in a folder that does not exist", dict(fileName=os.path.join("missing", "result.npy")), FileNotFoundError, "missing")):
|
|
239
|
+
path = os.path.join(tmpdir.name, given["fileName"])
|
|
240
|
+
methods = [("saveToFile()", mdl.saveToFile)] + ([("The compiled module's saveToFile()", mdl.mdl.saveToFile)] if expected is ValueError else [])
|
|
241
|
+
for name, method in methods:
|
|
242
|
+
error, output = raises(lambda: method(**{**given, "fileName": path}))
|
|
243
|
+
if not isinstance(error, expected) or words not in str(error):
|
|
244
|
+
sys.exit("Validation test failed at case 1: %s with %s raised %r, where it should raise a %s that says %r." % (name, label, error, expected.__name__, words))
|
|
245
|
+
if output or os.path.exists(path):
|
|
246
|
+
sys.exit("Validation test failed at case 1: %s with %s printed %r or wrote a file." % (name, label, output.strip()))
|
|
175
247
|
|
|
176
248
|
|
|
177
249
|
# ------------------------------------------------------------------
|
|
@@ -30,7 +30,7 @@ MDPSolver is a Python package for large Markov Decision Processes (MDPs) with in
|
|
|
30
30
|
| 5,000,000 | 500,000,000 | 6.3 GB | 5.03 s |
|
|
31
31
|
| 10,000,000 | 1,000,000,000 | 12.7 GB | 13.0 s |
|
|
32
32
|
|
|
33
|
-
Mean of 10 random models with 5 actions per state and 20 possible next states per state and action, loaded from NPY-files and solved by modified policy iteration under the discounted reward criterion with discount 0.9. Measured on an Apple M5 with 10 cores.
|
|
33
|
+
Mean of 10 random models with 5 actions per state and 20 possible next states per state and action, loaded from NPY-files and solved by modified policy iteration under the discounted reward criterion with discount $\lambda=0.9$ and tolerance $\epsilon = 0.001$. Measured on an Apple M5 with 10 cores.
|
|
34
34
|
|
|
35
35
|
# Installation
|
|
36
36
|
|
|
@@ -30,7 +30,7 @@ CMAKE_BUILD_TYPE = "Release"
|
|
|
30
30
|
|
|
31
31
|
[project]
|
|
32
32
|
name = "mdpsolver"
|
|
33
|
-
version = "0.12.
|
|
33
|
+
version = "0.12.1"
|
|
34
34
|
authors = [
|
|
35
35
|
{ name="Anders Reenberg Andersen", email="andersra@live.dk" },
|
|
36
36
|
{ name="Jesper Fink Andersen", email="jesperfinkandersen@gmail.com" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|