├── .gitignore ├── LICENSE.md ├── README.md ├── pyomotools ├── __init__.py ├── option.py ├── piecewise_nd.py ├── proxy.py ├── rapper.py └── tools.py └── sddp ├── SDDP.py ├── __init__.py ├── base_utilities.py ├── cut_oracles ├── DefaultCutOracle.py ├── LevelOneCutOracle.py └── __init__.py ├── defaultvaluefunction.py ├── example ├── HydroValley │ ├── __init__.py │ ├── hydro_valley.py │ ├── test.py │ └── test1.py ├── Newsvendor │ ├── __init__.py │ ├── newsvendor.py │ └── test1.py ├── __init__.py ├── simple_object_noise.py └── tools.py ├── noises.py ├── price_interpolation ├── __init__.py ├── discreate_distribution.py ├── dynamic_price_interpolation.py ├── dynamic_price_interpolation_oracle.py ├── price_interpolation.py └── static_price_interpolation.py ├── print.py ├── riskmeasures.py ├── sddip ├── __init__.py ├── binary_expansion.py └── solver.py ├── state.py ├── test ├── __init__.py ├── test_list.py ├── test_matplotlib.py └── test_pyomo.py ├── typedefinitions.py └── utilities.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The sddpy package is licensed under the Mozilla Public License, Version 2.0: 2 | 3 | > Copyright (c) 2017-2018: Oscar Dowson. 4 | > 5 | > 6 | > Mozilla Public License, version 2.0 7 | > 8 | > 9 | > 10 | > 1. Definitions 11 | > 12 | > 13 | > 14 | > 1.1. "Contributor" 15 | > 16 | > 17 | > 18 | > means each individual or legal entity that creates, contributes to the 19 | > 20 | > creation of, or owns Covered Software. 21 | > 22 | > 23 | > 24 | > 1.2. "Contributor Version" 25 | > 26 | > 27 | > 28 | > means the combination of the Contributions of others (if any) used by a 29 | > 30 | > Contributor and that particular Contributor's Contribution. 31 | > 32 | > 33 | > 34 | > 1.3. "Contribution" 35 | > 36 | > 37 | > 38 | > means Covered Software of a particular Contributor. 39 | > 40 | > 41 | > 42 | > 1.4. "Covered Software" 43 | > 44 | > 45 | > 46 | > means Source Code Form to which the initial Contributor has attached the 47 | > 48 | > notice in Exhibit A, the Executable Form of such Source Code Form, and 49 | > 50 | > Modifications of such Source Code Form, in each case including portions 51 | > 52 | > thereof. 53 | > 54 | > 55 | > 56 | > 1.5. "Incompatible With Secondary Licenses" 57 | > 58 | > means 59 | > 60 | > 61 | > 62 | > a. that the initial Contributor has attached the notice described in 63 | > 64 | > Exhibit B to the Covered Software; or 65 | > 66 | > 67 | > 68 | > b. that the Covered Software was made available under the terms of 69 | > 70 | > version 1.1 or earlier of the License, but not also under the terms of 71 | > 72 | > a Secondary License. 73 | > 74 | > 75 | > 76 | > 1.6. "Executable Form" 77 | > 78 | > 79 | > 80 | > means any form of the work other than Source Code Form. 81 | > 82 | > 83 | > 84 | > 1.7. "Larger Work" 85 | > 86 | > 87 | > 88 | > means a work that combines Covered Software with other material, in a 89 | > 90 | > separate file or files, that is not Covered Software. 91 | > 92 | > 93 | > 94 | > 1.8. "License" 95 | > 96 | > 97 | > 98 | > means this document. 99 | > 100 | > 101 | > 102 | > 1.9. "Licensable" 103 | > 104 | > 105 | > 106 | > means having the right to grant, to the maximum extent possible, whether 107 | > 108 | > at the time of the initial grant or subsequently, any and all of the 109 | > 110 | > rights conveyed by this License. 111 | > 112 | > 113 | > 114 | > 1.10. "Modifications" 115 | > 116 | > 117 | > 118 | > means any of the following: 119 | > 120 | > 121 | > 122 | > a. any file in Source Code Form that results from an addition to, 123 | > 124 | > deletion from, or modification of the contents of Covered Software; or 125 | > 126 | > 127 | > 128 | > b. any new file in Source Code Form that contains any Covered Software. 129 | > 130 | > 131 | > 132 | > 1.11. "Patent Claims" of a Contributor 133 | > 134 | > 135 | > 136 | > means any patent claim(s), including without limitation, method, 137 | > 138 | > process, and apparatus claims, in any patent Licensable by such 139 | > 140 | > Contributor that would be infringed, but for the grant of the License, 141 | > 142 | > by the making, using, selling, offering for sale, having made, import, 143 | > 144 | > or transfer of either its Contributions or its Contributor Version. 145 | > 146 | > 147 | > 148 | > 1.12. "Secondary License" 149 | > 150 | > 151 | > 152 | > means either the GNU General Public License, Version 2.0, the GNU Lesser 153 | > 154 | > General Public License, Version 2.1, the GNU Affero General Public 155 | > 156 | > License, Version 3.0, or any later versions of those licenses. 157 | > 158 | > 159 | > 160 | > 1.13. "Source Code Form" 161 | > 162 | > 163 | > 164 | > means the form of the work preferred for making modifications. 165 | > 166 | > 167 | > 168 | > 1.14. "You" (or "Your") 169 | > 170 | > 171 | > 172 | > means an individual or a legal entity exercising rights under this 173 | > 174 | > License. For legal entities, "You" includes any entity that controls, is 175 | > 176 | > controlled by, or is under common control with You. For purposes of this 177 | > 178 | > definition, "control" means (a) the power, direct or indirect, to cause 179 | > 180 | > the direction or management of such entity, whether by contract or 181 | > 182 | > otherwise, or (b) ownership of more than fifty percent (50%) of the 183 | > 184 | > outstanding shares or beneficial ownership of such entity. 185 | > 186 | > 187 | > 188 | > 189 | > 190 | > 2. License Grants and Conditions 191 | > 192 | > 193 | > 194 | > 2.1. Grants 195 | > 196 | > 197 | > 198 | > Each Contributor hereby grants You a world-wide, royalty-free, 199 | > 200 | > non-exclusive license: 201 | > 202 | > 203 | > 204 | > a. under intellectual property rights (other than patent or trademark) 205 | > 206 | > Licensable by such Contributor to use, reproduce, make available, 207 | > 208 | > modify, display, perform, distribute, and otherwise exploit its 209 | > 210 | > Contributions, either on an unmodified basis, with Modifications, or 211 | > 212 | > as part of a Larger Work; and 213 | > 214 | > 215 | > 216 | > b. under Patent Claims of such Contributor to make, use, sell, offer for 217 | > 218 | > sale, have made, import, and otherwise transfer either its 219 | > 220 | > Contributions or its Contributor Version. 221 | > 222 | > 223 | > 224 | > 2.2. Effective Date 225 | > 226 | > 227 | > 228 | > The licenses granted in Section 2.1 with respect to any Contribution 229 | > 230 | > become effective for each Contribution on the date the Contributor first 231 | > 232 | > distributes such Contribution. 233 | > 234 | > 235 | > 236 | > 2.3. Limitations on Grant Scope 237 | > 238 | > 239 | > 240 | > The licenses granted in this Section 2 are the only rights granted under 241 | > 242 | > this License. No additional rights or licenses will be implied from the 243 | > 244 | > distribution or licensing of Covered Software under this License. 245 | > 246 | > Notwithstanding Section 2.1(b) above, no patent license is granted by a 247 | > 248 | > Contributor: 249 | > 250 | > 251 | > 252 | > a. for any code that a Contributor has removed from Covered Software; or 253 | > 254 | > 255 | > 256 | > b. for infringements caused by: (i) Your and any other third party's 257 | > 258 | > modifications of Covered Software, or (ii) the combination of its 259 | > 260 | > Contributions with other software (except as part of its Contributor 261 | > 262 | > Version); or 263 | > 264 | > 265 | > 266 | > c. under Patent Claims infringed by Covered Software in the absence of 267 | > 268 | > its Contributions. 269 | > 270 | > 271 | > 272 | > This License does not grant any rights in the trademarks, service marks, 273 | > 274 | > or logos of any Contributor (except as may be necessary to comply with 275 | > 276 | > the notice requirements in Section 3.4). 277 | > 278 | > 279 | > 280 | > 2.4. Subsequent Licenses 281 | > 282 | > 283 | > 284 | > No Contributor makes additional grants as a result of Your choice to 285 | > 286 | > distribute the Covered Software under a subsequent version of this 287 | > 288 | > License (see Section 10.2) or under the terms of a Secondary License (if 289 | > 290 | > permitted under the terms of Section 3.3). 291 | > 292 | > 293 | > 294 | > 2.5. Representation 295 | > 296 | > 297 | > 298 | > Each Contributor represents that the Contributor believes its 299 | > 300 | > Contributions are its original creation(s) or it has sufficient rights to 301 | > 302 | > grant the rights to its Contributions conveyed by this License. 303 | > 304 | > 305 | > 306 | > 2.6. Fair Use 307 | > 308 | > 309 | > 310 | > This License is not intended to limit any rights You have under 311 | > 312 | > applicable copyright doctrines of fair use, fair dealing, or other 313 | > 314 | > equivalents. 315 | > 316 | > 317 | > 318 | > 2.7. Conditions 319 | > 320 | > 321 | > 322 | > Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 323 | > 324 | > Section 2.1. 325 | > 326 | > 327 | > 328 | > 329 | > 330 | > 3. Responsibilities 331 | > 332 | > 333 | > 334 | > 3.1. Distribution of Source Form 335 | > 336 | > 337 | > 338 | > All distribution of Covered Software in Source Code Form, including any 339 | > 340 | > Modifications that You create or to which You contribute, must be under 341 | > 342 | > the terms of this License. You must inform recipients that the Source 343 | > 344 | > Code Form of the Covered Software is governed by the terms of this 345 | > 346 | > License, and how they can obtain a copy of this License. You may not 347 | > 348 | > attempt to alter or restrict the recipients' rights in the Source Code 349 | > 350 | > Form. 351 | > 352 | > 353 | > 354 | > 3.2. Distribution of Executable Form 355 | > 356 | > 357 | > 358 | > If You distribute Covered Software in Executable Form then: 359 | > 360 | > 361 | > 362 | > a. such Covered Software must also be made available in Source Code Form, 363 | > 364 | > as described in Section 3.1, and You must inform recipients of the 365 | > 366 | > Executable Form how they can obtain a copy of such Source Code Form by 367 | > 368 | > reasonable means in a timely manner, at a charge no more than the cost 369 | > 370 | > of distribution to the recipient; and 371 | > 372 | > 373 | > 374 | > b. You may distribute such Executable Form under the terms of this 375 | > 376 | > License, or sublicense it under different terms, provided that the 377 | > 378 | > license for the Executable Form does not attempt to limit or alter the 379 | > 380 | > recipients' rights in the Source Code Form under this License. 381 | > 382 | > 383 | > 384 | > 3.3. Distribution of a Larger Work 385 | > 386 | > 387 | > 388 | > You may create and distribute a Larger Work under terms of Your choice, 389 | > 390 | > provided that You also comply with the requirements of this License for 391 | > 392 | > the Covered Software. If the Larger Work is a combination of Covered 393 | > 394 | > Software with a work governed by one or more Secondary Licenses, and the 395 | > 396 | > Covered Software is not Incompatible With Secondary Licenses, this 397 | > 398 | > License permits You to additionally distribute such Covered Software 399 | > 400 | > under the terms of such Secondary License(s), so that the recipient of 401 | > 402 | > the Larger Work may, at their option, further distribute the Covered 403 | > 404 | > Software under the terms of either this License or such Secondary 405 | > 406 | > License(s). 407 | > 408 | > 409 | > 410 | > 3.4. Notices 411 | > 412 | > 413 | > 414 | > You may not remove or alter the substance of any license notices 415 | > 416 | > (including copyright notices, patent notices, disclaimers of warranty, or 417 | > 418 | > limitations of liability) contained within the Source Code Form of the 419 | > 420 | > Covered Software, except that You may alter any license notices to the 421 | > 422 | > extent required to remedy known factual inaccuracies. 423 | > 424 | > 425 | > 426 | > 3.5. Application of Additional Terms 427 | > 428 | > 429 | > 430 | > You may choose to offer, and to charge a fee for, warranty, support, 431 | > 432 | > indemnity or liability obligations to one or more recipients of Covered 433 | > 434 | > Software. However, You may do so only on Your own behalf, and not on 435 | > 436 | > behalf of any Contributor. You must make it absolutely clear that any 437 | > 438 | > such warranty, support, indemnity, or liability obligation is offered by 439 | > 440 | > You alone, and You hereby agree to indemnify every Contributor for any 441 | > 442 | > liability incurred by such Contributor as a result of warranty, support, 443 | > 444 | > indemnity or liability terms You offer. You may include additional 445 | > 446 | > disclaimers of warranty and limitations of liability specific to any 447 | > 448 | > jurisdiction. 449 | > 450 | > 451 | > 452 | > 4. Inability to Comply Due to Statute or Regulation 453 | > 454 | > 455 | > 456 | > If it is impossible for You to comply with any of the terms of this License 457 | > 458 | > with respect to some or all of the Covered Software due to statute, 459 | > 460 | > judicial order, or regulation then You must: (a) comply with the terms of 461 | > 462 | > this License to the maximum extent possible; and (b) describe the 463 | > 464 | > limitations and the code they affect. Such description must be placed in a 465 | > 466 | > text file included with all distributions of the Covered Software under 467 | > 468 | > this License. Except to the extent prohibited by statute or regulation, 469 | > 470 | > such description must be sufficiently detailed for a recipient of ordinary 471 | > 472 | > skill to be able to understand it. 473 | > 474 | > 475 | > 476 | > 5. Termination 477 | > 478 | > 479 | > 480 | > 5.1. The rights granted under this License will terminate automatically if You 481 | > 482 | > fail to comply with any of its terms. However, if You become compliant, 483 | > 484 | > then the rights granted under this License from a particular Contributor 485 | > 486 | > are reinstated (a) provisionally, unless and until such Contributor 487 | > 488 | > explicitly and finally terminates Your grants, and (b) on an ongoing 489 | > 490 | > basis, if such Contributor fails to notify You of the non-compliance by 491 | > 492 | > some reasonable means prior to 60 days after You have come back into 493 | > 494 | > compliance. Moreover, Your grants from a particular Contributor are 495 | > 496 | > reinstated on an ongoing basis if such Contributor notifies You of the 497 | > 498 | > non-compliance by some reasonable means, this is the first time You have 499 | > 500 | > received notice of non-compliance with this License from such 501 | > 502 | > Contributor, and You become compliant prior to 30 days after Your receipt 503 | > 504 | > of the notice. 505 | > 506 | > 507 | > 508 | > 5.2. If You initiate litigation against any entity by asserting a patent 509 | > 510 | > infringement claim (excluding declaratory judgment actions, 511 | > 512 | > counter-claims, and cross-claims) alleging that a Contributor Version 513 | > 514 | > directly or indirectly infringes any patent, then the rights granted to 515 | > 516 | > You by any and all Contributors for the Covered Software under Section 517 | > 518 | > 2.1 of this License shall terminate. 519 | > 520 | > 521 | > 522 | > 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 523 | > 524 | > license agreements (excluding distributors and resellers) which have been 525 | > 526 | > validly granted by You or Your distributors under this License prior to 527 | > 528 | > termination shall survive termination. 529 | > 530 | > 531 | > 532 | > 6. Disclaimer of Warranty 533 | > 534 | > 535 | > 536 | > Covered Software is provided under this License on an "as is" basis, 537 | > 538 | > without warranty of any kind, either expressed, implied, or statutory, 539 | > 540 | > including, without limitation, warranties that the Covered Software is free 541 | > 542 | > of defects, merchantable, fit for a particular purpose or non-infringing. 543 | > 544 | > The entire risk as to the quality and performance of the Covered Software 545 | > 546 | > is with You. Should any Covered Software prove defective in any respect, 547 | > 548 | > You (not any Contributor) assume the cost of any necessary servicing, 549 | > 550 | > repair, or correction. This disclaimer of warranty constitutes an essential 551 | > 552 | > part of this License. No use of any Covered Software is authorized under 553 | > 554 | > this License except under this disclaimer. 555 | > 556 | > 557 | > 558 | > 7. Limitation of Liability 559 | > 560 | > 561 | > 562 | > Under no circumstances and under no legal theory, whether tort (including 563 | > 564 | > negligence), contract, or otherwise, shall any Contributor, or anyone who 565 | > 566 | > distributes Covered Software as permitted above, be liable to You for any 567 | > 568 | > direct, indirect, special, incidental, or consequential damages of any 569 | > 570 | > character including, without limitation, damages for lost profits, loss of 571 | > 572 | > goodwill, work stoppage, computer failure or malfunction, or any and all 573 | > 574 | > other commercial damages or losses, even if such party shall have been 575 | > 576 | > informed of the possibility of such damages. This limitation of liability 577 | > 578 | > shall not apply to liability for death or personal injury resulting from 579 | > 580 | > such party's negligence to the extent applicable law prohibits such 581 | > 582 | > limitation. Some jurisdictions do not allow the exclusion or limitation of 583 | > 584 | > incidental or consequential damages, so this exclusion and limitation may 585 | > 586 | > not apply to You. 587 | > 588 | > 589 | > 590 | > 8. Litigation 591 | > 592 | > 593 | > 594 | > Any litigation relating to this License may be brought only in the courts 595 | > 596 | > of a jurisdiction where the defendant maintains its principal place of 597 | > 598 | > business and such litigation shall be governed by laws of that 599 | > 600 | > jurisdiction, without reference to its conflict-of-law provisions. Nothing 601 | > 602 | > in this Section shall prevent a party's ability to bring cross-claims or 603 | > 604 | > counter-claims. 605 | > 606 | > 607 | > 608 | > 9. Miscellaneous 609 | > 610 | > 611 | > 612 | > This License represents the complete agreement concerning the subject 613 | > 614 | > matter hereof. If any provision of this License is held to be 615 | > 616 | > unenforceable, such provision shall be reformed only to the extent 617 | > 618 | > necessary to make it enforceable. Any law or regulation which provides that 619 | > 620 | > the language of a contract shall be construed against the drafter shall not 621 | > 622 | > be used to construe this License against a Contributor. 623 | > 624 | > 625 | > 626 | > 627 | > 628 | > 10. Versions of the License 629 | > 630 | > 631 | > 632 | > 10.1. New Versions 633 | > 634 | > 635 | > 636 | > Mozilla Foundation is the license steward. Except as provided in Section 637 | > 638 | > 10.3, no one other than the license steward has the right to modify or 639 | > 640 | > publish new versions of this License. Each version will be given a 641 | > 642 | > distinguishing version number. 643 | > 644 | > 645 | > 646 | > 10.2. Effect of New Versions 647 | > 648 | > 649 | > 650 | > You may distribute the Covered Software under the terms of the version 651 | > 652 | > of the License under which You originally received the Covered Software, 653 | > 654 | > or under the terms of any subsequent version published by the license 655 | > 656 | > steward. 657 | > 658 | > 659 | > 660 | > 10.3. Modified Versions 661 | > 662 | > 663 | > 664 | > If you create software not governed by this License, and you want to 665 | > 666 | > create a new license for such software, you may create and use a 667 | > 668 | > modified version of this License if you rename the license and remove 669 | > 670 | > any references to the name of the license steward (except to note that 671 | > 672 | > such modified license differs from this License). 673 | > 674 | > 675 | > 676 | > 10.4. Distributing Source Code Form that is Incompatible With Secondary 677 | > 678 | > Licenses If You choose to distribute Source Code Form that is 679 | > 680 | > Incompatible With Secondary Licenses under the terms of this version of 681 | > 682 | > the License, the notice described in Exhibit B of this License must be 683 | > 684 | > attached. 685 | > 686 | > 687 | > 688 | > Exhibit A - Source Code Form License Notice 689 | > 690 | > 691 | > 692 | > This Source Code Form is subject to the 693 | > 694 | > terms of the Mozilla Public License, v. 695 | > 696 | > 2.0. If a copy of the MPL was not 697 | > 698 | > distributed with this file, You can 699 | > 700 | > obtain one at 701 | > 702 | > http://mozilla.org/MPL/2.0/. 703 | > 704 | > 705 | > 706 | > If it is not possible or desirable to put the notice in a particular file, 707 | > 708 | > then You may include the notice in a location (such as a LICENSE file in a 709 | > 710 | > relevant directory) where a recipient would be likely to look for such a 711 | > 712 | > notice. 713 | > 714 | > 715 | > 716 | > You may add additional accurate notices of copyright ownership. 717 | > 718 | > 719 | > 720 | > Exhibit B - "Incompatible With Secondary Licenses" Notice 721 | > 722 | > 723 | > 724 | > This Source Code Form is "Incompatible 725 | > 726 | > With Secondary Licenses", as defined by 727 | > 728 | > the Mozilla Public License, v. 2.0. 729 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sddpy 2 | Python for Stochastic Dual Dynamic Programming Algorithm 3 | 4 | The codes are tested on python 3.6 and pyomo 5.7.3. 5 | 6 | # Documentation 7 | 8 | examples 9 | 10 | # Acknowledge 11 | 12 | This package can be seen a python version of [SDDP.jl v0.0.2.](https://github.com/odow/SDDP.jl/tree/v0.0.2/src) The algorithmic principles and structure of the code are inspired by sddp.jl. 13 | 14 | -------------------------------------------------------------------------------- /pyomotools/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | -------------------------------------------------------------------------------- /pyomotools/option.py: -------------------------------------------------------------------------------- 1 | """ 2 | --help 3 | --verbose 4 | --model-location=MODEL DIRECTORY 5 | --scenario-tree-location=INSTANCE DIRECTORY 6 | --output-file=OUTPUT FILE 7 | --solve 8 | --solver=SOLVER TYPE 9 | --linearize-nonbinary-penalty-terms. 10 | --solver-options=SOLVER OPTIONS 11 | --solver=glpk --solver-options="mipgap=0.02 cuts=" 12 | --solver-options="mipgap=0.001 emphasis numerical=y" 13 | --output-solver-log 14 | """ 15 | # class PHOpt: 16 | 17 | -------------------------------------------------------------------------------- /pyomotools/piecewise_nd.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from typing import List 3 | 4 | from pyomo.core import Block, Var, Binary, NonNegativeReals, Constraint, Expression 5 | from pyomo.core.base.var import SimpleVar 6 | import scipy.spatial.qhull as qhull 7 | import numpy as np 8 | # from pyomo.core.kernel.component_piecewise.util import generate_gray_code 9 | 10 | # Copyright 2017, Oscar Dowson 11 | # This Source Code Form is subject to the terms of the Mozilla Public 12 | # License, v. 2.0. If a copy of the MPL was not distributed with this 13 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 14 | ############################################################################# 15 | 16 | 17 | from pyomotools.tools import generate_points, generate_gray_code 18 | 19 | 20 | def cc(m: Block, tri: qhull.Delaunay, 21 | values: List[float], 22 | input: List[SimpleVar] = None, 23 | output: SimpleVar = None, 24 | bound: str = 'eq', **kw): 25 | values = np.array(values).tolist() 26 | ndim = len(input) 27 | nsimplices = len(tri.simplices) 28 | npoints = len(tri.points) 29 | pointsT = list(zip(*tri.points)) 30 | # create index objects 31 | dimensions = list(range(ndim)) 32 | simplices = list(range(nsimplices)) # 跟单纯形 数量一致 33 | vertices = list(range(npoints)) 34 | bound = bound.lower() 35 | 36 | m.lmbda = Var(vertices, domain=NonNegativeReals) # 非负 37 | m.y = Var(simplices, domain=Binary) # 二进制 38 | # m.y = Var(simplices, domain=NonNegativeReals, bounds=(0, 1)) # 二进制 39 | 40 | m.a0 = Constraint(dimensions, rule=lambda m, d: sum(m.lmbda[v] * pointsT[d][v] for v in vertices) == input[d]) 41 | if bound == 'eq': 42 | m.a1 = Constraint(expr=output == sum(m.lmbda[v] * values[v] for v in vertices)) 43 | elif bound == 'lb': 44 | m.a1 = Constraint(expr=output <= sum(m.lmbda[v] * values[v] for v in vertices)) 45 | elif bound == 'ub': 46 | m.a1 = Constraint(expr=output >= sum(m.lmbda[v] * values[v] for v in vertices)) 47 | else: 48 | raise RuntimeError("bound值错误!bound=" + bound) 49 | 50 | m.b = Constraint(expr=sum(m.lmbda[v] for v in vertices) == 1) 51 | 52 | # generate a map from vertex index to simplex index, 53 | # which avoids an n^2 lookup when generating the 54 | # constraint 55 | vertex_to_simplex = [[] for _ in vertices] 56 | for s, simplex in enumerate(tri.simplices): 57 | for v in simplex: 58 | vertex_to_simplex[v].append(s) 59 | m.c0 = Constraint(vertices, rule=lambda m, v: m.lmbda[v] <= sum(m.y[s] for s in vertex_to_simplex[v])) 60 | m.c1 = Constraint(expr=sum(m.y[s] for s in simplices) == 1) 61 | return m 62 | 63 | 64 | # def generate_union_jack2(vars:List[SimpleVar],num=10): 65 | # """ 66 | # 生成米字形格子 67 | # :param vars: 68 | # :param num: 69 | # :return: 70 | # """ 71 | def log_lp(m: Block, tri: qhull.Delaunay, 72 | values: List[float], 73 | input: List[SimpleVar] = None, 74 | output: SimpleVar = None, 75 | bound: str = 'eq', **kw): 76 | num = kw["num"] 77 | values = np.array(values).tolist() 78 | ndim = len(input) 79 | npoints = len(tri.points) 80 | pointsT = list(zip(*tri.points)) 81 | dims = list(range(ndim)) 82 | vertices = list(range(npoints)) 83 | bound = bound.lower() 84 | # 与 npoints 匹配的索引 85 | # vertices_idx = list(zip(*generate_points([list(range(num)) for _ in range(ndim)]))) 86 | vertices_idx = generate_points([list(range(num)) for _ in range(ndim)]).tolist() 87 | if len(vertices_idx) != len(vertices): 88 | raise RuntimeError("生成的的tri需要是米字形!") 89 | # (9a) 90 | m.lmbda = Var(vertices, domain=NonNegativeReals) # 非负 91 | m.a0 = Constraint(dims, rule=lambda m, d: sum(m.lmbda[v] * pointsT[d][v] for v in vertices) == input[d]) 92 | m.a_sum = Expression(expr=sum(m.lmbda[v] * values[v] for v in vertices)) 93 | if bound == 'eq': 94 | m.a1 = Constraint(expr=output == m.a_sum) 95 | elif bound == 'lb': 96 | m.a1 = Constraint(expr=output <= m.a_sum) 97 | elif bound == 'ub': 98 | m.a1 = Constraint(expr=output >= m.a_sum) 99 | else: 100 | raise RuntimeError("bound值错误!bound=" + bound) 101 | # (9b) 102 | m.b = Constraint(expr=sum(m.lmbda[v] for v in vertices) == 1) 103 | 104 | # (9c) 105 | 106 | # 约束a和b与cc方法是一样的 107 | K = num - 1 # K 必须是偶数 108 | N = list(range(1, ndim + 1)) 109 | log2K = math.ceil(math.log2(K)) 110 | L = list(range(1, log2K + 1)) 111 | G = generate_gray_code(log2K) 112 | 113 | def O(l, b): 114 | # k == 0 或者 k == K 的意思是不能是第一个和最后一个,避免越界 115 | res = [] 116 | for k in range(K + 1): 117 | if (k == 0 or G[k - 1][l - 1] == b) and (k == K or G[k][l - 1] == b): 118 | res.append(k) 119 | return res 120 | # return [k for k in range(K + 1) if (k == 0 or G[k][l] == b) and (k == K or G[k + 1][l] == b)] 121 | 122 | m.y_s1 = Var(N, L, domain=NonNegativeReals, bounds=(0, 1)) # 二进制 123 | m.c1_s1 = Constraint(N, L, rule=lambda m, s1, s2: sum( 124 | m.lmbda[v] for v, idx in zip(vertices, vertices_idx) if idx[s1 - 1] in O(s2, 1) 125 | ) <= m.y_s1[s1, s2]) 126 | m.c2_s1 = Constraint(N, L, rule=lambda m, s1, s2: sum( 127 | m.lmbda[v] for v, idx in zip(vertices, vertices_idx) if idx[s1 - 1] in O(s2, 0) 128 | ) <= 1 - m.y_s1[s1, s2]) 129 | S2 = [(s1, s2) for s1 in N for s2 in N if s1 < s2] 130 | S2_idx = list(range(len(S2))) 131 | m.y_s2 = Var(S2_idx, domain=NonNegativeReals, bounds=(0, 1)) 132 | m.c1_s2 = Constraint(S2_idx, rule=lambda m, i: sum( 133 | m.lmbda[v] for v, idx in zip(vertices, vertices_idx) if 134 | idx[S2[i][0] - 1] % 2 == 0 and idx[S2[i][1] - 1] % 2 == 1 135 | ) <= m.y_s2[i]) 136 | m.c2_s2 = Constraint(S2_idx, rule=lambda m, i: sum( 137 | m.lmbda[v] for v, idx in zip(vertices, vertices_idx) if 138 | idx[S2[i][0] - 1] % 2 == 1 and idx[S2[i][1] - 1] % 2 == 0 139 | ) <= 1 - m.y_s2[i]) 140 | return m 141 | 142 | 143 | def log(m: Block, tri: qhull.Delaunay, 144 | values: List[float], 145 | input: List[SimpleVar] = None, 146 | output: SimpleVar = None, 147 | bound: str = 'eq', **kw): 148 | """ 149 | 只适用于米字形分割方式 150 | :param num: 151 | :param m: 152 | :param tri: 153 | :param values: 154 | :param input: 155 | :param output: 156 | :param bound: 157 | :return: 158 | """ 159 | num = kw["num"] 160 | values = np.array(values).tolist() 161 | ndim = len(input) 162 | npoints = len(tri.points) 163 | pointsT = list(zip(*tri.points)) 164 | dims = list(range(ndim)) 165 | vertices = list(range(npoints)) 166 | bound = bound.lower() 167 | # 与 npoints 匹配的索引 168 | # vertices_idx = list(zip(*generate_points([list(range(num)) for _ in range(ndim)]))) 169 | vertices_idx = generate_points([list(range(num)) for _ in range(ndim)]).tolist() 170 | if len(vertices_idx) != len(vertices): 171 | raise RuntimeError("生成的的tri需要是米字形!") 172 | # (9a) 173 | m.lmbda = Var(vertices, domain=NonNegativeReals) # 非负 174 | m.a0 = Constraint(dims, rule=lambda m, d: sum(m.lmbda[v] * pointsT[d][v] for v in vertices) == input[d]) 175 | m.a_sum = Expression(expr=sum(m.lmbda[v] * values[v] for v in vertices)) 176 | if bound == 'eq': 177 | m.a1 = Constraint(expr=output == m.a_sum) 178 | elif bound == 'lb': 179 | m.a1 = Constraint(expr=output <= m.a_sum) 180 | elif bound == 'ub': 181 | m.a1 = Constraint(expr=output >= m.a_sum) 182 | else: 183 | raise RuntimeError("bound值错误!bound=" + bound) 184 | # (9b) 185 | m.b = Constraint(expr=sum(m.lmbda[v] for v in vertices) == 1) 186 | 187 | # (9c) 188 | 189 | # 约束a和b与cc方法是一样的 190 | K = num - 1 # K 必须是偶数 191 | N = list(range(1, ndim + 1)) 192 | log2K = math.ceil(math.log2(K)) 193 | L = list(range(1, log2K + 1)) 194 | G = generate_gray_code(log2K) 195 | 196 | def O(l, b): 197 | # k == 0 或者 k == K 的意思是不能是第一个和最后一个,避免越界 198 | res = [] 199 | for k in range(K + 1): 200 | if (k == 0 or G[k - 1][l - 1] == b) and (k == K or G[k][l - 1] == b): 201 | res.append(k) 202 | return res 203 | # return [k for k in range(K + 1) if (k == 0 or G[k][l] == b) and (k == K or G[k + 1][l] == b)] 204 | 205 | m.y_s1 = Var(N, L, domain=Binary) # 二进制 206 | # m.y_s1 = Var(N, L, domain=NonNegativeReals,bounds=(0,1)) # 二进制 207 | m.c1_s1 = Constraint(N, L, rule=lambda m, s1, s2: sum( 208 | m.lmbda[v] for v, idx in zip(vertices, vertices_idx) if idx[s1 - 1] in O(s2, 1) 209 | ) <= m.y_s1[s1, s2]) 210 | m.c2_s1 = Constraint(N, L, rule=lambda m, s1, s2: sum( 211 | m.lmbda[v] for v, idx in zip(vertices, vertices_idx) if idx[s1 - 1] in O(s2, 0) 212 | ) <= 1 - m.y_s1[s1, s2]) 213 | S2 = [(s1, s2) for s1 in N for s2 in N if s1 < s2] 214 | S2_idx = list(range(len(S2))) 215 | m.y_s2 = Var(S2_idx, domain=Binary) 216 | # m.y_s2 = Var(S2_idx, domain=NonNegativeReals,bounds=(0,1)) 217 | m.c1_s2 = Constraint(S2_idx, rule=lambda m, i: sum( 218 | m.lmbda[v] for v, idx in zip(vertices, vertices_idx) if 219 | idx[S2[i][0] - 1] % 2 == 0 and idx[S2[i][1] - 1] % 2 == 1 220 | ) <= m.y_s2[i]) 221 | m.c2_s2 = Constraint(S2_idx, rule=lambda m, i: sum( 222 | m.lmbda[v] for v, idx in zip(vertices, vertices_idx) if 223 | idx[S2[i][0] - 1] % 2 == 1 and idx[S2[i][1] - 1] % 2 == 0 224 | ) <= 1 - m.y_s2[i]) 225 | return m 226 | 227 | 228 | import math 229 | 230 | 231 | def dlog(m: Block, tri: qhull.Delaunay, 232 | values: List[float], 233 | input: List[SimpleVar] = None, 234 | output: SimpleVar = None, 235 | bound: str = 'eq', **kw): 236 | values = np.array(values).tolist() 237 | ndim = len(input) 238 | nsimplices = len(tri.simplices) 239 | npoints = len(tri.points) 240 | pointsT = list(zip(*tri.points)) 241 | # create index objects 242 | dimensions = list(range(ndim)) 243 | simplices = list(range(nsimplices)) # 跟单纯形 数量一致 244 | vertices = list(range(npoints)) 245 | bound = bound.lower() 246 | L = int(math.ceil(math.log2(nsimplices))) 247 | L_Range = list(range(L)) 248 | vp = [0, 1, 2] 249 | # 250 | m.lmbda = Var(simplices, vp, domain=NonNegativeReals) # 非负 251 | m.a0 = Constraint(dimensions, rule=lambda m, d: sum( 252 | m.lmbda[s, v] * pointsT[d][tri.simplices[s][v]] for s in simplices for v in vp) == input[d]) 253 | if bound == 'eq': 254 | m.a1 = Constraint( 255 | expr=output == sum(m.lmbda[s, v] * values[tri.simplices[s][v]] for s in simplices for v in vp)) 256 | elif bound == 'lb': 257 | m.a1 = Constraint( 258 | expr=output <= sum(m.lmbda[s, v] * values[tri.simplices[s][v]] for s in simplices for v in vp)) 259 | elif bound == 'ub': 260 | m.a1 = Constraint( 261 | expr=output >= sum(m.lmbda[s, v] * values[tri.simplices[s][v]] for s in simplices for v in vp)) 262 | else: 263 | raise RuntimeError("bound值错误!bound=" + bound) 264 | 265 | m.b1 = Constraint(expr=sum(m.lmbda[s, v] for s in simplices for v in vp) == 1) 266 | 267 | m.y = Var(L_Range, domain=Binary) # 二进制 268 | 269 | m.c0 = Constraint(L_Range, 270 | rule=lambda m, l: sum( 271 | m.lmbda[s, v] for s in simplices if bin(s)[2:].zfill(L)[l] == '1' for v in vp) <= 272 | m.y[l]) 273 | m.c1 = Constraint(L_Range, 274 | rule=lambda m, l: sum( 275 | m.lmbda[s, v] for s in simplices if bin(s)[2:].zfill(L)[l] == '0' for v in vp) <= 276 | 1 - m.y[l]) 277 | return m 278 | 279 | 280 | # def piecewise_ndlog() 281 | 282 | def piecewise_nd(tri: qhull.Delaunay, 283 | values: List[float], 284 | input: List[SimpleVar] = None, 285 | output: SimpleVar = None, 286 | bound: str = 'eq', 287 | repn: str = 'cc', parent=None, **kw): 288 | """ 289 | 添加多维线性插值,values 必须是float类型的浮点数,不能是np的 290 | tri (scipy.spatial.Delaunay): A triangulation over 291 | the discretized variable domain. Can be 292 | generated using a list of variables using the 293 | utility function :func:`util.generate_delaunay`. 294 | Required attributes: 295 | - points: An (npoints, D) shaped array listing 296 | the D-dimensional coordinates of the 297 | discretization points. 298 | - simplices: An (nsimplices, D+1) shaped array 299 | of integers specifying the D+1 indices of 300 | the points vector that define each simplex 301 | of the triangulation. 302 | values (numpy.array): An (npoints,) shaped array of 303 | the values of the piecewise function at each of 304 | coordinates in the triangulation points array. 305 | input: A D-length list of variables or expressions 306 | bound as the inputs of the piecewise function. 307 | output: The variable constrained to be the output of 308 | the piecewise linear function. 309 | bound (str): The type of bound to impose on the 310 | output expression. Can be one of: 311 | - 'lb': y <= f(x) 312 | - 'eq': y = f(x) 313 | - 'ub': y >= f(x) 314 | repn (str): The type of piecewise representation to 315 | use. Can be one of: 316 | - 'cc': convex combination 317 | 318 | """ 319 | if repn == "cc": 320 | pf = cc 321 | elif repn == "dlog": 322 | pf = dlog 323 | elif repn == "log": 324 | pf = log 325 | elif repn == "log_lp": 326 | pf = log_lp 327 | else: 328 | raise RuntimeError(f'{repn} 不支持!') 329 | 330 | _f = partial(pf, tri=tri, values=values, input=input, output=output, bound=bound, **kw) 331 | 332 | if parent is None: 333 | # raise RuntimeError("parent 不能为空,必须提前声明好,否则无法添加到model中") 334 | m = Block(rule=_f) 335 | else: 336 | m = parent 337 | _f(m) 338 | return m 339 | 340 | 341 | def piecewise_nd_old(tri: qhull.Delaunay, 342 | values: List[float], 343 | input: List[SimpleVar] = None, 344 | output: SimpleVar = None, 345 | bound: str = 'eq', 346 | repn: str = 'cc', parent=None): 347 | """ 348 | 添加多维线性插值,values 必须是float类型的浮点数,不能是np的 349 | tri (scipy.spatial.Delaunay): A triangulation over 350 | the discretized variable domain. Can be 351 | generated using a list of variables using the 352 | utility function :func:`util.generate_delaunay`. 353 | Required attributes: 354 | - points: An (npoints, D) shaped array listing 355 | the D-dimensional coordinates of the 356 | discretization points. 357 | - simplices: An (nsimplices, D+1) shaped array 358 | of integers specifying the D+1 indices of 359 | the points vector that define each simplex 360 | of the triangulation. 361 | values (numpy.array): An (npoints,) shaped array of 362 | the values of the piecewise function at each of 363 | coordinates in the triangulation points array. 364 | input: A D-length list of variables or expressions 365 | bound as the inputs of the piecewise function. 366 | output: The variable constrained to be the output of 367 | the piecewise linear function. 368 | bound (str): The type of bound to impose on the 369 | output expression. Can be one of: 370 | - 'lb': y <= f(x) 371 | - 'eq': y = f(x) 372 | - 'ub': y >= f(x) 373 | repn (str): The type of piecewise representation to 374 | use. Can be one of: 375 | - 'cc': convex combination 376 | 377 | """ 378 | values = np.array(values).tolist() 379 | ndim = len(input) 380 | nsimplices = len(tri.simplices) 381 | npoints = len(tri.points) 382 | pointsT = list(zip(*tri.points)) 383 | # create index objects 384 | dimensions = list(range(ndim)) 385 | simplices = list(range(nsimplices)) # 跟单纯形 数量一致 386 | vertices = list(range(npoints)) 387 | bound = bound.lower() 388 | 389 | def _f(m: Block): 390 | m.lmbda = Var(vertices, domain=NonNegativeReals) # 非负 391 | m.y = Var(simplices, domain=Binary) # 二进制 392 | 393 | m.a0 = Constraint(dimensions, rule=lambda m, d: sum(m.lmbda[v] * pointsT[d][v] for v in vertices) == input[d]) 394 | if bound == 'eq': 395 | m.a1 = Constraint(expr=output == sum(m.lmbda[v] * values[v] for v in vertices)) 396 | elif bound == 'lb': 397 | m.a1 = Constraint(expr=output <= sum(m.lmbda[v] * values[v] for v in vertices)) 398 | elif bound == 'ub': 399 | m.a1 = Constraint(expr=output >= sum(m.lmbda[v] * values[v] for v in vertices)) 400 | else: 401 | raise RuntimeError("bound值错误!bound=" + bound) 402 | 403 | m.b = Constraint(expr=sum(m.lmbda[v] for v in vertices) == 1) 404 | 405 | # generate a map from vertex index to simplex index, 406 | # which avoids an n^2 lookup when generating the 407 | # constraint 408 | vertex_to_simplex = [[] for _ in vertices] 409 | for s, simplex in enumerate(tri.simplices): 410 | for v in simplex: 411 | vertex_to_simplex[v].append(s) 412 | m.c0 = Constraint(vertices, rule=lambda m, v: m.lmbda[v] <= sum(m.y[s] for s in vertex_to_simplex[v])) 413 | m.c1 = Constraint(expr=sum(m.y[s] for s in simplices) == 1) 414 | return m 415 | 416 | if parent is None: 417 | # raise RuntimeError("parent 不能为空,必须提前声明好,否则无法添加到model中") 418 | m = Block(rule=_f) 419 | else: 420 | m = parent 421 | _f(m) 422 | return m 423 | # 生成变量 424 | -------------------------------------------------------------------------------- /pyomotools/proxy.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | from pyomo.environ import * 8 | from strgen import StringGenerator as SG 9 | # from pyomo.util.modeling import unique_component_name 10 | from pyomotools.tools import unique_component_name 11 | 12 | 13 | class ModelProxy: 14 | def __init__(self, model: Model): 15 | self.model = model 16 | self._anonymous = None 17 | 18 | @property 19 | def anonymous(self): 20 | return self._anonymous 21 | 22 | @anonymous.setter 23 | def anonymous(self, value): 24 | random_name = SG("[\w]{3}").render() 25 | random_name = unique_component_name(self.model, random_name) 26 | setattr(self.model, random_name, value) 27 | self._anonymous = value 28 | 29 | @staticmethod 30 | def get_model(m): 31 | if isinstance(m, ModelProxy): 32 | return m.model 33 | return m 34 | 35 | def __getattr__(self, item): 36 | """ 37 | 只有自身没有这个属性的时候才会调用这个属性 38 | :param item: 39 | :return: 40 | """ 41 | return getattr(self.model, item) 42 | 43 | def __setattr__(self, key, value): 44 | if key in ["model", "_anonymous", "anonymous"]: 45 | super(ModelProxy, self).__setattr__(key, value) 46 | else: 47 | setattr(self.model, key, value) 48 | 49 | 50 | -------------------------------------------------------------------------------- /pyomotools/rapper.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | 8 | """ 9 | A class and some utilities to wrap PySP. 10 | In particular to enable programmatic access to some of 11 | the functionality in runef and runph for ConcreteModels 12 | Author: David L. Woodruff, started February 2017 13 | """ 14 | 15 | from pyomo.environ import * 16 | from pyomo.pysp.ph import ProgressiveHedging 17 | from pyomo.pysp.scenariotree.instance_factory \ 18 | import ScenarioTreeInstanceFactory 19 | from pyomo.pysp.ef import create_ef_instance 20 | import pyomo.pysp.phinit as phinit 21 | import os 22 | 23 | 24 | def _kwfromphopts(phopts): 25 | """ 26 | This is really local to the StochSolver __init__ but 27 | I moved it way out to make the init more readable. The 28 | function takes the phopts dictionary and returns 29 | a kwargs dictionary suitable for a call to generate_scenario_tree. 30 | Note that only some options (i.e., bundle options) are needed 31 | when the tree is created. The rest can be passed in when the 32 | ph object is created. 33 | inputs: 34 | phopts: a ph options dictionary. 35 | return: 36 | kwargs: a dictionary suitable for a call to generate_scenario_tree. 37 | """ 38 | kwargs = {} 39 | 40 | def dointpair(pho, fo): 41 | if pho in phopts and phopts[pho] is not None: 42 | kwargs[fo] = int(phopts[pho]) 43 | else: 44 | kwargs[fo] = None 45 | 46 | if phopts is not None: 47 | dointpair("--create-random-bundles", 'random_bundles') 48 | dointpair("--scenario-tree-seed", 'random_seed') 49 | if "--scenario-tree-downsample-fraction" in phopts: 50 | kwargs['downsample_fraction'] = \ 51 | float(phopts["--scenario-tree-downsample-fraction"]) 52 | else: 53 | kwargs['downsample_fraction'] = None 54 | 55 | if "--scenario-bundle-specification" in phopts: 56 | kwargs['bundles'] = phopts["--scenario-tree-bundle-specification"] 57 | else: 58 | kwargs['bundles'] = None 59 | 60 | return kwargs 61 | 62 | 63 | # ================================== 64 | class StochSolver: 65 | """A class for solving stochastic versions of concrete models. 66 | Inspired by the IDAES use case and by daps ability to create tree models. 67 | Author: David L. Woodruff, February 2017 68 | 69 | Args: 70 | fsfile (str): is a file that contains the the scenario callback. 71 | fsfct (str): function name in the file, which defaults to 72 | "pysp_instance_creation_callback" 73 | tree_model: gives the tree as a concrete model. 74 | If it is None, then look for a function in fsfile called 75 | "pysp_scenario_tree_model_callback" that will return it. 76 | phopts: dictionary of ph options; needed during construction 77 | if there is bundling. 78 | 79 | Attributes: 80 | scenario_tree: scenario tree object (that includes data) 81 | 82 | """ 83 | 84 | def __init__(self, fsfile, 85 | fsfct="pysp_instance_creation_callback", 86 | tree_model=None, 87 | phopts=None): 88 | """Initialize a StochSolver object. 89 | """ 90 | fsfile = fsfile.replace('.py', '') # import does not like .py 91 | # __import__ only gives the top level module 92 | # probably need to be dealing with modules installed via setup.py 93 | m = __import__(fsfile) 94 | for n in fsfile.split(".")[1:]: 95 | m = getattr(m, n) 96 | scen_function = getattr(m, fsfct) 97 | 98 | if tree_model is None: 99 | tree_maker = getattr(m, \ 100 | "pysp_scenario_tree_model_callback") 101 | 102 | tree = tree_maker() 103 | # tree_model = tree.as_concrete_model() 104 | tree_model = tree 105 | 106 | # DLW March 21: still not correct 107 | scenario_instance_factory = \ 108 | ScenarioTreeInstanceFactory(scen_function, tree_model) 109 | # ScenarioTreeInstanceFactory("ReferenceModel.py", tree_model) 110 | 111 | else: 112 | # DLW March 21: still not correct 113 | scenario_instance_factory = \ 114 | ScenarioTreeInstanceFactory(scen_function, tree_model) 115 | 116 | kwargs = _kwfromphopts(phopts) 117 | self.scenario_tree = \ 118 | scenario_instance_factory.generate_scenario_tree(**kwargs) # verbose = True) 119 | instances = scenario_instance_factory. \ 120 | construct_instances_for_scenario_tree(self.scenario_tree) 121 | self.scenario_tree.linkInInstances(instances) 122 | 123 | # ========================= 124 | 125 | def make_ef(self, verbose=False): 126 | """ Make an ef object (used by solve_ef) 127 | 128 | Args: 129 | verbose (boolean): indicates verbosity to PySP for construction 130 | 131 | Returns: 132 | ef_instance: the ef object 133 | """ 134 | return create_ef_instance(self.scenario_tree, verbose_output=verbose) 135 | 136 | def solve_ef(self, subsolver, sopts=None, tee=False, need_gap=False): 137 | """Solve the stochastic program directly using the extensive form. 138 | 139 | Args: 140 | subsolver (str): the solver to call (e.g., 'ipopt') 141 | sopts (dict): solver options 142 | tee (bool): indicates dynamic solver output to terminal. 143 | need_gap (bool): indicates the need for the optimality gap 144 | 145 | Returns: (`Pyomo solver result`, `float`) 146 | 147 | solve_result is the solver return value. 148 | 149 | absgap is the absolute optimality gap (might not be valid); only if requested 150 | 151 | Note: 152 | Also update the scenario tree, populated with the solution. 153 | Also attach the full ef instance to the object. So you might want 154 | obj = pyo.value(stsolver.ef_instance.MASTER) 155 | This needs more work to deal with solver failure (dlw, March, 2018) 156 | 157 | """ 158 | 159 | self.ef_instance = self.make_ef() 160 | solver = SolverFactory(subsolver) 161 | if sopts is not None: 162 | for key in sopts: 163 | solver.options[key] = sopts[key] 164 | 165 | if need_gap: 166 | solve_result = solver.solve(self.ef_instance, tee=tee, load_solutions=False) 167 | if len(solve_result.solution) > 0: 168 | absgap = solve_result.solution(0).gap 169 | else: 170 | absgap = None 171 | self.ef_instance.solutions.load_from(solve_result) 172 | else: 173 | solve_result = solver.solve(self.ef_instance, tee=tee) 174 | 175 | # note: the objective is probably called MASTER 176 | # print ("debug value(ef_instance.MASTER)=",value(ef_instance.MASTER)) 177 | self.scenario_tree.pullScenarioSolutionsFromInstances() 178 | self.scenario_tree.snapshotSolutionFromScenarios() # update nodes 179 | if need_gap: 180 | return solve_result, absgap 181 | else: 182 | return solve_result 183 | 184 | # ========================= 185 | def solve_ph(self, subsolver, default_rho, phopts=None, sopts=None)->ProgressiveHedging: 186 | """Solve the stochastic program given by this.scenario_tree using ph 187 | 188 | Args: 189 | subsolver (str): the solver to call (e.g., 'ipopt') 190 | default_rho (float): the rho value to use by default 191 | phopts: dictionary of ph options (optional) 192 | sopts: dictionary of subsolver options (optional) 193 | 194 | Returns: the ph object 195 | 196 | Note: 197 | Updates the scenario tree, populated with the xbar values; 198 | however, you probably want to do 199 | obj, xhat = ph.compute_and_report_inner_bound_using_xhat() 200 | where ph is the return value. 201 | 202 | """ 203 | 204 | ph = None 205 | 206 | # Build up the options for PH. 207 | parser = phinit.construct_ph_options_parser("") 208 | phargslist = ['--default-rho', str(default_rho)] 209 | phargslist.append('--solver') 210 | phargslist.append(str(subsolver)) 211 | if phopts is not None: 212 | for key in phopts: 213 | phargslist.append(key) 214 | if phopts[key] is not None: 215 | phargslist.append(phopts[key]) 216 | 217 | # Subproblem options go to PH as space-delimited, equals-separated pairs. 218 | if sopts is not None: 219 | soptstring = "" 220 | for key in sopts: 221 | soptstring += key + '=' + str(sopts[key]) + ' ' 222 | phargslist.append('--scenario-solver-options') 223 | phargslist.append(soptstring) 224 | phoptions = parser.parse_args(phargslist) 225 | 226 | # construct the PH solver object 227 | try: 228 | ph = phinit.PHAlgorithmBuilder(phoptions, self.scenario_tree) 229 | except: 230 | print("Internal error: ph construction failed.") 231 | if ph is not None: 232 | ph.release_components() 233 | raise 234 | 235 | retval = ph.solve() 236 | if retval is not None: 237 | raise RuntimeError("ph Failure Encountered=" + str(retval)) 238 | # dlw May 2017: I am not sure if the next line is really needed 239 | ph.save_solution() 240 | 241 | return ph 242 | 243 | # ========================= 244 | def root_Var_solution(self): 245 | """Generator to loop over x-bar 246 | 247 | Yields: 248 | name, value pair for root node solution values 249 | """ 250 | root_node = self.scenario_tree.findRootNode() 251 | for variable_id in sorted(root_node._variable_ids): 252 | var_name, index = root_node._variable_ids[variable_id] 253 | name = var_name 254 | if index is not None: 255 | name += "[" + str(index) + "]" 256 | yield name, root_node._solution[variable_id] 257 | 258 | # ========================= 259 | def root_E_obj(self): 260 | """post solve Expected cost of the solution in the scenario tree (xbar) 261 | 262 | Returns: 263 | float: the expected costs of the solution in the tree (xbar) 264 | """ 265 | root_node = self.scenario_tree.findRootNode() 266 | return root_node.computeExpectedNodeCost() 267 | 268 | 269 | # ========================= 270 | def xhat_from_ph(ph): 271 | """a service fuction to wrap a call to get xhat 272 | 273 | Args: 274 | ph: a post-solve ph object 275 | 276 | Returns: (float, object) 277 | 278 | float: the expected cost of the xhat solution for the scenarios 279 | 280 | xhat: an object with the solution tree 281 | """ 282 | obj, xhat = ph.compute_and_report_inner_bound_using_xhat() 283 | return obj, xhat 284 | 285 | 286 | # ========================= 287 | def xhat_walker(xhat): 288 | """A service generator to walk over a given xhat 289 | 290 | Args: 291 | xhat (dict): an xhat solution (probably from xhat_from_ph) 292 | 293 | Yields: 294 | (nodename, varname, varvalue) 295 | """ 296 | for nodename in xhat: 297 | for varname, varvalue in xhat[nodename].items(): 298 | yield (nodename, varname, varvalue) 299 | 300 | 301 | -------------------------------------------------------------------------------- /pyomotools/tools.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | 8 | from random import random 9 | from typing import List 10 | 11 | from pyomo.environ import * 12 | from pyomo.solvers.plugins.solvers.CPLEX import * 13 | from pyomo.solvers.plugins.solvers.IPOPT import * 14 | from pyomo.solvers.plugins.solvers.GUROBI import * 15 | import numpy as np 16 | import scipy 17 | 18 | 19 | def model_str(model: ConcreteModel): 20 | print("===========obj==============") 21 | for obj in model.component_data_objects(Objective, active=True): 22 | print(obj.expr) 23 | print("========constraint==========") 24 | for cons in model.component_data_objects(Constraint, active=True): 25 | try: 26 | print("%s=%s" % (cons.name, cons.expr)) 27 | except: 28 | print("%s=错误" % cons.name) 29 | 30 | print("=========variables==========") 31 | print(["%s lb=%s ub=%s" % (v.name, v.lb, v.ub) for v in model.component_data_objects(Var)]) 32 | for v in model.component_data_objects(Var): 33 | try: 34 | print("%s=%f" % (v.name, value(v))) 35 | except: 36 | print("%s=错误" % v.name) 37 | 38 | print("==========params============") 39 | print(["%s=%f" % (v.name, value(v)) for v in model.component_data_objects(Param)]) 40 | 41 | 42 | def randint(a, b): 43 | """Our implementation of random.randint. 44 | 45 | The Python random.randint is not consistent between python versions 46 | and produces a series that is different in 3.x than 2.x. So that we 47 | can support deterministic testing (i.e., setting the random.seed and 48 | expecting the same sequence), we will implement a simple, but stable 49 | version of randint().""" 50 | return int((b - a + 1) * random()) 51 | 52 | 53 | def unique_component_name(instance, name): 54 | # test if this name already exists in model. If not, we're good. 55 | # Else, we add random numbers until it doesn't 56 | if instance.component(name) is None: 57 | return name 58 | name += '_%d' % (randint(0, 9),) 59 | while True: 60 | if instance.component(name) is None: 61 | return name 62 | else: 63 | name += str(randint(0, 9)) 64 | 65 | 66 | def generate_gray_code(nbits): 67 | """Generates a Gray code of nbits as list of lists""" 68 | bitset = [0 for i in xrange(nbits)] 69 | # important that we copy bitset each time 70 | graycode = [list(bitset)] 71 | 72 | for i in xrange(2, (1 << nbits) + 1): 73 | if i % 2: 74 | for j in xrange(-1, -nbits, -1): 75 | if bitset[j]: 76 | bitset[j - 1] = bitset[j - 1] ^ 1 77 | break 78 | else: 79 | bitset[-1] = bitset[-1] ^ 1 80 | # important that we copy bitset each time 81 | graycode.append(list(bitset)) 82 | 83 | return graycode 84 | 85 | 86 | def generate_points(linegrids: List[List[float or int]]): 87 | # 根据坐标自动生成多维的点 88 | points = np.vstack(np.meshgrid(*linegrids)). \ 89 | reshape(len(linegrids), -1).T 90 | return points 91 | 92 | 93 | def generate_delaunay(variables, num=10, **kwds): 94 | """ 95 | Generate a Delaunay triangulation of the D-dimensional 96 | bounded variable domain given a list of D variables. 97 | 98 | Requires numpy and scipy. 99 | 100 | Args: 101 | variables: A list of variables, each having a finite 102 | upper and lower bound. 103 | num (int): The number of grid points to generate for 104 | each variable (default=10). 105 | **kwds: All additional keywords are passed to the 106 | scipy.spatial.Delaunay constructor. 107 | 108 | Returns: 109 | A scipy.spatial.Delaunay object. 110 | """ 111 | linegrids = [] 112 | for v in variables: 113 | if v.has_lb() and v.has_ub(): 114 | linegrids.append(np.linspace(v.lb, v.ub, num)) 115 | else: 116 | raise ValueError( 117 | "Variable %s does not have a " 118 | "finite lower and upper bound.") 119 | # generates a meshgrid and then flattens and transposes 120 | # the meshgrid into an (npoints, D) shaped array of 121 | # coordinates 122 | points = generate_points(linegrids) 123 | return scipy.spatial.Delaunay(points, **kwds) 124 | 125 | 126 | def cplex() -> CPLEXSHELL: 127 | """ 128 | 整数,线性,二次规划求解器 129 | """ 130 | return SolverFactory('cplex', 131 | executable="/opt/ibm/ILOG/CPLEX_Studio128/cplex/bin/x86-64_linux/cplex") # type:CPLEXSHELL 132 | 133 | 134 | def gurobi() -> GUROBISHELL: 135 | """ 136 | 线性,整数规划求解器 137 | """ 138 | return SolverFactory('gurobi') # type:CPLEXSHELL 139 | 140 | 141 | def gurobi_python() -> GUROBISHELL: 142 | """ 143 | 线性,整数规划求解器 144 | """ 145 | return SolverFactory('gurobi_persistent') # type:CPLEXSHELL 146 | 147 | def ipopt() -> IPOPT: 148 | """ 149 | 非线性规划求解器 150 | """ 151 | return SolverFactory('ipopt') 152 | 153 | 154 | def glpk(): 155 | """ 156 | 线性,整数规划求解器 157 | """ 158 | return SolverFactory('glpk') 159 | -------------------------------------------------------------------------------- /sddp/SDDP.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | import copy 8 | # from sddp.defaultvaluefunction import DefaultValueFunction 9 | from sddp.riskmeasures import Expectation 10 | from sddp.state import setstates 11 | from sddp.utilities import * 12 | from sddp.cut_oracles.DefaultCutOracle import DefaultCutOracle 13 | # from .state import * 14 | from .typedefinitions import * 15 | from sddp.print import * 16 | 17 | 18 | def getel(x: float or List[float] or List[List[float]], t: int, i: int): 19 | dim = len(np.array(x).shape) 20 | if dim == 0: 21 | return x 22 | elif dim == 1: 23 | return x[t] 24 | elif dim == 2: 25 | return x[t][i] 26 | 27 | 28 | # 可以向每个子问题设置自身特别的解决方法 29 | def createSDDPModel(build_, # Callable[[Subproblem, int, int]] 30 | sense: Sense, 31 | stages: int = 1, 32 | objective_bound: float or List[float] or List[List[float]] = None, 33 | markov_transition: List[List[List[float]]] = None, 34 | risk_measure: AbstractRiskMeasure or List[AbstractRiskMeasure] or List[ 35 | List[AbstractRiskMeasure]] = Expectation(), 36 | cut_oracle=DefaultCutOracle(), 37 | solver=None, 38 | value_function=None 39 | # AbstractValueFunction or List[AbstractValueFunction] or List[List[AbstractValueFunction]] 40 | ): 41 | # 默认的value_function 42 | if value_function is None: 43 | from sddp.defaultvaluefunction import DefaultValueFunction 44 | value_function = DefaultValueFunction(cut_oracle) # 之后会进行深度复制 45 | 46 | if objective_bound is None: raise RuntimeError("You must specify the objective_bound keyword") 47 | 48 | m = SDDPModel(sense=sense, 49 | build=build_) 50 | for t in range(stages): 51 | # 二维数组 52 | markov_transition_matrix = markov_transition[t] # TODO 要进行转化 53 | stage = Stage.create(t=t, markov_transition=markov_transition_matrix) 54 | # 表示该时段markov状态数 55 | for i in range(len(markov_transition_matrix[0])): 56 | mod = Subproblem( 57 | finalstage=t == stages - 1, 58 | stage=t, 59 | markov_state=i, 60 | sense=sense, 61 | bound=getel(objective_bound, t, i), 62 | risk_measure=getel(risk_measure, t, i), 63 | value_function=copy.deepcopy(getel(value_function, t, i)) 64 | ) 65 | 66 | mod.set_solver(getel(solver, t, i)) 67 | build_(mod, t, i) 68 | # 没有设置概率的话,认为是等概率选择 69 | if len(mod.noises) != len(mod.noiseprobability): 70 | mod.noiseprobability = [1 / len(mod.noises)] * len(mod.noises) 71 | stage.subproblems.append(mod) 72 | m.stages.append(stage) 73 | return m 74 | 75 | 76 | def forwardpass(m: SDDPModel, setting: Settings, solutionstore=None) -> float: 77 | last_markov_state = 0 78 | noiseidx = 0 79 | obj = 0.0 80 | for t, stage in enumerate(m.stages): # type:int,Stage 81 | last_markov_state, sp = stage.samplesubproblem(last_markov_state, solutionstore) 82 | if t > 0: 83 | setstates(m, sp) 84 | if sp.hasnoises: 85 | noiseidx, noise = sp.samplenoise() 86 | sp.setnoise(noise) 87 | solvesubproblem(Direction.forwardpass, m, sp) 88 | obj += sp.getstageobjective() 89 | stage.savestates(sp) 90 | # 保存中间过程,用于模拟 TODO 91 | return obj 92 | 93 | 94 | def iteration_fun(m: SDDPModel, setting: Settings): 95 | """ 96 | 一正,一反为一次迭代 97 | """ 98 | t = time.time() 99 | simulation_objective = forwardpass(m, setting) 100 | time_forwards = time.time() - t 101 | vf = m.stages[0].subproblems[0].valueoracle # 默认采用第一个子问题的backwardpass方法 102 | objective_bound = vf.backwardpass(m, setting) # 不同的value_function 使用的方法可能不同 103 | time_backwards = time.time() - time_forwards - t 104 | return objective_bound, time_backwards, simulation_objective, time_forwards 105 | 106 | 107 | def solve(m: SDDPModel, settings: Settings = Settings()): 108 | status = Staus.solving 109 | time_simulating, time_cutting = 0.0, 0.0 110 | objectives = CachedVector([]) 111 | nsimulations, iteration, keep_iterating = 0, 1, True 112 | start_time = time.time() 113 | while keep_iterating: 114 | # add cuts 115 | objective_bound, time_backwards, simulation_objective, time_forwards = iteration_fun(m, settings) # 一次循环计算 116 | # update timers and bounds 117 | time_cutting += time_backwards + time_forwards # 一次循环认为建立的一个cut 118 | lower, upper = simulation_objective, simulation_objective 119 | 120 | if applicable(iteration, settings.cut_selection_frequency): # 对cut进行重新选择的频率 121 | if settings.print_level > 1: 122 | print("Running Cut Selection") 123 | rebuid(m) 124 | if applicable(iteration, settings.simulation.frequency): # 进行模拟,采用置信区间判断收敛的频率 125 | t = time.time() 126 | if settings.print_level > 1: 127 | print("Running Monte-Carlo Simulation") 128 | simidx = 0 129 | objectives.reset() 130 | # 进行step步模拟计算,判断是否收敛 131 | for i in range(settings.simulation.steps[-1]): 132 | objectives.append(forwardpass(m, settings)) 133 | nsimulations = +1 134 | # 从 min 一直到 max 都是在置信区间,才认为是收敛,只要有一个不是就不收敛 135 | if i == settings.simulation.steps[simidx] - 1: 136 | lower, upper = confidenceinterval(objectives, settings.simulation.confidence) 137 | if lower <= objective_bound <= upper: 138 | if settings.simulation.termination and simidx == len(settings.simulation.steps) - 1: 139 | status = Staus.converged 140 | keep_iterating = False 141 | else: 142 | break 143 | simidx += 1 144 | time_simulating += time.time() - t # 模拟运行判断是否收敛的时间 145 | total_time = time.time() - start_time 146 | # 打印日志 147 | addsolutionlog(m, settings, iteration, objective_bound, lower, upper, time_cutting, nsimulations, 148 | time_simulating, total_time, not applicable(iteration, settings.simulation.frequency)) 149 | status, keep_iterating = testboundstall(m, settings, status, keep_iterating) 150 | # 最长运行时间 151 | if total_time > settings.time_limit: 152 | status = Staus.time_limit 153 | keep_iterating = False 154 | 155 | iteration += 1 156 | # 最大循环次数 157 | if iteration > settings.max_iterations: 158 | status = Staus.max_iterations 159 | keep_iterating = False 160 | return status 161 | 162 | 163 | def rebuid(m: SDDPModel): 164 | for t, stage in enumerate(m.stages): 165 | if t == len(m.stages) - 1: 166 | continue 167 | for sp in stage.subproblems: 168 | sp.valueoracle.rebuildsubproblem(m, sp) 169 | 170 | 171 | def addsolutionlog(m: SDDPModel, settings: Settings, iteration: int, objective: float, lower: float, upper: float, 172 | cutting_time, simulations, 173 | simulation_time, total_time, printsingle: bool): 174 | m.log.append( 175 | SolutionLog(iteration, objective, lower, upper, cutting_time, simulations, simulation_time, total_time)) 176 | print_solutionLog(m.log[-1], printsingle, m.sense == Sense.Min) 177 | 178 | 179 | # 相对误差停止准则 180 | def testboundstall(m: SDDPModel, settings: Settings, status: Staus, keep_iteerating: bool): 181 | last_n_size = settings.bound_convergence.iterations 182 | if keep_iteerating: 183 | if settings.bound_convergence.iterations > 1 and len(m.log) >= last_n_size: 184 | last_n = np.array([l.bound for l in m.log[-last_n_size:]]) 185 | mean = np.mean(last_n) 186 | if np.all(last_n - mean < settings.bound_convergence.atol) or np.all( 187 | np.abs(last_n / mean - 1) < settings.bound_convergence.rtol): 188 | return Staus.stalling_convergence, False 189 | return status, keep_iteerating 190 | 191 | 192 | def solvesubproblem(direction: Direction, m: SDDPModel, sp: Subproblem, incoming_probablility: float = 1.0): 193 | if direction == Direction.forwardpass: 194 | pyomoSolve(direction, m, sp) 195 | 196 | elif direction == Direction.backwardpass: 197 | if sp.hasnoises: 198 | for i in range(len(sp.noiseprobability)): 199 | sp.setnoise(sp.noises[i]) 200 | pyomoSolve(direction, m, sp) 201 | Storage.push(m.storage.objective, sp.getobjectivevalue()) 202 | Storage.push(m.storage.noise, i) 203 | Storage.push(m.storage.probability, incoming_probablility * sp.noiseprobability[i]) 204 | Storage.push(m.storage.modifiedprobability, incoming_probablility * sp.noiseprobability[i]) 205 | Storage.push(m.storage.markov, sp.markov_state) 206 | Storage.push(m.storage.duals, [s.dual for s in sp.states]) # 状态的对偶值,是其约束的对偶值 207 | else: 208 | pyomoSolve(direction, m, sp) 209 | Storage.push(m.storage.objective, sp.getobjectivevalue()) 210 | Storage.push(m.storage.noise, 0) 211 | Storage.push(m.storage.probability, incoming_probablility) 212 | Storage.push(m.storage.modifiedprobability, incoming_probablility) 213 | Storage.push(m.storage.markov, sp.markov_state) 214 | Storage.push(m.storage.duals, [s.dual for s in sp.states]) # 状态的对偶值,是其约束的对偶值 215 | 216 | 217 | def solve_default(m: SDDPModel, 218 | iteration_limit: int = 1e9, 219 | time_limit: float = float("inf"), 220 | simulation=MonteCarloSimulation( 221 | frequency=0, 222 | steps=[20], 223 | confidence=0.95, 224 | termination=False 225 | ), 226 | bound_stalling=BoundStalling( 227 | iterations=0, 228 | rtol=0.0, 229 | atol=0.0 230 | ), 231 | cut_selection_frequency: int = 0, 232 | print_level: int = 1, 233 | log_file: str = "", 234 | solve_type=SolveType.Serial, 235 | reduce_memory_footprint=False, 236 | cut_output_file: str = "" 237 | ): 238 | cut_output_file_handle = "" 239 | is_asyncronous = solve_type == SolveType.Asyncronous 240 | print("is_asyncronous=%s" % is_asyncronous) 241 | settings = Settings( 242 | iteration_limit, 243 | time_limit, 244 | simulation, 245 | bound_stalling, 246 | cut_selection_frequency, 247 | print_level, 248 | log_file, 249 | reduce_memory_footprint, 250 | cut_output_file_handle, 251 | is_asyncronous=is_asyncronous 252 | ) 253 | printheader(m, solve_type.value) 254 | status = Staus.solving 255 | 256 | status = solve(m, settings) 257 | printfooter(m, settings, status.value, None) 258 | return status 259 | 260 | # print(status, "计算结束") 261 | -------------------------------------------------------------------------------- /sddp/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | -------------------------------------------------------------------------------- /sddp/base_utilities.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | from typing import Dict 8 | 9 | from sddp.typedefinitions import * 10 | import numpy as np 11 | 12 | 13 | def sample(x: List[float]): 14 | dim = len(np.shape(x)) 15 | if dim == 0: 16 | return 0 17 | res = np.random.choice(len(x), p=x) 18 | return int(res) 19 | -------------------------------------------------------------------------------- /sddp/cut_oracles/DefaultCutOracle.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | 8 | from typing import List 9 | 10 | from sddp.typedefinitions import * 11 | 12 | 13 | class DefaultCutOracle(AbstractCutOracle): 14 | def __init__(self, cuts: List[Cut]=[]): 15 | self.cuts = cuts 16 | 17 | def storecut(self, m: 'SDDPModel', sp: 'Subproblem', cut: 'Cut'): 18 | self.cuts.append(cut) 19 | 20 | def validcuts(self): 21 | return self.cuts 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /sddp/cut_oracles/LevelOneCutOracle.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | 8 | from typing import List 9 | import numpy as np 10 | 11 | from sddp.typedefinitions import AbstractCutOracle, Cut 12 | from sddp.utilities import dominates 13 | 14 | 15 | class StoredCut: 16 | def __init__(self, cut, non_dominated_count): 17 | self.non_dominated_count = non_dominated_count # type:int 18 | self.cut = cut # type:Cut 19 | 20 | 21 | class SampledState: 22 | def __init__(self, state: List[float], best_object: float, best_cut_index: int): 23 | self.best_cut_index = best_cut_index 24 | self.best_object = best_object 25 | self.state = state 26 | 27 | 28 | class LevelOneCutOracle(AbstractCutOracle): 29 | def __init__(self, cuts=None, states=None, sampled_states=None): 30 | if cuts is None: cuts = [] 31 | if states is None: states = [] 32 | if sampled_states is None: [] # TODO 33 | self.cuts = cuts # type:List[StoredCut] 34 | self.states = states # type:List[SampledState] 35 | self.sampled_states = sampled_states # type:List[List[float]] 36 | 37 | def storecut(self, m: 'SDDPModel', sp: 'Subproblem', cut: 'Cut'): 38 | sense = sp.sense 39 | self.cuts.append(StoredCut(cut, 0)) 40 | cut_index = len(self.cuts) - 1 41 | for state in self.states: 42 | y = cut.intercept + np.dot(cut.coefficients, state.state).__float__() 43 | if dominates(sense, y, state.best_object): 44 | self.cuts[state.best_cut_index].non_dominated_count -= 1 45 | self.cuts[cut_index].non_dominated_count += 1 46 | state.best_cut_index = cut_index 47 | state.best_object = y 48 | 49 | current_state = [f for f in m.stages[sp.stage].state] 50 | if len(current_state) == 0: 51 | return 52 | 53 | if current_state in self.sampled_states: 54 | return 55 | 56 | self.sampled_states.append(current_state) 57 | 58 | sampled_state = SampledState(current_state, 59 | cut.intercept + np.dot(cut.coefficients, current_state).__float__(), 60 | cut_index # assume that the new cut is the best 61 | ) 62 | self.states.append(sampled_state) 63 | self.cuts[cut_index].non_dominated_count += 1 64 | for (i, stored_cut) in enumerate(self.cuts): 65 | y = stored_cut.cut.intercept + np.dot(stored_cut.cut.coefficients, sampled_state.state) 66 | if dominates(sense, y, sampled_state.best_objective): 67 | # if new cut is strictly better 68 | # decrement the counter at the old cut 69 | self.cuts[sampled_state.best_cut_index].non_dominated_count -= 1 70 | # increment the counter at the new cut 71 | self.cuts[i].non_dominated_count += 1 72 | sampled_state.best_cut_index = i 73 | sampled_state.best_objective = y 74 | 75 | def validcuts(self): 76 | return [stored_cut.cut for stored_cut in self.cuts 77 | if stored_cut.non_dominated_count > 0] 78 | 79 | def allcuts(self): 80 | return [stored_cut.cut for stored_cut in self.cuts] 81 | -------------------------------------------------------------------------------- /sddp/cut_oracles/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | -------------------------------------------------------------------------------- /sddp/defaultvaluefunction.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | from copy import deepcopy 8 | from sddp.SDDP import solvesubproblem 9 | from sddp.state import setstates 10 | from sddp.cut_oracles.DefaultCutOracle import DefaultCutOracle 11 | from .utilities import * 12 | 13 | T = TypeVar('T') 14 | 15 | 16 | class DefaultValueFunction(AbstractValueFunction[T]): 17 | def __init__(self, cutmanager: T = DefaultCutOracle()): 18 | self._theta = None # value function 的值 19 | self._cutmanager = cutmanager 20 | 21 | @property 22 | def cutoracle(self) -> AbstractCutOracle: 23 | return self._cutmanager 24 | 25 | @property 26 | def theta(self): 27 | return self._theta 28 | 29 | @theta.setter 30 | def theta(self, value): 31 | self._theta = value 32 | 33 | def initializevaluefunction(self, sp: 'Subproblem', sense: Sense, bound: float): 34 | if sense == Sense.Min: 35 | self._theta = Var(bounds=(bound, None)) 36 | else: 37 | self._theta = Var(bounds=(None, bound)) 38 | sp.model.theta = self._theta 39 | 40 | 41 | @staticmethod 42 | def backwardpass(m: 'SDDPModel', setting: 'Settings'): 43 | for t in reversed(range(1, m.nstages)): # 倒序 44 | m.storage.reset() 45 | for sp in m.stages[t].subproblems: 46 | setstates(m, sp) 47 | solvesubproblem(Direction.backwardpass, m, sp) 48 | for sp in m.stages[t - 1].subproblems: 49 | modifyvaluefunction(m, setting, sp) 50 | m.storage.reset() # TODO 重新设置 51 | for sp in m.stages[0].subproblems: 52 | solvesubproblem(Direction.backwardpass, m, sp) 53 | # print(model_expression(sp.model)) 54 | return float(np.dot(m.storage.objective, m.storage.probability)) 55 | 56 | # @staticmethod 57 | def addcut(self, m: SDDPModel, sp: Subproblem, cut: Cut): 58 | # vf = sp.valueoracle # type:DefaultValueFunction 59 | vf = self 60 | vf.cutoracle.storecut(m, sp, cut) 61 | vf.addcuttoPyomoModel(sp, cut) 62 | 63 | def addcuttoPyomoModel(self, sp: Subproblem, cut: Cut): 64 | cut_expr = cut.intercept # type: Expression 65 | vf = self 66 | for c, s in zip(cut.coefficients, sp.states): 67 | cut_expr = c * s.variable + cut_expr 68 | if sp.sense == Sense.Min: 69 | sp.add_constraint(expr=vf.theta >= cut_expr) 70 | else: 71 | sp.add_constraint(expr=vf.theta <= cut_expr) 72 | 73 | def rebuildsubproblem(self, m: SDDPModel, sp: Subproblem): 74 | """ 75 | 重新构建子问题 76 | """ 77 | vf = self # type:DefaultValueFunction 78 | sp.states.clear() 79 | sp.noises.clear() 80 | sp.reset_mod() 81 | 82 | # if sp.sense == Sense.Max: 83 | # sp.model.theta = Var(domain=Reals, bounds=(None, sp.problembound)) 84 | # else: 85 | # sp.model.theta = Var(domain=Reals, bounds=(sp.problembound, None)) 86 | vf.theta = sp.model.theta 87 | m.build(sp, sp.stage, sp.markov_state) 88 | for cut in vf.cutoracle.validcuts(): 89 | vf.addcuttoPyomoModel(sp, cut) 90 | m.stages[sp.stage].subproblems[sp.markov_state] = sp 91 | 92 | def setstageobjective(self, sp: 'Subproblem', obj: Expression): 93 | if sp.finalstage: 94 | sp._obj = Objective(expr=obj, sense=sp.sense.value) 95 | else: 96 | sp._obj = Objective(expr=obj + self.theta, sense=sp.sense.value) 97 | 98 | def getstageobjective(self, sp: 'Subproblem'): 99 | if sp.finalstage: 100 | return sp.getobjectivevalue() 101 | else: 102 | return sp.getobjectivevalue() - value(self.theta) 103 | 104 | 105 | def modifyvaluefunction(m: SDDPModel, setting: Settings, sp: Subproblem): 106 | vf = sp.valueoracle # type:DefaultValueFunction 107 | # vf = self 108 | # 此时m.storate 存储的是下一阶段所有的计算结果 109 | I = list(range(len(m.storage.objective))) 110 | current_transition = deepcopy(m.storage.probability.range(I)) 111 | for i in I: 112 | # 原来存储的是noise的概率,乘以转移概率以后就是真实的概率 113 | m.storage.probability[i] *= m.stages[sp.stage + 1].transitionprobabilities[sp.markov_state][ 114 | m.storage.markov[i]] 115 | modifiedprobability = sp.riskmeasure.modifyprobability(m.storage.modifiedprobability.range(I), 116 | m.storage.probability.range(I), 117 | m.storage.objective.range(I), 118 | m, sp) 119 | m.storage.modifiedprobability.put_range(I, modifiedprobability) 120 | cut = constructcut(m, sp) 121 | # TODO asynchronous 122 | vf.addcut(m, sp, cut) 123 | m.storage.probability.put_range(I, current_transition) 124 | -------------------------------------------------------------------------------- /sddp/example/HydroValley/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | -------------------------------------------------------------------------------- /sddp/example/HydroValley/hydro_valley.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | 8 | 9 | from sddp.cut_oracles.DefaultCutOracle import DefaultCutOracle 10 | from sddp.SDDP import createSDDPModel 11 | 12 | 13 | from sddp.riskmeasures import Expectation 14 | from sddp.typedefinitions import * 15 | 16 | solver = SolverFactory('gurobi') 17 | 18 | 19 | class Turbine: 20 | def __init__(self, flowknots: List[float], powerknots: List[float]): 21 | self.powerknots = powerknots 22 | self.flowknots = flowknots 23 | 24 | @property 25 | def nrange(self): 26 | return range(len(self.flowknots)) 27 | 28 | 29 | class Reservoir: 30 | def __init__(self, min, max, initial, turbine, spill_cost, inflows): 31 | self.inflows = inflows # type:List[float] 32 | self.spill_cost = spill_cost # type:float 33 | self.turbine = turbine # type:Turbine 34 | self.initial = initial # type:float 35 | self.max = max # type:float 36 | self.min = min # type:float 37 | 38 | 39 | def hydrovalleymodel( 40 | riskmeasure=Expectation(), 41 | cutoracle=DefaultCutOracle(), 42 | hasstagewiseinflows: bool = True, 43 | hasmarkovprice: bool = True, 44 | sense=Sense.Max): 45 | valley_chain = [ 46 | Reservoir(0, 200, 200, Turbine([50, 60, 70], [55, 65, 70]), 1000, [0, 20, 50]), 47 | Reservoir(0, 200, 200, Turbine([50, 60, 70], [55, 65, 70]), 1000, [0, 0, 20]) 48 | ] 49 | 50 | def turbine(r: int): 51 | return valley_chain[r].turbine 52 | 53 | prices = [ 54 | [1, 2, 0], 55 | [2, 1, 0], 56 | [3, 4, 0], 57 | ] 58 | if hasmarkovprice: 59 | transition = [ 60 | [[1.0]], 61 | [[0.6, 0.4]], 62 | [[0.6, 0.4, 0.0], [0.3, 0.7, 0.0]] 63 | ] 64 | else: 65 | transition = [[[1]], 66 | [[1]], 67 | [[1]]] 68 | 69 | flipobj = 1 if sense == Sense.Max else -1 70 | N = len(valley_chain) 71 | 72 | def build(sp: Subproblem, stage: int, markov_state: int): 73 | model = sp.model 74 | model.N = list(range(N)) 75 | 76 | # 设置状态 77 | model.reservoir = Var(model.N, bounds=lambda m, i: (valley_chain[i].min, valley_chain[i].max)) 78 | model.reservoir0 = Var(model.N) 79 | model.rp = Param(model.N, initialize=lambda m, i: valley_chain[i].initial, mutable=True) 80 | # model.rc = Constraint(model.N, rule=lambda m, i: model.reservoir0[i] == model.rp[i]) 81 | sp.add_state(model.reservoir, model.reservoir0, model.rp) 82 | 83 | # Additional variables 84 | model.outflow = Var(model.N, domain=NonNegativeReals) 85 | model.spill = Var(model.N, domain=NonNegativeReals) 86 | model.inflow = Var(model.N, domain=NonNegativeReals) 87 | model.generation_quantity = Var(domain=NonNegativeReals) 88 | model.dispatch = Var(model.N, turbine(0).nrange, bounds=lambda m, r, level: (0, 1)) 89 | 90 | # Constraints 91 | # 水量平衡方程 92 | 93 | def _bl(m, r): 94 | r = value(r) 95 | if r == 0: 96 | return m.reservoir[r] == m.reservoir0[r] + m.inflow[r] - m.outflow[r] - m.spill[r] 97 | else: 98 | return m.reservoir[r] == m.reservoir0[r] + m.inflow[r] - m.outflow[r] - m.spill[r] + m.spill[r - 1] + \ 99 | m.outflow[r - 1] 100 | 101 | sp.anonymous = Constraint(model.N, rule=_bl) 102 | sp.anonymous = Constraint(expr=sum( 103 | turbine(r).powerknots[level] * model.dispatch[r, level] for r in model.N for level in turbine(0).nrange) == 104 | model.generation_quantity) 105 | sp.anonymous = Constraint(model.N, rule=lambda model, i: model.outflow[i] == sum( 106 | turbine(i).flowknots[level] * model.dispatch[i, level] for level in turbine(0).nrange 107 | )) 108 | sp.anonymous = Constraint(model.N, 109 | rule=lambda model, r: sum( 110 | model.dispatch[r, level] for level in turbine(r).nrange) <= 1) 111 | 112 | model.blocks = Block(model.N) 113 | for i in model.N: 114 | if hasstagewiseinflows and stage > 0: 115 | model.blocks[i].rainfall = Param(mutable=True) 116 | model.blocks[i].rf = Constraint(expr=model.inflow[i] <= model.blocks[i].rainfall) 117 | sp.add_noise_constraint(valley_chain[i].inflows, model.blocks[i].rainfall) 118 | else: 119 | model.blocks[i].rf = Constraint(expr=model.inflow[i] <= valley_chain[i].inflows[0]) 120 | 121 | if hasmarkovprice: 122 | sp.obj = flipobj * (prices[stage][markov_state] * model.generation_quantity - sum( 123 | valley_chain[i].spill_cost * model.spill[i] for i in model.N)) 124 | else: 125 | sp.obj=flipobj * (prices[stage][0] * model.generation_quantity - sum( 126 | valley_chain[i].spill_cost * model.spill[i] for i in model.N)) 127 | 128 | m = createSDDPModel(build, 129 | sense=sense, 130 | stages=3, 131 | objective_bound=flipobj * 1e6, 132 | markov_transition=transition, 133 | risk_measure=riskmeasure, 134 | cut_oracle=cutoracle, 135 | solver=solver 136 | ) 137 | # print(model_exp) 138 | return m 139 | -------------------------------------------------------------------------------- /sddp/example/HydroValley/test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | 8 | import math 9 | import unittest 10 | 11 | from sddp.SDDP import solve_default, Sense, MonteCarloSimulation 12 | from sddp.example.HydroValley.hydro_valley import hydrovalleymodel 13 | from sddp.riskmeasures import EAVaR, DRO 14 | from sddp.utilities import isapprox 15 | 16 | 17 | class MyTestCase(unittest.TestCase): 18 | def test_deterministic(self): 19 | deterministic_model = hydrovalleymodel(hasmarkovprice=False, hasstagewiseinflows=False) 20 | status = solve_default(deterministic_model, iteration_limit=10, cut_selection_frequency=1, print_level=0) 21 | self.assertTrue(isapprox(deterministic_model.getbound(), 835.0, atol=1e-3)) 22 | 23 | def test_stagewise(self): 24 | stagewise_model = hydrovalleymodel(hasmarkovprice=False) 25 | solve_default(stagewise_model, iteration_limit=20, print_level=0) 26 | self.assertTrue(isapprox(stagewise_model.getbound(), 838.33, atol=1e-2)) 27 | 28 | def test_markov_prices(self): 29 | markov_model = hydrovalleymodel(hasstagewiseinflows=False) 30 | status = solve_default(markov_model, iteration_limit=10, print_level=0) 31 | self.assertTrue(isapprox(markov_model.getbound(), 851.8, atol=1e-3)) 32 | 33 | def test_stagewise_inflows_and_markov_prices(self): 34 | markov_stagewise_model = hydrovalleymodel(hasstagewiseinflows=True, hasmarkovprice=True) 35 | solve_default(markov_stagewise_model, iteration_limit=10, print_level=0) 36 | self.assertTrue(isapprox(markov_stagewise_model.getbound(), 855.0, atol=1e-3)) 37 | 38 | def test_riskaverse(self): 39 | """ 40 | 风险厌恶者 41 | """ 42 | riskaverse_model = hydrovalleymodel(riskmeasure=EAVaR(lamb=0.5, beta=0.66)) 43 | solve_default(riskaverse_model, iteration_limit=10, print_level=0) 44 | self.assertTrue(isapprox(riskaverse_model.getbound(), 828.157, atol=1e-3)) 45 | 46 | def test_worst_case(self): 47 | worst_case_model = hydrovalleymodel( 48 | riskmeasure=EAVaR(lamb=0.5, beta=0.0), sense=Sense.Min) 49 | solve_default(worst_case_model, 50 | iteration_limit=10, 51 | simulation=MonteCarloSimulation( 52 | frequency=2, 53 | steps=list(range(20, 51, 10)) 54 | )) 55 | self.assertTrue(isapprox(worst_case_model.getbound(), -780.867, atol=1e-3)) 56 | 57 | def test_DRO(self): 58 | dro_model = hydrovalleymodel(hasmarkovprice=False, riskmeasure=DRO(math.sqrt(2 / 3) - 1e-6)) 59 | solve_default(dro_model, iteration_limit=10, print_level=0) 60 | self.assertTrue(isapprox(dro_model.getbound(), 835.0, atol=1e-3)) 61 | 62 | def test_DRO2(self): 63 | dro_model = hydrovalleymodel(hasmarkovprice=False, riskmeasure=DRO(1/6)) 64 | solve_default(dro_model, iteration_limit=20, print_level=0) 65 | self.assertTrue(isapprox(dro_model.getbound(), 836.695, atol=1e-3)) 66 | 67 | 68 | 69 | 70 | 71 | if __name__ == '__main__': 72 | unittest.main() 73 | -------------------------------------------------------------------------------- /sddp/example/HydroValley/test1.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | 8 | from sddp.SDDP import solve_default 9 | from sddp.example.HydroValley.hydro_valley import hydrovalleymodel 10 | if __name__ == '__main__': 11 | deterministic_model = hydrovalleymodel(hasmarkovprice=False, hasstagewiseinflows=False) 12 | status = solve_default(deterministic_model, iteration_limit=100, cut_selection_frequency=1, print_level=0) -------------------------------------------------------------------------------- /sddp/example/Newsvendor/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sddp/example/Newsvendor/newsvendor.py: -------------------------------------------------------------------------------- 1 | 2 | from sddp.typedefinitions import * 3 | from sddp.SDDP import * 4 | 5 | CplexSolver = SolverFactory('cplex', 6 | executable="/opt/ibm/ILOG/CPLEX_Studio128/cplex/bin/x86-64_linux/cplex") # type:CPLEXSHELL 7 | 8 | 9 | def newsvendormodel(oracle=DefaultCutOracle(), riskmeasure=Expectation()): 10 | Demand = [ 11 | [10.0, 15.0], 12 | [12.0, 20.0], 13 | [8.0, 20.0] 14 | ] 15 | 16 | # Markov state purchase prices 17 | PurchasePrice = [5.0, 8.0] 18 | RetailPrice = 7.0 19 | # 一个,两个,两个 20 | Transition = [ 21 | [[1.0]], 22 | [[0.6, 0.4]], 23 | [[0.3, 0.7], [0.3, 0.7]] 24 | ] 25 | 26 | def build(sp: Subproblem, stage: int, markov_state: int): 27 | model = sp.model 28 | # state 29 | model.stock = Var(bounds=(0, 100)) 30 | model.stock0 = Var() 31 | model.initP = Param(initialize=5, mutable=True) 32 | model.stock0c = Constraint(expr=model.stock0 == model.initP) 33 | sp.add_state(model.stock, model.stock0, model.initP, model.stock0c) 34 | # other variables 35 | model.buy = Var(domain=NonNegativeReals) 36 | model.sell = Var(domain=NonNegativeReals) 37 | # Constrains 38 | model.D = Param(initialize=0, mutable=True) 39 | model.cs0 = Constraint(expr=model.sell <= model.D) 40 | model.cs1 = Constraint(expr=model.sell >= 0.5 * model.D) 41 | # model.cs = ConstraintList() 42 | # model.cs.add(Constraint(expr=model.sell <= model.D_noise)) 43 | # model.cs.add(Constraint(expr=model.sell >= 0.5 * model.D_noise)) 44 | # =添加噪音 45 | sp.add_noise_constraint(Demand[stage], model.D, [model.cs0, model.cs1]) # 添加噪音 46 | 47 | model.dc = Constraint(expr=model.stock == model.stock0 + model.buy - model.sell) 48 | # model.obj=Objective(expr=-model.sell * RetailPrice + model.buy * PurchasePrice[markov_state], sense=minimize) 49 | 50 | sp.obj = -model.sell * RetailPrice + model.buy * PurchasePrice[markov_state] 51 | 52 | m = createSDDPModel(build, 53 | sense=Sense.Min, 54 | stages=3, 55 | objective_bound=-1000, 56 | markov_transition=Transition, 57 | solver=CplexSolver, 58 | cut_oracle=oracle, 59 | risk_measure=riskmeasure 60 | ) 61 | 62 | return m 63 | -------------------------------------------------------------------------------- /sddp/example/Newsvendor/test1.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | # from sddp.SDDP import * 3 | from sddp.utilities import isapprox 4 | from .newsvendor import * 5 | 6 | 7 | 8 | 9 | 10 | class MyTestCase(unittest.TestCase): 11 | def setUp(self): 12 | pass 13 | 14 | def test_converge(self): 15 | self.news = newsvendormodel() 16 | status = solve_default(self.news, 17 | iteration_limit=20, 18 | cut_selection_frequency=10, 19 | simulation=MonteCarloSimulation( 20 | frequency=10, 21 | steps=list(range(10, 501, 10)) 22 | 23 | ), 24 | bound_stalling=BoundStalling( 25 | iterations=5, 26 | atol=1e-3 27 | ) 28 | ) 29 | self.assertEqual(status, Staus.stalling_convergence) 30 | self.assertTrue(isapprox(self.news.getbound(), -97.9, 1e-3)) 31 | 32 | def test_risk(self): 33 | pass 34 | 35 | 36 | 37 | 38 | 39 | if __name__ == '__main__': 40 | unittest.main() 41 | -------------------------------------------------------------------------------- /sddp/example/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | 8 | -------------------------------------------------------------------------------- /sddp/example/simple_object_noise.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | 8 | import unittest 9 | 10 | from pyomotools.tools import cplex 11 | from sddp.SDDP import createSDDPModel 12 | 13 | 14 | from sddp.typedefinitions import * 15 | 16 | CplexSolver = cplex() # type:CPLEXSHELL 17 | 18 | 19 | def solve_model(noise_probability:List[float]): 20 | def build(sp: Subproblem, t: int, markov_state: int): 21 | model = sp.model 22 | model.x = Var(domain=NonNegativeReals) 23 | model.x0 = Var(domain=NonNegativeReals) 24 | model.xp = Param(initialize=1.5, mutable=True) 25 | sp.add_state(model.x, model.x0, model.xp) 26 | # 变量 27 | model.u = Var(bounds=(0, 1)) 28 | # 约束 29 | sp.anonymous = Constraint(expr=model.x == model.x0 - model.u) 30 | if t == 0: 31 | sp.obj = model.u * 2 32 | else: 33 | sp.anonymous = Param(mutable=True) 34 | p = sp.anonymous 35 | sp.obj = p * model.u 36 | sp.setnoiseprobability(noise_probability) 37 | 38 | m=createSDDPModel(sense=Sense.Max,stages=2,objective_bound=5,) 39 | 40 | 41 | 42 | 43 | class MyTestCase(unittest.TestCase): 44 | def test_something(self): 45 | self.assertEqual(True, False) 46 | 47 | 48 | if __name__ == '__main__': 49 | unittest.main() 50 | -------------------------------------------------------------------------------- /sddp/example/tools.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | -------------------------------------------------------------------------------- /sddp/noises.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | 8 | -------------------------------------------------------------------------------- /sddp/price_interpolation/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | 8 | -------------------------------------------------------------------------------- /sddp/price_interpolation/discreate_distribution.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | 8 | from typing import List 9 | from sddp.utilities import * 10 | 11 | T = TypeVar('T') 12 | 13 | 14 | 15 | class NoiseRealization(Generic[T]): 16 | def __init__(self, observation: T, probability: float): 17 | # 具体的值 18 | self.observation = observation 19 | self.probability = probability 20 | 21 | 22 | class DiscreteDistribution(Generic[T]): 23 | def __init__(self, noises): 24 | self.noises = noises # type:List[NoiseRealization[T]] 25 | 26 | def create(self, observations: List, probabilities: List[float] = None): 27 | if probabilities is None: 28 | probabilities = [1 / len(observations)] * len(observations) 29 | 30 | if not isapprox(sum(probabilities), 1.0, atol=1e-6): 31 | raise RuntimeError("Finite discrete distribution must sum to 1.0") 32 | y = [NoiseRealization(xi, pi) for xi, pi in zip(observations, probabilities)] 33 | return DiscreteDistribution(y) 34 | 35 | @property 36 | def probabilities(self): 37 | return [n.probability for n in self.noises] 38 | 39 | def sample(self): 40 | nidx = sample(self.probabilities) 41 | return self.noises[nidx] 42 | 43 | def __getitem__(self, item): 44 | return self.noises[item] 45 | 46 | def __len__(self): 47 | return len(self.noises) 48 | # 49 | # def __next__(self): 50 | # return self.noises.__next__() 51 | -------------------------------------------------------------------------------- /sddp/price_interpolation/dynamic_price_interpolation.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | 8 | from typing import List, Generic, Callable 9 | from sddp.price_interpolation.discreate_distribution import * 10 | from sddp.price_interpolation.dynamic_price_interpolation_oracle import DynamicOracle 11 | from sddp.price_interpolation.price_interpolation import PriceInterpolationMethods 12 | from itertools import product 13 | 14 | T = TypeVar('T') 15 | C = TypeVar('C', DynamicOracle) 16 | T2 = TypeVar('T2') 17 | 18 | class DynamicPriceInterpolation(PriceInterpolationMethods, Generic[C, T, T2]): 19 | def __init__(self, 20 | initial_price: T, 21 | location: T, 22 | minprice: T, 23 | maxprice: T, 24 | noises: DiscreteDistribution[T2], 25 | objective: Callable, 26 | dynamics: Callable, 27 | mu: List[Var], 28 | oracle: C, 29 | lipschitz_constant: float, 30 | bound: float): 31 | self.initial_price = initial_price 32 | self.location = location 33 | self.minprice = minprice 34 | self.maxprice = maxprice 35 | self.noises = noises # type:DiscreteDistribution 36 | self.objective = objective 37 | self.dynamics = dynamics 38 | self.mu = mu # type:List[Var] 39 | self.oracle = oracle # type:C 40 | self.lipschitz_constant = lipschitz_constant # type:float 41 | self.bound = bound # type:float 42 | 43 | @staticmethod 44 | def create( 45 | dynamics=lambda p, w: p, 46 | initial_price: float = 0.0, 47 | min_price=0.0, 48 | max_price=1.0, 49 | noise=DiscreteDistribution.create([0.0, 1.0]), 50 | lipschitz_constant=1e6, 51 | cut_oracle=None): 52 | return DynamicPriceInterpolation( 53 | initial_price, 54 | initial_price, 55 | min_price, 56 | max_price, 57 | noise, 58 | None, # TODO (p)->QuadExpr(p) 59 | dynamics, 60 | [], 61 | cut_oracle, 62 | lipschitz_constant, 63 | 0.0 64 | ) 65 | 66 | def interpolate(self, price=None, mu=None): # TODO 67 | if mu is None: 68 | mu = self.mu 69 | if price is None: 70 | price = self.location 71 | return mu[0] + price * mu[1] # TODO 72 | 73 | def addpricecut(self, sense: Sense, sp: Subproblem, price, affexpr): 74 | if sense == Sense.Max: 75 | sp.anonymous = Constraint(expr=self.interpolate(price, self.mu) <= affexpr) 76 | else: 77 | sp.anonymous = Constraint(expr=self.interpolate(price, self.mu) >= affexpr) 78 | 79 | def initializevaluefunction(self, sp: 'Subproblem', sense: Sense, bound: float): 80 | N = 1 # TODO 根据不同类型,取不同的值 81 | if isinstance(self.location, tuple): 82 | N = len(self.location) # 对应 NTuple{N,T} 中的N 83 | self.bound = bound 84 | sp.anonymous = Var() 85 | self.mu.append(sp.anonymous) 86 | for i in range(N): 87 | sp.anonymous = Var(lb=-self.lipschitz_constant, ub=self.lipschitz_constant) 88 | self.mu.append(sp.anonymous) 89 | 90 | if 1 < N <= 4: 91 | for price in product(*zip(self.minprice, self.maxprice)): # TODO 92 | self.addpricecut(sense, sp, price, bound) 93 | else: 94 | self.addpricecut(sense, sp, self.minprice, bound) 95 | self.addpricecut(sense, sp, self.maxprice, bound) 96 | return self 97 | 98 | def addcut(self, m: SDDPModel, sp: Subproblem, current_price, cut: Cut): 99 | vf = self 100 | vf.oracle.storecut(m, sp, cut, current_price) 101 | affexpr = cuttoaffexpr(sp, cut) 102 | self.addpricecut(sp.sense, sp, current_price, affexpr) 103 | 104 | def updatevaluefunction(self, m: SDDPModel, settings: Settings, t: int, sp: Subproblem): 105 | vf = self # vf = sp.valueoracle # type:StaticPriceInterpolation 106 | current_price = vf.location 107 | cut = constructcut(m, sp, t, current_price) 108 | self.addcut(m, sp, current_price, cut) 109 | # TODO 并行 110 | 111 | @property 112 | def cutoracle(self) -> AbstractCutOracle: 113 | return self.oracle 114 | 115 | def modifyvaluefunction(self, m: 'SDDPModel', setting: 'Settings', sp: 'Subproblem'): 116 | pass 117 | 118 | def rebuildsubproblem(self, m: 'SDDPModel', sp: 'Subproblem'): 119 | vf = self 120 | sp.states.clear() 121 | sp.noises.clear() 122 | sp.reset_mod() 123 | N = len(vf.mu) - 1 124 | self.mu.clear() 125 | self.initializevaluefunction(sp, sp.sense, vf.bound, N) 126 | m.build(sp, sp.stage, sp.markov_state) 127 | for cut in vf.cutoracle.validcuts(): 128 | affexpr = cuttoaffexpr(sp, cut[0]) 129 | self.addpricecut(sp.sense, sp, cut[1], affexpr) 130 | m.stages[sp.stage].subproblems[sp.markov_state] = sp -------------------------------------------------------------------------------- /sddp/price_interpolation/dynamic_price_interpolation_oracle.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | 8 | from typing import Any, Tuple, List, TypeVar, Generic 9 | import numpy as np 10 | from sddp.typedefinitions import AbstractCutOracle, Cut 11 | 12 | T = TypeVar('T') 13 | 14 | 15 | class DynamicOracle(AbstractCutOracle, Generic[T]): 16 | def storecut(self, m: 'SDDPModel', sp: 'Subproblem', cut: 'Cut', price: T): 17 | pass 18 | 19 | def validcuts(self) -> List[Tuple[Cut, T]]: 20 | pass 21 | 22 | def __init__(self): 23 | pass 24 | 25 | 26 | class DefaultDynamicOracle(DynamicOracle[T]): 27 | def __init__(self, cuts=None): 28 | if cuts is None: 29 | cuts = [] 30 | self.cuts = cuts # type: List[Tuple[Cut,T]] 31 | 32 | def storecut(self, m: 'SDDPModel', sp: 'Subproblem', cut: 'Cut', price: T): 33 | self.cuts.append((cut, price)) 34 | 35 | def validcuts(self): 36 | return self.cuts 37 | 38 | 39 | class NanniciniOracle(DynamicOracle): 40 | def __init__(self, 41 | rho: int, 42 | cutsinmodel: int, 43 | cuts: List[Tuple[Cut, Any]], 44 | iterations_since_last_active: List[int]): 45 | self.iterations_since_last_active = iterations_since_last_active 46 | self.cuts = cuts 47 | self.cutsinmodel = cutsinmodel 48 | self.rho = rho 49 | 50 | def storecut(self, m: 'SDDPModel', sp: 'Subproblem', cut: 'Cut', price: T): 51 | self.cuts.append((cut, price)) 52 | self.iterations_since_last_active.append(0) 53 | self.cutsinmodel += 1 54 | idxs = [x for x in self.iterations_since_last_active if x < self.rho] 55 | if len(idxs) <= 0: 56 | raise RuntimeError("No cuts in model used in the last %d iterations." % self.rho) 57 | idx = idxs[0] 58 | self.cutsinmodel = len(self.cuts) - idx + 1 59 | return self.cuts[idx:] 60 | 61 | def validcuts(self): 62 | p = reversed(np.argsort(self.iterations_since_last_active)).tolist() 63 | self.cuts = [self.cuts[i] for i in p] 64 | self.iterations_since_last_active = [self.iterations_since_last_active[i] for i in p] 65 | -------------------------------------------------------------------------------- /sddp/price_interpolation/price_interpolation.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | from typing import Callable 8 | 9 | from sddp.price_interpolation.discreate_distribution import * 10 | from sddp.price_interpolation.dynamic_price_interpolation_oracle import DynamicOracle 11 | from sddp.state import setstates 12 | 13 | T = TypeVar('T') 14 | C = TypeVar('C', DynamicOracle) 15 | T2 = TypeVar('T2') 16 | 17 | 18 | class PriceInterpolationMethods(AbstractValueFunction): 19 | 20 | def __init__(self): 21 | self.objective = None # type:Callable 22 | self.location = None, 23 | self.initial_price = None 24 | self.noises = None # type:DiscreteDistribution 25 | 26 | def interpolate(self): 27 | raise NotImplementedError 28 | 29 | def updatevaluefunction(self, m: SDDPModel, settings: Settings, t: int, sp: Subproblem): 30 | raise NotImplementedError 31 | 32 | @staticmethod 33 | def backwardpass(m: SDDPModel, settings: Settings): 34 | for t in reversed(range(m.nstages - 1)): 35 | for sp in m.stages[t].subproblems: 36 | vf = sp.valueoracle # type:PriceInterpolationMethods 37 | vf.updatevaluefunction(m, settings, t, sp) 38 | calculatefirststagebound(m) 39 | 40 | @staticmethod 41 | def solvesubproblem(m: SDDPModel, sp: Subproblem, solutionstore, dirction=Direction.forwardpass): 42 | if dirction != Direction.forwardpass: 43 | return 44 | vf = sp.valueoracle # type:PriceInterpolationMethods 45 | if sp.stage == 0: 46 | vf.location = vf.initial_price 47 | p = vf.location 48 | w = samplepricenois(sp.stage, vf.noises, solutionstore) 49 | setobjective(sp, p, w) 50 | passpriceforward(m, sp) 51 | pyomoSolve(Direction.forwardpass, m, sp) 52 | 53 | @staticmethod 54 | def solvepricenoises(m: SDDPModel, sp: Subproblem, last_markov_state, price): 55 | vf = sp.valueoracle # type:PriceInterpolationMethods 56 | markov_prob = m.stages[sp.stage].transitionprobabilities[last_markov_state, sp.markov_state] 57 | for price_noice in vf.noises.noises: 58 | setobjective(sp, price, price_noice.observation) 59 | vf.solvesubproblem(m, sp, markov_prob * price_noice.observation) 60 | 61 | @staticmethod 62 | def constructcut(m: SDDPModel, sp: Subproblem, t, price): 63 | m.storage.reset() 64 | for sp2 in m.stages[t + 1].subproblems: 65 | setstates(m, sp2) 66 | vf = sp2.valueoracle # type:PriceInterpolationMethods 67 | vf.solvepricenoises(m, sp2, sp.markov_state, price) 68 | I = list(range(len(m.storage.objective))) 69 | modifiedprobability = sp.riskmeasure.modifyprobability(m.storage.modifiedprobability.range(I), 70 | m.storage.probability.range(I), 71 | m.storage.objective.range(I), 72 | m, sp) 73 | m.storage.modifiedprobability.put_range(I, modifiedprobability) 74 | return m.constructcut(sp, m.storage) 75 | 76 | def setstageobjective(self, 77 | sp: 'Subproblem', obj): 78 | self.objective = obj 79 | 80 | def getstageobjective(self, 81 | sp: 'Subproblem'): 82 | if sp.finalstage: 83 | return sp.getobjectivevalue() 84 | else: 85 | return sp.getobjectivevalue() - value(self.interpolate()) 86 | 87 | 88 | def samplepricenois(stage: int, noises: DiscreteDistribution, solutionstore: Dict = None): 89 | if "pricenoise" in solutionstore: 90 | noiseidx = solutionstore["pricenoise"][stage] 91 | return noises[noiseidx].observation 92 | else: 93 | return noises.sample() 94 | 95 | 96 | def calculatefirststagebound(m: SDDPModel): 97 | m.storage.reset() 98 | for sp in m.stages[0].subproblems: 99 | vf = sp.valueoracle # type:PriceInterpolationMethods 100 | vf.solvepricenoises(m, sp, 0, vf.initial_price) 101 | return float(np.dot(m.storage.objective, m.storage.probability)) 102 | 103 | 104 | def setobjective(sp: Subproblem, price, noise): 105 | vf = sp.valueoracle # type:StaticPriceInterpolation 106 | p = vf.dynamics(price, noise) 107 | vf.location = p # 目前的价格 108 | 109 | # stage objective obj 110 | stageobj = vf.objective(p) 111 | 112 | # future objective 113 | future_value = vf.interpolate() 114 | if sp.finalstage: 115 | sp._obj = Objective(expr=stageobj, sense=sp.sense.value) 116 | else: 117 | sp._obj = Objective(expr=stageobj + future_value, sense=sp.sense.value) 118 | 119 | 120 | def passpriceforward(m: SDDPModel, sp: Subproblem): 121 | stage = sp.stage 122 | if stage < m.nstages - 1: 123 | for sp2 in m.stages[stage + 1].subproblems: 124 | sp2.valueoracle.location = sp.valueoracle.location 125 | -------------------------------------------------------------------------------- /sddp/price_interpolation/static_price_interpolation.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | import copy 8 | 9 | from sddp.cut_oracles.DefaultCutOracle import DefaultCutOracle 10 | from sddp.price_interpolation.discreate_distribution import * 11 | from sddp.price_interpolation.price_interpolation import PriceInterpolationMethods 12 | 13 | 14 | class StaticPriceInterpolation(PriceInterpolationMethods): 15 | def __init__(self, 16 | initial_price, 17 | location, 18 | rib_locations: List, 19 | variables: List[Var], 20 | cutoracles, 21 | noises: DiscreteDistribution, 22 | objective, 23 | dynamics, 24 | bound: float): 25 | self.rib_locations = rib_locations 26 | self.location = location 27 | self.initial_price = initial_price 28 | self.variables = variables 29 | self.cutoracles = cutoracles # type:List[AbstractCutOracle] 30 | self.noises = noises # type:DiscreteDistribution 31 | self.objective = objective 32 | self.dynamics = dynamics 33 | self.bound = bound 34 | self._defalutcutoracle = None 35 | 36 | def set_default_cutoracle(self, cut_oracle): 37 | self._defalutcutoracle = cut_oracle 38 | 39 | def new_cutoracle_instence(self): 40 | return copy.deepcopy(self._defalutcutoracle) 41 | 42 | @staticmethod 43 | def create(cut_oracle=DefaultCutOracle(), 44 | dynamics=lambda p, w: p, 45 | initial_price: float = 0.0, 46 | rib_location=[0.0, 1.0], 47 | noise=DiscreteDistribution.create([0.0, 1.0]) 48 | ): 49 | res = StaticPriceInterpolation( 50 | initial_price, 51 | initial_price, 52 | copy.deepcopy(rib_location), 53 | [], 54 | [], 55 | noise, 56 | None, # TODO (p)->QuadExpr(p) 57 | dynamics, 58 | 0.0 59 | ) 60 | res.set_default_cutoracle(cut_oracle) 61 | return res 62 | 63 | def initializevaluefunction(self, sp: 'Subproblem', sense: Sense, bound: Reals) -> AbstractValueFunction: 64 | self.bound = bound 65 | for r in self.rib_locations: 66 | sp.anonymous = futureobjective(sense, bound) 67 | self.variables.append(sp.anonymous) 68 | self.cutoracles.append(self.new_cutoracle_instence()) 69 | return self 70 | 71 | def interpolate(self) -> Expression: 72 | # y = AffExpr(0.0) 截距 73 | vf = self 74 | y = 0 75 | if len(vf.rib_locations) == 1: 76 | y.append(vf.variables[0]) 77 | else: 78 | upper_idx = len(vf.rib_locations) 79 | for i in range(2, len(vf.rib_locations) + 1): 80 | if vf.location <= vf.rib_locations[i - 1]: 81 | upper_idx = i 82 | break 83 | lower_idx = upper_idx - 1 84 | lamb = (vf.location - vf.rib_locations[lower_idx - 1]) / ( 85 | vf.rib_locations[upper_idx - 1] - vf.rib_locations[lower_idx - 1]) 86 | if lamb < -1e-6 or lamb > 1.0 + 1e-6: 87 | raise RuntimeError( 88 | "The location %s is outside the interpolated region. lambda = %s" % (vf.location, lamb)) 89 | y = y + vf.variables[lower_idx - 1] * (1 - lamb) 90 | y = y + vf.variables[upper_idx - 1] * lamb 91 | return y 92 | 93 | @property 94 | def cutoracle(self) -> AbstractCutOracle: 95 | return self.cutoracle 96 | 97 | 98 | def addcuttoPyomoModel(self, sp: Subproblem, theta: Var, cut: Cut): 99 | cut_expr = cut.intercept # type: Expression 100 | for c, s in zip(cut.coefficients, self.states): 101 | cut_expr = c * s.variable + cut_expr 102 | if sp.sense == Sense.Min: 103 | sp.add_constraint(expr=theta >= cut_expr) 104 | else: 105 | sp.add_constraint(expr=theta <= cut_expr) 106 | 107 | def rebuildsubproblem(self, m: 'SDDPModel', sp: 'Subproblem'): 108 | vf = self 109 | # vf = sp.valueoracle # type:# StaticPriceInterpolation 110 | sp.states.clear() 111 | sp.noises.clear() 112 | sp.reset_mod() 113 | 114 | vf.variables.clear() 115 | for r in vf.rib_locations: 116 | sp.anonymous = futureobjective(sp.sense, sp.problembound) 117 | vf.variables.append(sp.anonymous) 118 | 119 | m.build(sp, sp.stage, sp.markov_state) 120 | for i in range(len(vf.variables)): 121 | for cut in vf.cutoracles[i]: 122 | vf.addcuttoPyomoModel(sp, vf.variables[i], cut) 123 | m.stages[sp.stage].subproblems[sp.markov_state] = sp 124 | 125 | def updatevaluefunction(self, m: SDDPModel, settings: Settings, t: int, sp: Subproblem): 126 | vf = self # vf = sp.valueoracle # type:StaticPriceInterpolation 127 | for i, (rib, theta, cutoracle) in enumerate(zip(vf.rib_locations, vf.variables, vf.cutoracles)): 128 | cut = self.constructcut(m, sp, t, rib) # 进行了计算 129 | # TODO write cut 130 | cutoracle.storecut(m, sp, cut) 131 | vf.addcuttoPyomoModel(sp, theta, cut) 132 | if settings.is_asyncronous: # TODO 133 | pass -------------------------------------------------------------------------------- /sddp/print.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | from sddp.typedefinitions import SDDPModel, Settings, SolutionLog 8 | # from prettytable import PrettyTable 9 | 10 | 11 | def atol(x, y): abs(x - y) 12 | 13 | 14 | def rtol(x, y): 15 | ar = abs(x - y) 16 | br = (1 + abs(y)) 17 | return ar / br 18 | 19 | 20 | def humanize(value: int) -> str: 21 | if 1000 > value > -1000: 22 | return "%5d" % value 23 | else: 24 | return "%5.1f" % value 25 | 26 | 27 | def printheader(m: SDDPModel, solve_type="todo"): 28 | n = m.nstages 29 | print(""" 30 | ------------------------------------------------------------------------------- 31 | SDDP 32 | ------------------------------------------------------------------------------- 33 | Solver: 34 | %s 35 | Model: 36 | Stages: %d 37 | States: %d 38 | Subproblems: %d 39 | Value Function: === 40 | ------------------------------------------------------------------------------- 41 | """ % (solve_type, 42 | m.nstages, 43 | m.stages[0].subproblems[0].nstates, 44 | sum(len(s.subproblems) for s in m.stages) 45 | )) 46 | print(" Objective | Cut Passes Simulations Total ") 47 | print(" Simulation Bound % Gap | # Time # Time Time ") 48 | print("-------------------------------------------------------------------------------") 49 | 50 | 51 | def print_solutionLog(l: SolutionLog, printmean: bool = False, is_min=True): 52 | if printmean: 53 | bound_string = " " + "%8.3f" % (0.5 * (l.lower_statistical_bound + l.upper_statistical_bound)) + " " 54 | rtol_string = " " 55 | else: 56 | bound_string = "%8.3f" % (l.lower_statistical_bound) + " " + "%8.3f" % (l.upper_statistical_bound) 57 | if is_min: 58 | tt = rtol(l.lower_statistical_bound, l.bound) 59 | tol = 100 * tt 60 | else: 61 | tol = -100 * rtol(l.upper_statistical_bound, l.bound) 62 | rtol_string = " %5.1f" % tol 63 | 64 | res_str = "%s %8.3f %s | %s %8.1f %s %s %8.1f" % (bound_string, 65 | l.bound, 66 | rtol_string, 67 | humanize(l.iteration), 68 | l.timecuts, 69 | humanize(l.simulations), 70 | humanize(l.timesimulations), 71 | l.timetotal 72 | # humanize(l.timetotal) 73 | ) 74 | print(res_str) 75 | 76 | 77 | def printfooter(m: SDDPModel, settings: Settings, status, timer): 78 | print("-------------------------------------------------------------------------------") 79 | # if settings.print_level > 1: 80 | # print_timer(io, timer, title="Timing statistics") 81 | # print(io, "\n") 82 | # end 83 | print(""" Other Statistics: 84 | Iterations: %d 85 | Termination Status: %s 86 | ===============================================================================""" % (m.log[-1].iteration, status)) 87 | -------------------------------------------------------------------------------- /sddp/riskmeasures.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | import math 8 | from enum import Enum 9 | from typing import List, Tuple 10 | from sddp.typedefinitions import SDDPModel, Subproblem, AbstractRiskMeasure, Sense 11 | import numpy as np 12 | 13 | 14 | # 15 | # def track_data(sourc: List, dest: List): 16 | # dest.clear() 17 | from sddp.utilities import dominates 18 | 19 | 20 | class Expectation(AbstractRiskMeasure): 21 | def __init__(self): 22 | pass 23 | 24 | def modifyprobability(self, riskadjusted_distribution: List[float], original_distribution: List[float], 25 | observations: List[float], m: SDDPModel, sp: Subproblem): 26 | riskadjusted_distribution = [d for d in original_distribution] 27 | # riskadjusted_distribution.clear() 28 | # riskadjusted_distribution.extend(original_distribution) 29 | return riskadjusted_distribution 30 | 31 | 32 | class WorstCase(AbstractRiskMeasure): 33 | def __init__(self): 34 | pass 35 | 36 | def modifyprobability(self, riskadjusted_distribution: List[float], original_distribution: List[float], 37 | observations: List[float], m: 'SDDPModel', sp: 'Subproblem'): 38 | riskadjusted_distribution = [0.0] * len(riskadjusted_distribution) 39 | worst_idx = 0 40 | worst_observation = float("inf") if m.sense == Sense.Max else -float("inf") 41 | for idx, (probability, observation) in enumerate(zip(original_distribution, observations)): 42 | if probability > 0 and dominates(m.sense, observation, worst_observation): 43 | worst_idx = idx 44 | worst_observation = observation 45 | riskadjusted_distribution[worst_idx] = 1 46 | return riskadjusted_distribution 47 | 48 | 49 | class RiskMeasures(Enum): 50 | Expectation = Expectation() 51 | WorstCase = WorstCase() 52 | 53 | 54 | class AVaR(AbstractRiskMeasure): 55 | def __init__(self, beta: float): 56 | if beta > 1.0 or beta < 0: 57 | raise RuntimeError( 58 | "Beta must be in the range [0, 1]. Increasing values of beta are less risk averse. beta=1 is identical to taking the expectation.") 59 | self.beta = beta 60 | 61 | def modifyprobability(self, riskadjusted_distribution: List[float], original_distribution: List[float], 62 | observations: List[float], m: 'SDDPModel', sp: 'Subproblem'): 63 | if self.beta < 1e-8: 64 | return RiskMeasures.WorstCase.value.modifyprobability(riskadjusted_distribution, original_distribution, 65 | observations, m, sp) 66 | elif self.beta > 1.0 - 1e-8: 67 | return RiskMeasures.Expectation.value.modifyprobability(riskadjusted_distribution, original_distribution, 68 | observations, m, sp) 69 | else: 70 | ismax = sp.sense == Sense.Max 71 | riskadjusted_distribution = [0.0] * len(riskadjusted_distribution) 72 | q = 0.0 73 | idx = np.argsort(observations) 74 | if not ismax: 75 | idx = reversed(idx) 76 | for i in idx: 77 | if q >= self.beta: 78 | break 79 | avar_prob = min(original_distribution[i], self.beta - q) / self.beta 80 | riskadjusted_distribution[i] = avar_prob 81 | q += avar_prob * self.beta 82 | return riskadjusted_distribution 83 | 84 | 85 | 86 | 87 | 88 | class ConvexCombination(AbstractRiskMeasure): 89 | def __init__(self, riskmeasures: List[Tuple[float, AbstractRiskMeasure]]): 90 | self.measures = riskmeasures # type:List[Tuple[float,AbstractRiskMeasure]] 91 | 92 | def modifyprobability(self, riskadjusted_distribution: List[float], original_distribution: List[float], 93 | observations: List[float], m: 'SDDPModel', sp: 'Subproblem'): 94 | riskadjusted_distribution = np.array([0.0] * len(riskadjusted_distribution)) 95 | for wight, measure in self.measures: 96 | y = [0.0] * len(original_distribution) 97 | y = measure.modifyprobability(y, original_distribution, observations, m, sp) 98 | riskadjusted_distribution = np.multiply(wight, y) + riskadjusted_distribution 99 | return riskadjusted_distribution.tolist() 100 | 101 | 102 | class EAVaR(ConvexCombination): 103 | def __init__(self, lamb: float = 1, beta: float = 0): 104 | if lamb > 1.0 or lamb < 0.0: 105 | raise RuntimeError( 106 | "Lambda must be in the range [0, 1]. Increasing values of lambda are less risk averse. lambda=1 is identical to taking the expectation.") 107 | if beta > 1.0 or beta < 0.0: 108 | raise RuntimeError( 109 | "Beta must be in the range [0, 1]. Increasing values of beta are less risk averse. beta=1 is identical to taking the expectation.") 110 | self.measures = [ 111 | (lamb, Expectation()), 112 | (1 - lamb, AVaR(beta)) 113 | ] 114 | 115 | 116 | class DRO(AbstractRiskMeasure): 117 | def __init__(self, radius): 118 | self.radius = radius # type:float 119 | 120 | def popvar(self, x: List[float]) -> float: 121 | """ 122 | 方差 123 | """ 124 | ninv = 1 / len(x) 125 | return ninv * sum(_x ** 2 for _x in x) - (ninv * sum(x)) ** 2 126 | 127 | def popstd(self, x): 128 | """ 129 | 标准差 130 | """ 131 | return math.sqrt(self.popvar(x)) 132 | 133 | def is_dro_applicable(self, radius: float, observations: List[float]): 134 | if abs(radius) < 1e-9: 135 | return False 136 | elif abs(self.popstd(observations)) < 1e-9: 137 | return False 138 | return True 139 | 140 | def getconstfactor(self, S: int, k: int, radius: float, permuted_observations: List[float]): 141 | stdz = self.popstd(permuted_observations[k :S]) 142 | return math.sqrt((S - k) * radius ** 2 - k / S) / (stdz * (S - k)) 143 | 144 | def getconstadditive(self, S: int, k: int, const_factor: float, permuted_observations: List[float]): 145 | avgz = np.mean(permuted_observations[k:S ]) 146 | return 1 / (S - k) + const_factor * avgz 147 | 148 | def modifyprobability(self, riskadjusted_distribution: List[float], 149 | original_distribution: List[float], observations: List[float], m: 'SDDPModel', 150 | sp: 'Subproblem'): 151 | S = len(observations) 152 | r = self.radius 153 | if not self.is_dro_applicable(r, observations): 154 | riskadjusted_distribution = [1 / S] * S 155 | return riskadjusted_distribution 156 | 157 | if sp.sense == Sense.Min: 158 | perm = np.argsort(observations) 159 | permuted_observations = (-np.sort(observations)).tolist() 160 | else: 161 | perm = (np.argsort(observations)[::-1]).tolist() 162 | permuted_observations = (np.sort(observations)[::-1]).tolist() 163 | 164 | for k in range(S - 1): 165 | if k > 0: 166 | riskadjusted_distribution[perm[k - 1]] = 0.0 167 | const_factor = self.getconstfactor(S, k, r, permuted_observations) 168 | const_additive = self.getconstadditive(S, k, const_factor, permuted_observations) 169 | for i in range(k+1, S+1): 170 | riskadjusted_distribution[perm[i-1]] = const_additive - const_factor * permuted_observations[i-1] 171 | 172 | if riskadjusted_distribution[perm[k]] >= 0.0: 173 | break 174 | return riskadjusted_distribution 175 | -------------------------------------------------------------------------------- /sddp/sddip/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | 8 | -------------------------------------------------------------------------------- /sddp/sddip/binary_expansion.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | import math 8 | import sys 9 | 10 | import numpy as np 11 | from typing import List 12 | 13 | log2inv = 1 / math.log(2) 14 | _2i_ = [2 ** i for i in range(math.floor(math.log(sys.maxsize) * log2inv))] 15 | _2i_L = len(_2i_) 16 | 17 | 18 | def binexpand_(y: List[int], x: int): 19 | if x < 0: 20 | raise RuntimeError("Values to be expanded must be nonnegative. Currently x = %d" % x) 21 | for i in reversed(range(len(y))): 22 | k = _2i_[i] 23 | if x >= k: 24 | y[i] = 1 25 | x -= k 26 | if x > 0: 27 | raise RuntimeError("Unable to expand binary. Overflow of %d" % x) 28 | 29 | 30 | def bitsrequired(x: int or float, eps=0.1): 31 | if type(x) == float: 32 | x = np.round(x / eps) 33 | return math.floor(math.log(x) / log2inv) + 1 34 | 35 | 36 | def binexpand_int(x: int, length: int = -1, maximun: float = -1): 37 | if x < 0: 38 | raise RuntimeError("Cannot perform binary expansion on a negative number.") 39 | if maximun != -1: 40 | length = bitsrequired(math.floor(maximun)) 41 | if length == -1: 42 | y = [0] * bitsrequired(x) 43 | else: 44 | y = [0] * length 45 | binexpand_(y, x) 46 | return y 47 | 48 | 49 | def binexpand_float(x: float, eps: float = 0.1, length: int = -1, maximun: float = -1): 50 | if x < 0: 51 | raise RuntimeError("Cannot perform binary expansion on a negative number.") 52 | if eps <= 0: 53 | raise RuntimeError("Epsilon tolerance for Float binary expansion must be strictly greater than 0.") 54 | xx = np.round(x / eps) 55 | if maximun != -1: 56 | length = bitsrequired(math.floor(maximun / eps)) 57 | binexpand_(xx, length=length) 58 | return xx 59 | 60 | 61 | def bincontract_2i_(y: List): 62 | x = 0 63 | for i in range(len(y)): 64 | x += _2i_[i] * y[i] 65 | return x 66 | 67 | 68 | def bincontract_pow(y: List): 69 | x = 0 70 | for i in range(len(y)): 71 | x += 2 ** i * y[i] 72 | return x 73 | 74 | 75 | def bincontract(y: List): 76 | if len(y) < _2i_L: 77 | return bincontract_2i_(y) 78 | else: 79 | return bincontract_pow(y) 80 | 81 | 82 | def bincontract_float(y: List, eps: float = 0.1): 83 | if eps <= 0: 84 | raise RuntimeError("Epsilon tolerance for Float binary contraction must be strictly greater than 0.") 85 | return binexpand_(y) * eps 86 | 87 | -------------------------------------------------------------------------------- /sddp/sddip/solver.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | -------------------------------------------------------------------------------- /sddp/state.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | 8 | # from sddp.typedefinitions import * 9 | from sddp.typedefinitions import SDDPModel, Subproblem 10 | 11 | 12 | def setstates(m: SDDPModel, sp: Subproblem): 13 | """ 14 | 将上一阶段算出的结果作为当前时段的初始值 15 | """ 16 | s = m.stages[sp.stage - 1] 17 | # TODO SDDP.jl号称是数值问题, 18 | # https://github.com/odow/SDDP.jl/issues/6#issuecomment-343022931 19 | for st, v in zip(sp.states, s.state): # type:State,float 20 | lb = st.variable.lb 21 | up = st.variable.ub 22 | if v < lb: 23 | st.setvalue(lb) 24 | elif v > up: 25 | st.setvalue(v) 26 | else: 27 | st.setvalue(v) 28 | 29 | 30 | -------------------------------------------------------------------------------- /sddp/test/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | 8 | -------------------------------------------------------------------------------- /sddp/test/test_list.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | import unittest 8 | from sddp.typedefinitions import CachedVector 9 | 10 | 11 | class MyTestCase(unittest.TestCase): 12 | def test_something(self): 13 | ls = [1, 2, 3, 4, 5] 14 | cl = CachedVector(ls) 15 | self.assertEqual(cl[1], ls[1]) 16 | cl[1] = 10 17 | self.assertEqual(10, ls[1]) 18 | self.assertEqual(len(cl), len(ls)) 19 | cl[1] *= 2 20 | self.assertEqual(cl[1], ls[1]) 21 | print("c1[1]=%f" % cl[1]) 22 | self.assertEqual(cl[1], 20) 23 | print(cl.range(list(range(3)))) 24 | 25 | 26 | if __name__ == '__main__': 27 | unittest.main() 28 | -------------------------------------------------------------------------------- /sddp/test/test_matplotlib.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | import unittest 8 | 9 | import numpy as np 10 | import matplotlib.pyplot as plt 11 | 12 | plt.rcParams['font.sans-serif'] = ['SimHei'] # 步骤一(替换sans-serif字体) 13 | plt.rcParams['axes.unicode_minus'] = False # 步骤二(解决坐标轴负数的负号显示问题) 14 | 15 | 16 | class MyTestCase(unittest.TestCase): 17 | def test_something(self): 18 | x = [1, 2, 3, 4, 5] 19 | y1 = [1, 1, 2, 3, 5] 20 | y2 = [0, 4, 2, 6, 8] 21 | y3 = [1, 3, 5, 7, 9] 22 | ys = [y1, y2, y3] 23 | y = np.vstack([y1, y2, y3]) 24 | 25 | labels = ["测试0 ", "测试1", "测试2"] 26 | 27 | fig, ax = plt.subplots() 28 | # ax.stackplot(x, y1, y2, y3, labels=labels) 29 | ax.stackplot(x, *ys, labels=labels) 30 | ax.legend(loc='upper left') 31 | plt.show() 32 | # 33 | # fig, ax = plt.subplots() 34 | # ax.stackplot(x, y) 35 | # plt.show() 36 | 37 | 38 | if __name__ == '__main__': 39 | unittest.main() 40 | -------------------------------------------------------------------------------- /sddp/test/test_pyomo.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | import unittest 8 | from pyomo.solvers.plugins.solvers.CPLEX import * 9 | from pyomo.environ import * 10 | from pyomo.opt import SolverFactory 11 | from pyomo.core.base.var import * 12 | from pyomo.core.base.param import SimpleParam 13 | # from sddp.pyomo_tool import * 14 | from pyomotools.tools import model_str 15 | 16 | 17 | class MyTestCase(unittest.TestCase): 18 | def setUp(self): 19 | self.opt = SolverFactory('cplex', 20 | executable="/opt/ibm/ILOG/CPLEX_Studio128/cplex/bin/x86-64_linux/cplex") # type:CPLEXSHELL 21 | self.model = ConcreteModel() 22 | 23 | def test_base(self): 24 | """ 25 | min x**2 26 | """ 27 | model = self.model 28 | model.x = Var(domain=Reals, bounds=(1, None)) 29 | model.obj = Objective(expr=model.x ** 2, sense=minimize) 30 | result = self.opt.solve(model) 31 | self.assertEquals(value(model.x), 1) 32 | self.assertEquals(value(model.obj), 1) 33 | print(type(result)) 34 | print(result.solver.status) 35 | def test_model_expresion(self): 36 | """ 37 | min x**2 38 | """ 39 | model = self.model 40 | model.x = Var(domain=Reals, bounds=(1, None)) 41 | model.obj = Objective(expr=model.x ** 2, sense=minimize) 42 | result = self.opt.solve(model) 43 | print(model_str(model)) 44 | 45 | 46 | 47 | 48 | if __name__ == '__main__': 49 | unittest.main() 50 | -------------------------------------------------------------------------------- /sddp/typedefinitions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | 7 | 8 | 9 | from enum import Enum 10 | from typing import List, TypeVar, Generic, Dict 11 | from pyomo.core import ConcreteModel, Var, Constraint, Expression 12 | from pyomo.core.base.constraint import _GeneralConstraintData, SimpleConstraint 13 | from pyomo.core.base.var import _GeneralVarData, SimpleVar, IndexedVar 14 | from pyomo.core.base.param import SimpleParam, _ParamData, _NotValid 15 | # from pyomo.core.kernel import value 16 | from pyomo.environ import * 17 | from pyomo.solvers.plugins.solvers.CPLEX import * 18 | from strgen import StringGenerator as SG 19 | 20 | # from .riskmeasures import AbstractRiskMeasure 21 | # from pyomo.util.modeling import unique_component_name 22 | from pyomotools.tools import unique_component_name 23 | from .base_utilities import sample 24 | 25 | T = TypeVar('T') 26 | 27 | 28 | class Direction(Enum): 29 | forwardpass = "forwardpass" 30 | backwardpass = "backwardpass" 31 | 32 | 33 | class Staus(Enum): 34 | solving = "solving" 35 | converged = "converged" 36 | stalling_convergence = "stalling_convergence" 37 | time_limit = "time_limit" 38 | max_iterations = "max_iterations" 39 | 40 | 41 | class Sense(Enum): 42 | Min = minimize 43 | Max = maximize 44 | 45 | 46 | class SolveType(Enum): 47 | Asyncronous = "Asyncronous" 48 | Serial = "Serial" 49 | 50 | 51 | # 抽象类 52 | class AbstractCutOracle: 53 | def __init__(self): 54 | pass 55 | 56 | def storecut(self, m: 'SDDPModel', sp: 'Subproblem', cut: 'Cut'): 57 | raise NotImplementedError 58 | 59 | def validcuts(self): 60 | raise NotImplementedError 61 | 62 | 63 | class AbstractValueFunction(Generic[T]): 64 | def __init__(self): 65 | pass 66 | 67 | @property 68 | def cutoracle(self) -> AbstractCutOracle: 69 | raise NotImplementedError 70 | 71 | def initializevaluefunction(self, sp: 'Subproblem', sense: Sense, bound: float): 72 | raise NotImplementedError 73 | 74 | @staticmethod 75 | def backwardpass(m: 'SDDPModel', settings: 'Settings'): 76 | raise NotImplementedError 77 | 78 | def rebuildsubproblem(self, m: 'SDDPModel', sp: 'Subproblem'): 79 | raise NotImplementedError 80 | 81 | def setstageobjective(self, sp: 'Subproblem', obj): 82 | """ 83 | set stage objective, 84 | """ 85 | raise NotImplementedError 86 | 87 | def getstageobjective(self, sp: 'Subproblem'): 88 | """ 89 | get stage object 90 | """ 91 | raise NotImplementedError 92 | 93 | 94 | class AbstractRiskMeasure: 95 | def __init__(self): 96 | pass 97 | 98 | def modifyprobability(self, riskadjusted_distribution: List[float], 99 | original_distribution: List[float], observations: List[float], m: 'SDDPModel', 100 | sp: 'Subproblem'): 101 | raise NotImplementedError 102 | 103 | 104 | class State: 105 | def __init__(self, 106 | variable: _GeneralVarData, 107 | variable_in: _GeneralVarData, 108 | constraint: _GeneralConstraintData, 109 | param: _ParamData): 110 | self.variable = variable 111 | self.variable_in = variable_in # 时段处的状态值 112 | self.param = param 113 | self.constraint = constraint 114 | 115 | @property 116 | def model(self) -> ConcreteModel: 117 | return self.variable.model() 118 | 119 | # 设置时段初的状态的值,用于日后求对偶值 120 | def setvalue(self, v: float): 121 | self.param.value = v 122 | # self.constraint.set_value(expr=self.variable_in == v) 123 | 124 | @property 125 | def value(self): 126 | return value(self.variable) 127 | 128 | @property 129 | def dual(self): 130 | return self.model.dual.get(self.constraint) 131 | 132 | 133 | class Cut: 134 | def __init__(self, intercept: float, coefficients: List[float]): 135 | self.coefficients = coefficients 136 | self.intercept = intercept 137 | 138 | 139 | # parameter 需要设置成 mutable=true 140 | class Noise: 141 | """ 142 | 均通过修改param_value,约束有可能是多个,params值能是一个,obj 143 | 约束和 values 是对应的 144 | 不同noise 对应的约束应该是相同的,但是values和 145 | len(values)==len(noise_param) 146 | """ 147 | 148 | def __init__(self, has_objective_noise: bool = False, values: List[float] = None, 149 | noise_params: List[_ParamData] = None, constraints: List = None, obj: Expression = None): 150 | if values is None: values = [] 151 | if noise_params is None: noise_params = [] 152 | 153 | self.constraints = constraints 154 | self.obj = obj 155 | self.has_objective_noise = has_objective_noise 156 | # =============== 157 | self.values = values 158 | self.params = noise_params 159 | self.obj_noise = [] 160 | self.obj_param = [] # type:List[_ParamData] 161 | 162 | def add_obj_noise(self, nv: float, param: _ParamData): 163 | self.obj_noise.append(nv) 164 | self.obj_param.append(param) 165 | 166 | def works(self): 167 | """ 168 | 生效 169 | """ 170 | for v, p in zip(self.values, self.params): 171 | p.value = v 172 | 173 | for v, p in zip(self.obj_noise, self.obj_param): 174 | p.value = v 175 | 176 | 177 | # class ModelWrapper: 178 | # def __init__(self, model: ConcreteModel): 179 | # self.model = model 180 | # self.states = [] # type:List[Var] 181 | # 182 | # def state(self, var: Var): 183 | # for index in var.index_set(): 184 | # self.states.append(var[index]) 185 | 186 | 187 | class Subproblem: 188 | def __init__(self, finalstage=False, stage=1, markov_state=1, sense: Sense = Sense.Min, 189 | bound: float = -1e6, 190 | states: List[State] = None, 191 | noises: List[Noise] = None, 192 | value_function: AbstractValueFunction = None, 193 | noiseprobability: List[float] = None, 194 | risk_measure: 'AbstractRiskMeasure' = None 195 | ): 196 | if states is None: states = [] 197 | if noises is None: noises = [] 198 | if noiseprobability is None: noiseprobability = [] 199 | 200 | self.finalstage = finalstage 201 | self.stage = stage 202 | self.markov_state = markov_state 203 | self.problembound = bound 204 | self.sense = sense 205 | self.states = states # type:List[State] 206 | self.valueoracle = value_function # type:AbstractValueFunction 207 | self.noises = noises 208 | self.noiseprobability = noiseprobability 209 | self.riskmeasure = risk_measure 210 | self.solver = None # type:CPLEXSHELL 211 | # model relevant 212 | self.model = None 213 | self.reset_mod() 214 | # temporary variable 215 | self._anonymous = None 216 | 217 | def add_constraint(self, expr: Expression): 218 | self.model.cuts.add(expr) 219 | 220 | def reset_mod(self): 221 | self.model = ConcreteModel() # type:ConcreteModel 222 | ########### 223 | self.model.dual = Suffix(direction=Suffix.IMPORT_EXPORT) # 需要获取对偶值 224 | self.model.cuts = ConstraintList() 225 | self.valueoracle.initializevaluefunction(self, self.sense, bound=self.problembound) 226 | 227 | @property 228 | def nstates(self): 229 | return len(self.states) 230 | 231 | def set_solver(self, solver): 232 | self.solver = solver 233 | 234 | # def get_dual(self)->List[float]: 235 | # return [s. for s in self.states] 236 | 237 | def setnoiseprobability(self, noise_probability:List[float]): 238 | self.noiseprobability = noise_probability 239 | 240 | @property 241 | def hasnoises(self): 242 | return len(self.noises) > 0 243 | 244 | def samplenoise(self, solutionstore=None): # TODO 245 | if solutionstore is None: 246 | noiseidx = sample(self.noiseprobability) 247 | return noiseidx, self.noises[noiseidx] 248 | 249 | def setnoise(self, noise: Noise): 250 | """ 251 | 对约束和目标函数中的噪声项进行赋值 252 | """ 253 | noise.works() 254 | 255 | def _get_obj_list(self): 256 | return [obj for obj in self.model.component_data_objects(Objective)] 257 | 258 | def _del_com(self, name: str): 259 | if hasattr(self.model, name): 260 | self.model.__delattr__(name) 261 | 262 | # 263 | # def setstageobjective(self, obj): 264 | # self.valueoracle.setstageobjective(self, obj) 265 | # # self._del_com("obj") 266 | # # if self.finalstage: 267 | # # self.model.obj = Objective(expr=expr, sense=self.sense.value) 268 | # # else: 269 | # # self.model.obj = Objective(expr=expr + self.valueoracle.theta, sense=self.sense.value) 270 | 271 | def getstageobjective(self): 272 | return self.valueoracle.getstageobjective(self) 273 | 274 | def getobjectivevalue(self): 275 | return value(self.model.obj) 276 | 277 | @property 278 | def obj(self): 279 | return self.model.obj 280 | 281 | @obj.setter 282 | def obj(self, value): 283 | self.valueoracle.setstageobjective(self, value) 284 | 285 | @property 286 | def _obj(self): 287 | """ 288 | call by developer 289 | """ 290 | return self.model.obj 291 | 292 | @_obj.setter 293 | def _obj(self, value): 294 | """ 295 | 重新设置目标函数 296 | """ 297 | self._del_com("obj") 298 | self.model.obj = value 299 | 300 | # TODO 多维可能存在问题 301 | def add_state(self, state: SimpleVar, state0: SimpleVar, param: SimpleParam, cons: SimpleConstraint = None): 302 | if cons is None: 303 | cons = Constraint(param.index_set(), rule=lambda m, i: state0[i] == param[i]) 304 | self.anonymous = cons 305 | for i in param.index_set(): 306 | s = State(state[i], state0[i], cons[i], param[i]) 307 | self.states.append(s) 308 | 309 | # def state(self, state, state0, param: SimpleParam): 310 | # cons = Constraint(param.index_set(), rule=lambda m, i: state0[i] == param[i]) 311 | # self.anonymous = cons 312 | # self.add_state(state, state0, cons) 313 | 314 | def add_noise_constraint(self, noises: List[float], param: _ParamData, 315 | cons: SimpleConstraint or List[SimpleConstraint] = None): 316 | """ 317 | 只能值单个param,不能是带有index的param 318 | :param cons: 使用到这个param的约束,可以是单个约束也可以是一个List 319 | :return: 320 | """ 321 | if len(self.noises) <= 0: 322 | for nv in noises: 323 | self.noises.append(Noise(has_objective_noise=False, values=[nv], noise_params=[param], 324 | constraints=[cons])) 325 | else: 326 | for i, nv in enumerate(noises): 327 | self.noises[i].params.append(param) 328 | self.noises[i].constraints.append(cons) 329 | self.noises[i].values.append(nv) 330 | 331 | def add_nosise_object(self, noises: List[float], param: _ParamData, obj: Expression): 332 | if len(self.noises) <= 0: 333 | for _ in noises: 334 | self.noises.append(Noise()) 335 | for i, nv in enumerate(noises): 336 | self.noises[i].add_obj_noise(nv, param) 337 | self.noises[i].obj = obj 338 | 339 | # # delegate method 340 | # def rebuildsubproblem(self, m: "SDDPModel"): 341 | # self.valueoracle.rebuildsubproblem(m, self) 342 | 343 | @property 344 | def anonymous(self): 345 | return self._anonymous 346 | 347 | @anonymous.setter 348 | def anonymous(self, value): 349 | """ 350 | add anonymous component to model 351 | """ 352 | random_name = SG("[\w]{3}").render() 353 | random_name = unique_component_name(self.model, random_name) 354 | setattr(self.model, random_name, value) 355 | self._anonymous = value 356 | 357 | 358 | class Stage: 359 | def __init__(self, t: int = 1, subproblems: List[Subproblem] = None, 360 | transitionprobabilities: List[List[float]] = None, state: List[float] = None): 361 | if subproblems is None: subproblems = [] 362 | if transitionprobabilities is None: transitionprobabilities = [] 363 | if state is None: state = [] 364 | 365 | self._state = state # type:List[float] 366 | self.transitionprobabilities = transitionprobabilities 367 | self.subproblems = subproblems 368 | self.t = t 369 | 370 | def savestates(self, sp: Subproblem): # TODO 和SDDP.jl实现不同 371 | self._state = [s.value for s in sp.states] 372 | 373 | @property 374 | def state(self): 375 | return self._state 376 | 377 | def samplesubproblem(self, last_markov_state, solutionstore: Dict = None) -> (int, Subproblem): 378 | if solutionstore is None: 379 | newidx = sample(self.transitionprobabilities[last_markov_state]) 380 | return newidx, self.subproblems[newidx] 381 | else: 382 | # TODO 仿照julia中的写 383 | pass 384 | 385 | @staticmethod 386 | def create(t: int, markov_transition=None) -> 'Stage': 387 | if markov_transition is None: markov_transition = [] 388 | return Stage(t=t, transitionprobabilities=markov_transition) 389 | 390 | 391 | import numpy as np 392 | 393 | 394 | class CachedVector(Generic[T]): 395 | """ 396 | 泛型 397 | """ 398 | 399 | def __init__(self, data: List = None, n: int = None): 400 | if data is None: data = [] 401 | self.data = data 402 | self.n = n 403 | 404 | def __setitem__(self, key, value): 405 | self.data[key] = value 406 | 407 | def __getitem__(self, key): 408 | return self.data[key] 409 | 410 | def __len__(self): 411 | return len(self.data) 412 | 413 | def range(self, index_list: List[int]): 414 | data = np.array(self.data) 415 | return data[index_list].tolist() 416 | 417 | def put_range(self, index_list: List[int], lst: List[T]): 418 | for index, datum in zip(index_list, lst): 419 | self.data[index] = datum 420 | 421 | def reset(self): 422 | self.n = 0 423 | self.data.clear() 424 | 425 | def append(self, v): 426 | self.data.append(v) 427 | 428 | @property 429 | def len(self): 430 | return len(self.data) 431 | 432 | 433 | class Storage: 434 | def __init__(self, state: List[float], 435 | noise: CachedVector[int], 436 | markov: CachedVector[int], 437 | duals: CachedVector[List[float]], 438 | objective: CachedVector[float], 439 | probability: CachedVector[float], 440 | modifiedprobability: CachedVector[float]): 441 | """ 442 | 443 | :param state: 444 | :param noise: 445 | :param markov: 446 | :param duals: 447 | :param objective: 448 | :param probability: 449 | :param modifiedprobability: 450 | """ 451 | self.modifiedprobability = modifiedprobability 452 | self.probability = probability 453 | self.objective = objective 454 | self.duals = duals 455 | self.markov = markov 456 | self.noise = noise 457 | self.state = state 458 | self.n = 0 459 | 460 | def reset(self): 461 | """ 462 | TODO 463 | """ 464 | self.n = 0 465 | self.modifiedprobability.reset() 466 | self.probability.reset() 467 | self.objective.reset() 468 | self.duals.reset() 469 | self.markov.reset() 470 | self.noise.reset() 471 | # self.state.reset() 472 | 473 | @staticmethod 474 | def push(cached: CachedVector[T], ele: T): 475 | cached.data.append(ele) 476 | 477 | @staticmethod 478 | def create(): 479 | return Storage([], CachedVector(), CachedVector(), CachedVector(), 480 | CachedVector(), CachedVector(), CachedVector()) 481 | 482 | 483 | class SolutionLog: 484 | def __init__(self, 485 | iteration: int, bound: float, lower_statistical_bound: float, upper_statistical_bound: float, 486 | timecuts: float, 487 | simulations: int, timesimulations: float, timetotal: float): 488 | self.simulations = simulations 489 | self.timecuts = timecuts 490 | self.bound = bound 491 | self.upper_statistical_bound = upper_statistical_bound 492 | self.lower_statistical_bound = lower_statistical_bound 493 | self.timesimulations = timesimulations 494 | self.timetotal = timetotal 495 | self.iteration = iteration 496 | 497 | def __str__(self): 498 | return str(self.__dict__) 499 | 500 | 501 | class SDDPModel: 502 | def __init__(self, sense: Sense, build: [Subproblem, int, int], 503 | stages: List[Stage] = None, 504 | storage: Storage = None, log: List[SolutionLog] = None): 505 | if stages is None: stages = [] 506 | if log is None: log = [] 507 | if storage is None: storage = Storage.create() 508 | 509 | self.log = log # typeList[SolutionLog] 510 | self.build = build 511 | self.storage = storage 512 | self.stages = stages 513 | self.sense = sense 514 | 515 | @property 516 | def nstages(self): 517 | return len(self.stages) 518 | 519 | def getbound(self): 520 | if len(self.log) > 0: 521 | return self.log[-1].bound 522 | else: 523 | raise RuntimeError("模型还没没有解决!") 524 | 525 | def constructcut(self, sp: 'Subproblem', storage: Storage = None): 526 | if storage is None: 527 | storage = self.storage 528 | m = self 529 | intercept = 0.0 530 | coefficients = [0.] * sp.nstates 531 | for i in range(storage.objective.len): 532 | intercept += storage.modifiedprobability[i] * (storage.objective[i] - float(np.dot( 533 | storage.duals[i], m.stages[sp.stage].state) 534 | )) 535 | # E[πᵀ]=a1π1ᵀ+a2π2ᵀ...anπnᵀ 536 | for j in range(sp.nstates): 537 | coefficients[j] += storage.modifiedprobability[i] * storage.duals[i][j] 538 | return Cut(intercept, coefficients) 539 | 540 | 541 | class BoundStalling: 542 | def __init__(self, iterations: int = 0, rtol: float = 0, atol: float = 0): 543 | """ 544 | 545 | :param iterations: len(last_n) 546 | :param rtol: 相对误差, last_n-mean(last_n) 547 | :param atol: 绝对误差 last_n/mean(last_n)-1 548 | """ 549 | self.iterations = iterations 550 | self.rtol = rtol 551 | self.atol = atol 552 | 553 | 554 | class MonteCarloSimulation: 555 | def __init__(self, frequency: int = 0, steps: List[int] = [20], confidence: float = 0.95, 556 | termination: bool = False): 557 | self.termination = termination 558 | self.confidence = confidence 559 | self.steps = steps 560 | self.frequency = frequency 561 | 562 | 563 | class Settings: 564 | def __init__(self, 565 | max_iterations: int = 0, 566 | time_limit: float = 600, 567 | simulation: MonteCarloSimulation = MonteCarloSimulation(), 568 | bound_convergence: BoundStalling = BoundStalling(), 569 | cut_selection_frequency: int = 0, 570 | print_level: int = 0, 571 | log_file: str = "", 572 | reduce_memory_footprint: bool = False, 573 | cut_output_file_handle=None, 574 | is_asyncronous: bool = False): 575 | self.is_asyncronous = is_asyncronous 576 | self.reduce_memory_footprint = reduce_memory_footprint 577 | self.log_file = log_file 578 | self.print_level = print_level 579 | self.cut_selection_frequency = cut_selection_frequency 580 | self.bound_convergence = bound_convergence 581 | self.simulation = simulation 582 | self.time_limit = time_limit 583 | self.max_iterations = max_iterations 584 | -------------------------------------------------------------------------------- /sddp/utilities.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Oscar Dowson, Zhao Zhipeng 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | ############################################################################# 6 | from .typedefinitions import * 7 | import numpy as np 8 | from scipy.stats import t, sem 9 | 10 | 11 | def constructcut(m: SDDPModel, sp: Subproblem): 12 | # theta <=/>= E[ (y - πᵀx̄) + πᵀx ] 13 | intercept = 0.0 14 | coefficients = [0.] * sp.nstates 15 | for i in range(m.storage.objective.len): 16 | intercept += m.storage.modifiedprobability[i] * (m.storage.objective[i] - float(np.dot( 17 | m.storage.duals[i], m.stages[sp.stage].state) 18 | )) 19 | # E[πᵀ]=a1π1ᵀ+a2π2ᵀ...anπnᵀ 20 | for j in range(sp.nstates): 21 | coefficients[j] += m.storage.modifiedprobability[i] * m.storage.duals[i][j] 22 | return Cut(intercept, coefficients) 23 | 24 | 25 | def applicable(iteration: int, frequency: int): 26 | """ 27 | 判断iteration 是否是整数倍: 28 | """ 29 | return frequency > 0 and np.mod(iteration, frequency) == 0 30 | 31 | 32 | def confidenceinterval(x: List[float], conf_level=0.95): 33 | """ 34 | 获得置信区间 35 | """ 36 | a = 1.0 * np.array(x) 37 | n = len(a) 38 | m, se = np.mean(a), sem(a) 39 | h = se * t._ppf((1 + conf_level) / 2., n - 1) 40 | return m - h, m + h 41 | 42 | 43 | def isapprox(a, b, atol): 44 | """ 45 | a,b 相对误差小于某值 46 | :param a: 47 | :param b: 48 | :param atol: 49 | :return: 50 | """ 51 | return abs(a - b) / abs(b) <= atol 52 | 53 | 54 | def futureobjective(sense: Sense, bound): 55 | if sense == Sense.Min: 56 | return Var(bounds=(bound, None)) 57 | else: 58 | return Var(bounds=(None, bound)) 59 | 60 | 61 | def cuttoaffexpr(sp: Subproblem, cut: Cut) -> Expression: 62 | """ 63 | generator cut expression 64 | """ 65 | expr = cut.intercept 66 | for idx, coef in enumerate(cut.coefficients): 67 | x = x + coef * sp.states[idx].variable 68 | return expr 69 | 70 | 71 | def pyomoSolve(direction: Direction, m: SDDPModel, sp: Subproblem): 72 | """ 73 | TODO 前处理后处理 74 | """ 75 | # print(model_expression(sp.model)) 76 | res = sp.solver.solve(sp.model) # type:SolverResults 77 | 78 | status = res.solver.status 79 | if res.solver.status == SolverStatus.ok: 80 | pass # 可行解或者最优解 81 | else: 82 | print("%s solver.status=%s" % direction.value, status) 83 | 84 | 85 | def dominates(sense: Sense, trial: float, incumbent: float): 86 | """ 87 | 如果 incumbent 比 trial 好 true 88 | """ 89 | if sense == Sense.Min: 90 | return trial > incumbent 91 | else: 92 | return trial < incumbent 93 | --------------------------------------------------------------------------------