Repository: ds4dm/Tulip.jl Branch: master Commit: aa9f803d4a11 Files: 106 Total size: 283.8 KB Directory structure: gitextract_9mrivou2/ ├── .github/ │ └── workflows/ │ ├── CompatHelper.yml │ ├── Documentation.yml │ ├── TagBot.yml │ └── ci.yml ├── .gitignore ├── CITATION.bib ├── LICENSE.md ├── NEWS.md ├── Project.toml ├── README.md ├── app/ │ ├── .gitignore │ ├── Project.toml │ ├── README.md │ ├── precompile_app.jl │ └── src/ │ └── TulipCL.jl ├── docs/ │ ├── .gitignore │ ├── Project.toml │ ├── make.jl │ └── src/ │ ├── index.md │ ├── manual/ │ │ ├── IPM/ │ │ │ ├── HSD.md │ │ │ └── MPC.md │ │ ├── formulation.md │ │ └── options.md │ ├── reference/ │ │ ├── API.md │ │ ├── KKT/ │ │ │ ├── kkt.md │ │ │ ├── tlp_cholmod.md │ │ │ ├── tlp_dense.md │ │ │ ├── tlp_krylov.md │ │ │ └── tlp_ldlfact.md │ │ ├── Presolve/ │ │ │ └── presolve.md │ │ ├── attributes.md │ │ └── options.md │ └── tutorials/ │ └── lp_example.md ├── examples/ │ ├── .gitignore │ ├── freevars.jl │ ├── infeasible.jl │ ├── optimal.jl │ ├── optimal_other_type.jl │ └── unbounded.jl ├── src/ │ ├── IPM/ │ │ ├── HSD/ │ │ │ ├── HSD.jl │ │ │ └── step.jl │ │ ├── IPM.jl │ │ ├── MPC/ │ │ │ ├── MPC.jl │ │ │ └── step.jl │ │ ├── ipmdata.jl │ │ ├── options.jl │ │ ├── point.jl │ │ └── residuals.jl │ ├── Interfaces/ │ │ ├── MOI/ │ │ │ ├── MOI_wrapper.jl │ │ │ ├── attributes.jl │ │ │ ├── constraints.jl │ │ │ ├── objective.jl │ │ │ └── variables.jl │ │ └── tulip_julia_api.jl │ ├── KKT/ │ │ ├── Cholmod/ │ │ │ ├── cholmod.jl │ │ │ ├── spd.jl │ │ │ └── sqd.jl │ │ ├── Dense/ │ │ │ └── lapack.jl │ │ ├── KKT.jl │ │ ├── Krylov/ │ │ │ ├── defs.jl │ │ │ ├── krylov.jl │ │ │ ├── sid.jl │ │ │ ├── spd.jl │ │ │ └── sqd.jl │ │ ├── LDLFactorizations/ │ │ │ └── ldlfact.jl │ │ ├── Test/ │ │ │ └── test.jl │ │ └── systems.jl │ ├── LinearAlgebra/ │ │ └── LinearAlgebra.jl │ ├── Presolve/ │ │ ├── Presolve.jl │ │ ├── dominated_column.jl │ │ ├── empty_column.jl │ │ ├── empty_row.jl │ │ ├── fixed_variable.jl │ │ ├── forcing_row.jl │ │ ├── free_column_singleton.jl │ │ └── row_singleton.jl │ ├── Tulip.jl │ ├── attributes.jl │ ├── model.jl │ ├── parameters.jl │ ├── problemData.jl │ ├── solution.jl │ ├── status.jl │ └── utils.jl └── test/ ├── Core/ │ └── problemData.jl ├── IPM/ │ ├── HSD.jl │ └── MPC.jl ├── Interfaces/ │ ├── MOI_wrapper.jl │ ├── julia_api.jl │ ├── lp.mps │ └── lp.mps.bz2 ├── KKT/ │ ├── Cholmod/ │ │ └── cholmod.jl │ ├── Dense/ │ │ └── lapack.jl │ ├── KKT.jl │ ├── Krylov/ │ │ ├── krylov.jl │ │ ├── sid.jl │ │ ├── spd.jl │ │ └── sqd.jl │ └── LDLFactorizations/ │ └── ldlfact.jl ├── Presolve/ │ ├── empty_column.jl │ ├── empty_row.jl │ ├── fixed_variable.jl │ └── presolve.jl ├── Project.toml ├── examples.jl └── runtests.jl ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/CompatHelper.yml ================================================ name: CompatHelper on: schedule: - cron: 0 0 * * * workflow_dispatch: jobs: CompatHelper: runs-on: ubuntu-latest steps: - name: Pkg.add("CompatHelper") run: julia -e 'using Pkg; Pkg.add("CompatHelper")' - name: CompatHelper.main() env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} run: julia -e 'using CompatHelper; CompatHelper.main()' ================================================ FILE: .github/workflows/Documentation.yml ================================================ name: Documentation on: push: branches: [master] tags: '*' pull_request: types: [opened, synchronize, reopened] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: julia-actions/setup-julia@latest with: version: '1' - name: Install dependencies run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' - name: Build and deploy env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # For authentication with GitHub Actions token DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # For authentication with SSH deploy key run: julia --project=docs/ docs/make.jl ================================================ FILE: .github/workflows/TagBot.yml ================================================ name: TagBot on: issue_comment: types: - created workflow_dispatch: jobs: TagBot: if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' runs-on: ubuntu-latest steps: - uses: JuliaRegistries/TagBot@v1 with: token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - master - /^release-.*$/ pull_request: types: [opened, synchronize, reopened] jobs: test: name: Julia ${{ matrix.julia-version }} - ${{ matrix.os }} - ${{ matrix.julia-arch }} - ${{ github.event_name }} runs-on: ${{ matrix.os }} strategy: matrix: julia-version: ['lts', '1', 'pre'] julia-arch: [x64] os: [ubuntu-latest, windows-latest, macOS-latest] steps: - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.julia-version }} arch: ${{ matrix.julia-arch }} - uses: julia-actions/cache@v2 - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 with: annotate: true - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v1 with: file: lcov.info ================================================ FILE: .gitignore ================================================ # Julia *.jl.cov *.jl.*.cov *.jl.mem Manifest.toml /dat /exp /.vscode /*.jl ================================================ FILE: CITATION.bib ================================================ @TechReport{Tulip.jl, title = {{Tulip}.jl: an open-source interior-point linear optimization solver with abstract linear algebra}, url = {https://www.gerad.ca/fr/papers/G-2019-36}, Journal = {Les Cahiers du Gerad}, Author = {Anjos, Miguel F. and Lodi, Andrea and Tanneau, Mathieu}, year = {2019} } ================================================ FILE: LICENSE.md ================================================ Copyright (c) 2018-2019: Mathieu Tanneau Tulip.jl is licensed under the [MPL version 2.0](https://www.mozilla.org/MPL/2.0/). ## License Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. ================================================ FILE: NEWS.md ================================================ # Tulip release notes # v0.7.1 (January 8, 2021) ## Bug fixes * Fix a bug in `add_variable!` and the handling of zero coefficients(#77, #79) * Fix some type instabilities (#71, #76) * Fix docs URL in README (#70) ## New features * Tulip's version is exposed via `Tulip.version()` (#81) * Multiple centrality corrections in MPC algorithm (#75) * Support reading `.gz` and `.bz2` files (#72) ## Others * Move CI to GitHub actions (#73) * Use new convention for test-specific dependencies (#80) * Bump deps (#71,#74) # v0.5.1 (August 15, 2020) * Fix URLs following migration to jump.dev (#51) * Fix bug in constraint/variable deletion (#52, #53) # v0.5.0 * Support the MOI attribute `Name` (#47) * Simplify the user interface for choosing between different linear solvers (#48) # v0.4.0 (April 25, 2020) * Re-write data structure and interface (#44) * Add presolve module (#45) * Move `UnitBlockAngular` code to a separate package (#46) # v0.3.0 (February 29, 2020) * Improved documentation for parameter management (#43) * More flexible management of linear solvers (#37, #40) * Introduce two new parameters to choose linear solver * `ls_backend` specifies which backend is used to solve linear systems * `ls_system` specifies which linear system (augmented system or normal equations) is solved * Generic tests for custom linear solvers * Performance improvements when using CHOLMOD * Improved MPS reader (#36) # v0.2.0 (December 26, 2019) * Switch type unions to constant in the MOI wrapper (#32) * Free MPS format reader (#33). Some `.mps` files in fixed MPS format may no longer be readable, due to, e.g., * spaces in constraint/variable names * empty name field in RHS section * Re-write of the linear algebra layer (#34) * Integration of [LDLFactorizations.jl](https://github.com/JuliaSmoothOptimizers/LDLFactorizations.jl) (#35) for solving problems in arbitrary precision. # v0.1.1 (October 25, 2019) * Support for MOI v0.9.5 (#31) * Catch exceptions during solving (#29) * Numerical trouble during factorization * Iteration and memory limits * User interruptions * Create a `CITATION.bib` file (#26) * Describe problem formulations in the docs (#26) ================================================ FILE: Project.toml ================================================ name = "Tulip" uuid = "6dd1b50a-3aae-11e9-10b5-ef983d2400fa" version = "0.9.8" authors = ["Mathieu Tanneau "] [deps] CodecBzip2 = "523fee87-0ab8-5b00-afb7-3ecf72e48cfd" CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193" Krylov = "ba0b0d4f-ebba-5204-a429-3ac8c609bfb7" LDLFactorizations = "40e66cde-538c-5869-a4ad-c39174c6795b" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" LinearOperators = "5c8ed15e-5a4c-59e4-a42b-c7e8811fb125" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" QPSReader = "10f199a5-22af-520b-b891-7ce84a7b1bd0" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" [compat] CodecBzip2 = "0.7.2, 0.8" CodecZlib = "0.7.0" Krylov = "0.10" LDLFactorizations = "0.10" LinearOperators = "2.0" MathOptInterface = "1" QPSReader = "0.2" TOML = "1" TimerOutputs = "0.5.6" julia = "1.10" ================================================ FILE: README.md ================================================ # Tulip [![DOI](https://zenodo.org/badge/131298750.svg)](https://zenodo.org/badge/latestdoi/131298750) [![](https://github.com/ds4dm/Tulip.jl/workflows/CI/badge.svg?branch=master)](https://github.com/ds4dm/Tulip.jl/actions?query=workflow%3ACI) [![](https://codecov.io/github/ds4dm/Tulip.jl/coverage.svg?branch=master)](https://codecov.io/github/ds4dm/Tulip.jl?branch=master) [Tulip](https://github.com/ds4dm/Tulip.jl) is an open-source interior-point solver for linear optimization, written in pure Julia. It implements the homogeneous primal-dual interior-point algorithm with multiple centrality corrections, and therefore handles unbounded and infeasible problems. Tulip’s main feature is that its algorithmic framework is disentangled from linear algebra implementations. This allows to seamlessly integrate specialized routines for structured problems. ## License Tulip is licensed under the [MPL 2.0 license](https://github.com/ds4dm/Tulip.jl/blob/master/LICENSE.md). ## Installation Install Tulip using the Julia package manager: ```julia import Pkg Pkg.add("Tulip") ``` ## Usage The recommended way of using Tulip is through [JuMP](https://github.com/jump-dev/JuMP.jl) or [MathOptInterface](https://github.com/jump-dev/MathOptInterface.jl) (MOI). The low-level interface is still under development and is likely change in the future. The JuMP/MOI interface is more stable and regularly tested. ### Using with JuMP Tulip can be used with JuMP in indirect and [direct](https://jump.dev/JuMP.jl/stable/manual/models/#Direct-mode) modes. Linear objectives, linear constraints and lower/upper bounds on variables are supported. ```julia using JuMP import Tulip # With a JuMP-level cache model = Model(Tulip.Optimizer) # Direct mode (faster incremental modifications) model = direct_model(Tulip.Optimizer()) # You can also add the optimizer later model = Model() ... set_optimizer(model, Tulip.Optimizer) ``` To use a non-default numeric type (e.g., `BigFloat`), use `JuMP.GenericModel{T}` (requires JuMP v1.13): ```julia using JuMP import Tulip model = JuMP.GenericModel{BigFloat}(Tulip.Optimizer{BigFloat}) ``` Note that The correct syntax is `JuMP.GenericModel{T}(Tulip.Optimizer{T})`, i.e., the type parameter **must** match between Tulip and JuMP. ```julia # Both examples below will error when calling optimize! # because the JuMP- and Tulip-level numerical types are different model = JuMP.GenericModel{BigFloat}(Tulip.Optimizer) model = JuMP.GenericModel{Float64}(Tulip.Optimizer{BigFloat}) ``` ### Using with MOI The MOI-level interface is not recommended for most users, and is considered an advanced feature. The type `Tulip.Optimizer` is parametrized by the model's arithmetic, for example, `Float64` or `BigFloat`. This allows to solve problem in higher numerical precision. See the documentation for more details. ```julia import MathOptInterface as MOI import Tulip model = Tulip.Optimizer{Float64}() # Create a model in Float64 precision model = Tulip.Optimizer() # Defaults to the above call model = Tulip.Optimizer{BigFloat}() # Create a model in BigFloat precision ``` ## Solver parameters See the [documentation](https://ds4dm.github.io/Tulip.jl/stable/reference/options/) for a full list of parameters. To set parameters in JuMP, use: ```julia using JuMP, Tulip model = Model(Tulip.Optimizer) set_attribute(model, "IPM_IterationsLimit", 200) ``` To set parameters in MathOptInterface, use: ```julia using Tulip import MathOptInterface as MOI model = Tulip.Optimizer{Float64}() MOI.set(model, MOI.RawOptimizerAttribute("IPM_IterationsLimit"), 200) ``` To set parameters in the Tulip API, use: ```julia using Tulip model = Tulip.Model{Float64}() Tulip.set_parameter(model, "IPM_IterationsLimit", 200) ``` ## Command-line executable See [app building instructions](https://github.com/ds4dm/Tulip.jl/blob/master/app/README.md). ## Citing `Tulip.jl` If you use Tulip in your work, we kindly ask that you cite the following [reference](https://doi.org/10.1007/s12532-020-00200-8) (preprint available [here](https://arxiv.org/abs/2006.08814)). ``` @Article{Tulip.jl, author = {Tanneau, Mathieu and Anjos, Miguel F. and Lodi, Andrea}, journal = {Mathematical Programming Computation}, title = {Design and implementation of a modular interior-point solver for linear optimization}, year = {2021}, issn = {1867-2957}, month = feb, doi = {10.1007/s12532-020-00200-8}, language = {en}, url = {https://doi.org/10.1007/s12532-020-00200-8}, urldate = {2021-03-07}, } ``` ================================================ FILE: app/.gitignore ================================================ # Julia Manifest.toml /*.so /tulip_cl ================================================ FILE: app/Project.toml ================================================ name = "TulipCL" uuid = "c587cd3b-9b28-46da-94c0-0791404bab14" authors = ["mtanneau "] version = "0.1.0" [deps] ArgParse = "c7e460c6-2fb9-53a9-8c5b-16f535851c63" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" QPSReader = "10f199a5-22af-520b-b891-7ce84a7b1bd0" Tulip = "6dd1b50a-3aae-11e9-10b5-ef983d2400fa" ================================================ FILE: app/README.md ================================================ # TulipCL App for building a command-line executable of the [Tulip.jl](https://github.com/ds4dm/Tulip.jl) interior-point solver. All commands shown below are executed in the `Tulip.jl/app/` directory. ## Installation instructions 1. Download and install Julia (version 1.3.1 or newer). 1. Install `PackageCompiler` ```bash $ julia -e 'using Pkg; Pkg.add("PackageCompiler")' ``` 1. Instantiate the current environment ```bash $ julia --project=. -e 'using Pkg; Pkg.instantiate()' 1. Build the command-line executable ```julia $ julia -q --project=. julia> using PackageCompiler julia> create_app(".", "tulip_cl", force=true, precompile_execution_file="precompile_app.jl", app_name="tulip_cl"); julia> exit() ``` The executable will be located at `tulip_cl/bin/tulip_cl`. While building the command-line executable, a precompilation script is executed wherein a small number of problems will be solved, thereby producing a number of logs. This process is normal. For more information on how to build the app, take a look at the [PackageCompiler documentation](https://julialang.github.io/PackageCompiler.jl/dev/apps/). ### Using a different version of Tulip Following the completion of step 3. above, this directory will contain a `Manifest.toml` that specifies the version of all packages, including that of `Tulip`. By default, this will be the most recently tagged version. To build the executable with a different version/branch of Tulip follow these instructions, tag the particular version/branch before creating the app. For instance, to try out a local development branch, running ```julia julia> ] pkg> dev .. ``` will use the current state of the Tulip.jl repository. Finally, follow the last step in the above installation guide to generate the command-line executable. ## Running the command-line executable Once the build step is performed, the executable can be called from the command line as follows: ```bash cd tulip_cl/bin ./tulip_cl [options] finst ``` where `finst` is the problem file. For instance, ```bash tulip_cl --Threads 1 --TimeLimit 3600 afiro.mps ``` will solve the problem `afiro.mps` using one thread and up to 1 hour of computing time. Currently, possible user options are | Option name | Type | Default | Description | |-------------|------|---------|-------------| | `Presolve` | `Int` | `1` | Set to `0` to disable presolve, `1` to activate it | | `Threads` | `Int` | `1` | Maximum number of threads | | `TimeLimit` | `Float64` | `Inf` | Time limit, in seconds | | `IterationsLimit` | `Int` | `500` | Maximum number of barrier iterations | | `Method` | `String` | `HSD` | Interior-point method | For more information, run `tulip_cl --help`, or look at Tulip's [documentation](https://ds4dm.github.io/Tulip.jl/stable/) for more details on parameters. ================================================ FILE: app/precompile_app.jl ================================================ using TulipCL const EXDIR = joinpath(dirname(pathof(TulipCL.Tulip)), "../examples/dat") # Run all example problems for finst in readdir(EXDIR) empty!(ARGS) append!(ARGS, ["--Threads", "1", "--TimeLimit", "10.0", "--Presolve", "1", "--Method", "HSD", joinpath(EXDIR, finst)]) TulipCL.julia_main() end const NETLIB = TulipCL.Tulip.QPSReader.fetch_netlib() for finst in readdir(NETLIB)[1:5], ipm in ["HSD", "MPC"] empty!(ARGS) append!(ARGS, ["--Threads", "1", "--TimeLimit", "10.0", "--Presolve", "1", "--Method", ipm, joinpath(NETLIB, finst)]) TulipCL.julia_main() end ================================================ FILE: app/src/TulipCL.jl ================================================ module TulipCL using Printf import Tulip const TLP = Tulip using ArgParse function julia_main()::Cint try tulip_cl() catch Base.invokelatest(Base.display_error, Base.catch_stack()) return 1 end return 0 end function parse_commandline(cl_args) s = ArgParseSettings() @add_arg_table! s begin "--TimeLimit" help = "Time limit, in seconds." arg_type = Float64 default = Inf "--IterationsLimit" help = "Maximum number of iterations" arg_type = Int default = 500 "--Threads" help = "Maximum number of threads." arg_type = Int default = 1 "--Presolve" help = "Presolve level" arg_type = Int default = 1 "--Method" help = "Interior-point method (HSD or MPC)" arg_type = String default = "HSD" "finst" help = "Name of instance file. Only Free MPS format is supported." required = true end return parse_args(cl_args, s) end function tulip_cl() parsed_args = parse_commandline(ARGS) # Read model and solve finst::String = parsed_args["finst"] m = TLP.Model{Float64}() t = @elapsed TLP.load_problem!(m, finst) println("Julia version: ", VERSION) println("Tulip version: ", Tulip.version()) println("Problem file : ", finst) @printf("Reading time : %.2fs\n\n", t) # Set parameters m.params.OutputLevel = 1 m.params.IPM.TimeLimit = parsed_args["TimeLimit"] m.params.Threads = parsed_args["Threads"] m.params.Presolve.Level = parsed_args["Presolve"] m.params.IPM.IterationsLimit = parsed_args["IterationsLimit"] if parsed_args["Method"] == "HSD" m.params.IPM.Factory = Tulip.Factory(Tulip.HSD) elseif parsed_args["Method"] == "MPC" m.params.IPM.Factory = Tulip.Factory(Tulip.MPC) else error("Invalid value for Method: $(parsed_args["Method"]) (must be HSD or MPC)") end TLP.optimize!(m) return nothing end if abspath(PROGRAM_FILE) == @__FILE__ tulip_cl() end end # module ================================================ FILE: docs/.gitignore ================================================ build/ Manifest.toml ================================================ FILE: docs/Project.toml ================================================ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" Tulip = "6dd1b50a-3aae-11e9-10b5-ef983d2400fa" ================================================ FILE: docs/make.jl ================================================ using Documenter, Tulip const _FAST = findfirst(isequal("--fast"), ARGS) !== nothing makedocs( sitename = "Tulip.jl", authors = "Mathieu Tanneau", format = Documenter.HTML(prettyurls = get(ENV, "CI", nothing) == "true"), doctest = !_FAST, pages = [ "Home" => "index.md", "Tutorials" => Any[ "tutorials/lp_example.md", ], "User manual" => Any[ "Problem formulation" => "manual/formulation.md", "Algorithms" => Any[ "Homogeneous Self-Dual" => "manual/IPM/HSD.md", "Predictor-Corrector" => "manual/IPM/MPC.md" ], "Setting options" => "manual/options.md" ], "Reference" => Any[ "Presolve" => "reference/Presolve/presolve.md", "KKT" => [ "reference/KKT/kkt.md", "reference/KKT/tlp_cholmod.md", "reference/KKT/tlp_dense.md", "reference/KKT/tlp_krylov.md", "reference/KKT/tlp_ldlfact.md", ], "Julia API" => "reference/API.md", "Attributes" => "reference/attributes.md", "Options" => "reference/options.md" ], ] ) deploydocs( repo = "github.com/ds4dm/Tulip.jl.git" ) ================================================ FILE: docs/src/index.md ================================================ ```@meta CurrentModule = Tulip ``` # Tulip.jl ## Overview Tulip is an open-source interior-point solver for linear programming. ## Installation Tulip is 100% Julia, so install it just like any Julia package: ```julia julia> ] pkg> add Tulip ``` No additional building step is required. ## Citing `Tulip.jl` If you use Tulip in your work, we kindly ask that you cite the following reference. The PDF is freely available [here](https://www.gerad.ca/fr/papers/G-2019-36/view), and serves as a user manual for advanced users. ``` @TechReport{Tulip.jl, title = {{Tulip}.jl: an open-source interior-point linear optimization solver with abstract linear algebra}, url = {https://www.gerad.ca/fr/papers/G-2019-36}, Journal = {Les Cahiers du Gerad}, Author = {Anjos, Miguel F. and Lodi, Andrea and Tanneau, Mathieu}, year = {2019} } ``` ================================================ FILE: docs/src/manual/IPM/HSD.md ================================================ # Homogeneous Self-Dual algorithm ```@docs Tulip.HSD ``` ## References * Anjos, M.F.; Lodi, A.; Tanneau, M. [Design and implementation of a modular interior-point solver for linear optimization](https://arxiv.org/abs/2006.08814) ================================================ FILE: docs/src/manual/IPM/MPC.md ================================================ # Predictor-Corrector algorithm ```@docs Tulip.MPC ``` ## References * Mehrotra, S. [On the Implementation of a Primal-Dual Interior Point Method](https://doi.org/10.1137/0802028) SIAM Journal on Optimization, 1992, 2, 575-601 ================================================ FILE: docs/src/manual/formulation.md ================================================ # Problem formulation ## Model input Tulip takes as input LP problems of the form ```math \begin{array}{rrcll} (P) \ \ \ \displaystyle \min_{x} && c^{T}x & + \ c_{0}\\ s.t. & l^{b}_{i} \leq & a_{i}^{T} x & \leq u^{b}_{i} & \forall i = 1, ..., m\\ & l^{x}_{j} \leq & x_{j} & \leq u^{x}_{j} & \forall j = 1, ..., n\\ \end{array} ``` where ``l^{b,x}, u^{b, x} \in \mathbb{R} \cup \{ - \infty, + \infty \}``, i.e., some of the bounds may be infinite. This original formulation is then converted to standard form. ## Standard form Internally, Tulip solves LPs of the form ```math \begin{array}{rl} (P) \ \ \ \displaystyle \min_{x} & c^{T} x + \ c_{0}\\ s.t. & A x = b\\ & l \leq x \leq u\\ \end{array} ``` where ``x, c \in \mathbb{R}^{n}``, ``A \in \mathbb{R}^{m \times n}``, ``b \in \mathbb{R}^{m}``, and ``l, u \in (\mathbb{R} \cup \{-\infty, +\infty \})^{n}``, i.e., some bounds may be infinite. The original problem is automatically reformulated into standard form before the optimization is performed. This transformation is transparent to the user. ================================================ FILE: docs/src/manual/options.md ================================================ ```@meta CurrentModule = Tulip ``` # Options management !!! info This part of the documentation is under construction See [Options reference](@ref) for a list of all available options and their signification. ## Handling options within JuMP ## Handling options within MOI ## Handling options directly ================================================ FILE: docs/src/reference/API.md ================================================ ```@autodocs Modules = [Tulip] Pages = ["tulip_julia_api.jl"] ``` ================================================ FILE: docs/src/reference/KKT/kkt.md ================================================ ```@meta CurrentModule = Tulip.KKT ``` # Overview The `KKT` module provides a modular, customizable interface for developing and selecting various approaches to solve the KKT systems. ## KKT backends ```@docs AbstractKKTBackend ``` ```@docs DefaultKKTBackend ``` ## KKT systems All formulations below refer to a linear program in primal-dual standard form ```math \begin{array}{rl} (P) \ \ \ \displaystyle \min_{x} & c^{\top} x\\ s.t. & A x = b\\ & l \leq x \leq u \end{array} \quad \quad \quad \begin{array}{rl} (D) \ \ \ \displaystyle \max_{y, z} & b^{\top} y + l^{\top}z^{l} - u^{\top}z^{u}\\ s.t. & A^{\top}y + z^{l} - z^{u} = c\\ & z^{l}, z^{u} \geq 0 \end{array} ``` ```@docs AbstractKKTSystem ``` ```@docs DefaultKKTSystem ``` ```@docs K2 ``` ```@docs K1 ``` ## KKT solvers ```@docs AbstractKKTSolver ``` Custom linear solvers should (preferably) inherit from the `AbstractKKTSolver` class, and extend the following functions: ```@docs setup ``` ```@docs update! ``` ```@docs solve! ``` ================================================ FILE: docs/src/reference/KKT/tlp_cholmod.md ================================================ ```@meta CurrentModule = Tulip.KKT ``` # TlpCholmod ```@docs TlpCholmod.Backend ``` ```@docs TlpCholmod.CholmodSolver ``` ================================================ FILE: docs/src/reference/KKT/tlp_dense.md ================================================ ```@meta CurrentModule = Tulip.KKT.TlpDense ``` # TlpDense ```@docs TlpDense.Backend ``` ```@docs TlpDense.DenseSolver ``` ================================================ FILE: docs/src/reference/KKT/tlp_krylov.md ================================================ ```@meta CurrentModule = Tulip.KKT.TlpKrylov ``` # TlpKrylov !!! warning Iterative methods are still an experimental feature. Some numerical and performance issues should be expected. ```@docs TlpKrylov.Backend ``` ```@docs TlpKrylov.AbstractKrylovSolver ``` ```@docs TlpKrylov.SPDSolver ``` ```@docs TlpKrylov.SIDSolver ``` ```@docs TlpKrylov.SQDSolver ``` ================================================ FILE: docs/src/reference/KKT/tlp_ldlfact.md ================================================ ```@meta CurrentModule = Tulip.KKT.TlpLDLFactorizations ``` # TlpLDLFactorizations ```@docs TlpLDLFactorizations.Backend ``` ```@docs TlpLDLFactorizations.LDLFactSolver ``` ================================================ FILE: docs/src/reference/Presolve/presolve.md ================================================ ================================================ FILE: docs/src/reference/attributes.md ================================================ ```@meta CurrentModule = Tulip ``` # Attribute reference Attributes are queried using [`get_attribute`](@ref) and set using [`set_attribute`](@ref). ## Model attributes | Name | Type | Description |:----------------------------------|:----------|:------------------------------ | [`ModelName`](@ref) | `String` | Name of the model | [`NumberOfConstraints`](@ref) | `Int` | Number of constraints in the model | [`NumberOfVariables`](@ref) | `Int` | Number of variables in the model | [`ObjectiveValue`](@ref) | `T` | Objective value of the current primal solution | [`DualObjectiveValue`](@ref) | `T` | Objective value of the current dual solution | [`ObjectiveConstant`](@ref) | `T` | Value of the objective constant | [`ObjectiveSense`](@ref) | | Optimization sense | [`Status`](@ref) | | Model status | [`BarrierIterations`](@ref) | `Int` | Number of barrier iterations | [`SolutionTime`](@ref) | `Float64` | Solution time, in seconds ## Variable attributes | Name | Type | Description |:-----------------------------------|:---------|:------------------------------ | [`VariableLowerBound`](@ref) | `T` | Variable lower bound | [`VariableUpperBound`](@ref) | `T` | Variable upper bound | [`VariableObjectiveCoeff`](@ref) | `T` | Variable objective coefficient | [`VariableName`](@ref) | `String` | Variable name ## Constraint attributes | Name | Type | Description |:-----------------------------------|:---------|:------------------------------ | [`ConstraintLowerBound`](@ref) | `T` | Constraint lower bound | [`ConstraintUpperBound`](@ref) | `T` | Constraint upper bound | [`ConstraintName`](@ref) | `String` | Constraint name ## Reference ### Model attributes ```@autodocs Modules = [Tulip] Pages = ["src/attributes.jl"] Filter = t -> typeof(t) === DataType && t <: Tulip.AbstractModelAttribute ``` ### Variable attributes ```@autodocs Modules = [Tulip] Pages = ["src/attributes.jl"] Filter = t -> typeof(t) === DataType && t <: Tulip.AbstractVariableAttribute ``` ### Constraint attributes ```@autodocs Modules = [Tulip] Pages = ["src/attributes.jl"] Filter = t -> typeof(t) === DataType && t <: Tulip.AbstractConstraintAttribute ``` ================================================ FILE: docs/src/reference/options.md ================================================ ```@meta CurrentModule = Tulip ``` # Options reference Parameters can be queried and set through the [`get_parameter`](@ref) and [`set_parameter`](@ref) functions. In all that follows, ``\epsilon`` refers to the numerical precision, which is given by `eps(Tv)` where `Tv` is the arithmetic of the current model. For instance, in double-precision arithmetic, i.e., `Tv=Float64`, we have ``\epsilon \simeq 10^{-16}``. ```@docs Factory ``` ## IPM ### Tolerances Numerical tolerances for the interior-point algorithm. | Parameter | Description | Default | |:----------|:------------|:--------| | `TolerancePFeas` | Primal feasibility tolerance | ``\sqrt{\epsilon}`` | `ToleranceDFeas` | Dual feasibility tolerance | ``\sqrt{\epsilon}`` | `ToleranceRGap` | Relative optimality gap tolerance | ``\sqrt{\epsilon}`` | `ToleranceIFeas` | Infeasibility tolerance | ``\sqrt{\epsilon}`` ### Algorithmic parameters | Parameter | Description | Default | |:----------|:------------|:--------| | `BarrierAlgorithm` | Interior-point algorithm | `1` | | `CorrectionLimit` | Maximum number of additional centrality corrections | `5` | | `StepDampFactor` | Step | `0.9995` | | `GammaMin` | Minimum value of ``\gamma`` for computing correctors | `0.1` | `CentralityOutlierThreshold` | Relative threshold for computing extra centrality corrections | `0.1` | `PRegMin` | Minimum value of primal regularization | ``\sqrt{\epsilon}`` | | `DRegMin` | Minimum value of dual regularization | ``\sqrt{\epsilon}`` ### Stopping criterion | Parameter | Description | Default | |:----------|:------------|:--------| | `IterationsLimit` | Maximum number of barrier iterations | `100` | | `TimeLimit` | Time limit, in seconds | `Inf` | ## KKT | Parameter | Description | Default | |:----------|:------------|:--------| | `Backend` | See [KKT backends](@ref) | [`KKT.DefaultKKTBackend`](@ref) | | `System` | See [KKT systems](@ref) | [`KKT.DefaultKKTSystem`](@ref) | ## Linear Algebra These parameters control the linear algebra implementation | Parameter | Description | Default | |:----------|:------------|:--------| | `MatrixFactory` | See [`Factory`](@ref) | `Factory(SparseMatrixCSC)` ## Presolve These parameters control the execution of the presolve phase. They should be called as `"Presolve_"`. ## Others | Parameter | Description | Default | |:----------|:------------|:--------| | `OutputLevel` | Controls the solver's output | `0` | | `Threads` | Maximum number of threads | `1` | | `Presolve` | Presolve (no presolve if set to ≤ 0) | `1` | ================================================ FILE: docs/src/tutorials/lp_example.md ================================================ # Toy example Tulip can be accessed in 3 ways: through [JuMP](https://github.com/jump-dev/JuMP.jl), through [MathOptInterface](https://github.com/jump-dev/MathOptInterface.jl), or directly. This tutorial illustrates, for each case, how to build a model, solve it, and query the solution value. In all cases, we consider the small LP ```math \begin{array}{rrrl} (LP) \ \ \ \displaystyle Z^{*} = \min_{x, y} & -2x & - y\\ s.t. & x & - y & \geq -2\\ & 2x &- y & \leq 4\\ & x &+ 2y & \leq 7\\ & x,& y & \geq 0\\ \end{array} ``` whose optimal value and solution are ``Z^{*} = -8`` and ``(x^{*}, y^{*}) = (3, 2)``. ## JuMP The default `Model(Tulip.Optimizer)` uses `Float64` arithmetic. To use a different numeric type, use `JuMP.GenericModel{T}`: ```julia using JuMP import Tulip model = JuMP.GenericModel{BigFloat}(Tulip.Optimizer{BigFloat}) ``` ```jldoctest; output = false using Printf using JuMP import Tulip # Instantiate JuMP model lp = Model(Tulip.Optimizer) # Create variables @variable(lp, x >= 0) @variable(lp, y >= 0) # Add constraints @constraint(lp, row1, x - y >= -2) @constraint(lp, row2, 2*x - y <= 4) @constraint(lp, row3, x + 2*y <= 7) # Set the objective @objective(lp, Min, -2*x - y) # Set some parameters set_optimizer_attribute(lp, "OutputLevel", 0) # disable output set_optimizer_attribute(lp, "Presolve_Level", 0) # disable presolve # Solve the problem optimize!(lp) # Check termination status st = termination_status(lp) println("Termination status: $st") # Query solution value objval = objective_value(lp) x_ = value(x) y_ = value(y) @printf "Z* = %.4f\n" objval @printf "x* = %.4f\n" x_ @printf "y* = %.4f\n" y_ # output Termination status: OPTIMAL Z* = -8.0000 x* = 3.0000 y* = 2.0000 ``` ## MOI ```jldoctest; output = false using Printf import MathOptInterface const MOI = MathOptInterface import Tulip lp = Tulip.Optimizer{Float64}() # Create variables x = MOI.add_variable(lp) y = MOI.add_variable(lp) # Set variable bounds MOI.add_constraint(lp, x, MOI.GreaterThan(0.0)) # x >= 0 MOI.add_constraint(lp, y, MOI.GreaterThan(0.0)) # y >= 0 # Add constraints row1 = MOI.add_constraint(lp, MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0, -1.0], [x, y]), 0.0), MOI.GreaterThan(-2.0) ) row2 = MOI.add_constraint(lp, MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, -1.0], [x, y]), 0.0), MOI.LessThan(4.0) ) row3 = MOI.add_constraint(lp, MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0, 2.0], [x, y]), 0.0), MOI.LessThan(7.0) ) # Set the objective MOI.set(lp, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([-2.0, -1.0], [x, y]), 0.0) ) MOI.set(lp, MOI.ObjectiveSense(), MOI.MIN_SENSE) # Set some parameters MOI.set(lp, MOI.Silent(), true) # disable output MOI.set(lp, MOI.RawOptimizerAttribute("Presolve_Level"), 0) # disable presolve # Solve the problem MOI.optimize!(lp) # Check status st = MOI.get(lp, MOI.TerminationStatus()) println("Termination status: $st") # Query solution value objval = MOI.get(lp, MOI.ObjectiveValue()) x_ = MOI.get(lp, MOI.VariablePrimal(), x) y_ = MOI.get(lp, MOI.VariablePrimal(), y) @printf "Z* = %.4f\n" objval @printf "x* = %.4f\n" x_ @printf "y* = %.4f\n" y_ # output Termination status: OPTIMAL Z* = -8.0000 x* = 3.0000 y* = 2.0000 ``` ## Tulip !!! warning Tulip's low-level API should not be considered stable nor complete. The recommended way to use Tulip is through JuMP/MOI as shown above. ```jldoctest; output = false using Printf import Tulip # Instantiate Tulip object lp = Tulip.Model{Float64}() pb = lp.pbdata # Internal problem data # Create variables x = Tulip.add_variable!(pb, Int[], Float64[], -2.0, 0.0, Inf, "x") y = Tulip.add_variable!(pb, Int[], Float64[], -1.0, 0.0, Inf, "y") # Add constraints row1 = Tulip.add_constraint!(pb, [x, y], [1.0, -1.0], -2.0, Inf, "row1") row2 = Tulip.add_constraint!(pb, [x, y], [2.0, -1.0], -Inf, 4.0, "row2") row3 = Tulip.add_constraint!(pb, [x, y], [1.0, 2.0], -Inf, 7.0, "row3") # Set the objective # Nothing to do here as objective is already declared # Set some parameters Tulip.set_parameter(lp, "OutputLevel", 0) # disable output Tulip.set_parameter(lp, "Presolve_Level", 0) # disable presolve # Solve the problem Tulip.optimize!(lp) # Check termination status st = Tulip.get_attribute(lp, Tulip.Status()) println("Termination status: $st") # Query solution value objval = Tulip.get_attribute(lp, Tulip.ObjectiveValue()) x_ = lp.solution.x[x] y_ = lp.solution.x[y] @printf "Z* = %.4f\n" objval @printf "x* = %.4f\n" x_ @printf "y* = %.4f\n" y_ # output Termination status: Trm_Optimal Z* = -8.0000 x* = 3.0000 y* = 2.0000 ``` ================================================ FILE: examples/.gitignore ================================================ dat/ ================================================ FILE: examples/freevars.jl ================================================ using LinearAlgebra using SparseArrays using Test import Tulip TLP = Tulip INSTANCE_DIR = joinpath(@__DIR__, "dat") function ex_freevars(::Type{Tv}; atol::Tv = 100 * sqrt(eps(Tv)), rtol::Tv = 100 * sqrt(eps(Tv)), kwargs... ) where{Tv} #= Example with free variables min x1 + x2 + x3 s.t. 2 x1 + x2 >= 2 x1 + 2 x2 >= 2 x1 + x2 + x3 >= 0 =# m = TLP.Model{Tv}() m.params.OutputLevel = 1 # Set optional parameters for (k, val) in kwargs TLP.set_parameter(m, String(k), val) end # Read problem and solve TLP.load_problem!(m, joinpath(INSTANCE_DIR, "lpex_freevars.mps")) TLP.optimize!(m) # Check status @test TLP.get_attribute(m, TLP.Status()) == TLP.Trm_Optimal z = TLP.get_attribute(m, TLP.ObjectiveValue()) @test isapprox(z, 0, atol=atol, rtol=rtol) @test m.solution.primal_status == TLP.Sln_Optimal @test m.solution.dual_status == TLP.Sln_Optimal # Check optimal solution x1 = m.solution.x[1] x2 = m.solution.x[2] x3 = m.solution.x[3] # Check primal feasibility (note there's no unique solution) @test 2*x1 + x2 >= 2 - atol @test x1 + 2*x2 >= 2 - atol @test x1 + x2 + x3 >= -atol # Free variables should have zero reduced cost s1 = m.solution.s_lower[1] - m.solution.s_upper[1] s2 = m.solution.s_lower[2] - m.solution.s_upper[2] s3 = m.solution.s_lower[3] - m.solution.s_upper[3] @test isapprox(s1, 0, atol=atol, rtol=rtol) @test isapprox(s2, 0, atol=atol, rtol=rtol) @test isapprox(s3, 0, atol=atol, rtol=rtol) end if abspath(PROGRAM_FILE) == @__FILE__ ex_freevars(Float64) end ================================================ FILE: examples/infeasible.jl ================================================ using LinearAlgebra using SparseArrays using Test import Tulip TLP = Tulip INSTANCE_DIR = joinpath(@__DIR__, "dat") function ex_infeasible(::Type{Tv}; atol::Tv = 100 * sqrt(eps(Tv)), rtol::Tv = 100 * sqrt(eps(Tv)), kwargs... ) where{Tv} #= Infeasible example min x1 + x2 s.t. x1 + x2 = 1 x1 - x2 = 0 x2 = 1 x1, x2 >= 0 =# m = TLP.Model{Tv}() m.params.OutputLevel = 1 # Set optional parameters for (k, val) in kwargs TLP.set_parameter(m, String(k), val) end # Read problem from .mps file and solve TLP.load_problem!(m, joinpath(INSTANCE_DIR, "lpex_inf.mps")) TLP.optimize!(m) # Check status @test TLP.get_attribute(m, TLP.Status()) == TLP.Trm_PrimalInfeasible @test m.solution.primal_status == TLP.Sln_Unknown @test m.solution.dual_status == TLP.Sln_InfeasibilityCertificate z = TLP.get_attribute(m, TLP.ObjectiveValue()) @test z == zero(Tv) # Primal infeasible --> solution is zero # Check unbounded dual ray y1 = m.solution.y_lower[1] - m.solution.y_upper[1] y2 = m.solution.y_lower[2] - m.solution.y_upper[2] y3 = m.solution.y_lower[3] - m.solution.y_upper[3] s1 = m.solution.s_lower[1] - m.solution.s_upper[1] s2 = m.solution.s_lower[2] - m.solution.s_upper[2] @test y1 + y3 >= atol # dual cost should be > 0 @test isapprox(y1 + y2 + s1, 0, atol=atol, rtol=rtol) @test isapprox(y1 - y2 + y3 + s2, 0, atol=atol, rtol=rtol) @test s1 >= -atol @test s2 >= -atol end if abspath(PROGRAM_FILE) == @__FILE__ ex_infeasible(Float64) end ================================================ FILE: examples/optimal.jl ================================================ using LinearAlgebra using SparseArrays using Test import Tulip TLP = Tulip INSTANCE_DIR = joinpath(@__DIR__, "dat") function ex_optimal(::Type{Tv}; atol::Tv = 100 * sqrt(eps(Tv)), rtol::Tv = 100 * sqrt(eps(Tv)), kwargs... ) where{Tv} #= Bounded example min x1 + 2*x2 s.t. x1 + x2 = 1 x1 - x2 = 0 0 <= x1 <= 1 0 <= x2 <= 1 =# m = TLP.Model{Tv}() m.params.OutputLevel = 1 # Set optional parameters for (k, val) in kwargs TLP.set_parameter(m, String(k), val) end # Read problem and solve TLP.load_problem!(m, joinpath(INSTANCE_DIR, "lpex_opt.mps")) TLP.optimize!(m) # Check status @test TLP.get_attribute(m, TLP.Status()) == TLP.Trm_Optimal z = TLP.get_attribute(m, TLP.ObjectiveValue()) @test isapprox(z, 3 // 2, atol=atol, rtol=rtol) @test m.solution.primal_status == TLP.Sln_Optimal @test m.solution.dual_status == TLP.Sln_Optimal # Check primal solution x1 = m.solution.x[1] x2 = m.solution.x[2] Ax1 = m.solution.Ax[1] Ax2 = m.solution.Ax[2] @test isapprox(x1, 1 // 2, atol=atol, rtol=rtol) @test isapprox(x2, 1 // 2, atol=atol, rtol=rtol) @test isapprox(Ax1, 1, atol=atol, rtol=rtol) @test isapprox(Ax2, 0, atol=atol, rtol=rtol) # Check duals y1 = m.solution.y_lower[1] - m.solution.y_upper[1] y2 = m.solution.y_lower[2] - m.solution.y_upper[2] s1 = m.solution.s_lower[1] - m.solution.s_upper[1] s2 = m.solution.s_lower[2] - m.solution.s_upper[2] @test isapprox(y1, 3 // 2, atol=atol, rtol=rtol) @test isapprox(y2, -1 // 2, atol=atol, rtol=rtol) @test isapprox(s1, 0, atol=atol, rtol=rtol) @test isapprox(s2, 0, atol=atol, rtol=rtol) end if abspath(PROGRAM_FILE) == @__FILE__ ex_optimal(Float64) end ================================================ FILE: examples/optimal_other_type.jl ================================================ using Tulip import MathOptInterface const MOI = MathOptInterface using Test const T = Float32 lp = Tulip.Optimizer{T}() # Create variables x = MOI.add_variable(lp) y = MOI.add_variable(lp) # Set variable bounds MOI.add_constraint(lp, x, MOI.GreaterThan(T(0))) # x >= 0 MOI.add_constraint(lp, y, MOI.GreaterThan(T(0))) # y >= 0 # Add constraints row1 = MOI.add_constraint(lp, MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(T[1.0, -1.0], [x, y]), T(0)), MOI.GreaterThan(T(-2)) ) row2 = MOI.add_constraint(lp, MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(T[2.0, -1.0], [x, y]), T(0)), MOI.LessThan(T(4)) ) row3 = MOI.add_constraint(lp, MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(T[1.0, 2.0], [x, y]), T(0)), MOI.LessThan(T(7)) ) # Set the objective MOI.set(lp, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float32}}(), MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(T[-2.0, -1.0], [x, y]), T(0)) ) MOI.set(lp, MOI.ObjectiveSense(), MOI.MIN_SENSE) MOI.optimize!(lp) objval = MOI.get(lp, MOI.ObjectiveValue()) x_ = MOI.get(lp, MOI.VariablePrimal(), x) y_ = MOI.get(lp, MOI.VariablePrimal(), y) @test objval ≈ -8 @test x_ ≈ 3 @test y_ ≈ 2 @test objval isa Float32 @test x_ isa Float32 @test y_ isa Float32 ================================================ FILE: examples/unbounded.jl ================================================ using LinearAlgebra using SparseArrays using Test import Tulip TLP = Tulip INSTANCE_DIR = joinpath(@__DIR__, "dat") function ex_unbounded(::Type{Tv}; atol::Tv = 100 * sqrt(eps(Tv)), rtol::Tv = 100 * sqrt(eps(Tv)), kwargs... ) where{Tv} #= Unbounded example min -x1 + -x2 x1 - x2 = 1 x1, x2 >= 0 =# m = TLP.Model{Tv}() m.params.OutputLevel = 1 # Set optional parameters for (k, val) in kwargs TLP.set_parameter(m, String(k), val) end # Read problem from .mps file and solve TLP.load_problem!(m, joinpath(INSTANCE_DIR, "lpex_ubd.mps")) TLP.optimize!(m) # Check status @test TLP.get_attribute(m, TLP.Status()) == TLP.Trm_DualInfeasible @test m.solution.primal_status == TLP.Sln_InfeasibilityCertificate @test m.solution.dual_status == TLP.Sln_Unknown # Check unbounded ray x1 = m.solution.x[1] x2 = m.solution.x[2] Ax1 = m.solution.Ax[1] @test x1 >= -atol @test x2 >= -atol @test isapprox(Ax1, 0, atol=atol, rtol=rtol) @test -x1 - x2 <= -atol zp = TLP.get_attribute(m, TLP.ObjectiveValue()) @test zp == (-x1 - x2) zd = TLP.get_attribute(m, TLP.DualObjectiveValue()) @test zd == zero(Tv) end if abspath(PROGRAM_FILE) == @__FILE__ ex_unbounded(Float64) end ================================================ FILE: src/IPM/HSD/HSD.jl ================================================ """ HSD Solver for the homogeneous self-dual algorithm. """ mutable struct HSD{T, Tv, Tb, Ta, Tk} <: AbstractIPMOptimizer{T} # Problem data, in standard form dat::IPMData{T, Tv, Tb, Ta} # ================= # Book-keeping # ================= niter::Int # Number of IPM iterations solver_status::TerminationStatus # Optimization status primal_status::SolutionStatus # Primal solution status dual_status::SolutionStatus # Dual solution status primal_objective::T # Primal objective value: (c'x) / τ dual_objective::T # Dual objective value: (b'y + l' zl - u'zu) / τ timer::TimerOutput #===================== Working memory =====================# pt::Point{T, Tv} # Current primal-dual iterate res::Residuals{T, Tv} # Residuals at current iterate kkt::Tk regP::Tv # primal regularization regD::Tv # dual regularization regG::T # gap regularization function HSD( dat::IPMData{T, Tv, Tb, Ta}, kkt_options::KKTOptions{T} ) where{T, Tv<:AbstractVector{T}, Tb<:AbstractVector{Bool}, Ta<:AbstractMatrix{T}} m, n = dat.nrow, dat.ncol p = sum(dat.lflag) + sum(dat.uflag) # Allocate some memory pt = Point{T, Tv}(m, n, p, hflag=true) res = Residuals( tzeros(Tv, m), tzeros(Tv, n), tzeros(Tv, n), tzeros(Tv, n), zero(T), zero(T), zero(T), zero(T), zero(T), zero(T) ) # Initial regularizations regP = tones(Tv, n) regD = tones(Tv, m) regG = one(T) kkt = KKT.setup(dat.A, kkt_options.System, kkt_options.Backend) Tk = typeof(kkt) return new{T, Tv, Tb, Ta, Tk}(dat, 0, Trm_Unknown, Sln_Unknown, Sln_Unknown, T(Inf), T(-Inf), TimerOutput(), pt, res, kkt, regP, regD, regG ) end end include("step.jl") """ compute_residuals!(::HSD, res, pt, A, b, c, uind, uval) In-place computation of primal-dual residuals at point `pt`. """ # TODO: check whether having just hsd as argument makes things slower # TODO: Update solution status function compute_residuals!(hsd::HSD{T} ) where{T} pt, res = hsd.pt, hsd.res dat = hsd.dat # Primal residual # rp = t*b - A*x axpby!(pt.τ, dat.b, zero(T), res.rp) mul!(res.rp, dat.A, pt.x, -one(T), one(T)) # Lower-bound residual # rl_j = τ*l_j - (x_j - xl_j) if l_j ∈ R # = 0 if l_j = -∞ @. res.rl = (- pt.x + pt.xl + pt.τ * dat.l) * dat.lflag # Upper-bound residual # ru_j = τ*u_j - (x_j + xu_j) if u_j ∈ R # = 0 if u_j = +∞ @. res.ru = (- pt.x - pt.xu + pt.τ * dat.u) * dat.uflag # Dual residual # rd = t*c - A'y - zl + zu axpby!(pt.τ, dat.c, zero(T), res.rd) mul!(res.rd, transpose(dat.A), pt.y, -one(T), one(T)) @. res.rd += pt.zu .* dat.uflag - pt.zl .* dat.lflag # Gap residual # rg = c'x - (b'y + l'zl - u'zu) + k res.rg = pt.κ + (dot(dat.c, pt.x) - ( dot(dat.b, pt.y) + dot(dat.l .* dat.lflag, pt.zl) - dot(dat.u .* dat.uflag, pt.zu) )) # Residuals norm res.rp_nrm = norm(res.rp, Inf) res.rl_nrm = norm(res.rl, Inf) res.ru_nrm = norm(res.ru, Inf) res.rd_nrm = norm(res.rd, Inf) res.rg_nrm = norm(res.rg, Inf) # Compute primal and dual bounds hsd.primal_objective = dot(dat.c, pt.x) / pt.τ + dat.c0 hsd.dual_objective = ( dot(dat.b, pt.y) + dot(dat.l .* dat.lflag, pt.zl) - dot(dat.u .* dat.uflag, pt.zu) ) / pt.τ + dat.c0 return nothing end """ update_solver_status!() Update status and return true if solver should stop. """ function update_solver_status!(hsd::HSD{T}, ϵp::T, ϵd::T, ϵg::T, ϵi::T) where{T} hsd.solver_status = Trm_Unknown pt, res = hsd.pt, hsd.res dat = hsd.dat ρp = max( res.rp_nrm / (pt.τ * (one(T) + norm(dat.b, Inf))), res.rl_nrm / (pt.τ * (one(T) + norm(dat.l .* dat.lflag, Inf))), res.ru_nrm / (pt.τ * (one(T) + norm(dat.u .* dat.uflag, Inf))) ) ρd = res.rd_nrm / (pt.τ * (one(T) + norm(dat.c, Inf))) ρg = abs(hsd.primal_objective - hsd.dual_objective) / (one(T) + abs(hsd.dual_objective)) # Check for feasibility if ρp <= ϵp hsd.primal_status = Sln_FeasiblePoint else hsd.primal_status = Sln_Unknown end if ρd <= ϵd hsd.dual_status = Sln_FeasiblePoint else hsd.dual_status = Sln_Unknown end # Check for optimal solution if ρp <= ϵp && ρd <= ϵd && ρg <= ϵg hsd.primal_status = Sln_Optimal hsd.dual_status = Sln_Optimal hsd.solver_status = Trm_Optimal return nothing end # Check for infeasibility certificates if max( norm(dat.A * pt.x, Inf), norm((pt.x .- pt.xl) .* dat.lflag, Inf), norm((pt.x .+ pt.xu) .* dat.uflag, Inf) ) * (norm(dat.c, Inf) / max(1, norm(dat.b, Inf))) < - ϵi * dot(dat.c, pt.x) # Dual infeasible, i.e., primal unbounded hsd.primal_status = Sln_InfeasibilityCertificate hsd.solver_status = Trm_DualInfeasible return nothing end δ = dat.A' * pt.y .+ (pt.zl .* dat.lflag) .- (pt.zu .* dat.uflag) if norm(δ, Inf) * max( norm(dat.l .* dat.lflag, Inf), norm(dat.u .* dat.uflag, Inf), norm(dat.b, Inf) ) / (max(one(T), norm(dat.c, Inf))) < (dot(dat.b, pt.y) + dot(dat.l .* dat.lflag, pt.zl)- dot(dat.u .* dat.uflag, pt.zu)) * ϵi # Primal infeasible hsd.dual_status = Sln_InfeasibilityCertificate hsd.solver_status = Trm_PrimalInfeasible return nothing end return nothing end """ optimize! """ function ipm_optimize!(hsd::HSD{T}, params::IPMOptions{T}) where{T} # TODO: pre-check whether model needs to be re-optimized. # This should happen outside of this function dat = hsd.dat # Initialization TimerOutputs.reset_timer!(hsd.timer) tstart = time() hsd.niter = 0 # Print information about the problem if params.OutputLevel > 0 @printf "\nOptimizer info (HSD)\n" @printf "Constraints : %d\n" dat.nrow @printf "Variables : %d\n" dat.ncol bmin, bmax = extrema(dat.b) @printf "RHS : [%+.2e, %+.2e]\n" bmin bmax lmin, lmax = extrema(dat.l .* dat.lflag) @printf "Lower bounds : [%+.2e, %+.2e]\n" lmin lmax lmin, lmax = extrema(dat.u .* dat.uflag) @printf "Upper bounds : [%+.2e, %+.2e]\n" lmin lmax @printf "\nLinear solver options\n" @printf " %-12s : %s\n" "Arithmetic" KKT.arithmetic(hsd.kkt) @printf " %-12s : %s\n" "Backend" KKT.backend(hsd.kkt) @printf " %-12s : %s\n" "System" KKT.linear_system(hsd.kkt) end # IPM LOG if params.OutputLevel > 0 @printf "\n%4s %14s %14s %8s %8s %8s %7s %4s\n" "Itn" "PObj" "DObj" "PFeas" "DFeas" "GFeas" "Mu" "Time" end # Set starting point hsd.pt.x .= zero(T) hsd.pt.xl .= one(T) .* dat.lflag hsd.pt.xu .= one(T) .* dat.uflag hsd.pt.y .= zero(T) hsd.pt.zl .= one(T) .* dat.lflag hsd.pt.zu .= one(T) .* dat.uflag hsd.pt.τ = one(T) hsd.pt.κ = one(T) update_mu!(hsd.pt) # Main loop # Iteration 0 corresponds to the starting point. # Therefore, there is no numerical factorization before the first log is printed. # If the maximum number of iterations is set to 0, the only computation that occurs # is computing the residuals at the initial point. @timeit hsd.timer "Main loop" while(true) # I.A - Compute residuals at current iterate @timeit hsd.timer "Residuals" compute_residuals!(hsd) update_mu!(hsd.pt) # I.B - Log # TODO: Put this in a logging function ttot = time() - tstart if params.OutputLevel > 0 # Display log @printf "%4d" hsd.niter # Objectives ϵ = dat.objsense ? one(T) : -one(T) @printf " %+14.7e" ϵ * hsd.primal_objective @printf " %+14.7e" ϵ * hsd.dual_objective # Residuals @printf " %8.2e" max(hsd.res.rp_nrm, hsd.res.ru_nrm) @printf " %8.2e" hsd.res.rd_nrm @printf " %8.2e" hsd.res.rg_nrm # Mu @printf " %7.1e" hsd.pt.μ # Time @printf " %.2f" ttot print("\n") end # TODO: check convergence status # TODO: first call an `compute_convergence status`, # followed by a check on the solver status to determine whether to stop # In particular, user limits should be checked last (if an optimal solution is found, # we want to report optimal, not user limits) @timeit hsd.timer "update status" update_solver_status!(hsd, params.TolerancePFeas, params.ToleranceDFeas, params.ToleranceRGap, params.ToleranceIFeas ) if ( hsd.solver_status == Trm_Optimal || hsd.solver_status == Trm_PrimalInfeasible || hsd.solver_status == Trm_DualInfeasible ) break elseif hsd.niter >= params.IterationsLimit hsd.solver_status = Trm_IterationLimit break elseif ttot >= params.TimeLimit hsd.solver_status = Trm_TimeLimit break end # TODO: step # For now, include the factorization in the step function # Q: should we use more arguments here? try @timeit hsd.timer "Step" compute_step!(hsd, params) catch err if isa(err, PosDefException) || isa(err, SingularException) # Numerical trouble while computing the factorization hsd.solver_status = Trm_NumericalProblem elseif isa(err, OutOfMemoryError) # Out of memory hsd.solver_status = Trm_MemoryLimit elseif isa(err, InterruptException) hsd.solver_status = Trm_Unknown else # Unknown error: rethrow rethrow(err) end break end hsd.niter += 1 end # TODO: print message based on termination status params.OutputLevel > 0 && println("Solver exited with status $((hsd.solver_status))") return nothing end ================================================ FILE: src/IPM/HSD/step.jl ================================================ """ compute_step!(hsd, params) Compute next IP iterate for the HSD formulation. # Arguments - `hsd`: The HSD optimizer model - `params`: Optimization parameters """ function compute_step!(hsd::HSD{T, Tv}, params::IPMOptions{T}) where{T, Tv<:AbstractVector{T}} # Names dat = hsd.dat pt = hsd.pt res = hsd.res m, n, p = pt.m, pt.n, pt.p A = dat.A b = dat.b c = dat.c # Compute scaling θl = (pt.zl ./ pt.xl) .* dat.lflag θu = (pt.zu ./ pt.xu) .* dat.uflag θinv = θl .+ θu # Update regularizations hsd.regP .= max.(params.PRegMin, hsd.regP ./ 10) hsd.regD .= max.(params.DRegMin, hsd.regD ./ 10) hsd.regG = max( params.PRegMin, hsd.regG / 10) # Update factorization nbump = 0 while nbump <= 3 try @timeit hsd.timer "Factorization" KKT.update!(hsd.kkt, θinv, hsd.regP, hsd.regD) break catch err isa(err, PosDefException) || isa(err, ZeroPivotException) || rethrow(err) # Increase regularization hsd.regD .*= 100 hsd.regP .*= 100 hsd.regG *= 100 nbump += 1 @warn "Increase regularizations to $(hsd.regG)" end end # TODO: throw a custom error for numerical issues nbump < 3 || throw(PosDefException(0)) # factorization could not be saved # Search directions # Predictor Δ = Point{T, Tv}(m, n, p, hflag=true) Δc = Point{T, Tv}(m, n, p, hflag=true) # Compute hx, hy, hz from first augmented system solve hx = tzeros(Tv, n) hy = tzeros(Tv, m) ξ_ = @. (dat.c - ((pt.zl / pt.xl) * dat.l) * dat.lflag - ((pt.zu / pt.xu) * dat.u) * dat.uflag) @timeit hsd.timer "Newton" begin @timeit hsd.timer "KKT" KKT.solve!(hx, hy, hsd.kkt, dat.b, ξ_) end # Recover h0 = ρg + κ / τ - c'hx + b'hy - u'hz # Some of the summands may take large values, # so care must be taken for numerical stability h0 = ( dot(dat.l .* dat.lflag, (dat.l .* θl) .* dat.lflag) + dot(dat.u .* dat.uflag, (dat.u .* θu) .* dat.uflag) - dot((@. (c + (θl * dat.l) * dat.lflag + (θu * dat.u) * dat.uflag)), hx) + dot(b, hy) + pt.κ / pt.τ + hsd.regG ) # Affine-scaling direction @timeit hsd.timer "Newton" solve_newton_system!(Δ, hsd, hx, hy, h0, # Right-hand side of Newton system res.rp, res.rl, res.ru, res.rd, res.rg, .-(pt.xl .* pt.zl) .* dat.lflag, .-(pt.xu .* pt.zu) .* dat.uflag, .-pt.τ * pt.κ ) # Step length for affine-scaling direction α = max_step_length(pt, Δ) γ = (one(T) - α)^2 * min(one(T) - α, params.GammaMin) η = one(T) - γ # Mehrotra corrector @timeit hsd.timer "Newton" solve_newton_system!(Δ, hsd, hx, hy, h0, # Right-hand side of Newton system η .* res.rp, η .* res.rl, η .* res.ru, η .* res.rd, η * res.rg, (.-pt.xl .* pt.zl .+ γ * pt.μ .- Δ.xl .* Δ.zl) .* dat.lflag, (.-pt.xu .* pt.zu .+ γ * pt.μ .- Δ.xu .* Δ.zu) .* dat.uflag, -pt.τ * pt.κ + γ * pt.μ - Δ.τ * Δ.κ ) α = max_step_length(pt, Δ) # Extra corrections ncor = 0 while ncor < params.CorrectionLimit && α < T(999 // 1000) α_ = α ncor += 1 # Compute extra-corrector αc = compute_higher_corrector!(Δc, hsd, γ, hx, hy, h0, Δ, α_, params.CentralityOutlierThreshold ) if αc > α_ # Use corrector Δ.x .= Δc.x Δ.xl .= Δc.xl Δ.xu .= Δc.xu Δ.y .= Δc.y Δ.zl .= Δc.zl Δ.zu .= Δc.zu Δ.τ = Δc.τ Δ.κ = Δc.κ α = αc end if αc < T(11 // 10) * α_ break end # if (1.0 - η * αc) >= 0.9*(1.0 - η * α_) # # not enough improvement, step correcting # break # end end # Update current iterate α *= params.StepDampFactor pt.x .+= α .* Δ.x pt.xl .+= α .* Δ.xl pt.xu .+= α .* Δ.xu pt.y .+= α .* Δ.y pt.zl .+= α .* Δ.zl pt.zu .+= α .* Δ.zu pt.τ += α * Δ.τ pt.κ += α * Δ.κ update_mu!(pt) return nothing end """ solve_newton_system!(Δ, hsd, hx, hy, h0, ξp, ξd, ξu, ξg, ξxs, ξwz, ξtk) Solve the Newton system ```math \\begin{bmatrix} A & & & R_{d} & & & -b\\\\ I & -I & & & & & -l\\\\ I & & -I & & & & -u\\\\ -R_{p} & & & A^{T} & I & -I & -c\\\\ -c^{T} & & & b^{T} & l^{T} & -u^{T} & ρ_{g} & -1\\\\ & Z_{l} & & & X_{l}\\\\ & & Z_{u} & & & X_{u}\\\\ &&&&&& κ & τ \\end{bmatrix} \\begin{bmatrix} Δ x\\\\ Δ x_{l}\\\\ Δ x_{u}\\\\ Δ y\\\\ Δ z_{l} \\\\ Δ z_{u} \\\\ Δ τ\\\\ Δ κ\\\\ \\end{bmatrix} = \\begin{bmatrix} ξ_p\\\\ ξ_l\\\\ ξ_u\\\\ ξ_d\\\\ ξ_g\\\\ ξ_{xz}^{l}\\\\ ξ_{xz}^{u}\\\\ ξ_tk \\end{bmatrix} ``` # Arguments - `Δ`: Search direction, modified - `hsd`: The HSD optimizer - `hx, hy, hz, h0`: Terms obtained in the preliminary augmented system solve - `ξp, ξd, ξu, ξg, ξxs, ξwz, ξtk`: Right-hand side vectors """ function solve_newton_system!(Δ::Point{T, Tv}, hsd::HSD{T, Tv}, # Information from initial augmented system solve hx::Tv, hy::Tv, h0::T, # Right-hand side ξp::Tv, ξl::Tv, ξu::Tv, ξd::Tv, ξg::T, ξxzl::Tv, ξxzu::Tv, ξtk::T ) where{T, Tv<:AbstractVector{T}} pt = hsd.pt dat = hsd.dat # I. Solve augmented system @timeit hsd.timer "ξd_" begin ξd_ = copy(ξd) @. ξd_ += -((ξxzl + pt.zl .* ξl) ./ pt.xl) .* dat.lflag + ((ξxzu - pt.zu .* ξu) ./ pt.xu) .* dat.uflag end @timeit hsd.timer "KKT" KKT.solve!(Δ.x, Δ.y, hsd.kkt, ξp, ξd_) # II. Recover Δτ, Δx, Δy # Compute Δτ @timeit hsd.timer "ξg_" ξg_ = (ξg + ξtk / pt.τ - dot((ξxzl ./ pt.xl) .* dat.lflag, dat.l .* dat.lflag) # l'(Xl)^-1 * ξxzl + dot((ξxzu ./ pt.xu) .* dat.uflag, dat.u .* dat.uflag) - dot(((pt.zl ./ pt.xl) .* ξl) .* dat.lflag, dat.l .* dat.lflag) - dot(((pt.zu ./ pt.xu) .* ξu) .* dat.uflag, dat.u .* dat.uflag) # ) @timeit hsd.timer "Δτ" Δ.τ = ( ξg_ + dot((@. (dat.c + ((pt.zl / pt.xl) * dat.l) * dat.lflag + ((pt.zu / pt.xu) * dat.u) * dat.uflag)) , Δ.x) - dot(dat.b, Δ.y) ) / h0 # Compute Δx, Δy @timeit hsd.timer "Δx" Δ.x .+= Δ.τ .* hx @timeit hsd.timer "Δy" Δ.y .+= Δ.τ .* hy # III. Recover Δxl, Δxu @timeit hsd.timer "Δxl" begin @. Δ.xl = (-ξl + Δ.x - Δ.τ .* (dat.l .* dat.lflag)) * dat.lflag end @timeit hsd.timer "Δxu" begin @. Δ.xu = ( ξu - Δ.x + Δ.τ .* (dat.u .* dat.uflag)) * dat.uflag end # IV. Recover Δzl, Δzu @timeit hsd.timer "Δzl" @. Δ.zl = ((ξxzl - pt.zl .* Δ.xl) ./ pt.xl) .* dat.lflag @timeit hsd.timer "Δzu" @. Δ.zu = ((ξxzu - pt.zu .* Δ.xu) ./ pt.xu) .* dat.uflag # V. Recover Δκ Δ.κ = (ξtk - pt.κ * Δ.τ) / pt.τ # Check Newton residuals # @printf "Newton residuals:\n" # @printf "|rp| = %16.8e\n" norm(dat.A * Δ.x + hsd.regD .* Δ.y - dat.b .* Δ.τ - ξp, Inf) # @printf "|rl| = %16.8e\n" norm((Δ.x - Δ.xl - (dat.l .* Δ.τ)) .* dat.lflag - ξl, Inf) # @printf "|ru| = %16.8e\n" norm((Δ.x + Δ.xu - (dat.u .* Δ.τ)) .* dat.uflag - ξu, Inf) # @printf "|rd| = %16.8e\n" norm(-hsd.regP .* Δ.x + dat.A'Δ.y + Δ.zl - Δ.zu - dat.c .* Δ.τ - ξd, Inf) # @printf "|rg| = %16.8e\n" norm(-dat.c'Δ.x + dat.b'Δ.y + dot(dat.l .* dat.lflag, Δ.zl) - dot(dat.u .* dat.uflag, Δ.zu) + hsd.regG * Δ.τ - Δ.κ - ξg, Inf) # @printf "|rxzl| = %16.8e\n" norm(pt.zl .* Δ.xl + pt.xl .* Δ.zl - ξxzl, Inf) # @printf "|rxzu| = %16.8e\n" norm(pt.zu .* Δ.xu + pt.xu .* Δ.zu - ξxzu, Inf) # @printf "|rtk| = %16.8e\n" norm(pt.κ * Δ.τ + pt.τ * Δ.κ - ξtk, Inf) return nothing end """ max_step_length(x, dx) Compute the maximum value `a ≥ 0` such that `x + a*dx ≥ 0`, where `x ≥ 0`. """ function max_step_length(x::Vector{T}, dx::Vector{T}) where{T} n = size(x, 1) n == size(dx, 1) || throw(DimensionMismatch()) a = T(Inf) @inbounds for i in 1:n if dx[i] < zero(T) if (-x[i] / dx[i]) < a a = (-x[i] / dx[i]) end end end return a end """ max_step_length(pt, δ) Compute maximum length of homogeneous step. """ function max_step_length(pt::Point{T, Tv}, δ::Point{T, Tv}) where{T, Tv<:AbstractVector{T}} axl = max_step_length(pt.xl, δ.xl) axu = max_step_length(pt.xu, δ.xu) azl = max_step_length(pt.zl, δ.zl) azu = max_step_length(pt.zu, δ.zu) at = δ.τ < zero(T) ? (-pt.τ / δ.τ) : oneunit(T) ak = δ.κ < zero(T) ? (-pt.κ / δ.κ) : oneunit(T) α = min(one(T), axl, axu, azl, azu, at, ak) return α end """ compute_higher_corrector!(Δc, hsd, γ, hx, hy, h0, Δ, α, β) Compute higher-order corrected direction. Requires the solution of one Newton system. # Arguments - `Δc`: Corrected search direction, modified in-place - `hsd`: The HSD optimizer - `γ`: - `hx, hy, h0`: Terms obtained from the preliminary augmented system solve - `Δ`: Current predictor direction - `α`: Maximum step length in predictor direction - `β`: Relative threshold for centrality outliers """ function compute_higher_corrector!(Δc::Point{T, Tv}, hsd::HSD{T, Tv}, γ::T, hx::Tv, hy::Tv, h0::T, Δ::Point{T, Tv}, α::T, β::T, ) where{T, Tv<:AbstractVector{T}} # TODO: Sanity checks pt = hsd.pt dat = hsd.dat # Tentative step length α_ = min(one(T), T(2)*α) # Tentative cross products vl = ((pt.xl .+ α_ .* Δ.xl) .* (pt.zl .+ α_ .* Δ.zl)) .* dat.lflag vu = ((pt.xu .+ α_ .* Δ.xu) .* (pt.zu .+ α_ .* Δ.zu)) .* dat.uflag vt = (pt.τ + α_ * Δ.τ) * (pt.κ + α_ * Δ.κ) # Compute target cross-products mu_l = β * pt.μ * γ mu_u = γ * pt.μ / β for i in 1:pt.n dat.lflag[i] || continue if vl[i] < mu_l vl[i] = mu_l - vl[i] elseif vl[i] > mu_u vl[i] = mu_u - vl[i] else vl[i] = zero(T) end end for i in 1:pt.n dat.uflag[i] || continue if vu[i] < mu_l vu[i] = mu_l - vu[i] elseif vu[i] > mu_u vu[i] = mu_u - vu[i] else vu[i] = zero(T) end end if vt < mu_l vt = mu_l - vt elseif vt > mu_u vt = mu_u - vt else vt = zero(T) end # Shift target cross-product to satisfy `v' * e = 0` δ = (sum(vl) + sum(vu) + vt) / (pt.p + 1) vl .-= δ vu .-= δ vt -= δ # Compute corrector @timeit hsd.timer "Newton" solve_newton_system!(Δc, hsd, hx, hy, h0, # Right-hand sides tzeros(Tv, pt.m), tzeros(Tv, pt.n), tzeros(Tv, pt.n), tzeros(Tv, pt.n), zero(T), vl, vu, vt ) # Update corrector Δc.x .+= Δ.x Δc.xl .+= Δ.xl Δc.xu .+= Δ.xu Δc.y .+= Δ.y Δc.zl .+= Δ.zl Δc.zu .+= Δ.zu Δc.τ += Δ.τ Δc.κ += Δ.κ # Compute corrected step-length αc = max_step_length(pt, Δc) return αc end ================================================ FILE: src/IPM/IPM.jl ================================================ using Printf """ AbstractIPMOptimizer Abstraction layer for IPM solvers. An IPM solver implements an interior-point algorithm. Currently supported: * Homogeneous self-dual (HSD) """ abstract type AbstractIPMOptimizer{T} end include("ipmdata.jl") include("point.jl") include("residuals.jl") include("options.jl") """ ipm_optimize!(ipm) Run the interior-point optimizer of `ipm`. """ function ipm_optimize! end include("HSD/HSD.jl") include("MPC/MPC.jl") ================================================ FILE: src/IPM/MPC/MPC.jl ================================================ """ MPC Implements Mehrotra's Predictor-Corrector interior-point algorithm. """ mutable struct MPC{T, Tv, Tb, Ta, Tk} <: AbstractIPMOptimizer{T} # Problem data, in standard form dat::IPMData{T, Tv, Tb, Ta} # ================= # Book-keeping # ================= niter::Int # Number of IPM iterations solver_status::TerminationStatus # Optimization status primal_status::SolutionStatus # Primal solution status dual_status::SolutionStatus # Dual solution status primal_objective::T # Primal bound: c'x dual_objective::T # Dual bound: b'y + l' zl - u'zu timer::TimerOutput #===================== Working memory =====================# pt::Point{T, Tv} # Current primal-dual iterate res::Residuals{T, Tv} # Residuals at current iterate Δ::Point{T, Tv} # Predictor Δc::Point{T, Tv} # Corrector # Step sizes αp::T αd::T # Newton system RHS ξp::Tv ξl::Tv ξu::Tv ξd::Tv ξxzl::Tv ξxzu::Tv # KKT solver kkt::Tk regP::Tv # Primal regularization regD::Tv # Dual regularization function MPC( dat::IPMData{T, Tv, Tb, Ta}, kkt_options::KKTOptions{T} ) where{T, Tv<:AbstractVector{T}, Tb<:AbstractVector{Bool}, Ta<:AbstractMatrix{T}} m, n = dat.nrow, dat.ncol p = sum(dat.lflag) + sum(dat.uflag) # Working memory pt = Point{T, Tv}(m, n, p, hflag=false) res = Residuals( tzeros(Tv, m), tzeros(Tv, n), tzeros(Tv, n), tzeros(Tv, n), zero(T), zero(T), zero(T), zero(T), zero(T), zero(T) ) Δ = Point{T, Tv}(m, n, p, hflag=false) Δc = Point{T, Tv}(m, n, p, hflag=false) # Newton RHS ξp = tzeros(Tv, m) ξl = tzeros(Tv, n) ξu = tzeros(Tv, n) ξd = tzeros(Tv, n) ξxzl = tzeros(Tv, n) ξxzu = tzeros(Tv, n) # Initial regularizations regP = tones(Tv, n) regD = tones(Tv, m) kkt = KKT.setup(dat.A, kkt_options.System, kkt_options.Backend) Tk = typeof(kkt) return new{T, Tv, Tb, Ta, Tk}(dat, 0, Trm_Unknown, Sln_Unknown, Sln_Unknown, T(Inf), T(-Inf), TimerOutput(), pt, res, Δ, Δc, zero(T), zero(T), ξp, ξl, ξu, ξd, ξxzl, ξxzu, kkt, regP, regD ) end end include("step.jl") """ compute_residuals!(::MPC) In-place computation of primal-dual residuals at point `pt`. """ function compute_residuals!(mpc::MPC{T}) where{T} pt, res = mpc.pt, mpc.res dat = mpc.dat # Primal residual # rp = b - A*x res.rp .= dat.b mul!(res.rp, dat.A, pt.x, -one(T), one(T)) # Lower-bound residual # rl_j = l_j - (x_j - xl_j) if l_j ∈ R # = 0 if l_j = -∞ @. res.rl = ((dat.l + pt.xl) - pt.x) * dat.lflag # Upper-bound residual # ru_j = u_j - (x_j + xu_j) if u_j ∈ R # = 0 if u_j = +∞ @. res.ru = (dat.u - (pt.x + pt.xu)) * dat.uflag # Dual residual # rd = c - (A'y + zl - zu) res.rd .= dat.c mul!(res.rd, transpose(dat.A), pt.y, -one(T), one(T)) @. res.rd += pt.zu .* dat.uflag - pt.zl .* dat.lflag # Residuals norm res.rp_nrm = norm(res.rp, Inf) res.rl_nrm = norm(res.rl, Inf) res.ru_nrm = norm(res.ru, Inf) res.rd_nrm = norm(res.rd, Inf) # Compute primal and dual bounds mpc.primal_objective = dot(dat.c, pt.x) + dat.c0 mpc.dual_objective = ( dot(dat.b, pt.y) + dot(dat.l .* dat.lflag, pt.zl) - dot(dat.u .* dat.uflag, pt.zu) ) + dat.c0 return nothing end """ update_solver_status!() Update status and return true if solver should stop. """ function update_solver_status!(mpc::MPC{T}, ϵp::T, ϵd::T, ϵg::T, ϵi::T) where{T} mpc.solver_status = Trm_Unknown pt, res = mpc.pt, mpc.res dat = mpc.dat ρp = max( res.rp_nrm / (one(T) + norm(dat.b, Inf)), res.rl_nrm / (one(T) + norm(dat.l .* dat.lflag, Inf)), res.ru_nrm / (one(T) + norm(dat.u .* dat.uflag, Inf)) ) ρd = res.rd_nrm / (one(T) + norm(dat.c, Inf)) ρg = abs(mpc.primal_objective - mpc.dual_objective) / (one(T) + abs(mpc.primal_objective)) # Check for feasibility if ρp <= ϵp mpc.primal_status = Sln_FeasiblePoint else mpc.primal_status = Sln_Unknown end if ρd <= ϵd mpc.dual_status = Sln_FeasiblePoint else mpc.dual_status = Sln_Unknown end # Check for optimal solution if ρp <= ϵp && ρd <= ϵd && ρg <= ϵg mpc.primal_status = Sln_Optimal mpc.dual_status = Sln_Optimal mpc.solver_status = Trm_Optimal return nothing end # TODO: Primal/Dual infeasibility detection # Check for infeasibility certificates if max( norm(dat.A * pt.x, Inf), norm((pt.x .- pt.xl) .* dat.lflag, Inf), norm((pt.x .+ pt.xu) .* dat.uflag, Inf) ) * (norm(dat.c, Inf) / max(1, norm(dat.b, Inf))) < - ϵi * dot(dat.c, pt.x) # Dual infeasible, i.e., primal unbounded mpc.primal_status = Sln_InfeasibilityCertificate mpc.solver_status = Trm_DualInfeasible return nothing end δ = dat.A' * pt.y .+ (pt.zl .* dat.lflag) .- (pt.zu .* dat.uflag) if norm(δ, Inf) * max( norm(dat.l .* dat.lflag, Inf), norm(dat.u .* dat.uflag, Inf), norm(dat.b, Inf) ) / (max(one(T), norm(dat.c, Inf))) < (dot(dat.b, pt.y) + dot(dat.l .* dat.lflag, pt.zl)- dot(dat.u .* dat.uflag, pt.zu)) * ϵi # Primal infeasible mpc.dual_status = Sln_InfeasibilityCertificate mpc.solver_status = Trm_PrimalInfeasible return nothing end return nothing end """ optimize! """ function ipm_optimize!(mpc::MPC{T}, params::IPMOptions{T}) where{T} # TODO: pre-check whether model needs to be re-optimized. # This should happen outside of this function dat = mpc.dat # Initialization TimerOutputs.reset_timer!(mpc.timer) tstart = time() mpc.niter = 0 # Print information about the problem if params.OutputLevel > 0 @printf "\nOptimizer info (MPC)\n" @printf "Constraints : %d\n" dat.nrow @printf "Variables : %d\n" dat.ncol bmin, bmax = extrema(dat.b) @printf "RHS : [%+.2e, %+.2e]\n" bmin bmax lmin, lmax = extrema(dat.l .* dat.lflag) @printf "Lower bounds : [%+.2e, %+.2e]\n" lmin lmax lmin, lmax = extrema(dat.u .* dat.uflag) @printf "Upper bounds : [%+.2e, %+.2e]\n" lmin lmax @printf "\nLinear solver options\n" @printf " %-12s : %s\n" "Arithmetic" KKT.arithmetic(mpc.kkt) @printf " %-12s : %s\n" "Backend" KKT.backend(mpc.kkt) @printf " %-12s : %s\n" "System" KKT.linear_system(mpc.kkt) end # IPM LOG if params.OutputLevel > 0 @printf "\n%4s %14s %14s %8s %8s %8s %7s %4s\n" "Itn" "PObj" "DObj" "PFeas" "DFeas" "GFeas" "Mu" "Time" end # Set starting point @timeit mpc.timer "Initial point" compute_starting_point(mpc) # Main loop # Iteration 0 corresponds to the starting point. # Therefore, there is no numerical factorization before the first log is printed. # If the maximum number of iterations is set to 0, the only computation that occurs # is computing the residuals at the initial point. @timeit mpc.timer "Main loop" while(true) # I.A - Compute residuals at current iterate @timeit mpc.timer "Residuals" compute_residuals!(mpc) update_mu!(mpc.pt) # I.B - Log # TODO: Put this in a logging function ttot = time() - tstart if params.OutputLevel > 0 # Display log @printf "%4d" mpc.niter # Objectives ϵ = dat.objsense ? one(T) : -one(T) @printf " %+14.7e" ϵ * mpc.primal_objective @printf " %+14.7e" ϵ * mpc.dual_objective # Residuals @printf " %8.2e" max(mpc.res.rp_nrm, mpc.res.rl_nrm, mpc.res.ru_nrm) @printf " %8.2e" mpc.res.rd_nrm @printf " %8s" "--" # Mu @printf " %7.1e" mpc.pt.μ # Time @printf " %.2f" ttot print("\n") end # TODO: check convergence status # TODO: first call an `compute_convergence status`, # followed by a check on the solver status to determine whether to stop # In particular, user limits should be checked last (if an optimal solution is found, # we want to report optimal, not user limits) @timeit mpc.timer "update status" update_solver_status!(mpc, params.TolerancePFeas, params.ToleranceDFeas, params.ToleranceRGap, params.ToleranceIFeas ) if ( mpc.solver_status == Trm_Optimal || mpc.solver_status == Trm_PrimalInfeasible || mpc.solver_status == Trm_DualInfeasible ) break elseif mpc.niter >= params.IterationsLimit mpc.solver_status = Trm_IterationLimit break elseif ttot >= params.TimeLimit mpc.solver_status = Trm_TimeLimit break end # TODO: step # For now, include the factorization in the step function # Q: should we use more arguments here? try @timeit mpc.timer "Step" compute_step!(mpc, params) catch err if isa(err, PosDefException) || isa(err, SingularException) # Numerical trouble while computing the factorization mpc.solver_status = Trm_NumericalProblem elseif isa(err, OutOfMemoryError) # Out of memory mpc.solver_status = Trm_MemoryLimit elseif isa(err, InterruptException) mpc.solver_status = Trm_Unknown else # Unknown error: rethrow rethrow(err) end break end mpc.niter += 1 end # TODO: print message based on termination status params.OutputLevel > 0 && println("Solver exited with status $((mpc.solver_status))") return nothing end function compute_starting_point(mpc::MPC{T}) where{T} pt = mpc.pt dat = mpc.dat m, n, p = pt.m, pt.n, pt.p KKT.update!(mpc.kkt, zeros(T, n), ones(T, n), T(1e-6) .* ones(T, m)) # Get initial iterate KKT.solve!(zeros(T, n), pt.y, mpc.kkt, false .* mpc.dat.b, mpc.dat.c) # For y KKT.solve!(pt.x, zeros(T, m), mpc.kkt, mpc.dat.b, false .* mpc.dat.c) # For x # I. Recover positive primal-dual coordinates δx = one(T) + max( zero(T), (-3 // 2) * minimum((pt.x .- dat.l) .* dat.lflag), (-3 // 2) * minimum((dat.u .- pt.x) .* dat.uflag) ) @. pt.xl = ((pt.x - dat.l) + δx) * dat.lflag @. pt.xu = ((dat.u - pt.x) + δx) * dat.uflag z = dat.c - dat.A' * pt.y #= We set zl, zu such that `z = zl - zu` lⱼ | uⱼ | zˡⱼ | zᵘⱼ | ----+-----+--------+---------+ yes | yes | ¹/₂ zⱼ | ⁻¹/₂ zⱼ | yes | no | zⱼ | 0 | no | yes | 0 | -zⱼ | no | no | 0 | 0 | ----+-----+--------+---------+ =# @. pt.zl = ( z / (dat.lflag + dat.uflag)) * dat.lflag @. pt.zu = (-z / (dat.lflag + dat.uflag)) * dat.uflag δz = one(T) + max(zero(T), (-3 // 2) * minimum(pt.zl), (-3 // 2) * minimum(pt.zu)) pt.zl[dat.lflag] .+= δz pt.zu[dat.uflag] .+= δz mpc.pt.τ = one(T) mpc.pt.κ = zero(T) # II. Balance complementarity products μ = dot(pt.xl, pt.zl) + dot(pt.xu, pt.zu) dx = μ / ( 2 * (sum(pt.zl) + sum(pt.zu))) dz = μ / ( 2 * (sum(pt.xl) + sum(pt.xu))) pt.xl[dat.lflag] .+= dx pt.xu[dat.uflag] .+= dx pt.zl[dat.lflag] .+= dz pt.zu[dat.uflag] .+= dz # Update centrality parameter update_mu!(mpc.pt) return nothing end ================================================ FILE: src/IPM/MPC/step.jl ================================================ """ compute_step!(ipm, params) Compute next IP iterate for the MPC formulation. # Arguments - `ipm`: The MPC optimizer model - `params`: Optimization parameters """ function compute_step!(mpc::MPC{T, Tv}, params::IPMOptions{T}) where{T, Tv<:AbstractVector{T}} # Names dat = mpc.dat pt = mpc.pt res = mpc.res m, n, p = pt.m, pt.n, pt.p A = dat.A b = dat.b c = dat.c # Compute scaling θl = (pt.zl ./ pt.xl) .* dat.lflag θu = (pt.zu ./ pt.xu) .* dat.uflag θinv = θl .+ θu # Update regularizations mpc.regP ./= 10 mpc.regD ./= 10 clamp!(mpc.regP, sqrt(eps(T)), one(T)) clamp!(mpc.regD, sqrt(eps(T)), one(T)) # Update factorization nbump = 0 while nbump <= 3 try @timeit mpc.timer "Factorization" KKT.update!(mpc.kkt, θinv, mpc.regP, mpc.regD) break catch err isa(err, PosDefException) || isa(err, ZeroPivotException) || rethrow(err) # Increase regularization mpc.regD .*= 100 mpc.regP .*= 100 nbump += 1 @warn "Increase regularizations to $(mpc.regP[1])" end end # TODO: throw a custom error for numerical issues nbump < 3 || throw(PosDefException(0)) # factorization could not be saved # II. Compute search direction Δ = mpc.Δ Δc = mpc.Δc # Affine-scaling direction and associated step size @timeit mpc.timer "Predictor" compute_predictor!(mpc::MPC) mpc.αp, mpc.αd = max_step_length_pd(mpc.pt, mpc.Δ) # TODO: if step size is large enough, skip corrector # Corrector @timeit mpc.timer "Corrector" compute_corrector!(mpc::MPC) mpc.αp, mpc.αd = max_step_length_pd(mpc.pt, mpc.Δc) # TODO: the following is not needed if there are no additional corrections copyto!(Δ.x, Δc.x) copyto!(Δ.xl, Δc.xl) copyto!(Δ.xu, Δc.xu) copyto!(Δ.y, Δc.y) copyto!(Δ.zl, Δc.zl) copyto!(Δ.zu, Δc.zu) # Extra centrality corrections ncor = 0 ncor_max = params.CorrectionLimit # Zero out the Newton RHS. This only needs to be done once. # TODO: not needed if no additional corrections rmul!(mpc.ξp, zero(T)) rmul!(mpc.ξl, zero(T)) rmul!(mpc.ξu, zero(T)) rmul!(mpc.ξd, zero(T)) @timeit mpc.timer "Extra corr" while ncor < ncor_max compute_extra_correction!(mpc) # TODO: function to compute step size given Δ and Δc # This would avoid copying data around αp_c, αd_c = max_step_length_pd(mpc.pt, mpc.Δc) if αp_c >= 1.01 * mpc.αp && αd_c >= 1.01 * mpc.αd mpc.αp = αp_c mpc.αd = αd_c # Δ ⟵ Δc copyto!(Δ.x, Δc.x) copyto!(Δ.xl, Δc.xl) copyto!(Δ.xu, Δc.xu) copyto!(Δ.y, Δc.y) copyto!(Δ.zl, Δc.zl) copyto!(Δ.zu, Δc.zu) ncor += 1 else # Not enough improvement: abort break end end # Update current iterate mpc.αp *= params.StepDampFactor mpc.αd *= params.StepDampFactor pt.x .+= mpc.αp .* Δ.x pt.xl .+= mpc.αp .* Δ.xl pt.xu .+= mpc.αp .* Δ.xu pt.y .+= mpc.αd .* Δ.y pt.zl .+= mpc.αd .* Δ.zl pt.zu .+= mpc.αd .* Δ.zu update_mu!(pt) return nothing end """ solve_newton_system!(Δ, mpc, ξp, ξd, ξu, ξg, ξxs, ξwz, ξtk) Solve the Newton system ```math \\begin{bmatrix} A & & & R_{d} & & \\\\ I & -I & & & & \\\\ I & & I & & & \\\\ -R_{p} & & & A^{T} & I & -I \\\\ & Z_{l} & & & X_{l}\\\\ & & Z_{u} & & & X_{u}\\\\ \\end{bmatrix} \\begin{bmatrix} Δ x\\\\ Δ x_{l}\\\\ Δ x_{u}\\\\ Δ y\\\\ Δ z_{l} \\\\ Δ z_{u} \\end{bmatrix} = \\begin{bmatrix} ξ_p\\\\ ξ_l\\\\ ξ_u\\\\ ξ_d\\\\ ξ_{xz}^{l}\\\\ ξ_{xz}^{u} \\end{bmatrix} ``` # Arguments - `Δ`: Search direction, modified - `mpc`: The MPC optimizer - `hx, hy, hz, h0`: Terms obtained in the preliminary augmented system solve - `ξp, ξd, ξu, ξg, ξxs, ξwz, ξtk`: Right-hand side vectors """ function solve_newton_system!(Δ::Point{T, Tv}, mpc::MPC{T, Tv}, # Right-hand side ξp::Tv, ξl::Tv, ξu::Tv, ξd::Tv, ξxzl::Tv, ξxzu::Tv ) where{T, Tv<:AbstractVector{T}} pt = mpc.pt dat = mpc.dat # I. Solve augmented system @timeit mpc.timer "ξd_" begin ξd_ = copy(ξd) @. ξd_ += -((ξxzl + pt.zl .* ξl) ./ pt.xl) .* dat.lflag + ((ξxzu - pt.zu .* ξu) ./ pt.xu) .* dat.uflag end @timeit mpc.timer "KKT" KKT.solve!(Δ.x, Δ.y, mpc.kkt, ξp, ξd_) # II. Recover Δxl, Δxu @timeit mpc.timer "Δxl" begin @. Δ.xl = (-ξl + Δ.x) * dat.lflag end @timeit mpc.timer "Δxu" begin @. Δ.xu = ( ξu - Δ.x) * dat.uflag end # III. Recover Δzl, Δzu @timeit mpc.timer "Δzl" @. Δ.zl = ((ξxzl - pt.zl .* Δ.xl) ./ pt.xl) .* dat.lflag @timeit mpc.timer "Δzu" @. Δ.zu = ((ξxzu - pt.zu .* Δ.xu) ./ pt.xu) .* dat.uflag # IV. Set Δτ, Δκ to zero Δ.τ = zero(T) Δ.κ = zero(T) # Check Newton residuals # @printf "Newton residuals:\n" # @printf "|rp| = %16.8e\n" norm(dat.A * Δ.x - ξp, Inf) # @printf "|rl| = %16.8e\n" norm((Δ.x - Δ.xl) .* dat.lflag - ξl, Inf) # @printf "|ru| = %16.8e\n" norm((Δ.x + Δ.xu) .* dat.uflag - ξu, Inf) # @printf "|rd| = %16.8e\n" norm(dat.A'Δ.y + Δ.zl - Δ.zu - ξd, Inf) # @printf "|rxzl| = %16.8e\n" norm(pt.zl .* Δ.xl + pt.xl .* Δ.zl - ξxzl, Inf) # @printf "|rxzu| = %16.8e\n" norm(pt.zu .* Δ.xu + pt.xu .* Δ.zu - ξxzu, Inf) return nothing end """ max_step_length_pd(pt, δ) Compute maximum primal-dual step length. """ function max_step_length_pd(pt::Point{T, Tv}, δ::Point{T, Tv}) where{T, Tv<:AbstractVector{T}} axl = max_step_length(pt.xl, δ.xl) axu = max_step_length(pt.xu, δ.xu) azl = max_step_length(pt.zl, δ.zl) azu = max_step_length(pt.zu, δ.zu) αp = min(one(T), axl, axu) αd = min(one(T), azl, azu) return αp, αd end """ compute_predictor!(mpc::MPC) -> Nothing """ function compute_predictor!(mpc::MPC) # Newton RHS copyto!(mpc.ξp, mpc.res.rp) copyto!(mpc.ξl, mpc.res.rl) copyto!(mpc.ξu, mpc.res.ru) copyto!(mpc.ξd, mpc.res.rd) @. mpc.ξxzl = -(mpc.pt.xl .* mpc.pt.zl) .* mpc.dat.lflag @. mpc.ξxzu = -(mpc.pt.xu .* mpc.pt.zu) .* mpc.dat.uflag # Compute affine-scaling direction @timeit mpc.timer "Newton" solve_newton_system!(mpc.Δ, mpc, mpc.ξp, mpc.ξl, mpc.ξu, mpc.ξd, mpc.ξxzl, mpc.ξxzu ) # TODO: check Newton system residuals, perform iterative refinement if needed return nothing end """ compute_corrector!(mpc::MPC) -> Nothing """ function compute_corrector!(mpc::MPC{T, Tv}) where{T, Tv<:AbstractVector{T}} dat = mpc.dat pt = mpc.pt Δ = mpc.Δ Δc = mpc.Δc # Step length for affine-scaling direction αp_aff, αd_aff = mpc.αp, mpc.αd μₐ = ( dot((@. ((pt.xl + αp_aff * Δ.xl) * dat.lflag)), pt.zl .+ αd_aff .* Δ.zl) + dot((@. ((pt.xu + αp_aff * Δ.xu) * dat.uflag)), pt.zu .+ αd_aff .* Δ.zu) ) / pt.p σ = clamp((μₐ / pt.μ)^3, sqrt(eps(T)), one(T) - sqrt(eps(T))) # Newton RHS # compute_predictor! was called ⟹ ξp, ξl, ξu, ξd are already set @. mpc.ξxzl = (σ * pt.μ .- Δ.xl .* Δ.zl .- pt.xl .* pt.zl) .* dat.lflag @. mpc.ξxzu = (σ * pt.μ .- Δ.xu .* Δ.zu .- pt.xu .* pt.zu) .* dat.uflag # Compute corrector @timeit mpc.timer "Newton" solve_newton_system!(mpc.Δc, mpc, mpc.ξp, mpc.ξl, mpc.ξu, mpc.ξd, mpc.ξxzl, mpc.ξxzu ) # TODO: check Newton system residuals, perform iterative refinement if needed return nothing end """ compute_extra_correction!(mpc) -> Nothing """ function compute_extra_correction!(mpc::MPC{T, Tv}; δ::T = T(3 // 10), γ::T = T(1 // 10), ) where{T, Tv<:AbstractVector{T}} pt = mpc.pt Δ = mpc.Δ Δc = mpc.Δc dat = mpc.dat # Tentative step sizes and centrality parameter αp, αd = mpc.αp, mpc.αd αp_ = min(αp + δ, one(T)) αd_ = min(αd + δ, one(T)) g = dot(pt.xl, pt.zl) + dot(pt.xu, pt.zu) gₐ = dot((@. ((pt.xl + mpc.αp * Δ.xl) * dat.lflag)), pt.zl .+ mpc.αd .* Δ.zl) + dot((@. ((pt.xu + mpc.αp * Δ.xu) * dat.uflag)), pt.zu .+ mpc.αd .* Δ.zu) μ = (gₐ / g) * (gₐ / g) * (gₐ / pt.p) # Newton RHS # ξp, ξl, ξu, ξd are already at zero @timeit mpc.timer "target" begin compute_target!(mpc.ξxzl, pt.xl, Δ.xl, pt.zl, Δ.zl, αp_, αd_, γ, μ) compute_target!(mpc.ξxzu, pt.xu, Δ.xu, pt.zu, Δ.zu, αp_, αd_, γ, μ) end @timeit mpc.timer "Newton" solve_newton_system!(Δc, mpc, mpc.ξp, mpc.ξl, mpc.ξu, mpc.ξd, mpc.ξxzl, mpc.ξxzu ) # Δc ⟵ Δp + Δc axpy!(one(T), Δ.x, Δc.x) axpy!(one(T), Δ.xl, Δc.xl) axpy!(one(T), Δ.xu, Δc.xu) axpy!(one(T), Δ.y, Δc.y) axpy!(one(T), Δ.zl, Δc.zl) axpy!(one(T), Δ.zu, Δc.zu) # TODO: check Newton residuals return nothing end """ compute_target!(t, x, z, γ, μ) Compute centrality target. """ function compute_target!( t::Vector{T}, x::Vector{T}, δx::Vector{T}, z::Vector{T}, δz::Vector{T}, αp::T, αd::T, γ::T, μ::T ) where{T} n = length(t) tmin = μ * γ tmax = μ / γ @inbounds for j in 1:n v = (x[j] + αp * δx[j]) * (z[j] + αd * δz[j]) if v < tmin t[j] = tmin - v elseif v > tmax t[j] = tmax - v else t[j] = zero(T) end end return nothing end ================================================ FILE: src/IPM/ipmdata.jl ================================================ """ IPMData{T, Tv, Ta} Holds data about an interior point method. The problem is represented as ``` min c'x + c0 s.t. A x = b l ≤ x ≤ u ``` where `l`, `u` may take infinite values. """ struct IPMData{T, Tv, Tb, Ta} # Problem size nrow::Int ncol::Int # Objective objsense::Bool # min (true) or max (false) c0::T c::Tv # Constraint matrix A::Ta # RHS b::Tv # Variable bounds (may contain infinite values) l::Tv u::Tv # Variable bound flags (we template with `Tb` to ease GPU support) # These should be vectors of the same type as `l`, `u`, but `Bool` eltype. # They should not be passed as arguments, but computed at instantiation as # `lflag = isfinite.(l)` and `uflag = isfinite.(u)` lflag::Tb uflag::Tb function IPMData( A::Ta, b::Tv, objsense::Bool, c::Tv, c0::T, l::Tv, u::Tv ) where{T, Tv<:AbstractVector{T}, Ta<:AbstractMatrix{T}} nrow, ncol = size(A) lflag = isfinite.(l) uflag = isfinite.(u) Tb = typeof(lflag) return new{T, Tv, Tb, Ta}( nrow, ncol, objsense, c0, c, A, b, l, u, lflag, uflag ) end end # TODO: extract IPM data from presolved problem """ IPMData(pb::ProblemData, options::MatrixOptions) Extract problem data to standard form. """ function IPMData(pb::ProblemData{T}, mfact::Factory) where{T} # Problem size m, n = pb.ncon, pb.nvar # Extract right-hand side and slack variables nzA = 0 # Number of non-zeros in A b = zeros(T, m) # RHS sind = Int[] # Slack row index sval = T[] # Slack coefficient lslack = T[] # Slack lower bound uslack = T[] # Slack upper bound for (i, (lb, ub)) in enumerate(zip(pb.lcon, pb.ucon)) if lb == ub # Equality row b[i] = lb elseif -T(Inf) == lb && T(Inf) == ub # Free row push!(sind, i) push!(sval, one(T)) push!(lslack, -T(Inf)) push!(uslack, T(Inf)) b[i] = zero(T) elseif -T(Inf) == lb && isfinite(ub) # a'x <= b --> a'x + s = b push!(sind, i) push!(sval, one(T)) push!(lslack, zero(T)) push!(uslack, T(Inf)) b[i] = ub elseif isfinite(lb) && ub == Inf # a'x >= b --> a'x - s = b push!(sind, i) push!(sval, -one(T)) push!(lslack, zero(T)) push!(uslack, T(Inf)) b[i] = lb elseif isfinite(lb) && isfinite(ub) # lb <= a'x <= ub # Two options: # --> a'x + s = ub, 0 <= s <= ub - lb # --> a'x - s = lb, 0 <= s <= ub - lb push!(sind, i) push!(sval, one(T)) push!(lslack, zero(T)) push!(uslack, ub - lb) b[i] = ub else error("Invalid bounds for row $i: [$lb, $ub]") end # This line assumes that there are no dupplicate coefficients in Arows # Numerical zeros will also be counted as non-zeros nzA += length(pb.arows[i].nzind) end nslack = length(sind) # Objective c = [pb.obj; zeros(T, nslack)] c0 = pb.obj0 if !pb.objsense # Flip objective for maximization problem c .= .-c c0 = -c0 end # Instantiate A aI = Vector{Int}(undef, nzA + nslack) aJ = Vector{Int}(undef, nzA + nslack) aV = Vector{T}(undef, nzA + nslack) # populate non-zero coefficients by column nz_ = 0 for (j, col) in enumerate(pb.acols) for (i, aij) in zip(col.nzind, col.nzval) nz_ += 1 aI[nz_] = i aJ[nz_] = j aV[nz_] = aij end end # populate slack coefficients for (j, (i, a)) in enumerate(zip(sind, sval)) nz_ += 1 aI[nz_] = i aJ[nz_] = n + j aV[nz_] = a end # At this point, we should have nz_ == nzA + nslack # If not, this means the data between rows and columns in `pb` # do not match each other nz_ == (nzA + nslack) || error("Found $(nz_) non-zero coeffs (expected $(nzA + nslack))") A = construct_matrix(mfact.T, m, n + nslack, aI, aJ, aV, mfact.options...) # Variable bounds l = [pb.lvar; lslack] u = [pb.uvar; uslack] return IPMData(A, b, pb.objsense, c, c0, l, u) end ================================================ FILE: src/IPM/options.jl ================================================ Base.@kwdef mutable struct IPMOptions{T} OutputLevel::Int = 0 # User limits IterationsLimit::Int = 100 TimeLimit::Float64 = Inf # Numerical tolerances TolerancePFeas::T = sqrt(eps(T)) # primal feasibility ToleranceDFeas::T = sqrt(eps(T)) # dual feasibility ToleranceRGap::T = sqrt(eps(T)) # optimality ToleranceIFeas::T = sqrt(eps(T)) # infeasibility # Algorithmic parameters CorrectionLimit::Int = 3 # Maximum number of centrality corrections StepDampFactor::T = T(9_995 // 10_000) # Damp step size by this much GammaMin::T = T(1 // 10) CentralityOutlierThreshold::T = T(1 // 10) # Relative threshold for centrality outliers PRegMin::T = sqrt(eps(T)) # primal DRegMin::T = sqrt(eps(T)) # dual Factory::Factory{<:AbstractIPMOptimizer} = Factory(HSD) end ================================================ FILE: src/IPM/point.jl ================================================ """ Point{T, Tv} Primal-dual point. """ mutable struct Point{T, Tv} # Dimensions m::Int # Number of constraints n::Int # Number of variables p::Int # Total number of finite variable bounds (lower and upper) hflag::Bool # Is homogeneous embedding used? # Primal variables x::Tv # Original variables xl::Tv # Lower-bound slack: `x - xl == l` (zero if `l == -∞`) xu::Tv # Upper-bound slack: `x + xu == u` (zero if `u == +∞`) # Dual variables y::Tv # Dual variables zl::Tv # Lower-bound dual, zero if `l == -∞` zu::Tv # Upper-bound dual, zero if `u == +∞` # HSD variables, only used with homogeneous form # Otherwise, one must ensure that (τ, κ) = (1, 0) τ::T κ::T # Centrality parameter μ::T # Constructor Point{T, Tv}(m, n, p; hflag::Bool) where{T, Tv<:AbstractVector{T}} = new{T, Tv}( m, n, p, hflag, # Primal variables tzeros(Tv, n), tzeros(Tv, n), tzeros(Tv, n), # Dual variables tzeros(Tv, m), tzeros(Tv, n), tzeros(Tv, n), # Homogeneous variables one(T), one(T), # Centrality parameter one(T) ) end function update_mu!(pt::Point) pt.μ = (dot(pt.xl, pt.zl) + dot(pt.xu, pt.zu) + pt.hflag * (pt.τ * pt.κ)) / (pt.p + pt.hflag) return nothing end ================================================ FILE: src/IPM/residuals.jl ================================================ """ Residuals{T, Tv} Data structure for IPM residual vectors. """ mutable struct Residuals{T, Tv} # Primal residuals rp::Tv # rp = τ*b - A*x rl::Tv # rl = τ*l - (x - xl) ru::Tv # ru = τ*u - (x + xu) # Dual residuals rd::Tv # rd = τ*c - (A'y + zl - zu) rg::T # rg = c'x - (b'y + l'zl - u'zu) + κ # Residuals' norms rp_nrm::T # |rp| rl_nrm::T # |rl| ru_nrm::T # |ru| rd_nrm::T # |rd| rg_nrm::T # |rg| end ================================================ FILE: src/Interfaces/MOI/MOI_wrapper.jl ================================================ import MathOptInterface as MOI # ============================================================================== # HELPER FUNCTIONS # ============================================================================== """ MOITerminationStatus(st::TerminationStatus) Convert a Tulip `TerminationStatus` into a `MOI.TerminationStatusCode`. """ function MOITerminationStatus(st::TerminationStatus)::MOI.TerminationStatusCode if st == Trm_NotCalled return MOI.OPTIMIZE_NOT_CALLED elseif st == Trm_Optimal return MOI.OPTIMAL elseif st == Trm_PrimalInfeasible return MOI.INFEASIBLE elseif st == Trm_DualInfeasible return MOI.DUAL_INFEASIBLE elseif st == Trm_IterationLimit return MOI.ITERATION_LIMIT elseif st == Trm_TimeLimit return MOI.TIME_LIMIT elseif st == Trm_MemoryLimit return MOI.MEMORY_LIMIT else return MOI.OTHER_ERROR end end """ MOISolutionStatus(st::SolutionStatus) Convert a Tulip `SolutionStatus` into a `MOI.ResultStatusCode`. """ function MOISolutionStatus(st::SolutionStatus)::MOI.ResultStatusCode if st == Sln_Unknown return MOI.UNKNOWN_RESULT_STATUS elseif st == Sln_Optimal || st == Sln_FeasiblePoint return MOI.FEASIBLE_POINT elseif st == Sln_InfeasiblePoint return MOI.INFEASIBLE_POINT elseif st == Sln_InfeasibilityCertificate return MOI.INFEASIBILITY_CERTIFICATE else return MOI.OTHER_RESULT_STATUS end end """ _bounds(s) """ _bounds(s::MOI.EqualTo{T}) where{T} = s.value, s.value _bounds(s::MOI.LessThan{T}) where{T} = T(-Inf), s.upper _bounds(s::MOI.GreaterThan{T}) where{T} = s.lower, T(Inf) _bounds(s::MOI.Interval{T}) where{T} = s.lower, s.upper const SCALAR_SETS{T} = Union{ MOI.LessThan{T}, MOI.GreaterThan{T}, MOI.EqualTo{T}, MOI.Interval{T} } where{T} @enum(ObjType, _SINGLE_VARIABLE, _SCALAR_AFFINE) # ============================================================================== # ============================================================================== # # S U P P O R T E D M O I F E A T U R E S # # ============================================================================== # ============================================================================== """ Optimizer{T} Wrapper for MOI. """ mutable struct Optimizer{T} <: MOI.AbstractOptimizer inner::Model{T} objective_sense::Union{Nothing,MOI.OptimizationSense} _obj_type::Union{Nothing,ObjType} # Map MOI Variable/Constraint indices to internal indices var_counter::Int # Should never be reset con_counter::Int # Should never be reset var_indices_moi::Vector{MOI.VariableIndex} var_indices::Dict{MOI.VariableIndex, Int} con_indices_moi::Vector{MOI.ConstraintIndex} con_indices::Dict{MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, <:SCALAR_SETS{T}}, Int} # Variable and constraint names name2var::Dict{String, Set{MOI.VariableIndex}} name2con::Dict{String, Set{MOI.ConstraintIndex}} # Keep track of bound constraints var2bndtype::Dict{MOI.VariableIndex, Set{Type{<:MOI.AbstractScalarSet}}} # Tulip.Model does not record solution time... solve_time::Float64 function Optimizer{T}(;kwargs...) where{T} m = new{T}( Model{T}(), nothing, # objective_sense nothing, # _obj_type # Variable and constraint counters 0, 0, # Index mapping MOI.VariableIndex[], Dict{MOI.VariableIndex, Int}(), MOI.ConstraintIndex[], Dict{MOI.ConstraintIndex{MOI.ScalarAffineFunction, <:SCALAR_SETS{T}}, Int}(), # Name -> index mapping Dict{String, Set{MOI.VariableIndex}}(), Dict{String, Set{MOI.ConstraintIndex}}(), Dict{MOI.VariableIndex, Set{Type{<:MOI.AbstractScalarSet}}}(), 0.0 ) for (k, v) in kwargs set_parameter(m.inner, string(k), v) end return m end end Optimizer(;kwargs...) = Optimizer{Float64}(;kwargs...) function MOI.empty!(m::Optimizer) # Inner model empty!(m.inner) m.objective_sense = nothing m._obj_type = nothing # Reset index mappings m.var_indices_moi = MOI.VariableIndex[] m.con_indices_moi = MOI.ConstraintIndex[] m.var_indices = Dict{MOI.VariableIndex, Int}() m.con_indices = Dict{MOI.ConstraintIndex, Int}() # Reset name mappings m.name2var = Dict{String, Set{MOI.VariableIndex}}() m.name2con = Dict{String, Set{MOI.ConstraintIndex}}() # Reset bound tracking m.var2bndtype = Dict{MOI.VariableIndex, Set{MOI.ConstraintIndex}}() m.solve_time = 0.0 return nothing end function MOI.is_empty(m::Optimizer) m.objective_sense === nothing || return false m._obj_type === nothing || return false m.inner.pbdata.nvar == 0 || return false m.inner.pbdata.ncon == 0 || return false length(m.var_indices) == 0 || return false length(m.var_indices_moi) == 0 || return false length(m.con_indices) == 0 || return false length(m.con_indices_moi) == 0 || return false length(m.name2var) == 0 || return false length(m.name2con) == 0 || return false length(m.var2bndtype) == 0 || return false return true end function MOI.optimize!(m::Optimizer) t_solve = @elapsed optimize!(m.inner) m.solve_time = t_solve return nothing end MOI.supports_incremental_interface(::Optimizer) = true function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) return MOI.Utilities.default_copy_to(dest, src) end # ============================================================================== # I. Optimizer attributes # ============================================================================== # ============================================================================== # II. Model attributes # ============================================================================== include("./attributes.jl") # ============================================================================== # III. Variables # ============================================================================== include("./variables.jl") # ============================================================================== # IV. Constraints # ============================================================================== include("./constraints.jl") # ============================================================================== # V. Objective # ============================================================================== include("./objective.jl") ================================================ FILE: src/Interfaces/MOI/attributes.jl ================================================ # ============================================= # Supported attributes # ============================================= const SUPPORTED_OPTIMIZER_ATTR = Union{ MOI.NumberOfThreads, MOI.RawOptimizerAttribute, MOI.SolverName, MOI.SolverVersion, MOI.SolveTimeSec, MOI.Silent, MOI.TimeLimitSec, } MOI.supports(::Optimizer, ::A) where{A<:SUPPORTED_OPTIMIZER_ATTR} = true # ============================================= # 1. Optimizer attributes # ============================================= # # NumberOfThreads # MOI.get(m::Optimizer, ::MOI.NumberOfThreads) = m.inner.params.Threads function MOI.set(m::Optimizer, ::MOI.NumberOfThreads, n::Int) # TODO: use lower-level API m.inner.params.Threads = n return nothing end # # SolverName # MOI.get(::Optimizer, ::MOI.SolverName) = "Tulip" # # SolverVersion # MOI.get(::Optimizer, ::MOI.SolverVersion) = string(Tulip.version()) # # SolveTimeSec # MOI.get(m::Optimizer, ::MOI.SolveTimeSec) = m.solve_time # # Silent # MOI.get(m::Optimizer, ::MOI.Silent) = m.inner.params.OutputLevel <= 0 function MOI.set(m::Optimizer, ::MOI.Silent, flag::Bool) m.inner.params.OutputLevel = 1 - flag # TODO: make a decision about LogLevel return nothing end # # TimeLimitSec # function MOI.get(m::Optimizer, ::MOI.TimeLimitSec) value = m.inner.params.IPM.TimeLimit return value == Inf ? nothing : value end function MOI.set(m::Optimizer, ::MOI.TimeLimitSec, t::Union{Real,Nothing}) m.inner.params.IPM.TimeLimit = convert(Float64, something(t, Inf)) return nothing end # # RawParameter # MOI.get(m::Optimizer, attr::MOI.RawOptimizerAttribute) = get_parameter(m.inner, attr.name) MOI.set(m::Optimizer, attr::MOI.RawOptimizerAttribute, val) = set_parameter(m.inner, attr.name, val) # ============================================= # 2. Model attributes # ============================================= const SUPPORTED_MODEL_ATTR = Union{ MOI.Name, MOI.ObjectiveSense, MOI.NumberOfVariables, MOI.ListOfVariableIndices, MOI.ListOfConstraintIndices, MOI.NumberOfConstraints, # ListOfConstraints, # TODO MOI.ObjectiveFunctionType, MOI.ObjectiveValue, MOI.DualObjectiveValue, MOI.RelativeGap, # MOI.SolveTime, # TODO MOI.SimplexIterations, MOI.BarrierIterations, MOI.RawSolver, MOI.RawStatusString, MOI.ResultCount, MOI.TerminationStatus, MOI.PrimalStatus, MOI.DualStatus } MOI.supports(::Optimizer, ::SUPPORTED_MODEL_ATTR) = true # # ListOfModelAttributesSet # function MOI.get(m::Optimizer{T}, ::MOI.ListOfModelAttributesSet) where {T} ret = MOI.AbstractModelAttribute[] if !isempty(m.inner.pbdata.name) push!(ret, MOI.Name()) end if m.objective_sense !== nothing push!(ret, MOI.ObjectiveSense()) end if m._obj_type == _SINGLE_VARIABLE push!(ret, MOI.ObjectiveFunction{MOI.VariableIndex}()) elseif m._obj_type == _SCALAR_AFFINE push!(ret, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}()) end return ret end # # ListOfVariableIndices # function MOI.get(m::Optimizer, ::MOI.ListOfVariableIndices) return copy(m.var_indices_moi) end # # Name # MOI.get(m::Optimizer, ::MOI.Name) = m.inner.pbdata.name MOI.set(m::Optimizer, ::MOI.Name, name) = (m.inner.pbdata.name = name) # # NumberOfVariables # MOI.get(m::Optimizer, ::MOI.NumberOfVariables) = m.inner.pbdata.nvar # # ObjectiveFunctionType # function MOI.get(m::Optimizer{T}, ::MOI.ObjectiveFunctionType) where{T} if m._obj_type == _SINGLE_VARIABLE return MOI.VariableIndex end return MOI.ScalarAffineFunction{T} end # # ObjectiveSense # function MOI.get(m::Optimizer, ::MOI.ObjectiveSense) return something(m.objective_sense, MOI.FEASIBILITY_SENSE) end function MOI.set(m::Optimizer, ::MOI.ObjectiveSense, s::MOI.OptimizationSense) m.objective_sense = s if s == MOI.MIN_SENSE || s == MOI.FEASIBILITY_SENSE m.inner.pbdata.objsense = true else @assert s == MOI.MAX_SENSE m.inner.pbdata.objsense = false end return nothing end # # ObjectiveValue # function MOI.get(m::Optimizer{T}, attr::MOI.ObjectiveValue) where{T} MOI.check_result_index_bounds(m, attr) raw_z = get_attribute(m.inner, ObjectiveValue()) is_feas = MOI.get(m, MOI.ObjectiveSense()) == MOI.FEASIBILITY_SENSE return raw_z * !is_feas end # # DualObjectiveValue # function MOI.get(m::Optimizer{T}, attr::MOI.DualObjectiveValue) where{T} MOI.check_result_index_bounds(m, attr) return get_attribute(m.inner, DualObjectiveValue()) end MOI.get(m::Optimizer, ::MOI.ObjectiveBound) = MOI.get(m, MOI.DualObjectiveValue()) # # RawSolver # MOI.get(m::Optimizer, ::MOI.RawSolver) = m.inner # # RelativeGap # function MOI.get(m::Optimizer{T}, ::MOI.RelativeGap) where{T} # TODO: dispatch a function call on m.inner zp = m.inner.solver.primal_objective zd = m.inner.solver.dual_objective return (abs(zp - zd) / (T(1 // 10^6)) + abs(zd)) end # # RawStatusString # function MOI.get(m::Optimizer, ::MOI.RawStatusString) return string(m.inner.status) end # # ResultCount # function MOI.get(m::Optimizer, ::MOI.ResultCount) st = MOI.get(m, MOI.TerminationStatus()) if (st == MOI.OPTIMIZE_NOT_CALLED || st == MOI.OTHER_ERROR || st == MOI.MEMORY_LIMIT ) return 0 end return 1 end # # SimplexIterations # MOI.get(::Optimizer, ::MOI.SimplexIterations) = 0 # # BarrierIterations # # TODO: use inner query MOI.get(m::Optimizer, ::MOI.BarrierIterations) = get_attribute(m.inner, BarrierIterations()) # # TerminationStatus # # TODO: use inner query function MOI.get(m::Optimizer, ::MOI.TerminationStatus) return MOITerminationStatus(get_attribute(m.inner, Status())) end # # PrimalStatus # # TODO: use inner query function MOI.get(m::Optimizer, attr::MOI.PrimalStatus) attr.result_index == 1 || return MOI.NO_SOLUTION if isnothing(m.inner.solution) return MOI.NO_SOLUTION else MOISolutionStatus(m.inner.solution.primal_status) end end # # DualStatus # # TODO: use inner query function MOI.get(m::Optimizer, attr::MOI.DualStatus) attr.result_index == 1 || return MOI.NO_SOLUTION if isnothing(m.inner.solution) return MOI.NO_SOLUTION else MOISolutionStatus(m.inner.solution.dual_status) end end ================================================ FILE: src/Interfaces/MOI/constraints.jl ================================================ # ============================================= # 1. Supported constraints and attributes # ============================================= """ SUPPORTED_CONSTR_ATTR List of supported MOI `ConstraintAttribute`. """ const SUPPORTED_CONSTR_ATTR = Union{ MOI.ConstraintName, MOI.ConstraintPrimal, MOI.ConstraintDual, # MOI.ConstraintPrimalStart, # MOI.ConstraintDualStart, # once dual warm-start is supported # MOI.ConstraintBasisStatus, # once cross-over is supported MOI.ConstraintFunction, MOI.ConstraintSet } MOI.supports(::Optimizer, ::A, ::Type{<:MOI.ConstraintIndex}) where{A<:SUPPORTED_CONSTR_ATTR} = true function MOI.get( m::Optimizer, ::MOI.ListOfConstraintAttributesSet{F,S}, ) where {F,S} ret = MOI.AbstractConstraintAttribute[] for set in values(m.name2con) if any(ci -> ci isa MOI.ConstraintIndex{F,S}, set) push!(ret, MOI.ConstraintName()) break end end return ret end _type_tuple(::MOI.ConstraintIndex{F,S}) where {F,S} = (F, S) function MOI.get(m::Optimizer, ::MOI.ListOfConstraintTypesPresent) ret = Tuple{Type,Type}[] append!(ret, unique!(_type_tuple.(m.con_indices_moi))) for set in values(m.var2bndtype) for S in set push!(ret, (MOI.VariableIndex, S)) end end unique!(ret) return ret end # MOI boilerplate function MOI.supports(::Optimizer, ::MOI.ConstraintName, ::Type{<:MOI.ConstraintIndex{<:MOI.VariableIndex}}) throw(MOI.VariableIndexConstraintNameError()) end # Variable bounds function MOI.supports_constraint( ::Optimizer{T}, ::Type{MOI.VariableIndex}, ::Type{S} ) where {T, S<:SCALAR_SETS{T}} return true end # Linear constraints function MOI.supports_constraint( ::Optimizer{T}, ::Type{MOI.ScalarAffineFunction{T}}, ::Type{S} ) where {T, S<:SCALAR_SETS{T}} return true end function MOI.is_valid( m::Optimizer{T}, c::MOI.ConstraintIndex{MOI.VariableIndex, S} ) where{T, S <:SCALAR_SETS{T}} v = MOI.VariableIndex(c.value) MOI.is_valid(m, v) || return false res = S ∈ m.var2bndtype[v] return res end function MOI.is_valid( m::Optimizer{T}, c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, S} ) where{T, S<:SCALAR_SETS{T}} return haskey(m.con_indices, c) end # ============================================= # 2. Add constraints # ============================================= # TODO: make it clear that only finite bounds can be given in input. # To relax variable bounds, one should delete the associated bound constraint. function MOI.add_constraint( m::Optimizer{T}, v::MOI.VariableIndex, s::MOI.LessThan{T} ) where {T} # Check that variable exists MOI.throw_if_not_valid(m, v) # Check if upper bound already exists if MOI.LessThan{T} ∈ m.var2bndtype[v] throw(MOI.UpperBoundAlreadySet{MOI.LessThan{T}, MOI.LessThan{T}}(v)) elseif MOI.EqualTo{T} ∈ m.var2bndtype[v] throw(MOI.UpperBoundAlreadySet{MOI.EqualTo{T}, MOI.LessThan{T}}(v)) elseif MOI.Interval{T} ∈ m.var2bndtype[v] throw(MOI.UpperBoundAlreadySet{MOI.Interval{T}, MOI.LessThan{T}}(v)) end # Update inner model j = m.var_indices[v] # inner index set_attribute(m.inner, VariableUpperBound(), j, s.upper) # Update bound tracking push!(m.var2bndtype[v], MOI.LessThan{T}) return MOI.ConstraintIndex{MOI.VariableIndex, MOI.LessThan{T}}(v.value) end function MOI.add_constraint( m::Optimizer{T}, v::MOI.VariableIndex, s::MOI.GreaterThan{T} ) where{T} # Check that variable exists MOI.throw_if_not_valid(m, v) # Check if lower bound already exists if MOI.GreaterThan{T} ∈ m.var2bndtype[v] throw(MOI.LowerBoundAlreadySet{MOI.GreaterThan{T}, MOI.GreaterThan{T}}(v)) elseif MOI.EqualTo{T} ∈ m.var2bndtype[v] throw(MOI.LowerBoundAlreadySet{MOI.EqualTo{T}, MOI.GreaterThan{T}}(v)) elseif MOI.Interval{T} ∈ m.var2bndtype[v] throw(MOI.LowerBoundAlreadySet{MOI.Interval{T}, MOI.GreaterThan{T}}(v)) end # Update inner model j = m.var_indices[v] # inner index set_attribute(m.inner, VariableLowerBound(), j, s.lower) # Update upper-bound push!(m.var2bndtype[v], MOI.GreaterThan{T}) return MOI.ConstraintIndex{MOI.VariableIndex, MOI.GreaterThan{T}}(v.value) end function MOI.add_constraint( m::Optimizer{T}, v::MOI.VariableIndex, s::MOI.EqualTo{T} ) where{T} # Check that variable exists MOI.throw_if_not_valid(m, v) # Check if a bound already exists if MOI.LessThan{T} ∈ m.var2bndtype[v] throw(MOI.UpperBoundAlreadySet{MOI.LessThan{T}, MOI.EqualTo{T}}(v)) elseif MOI.GreaterThan{T} ∈ m.var2bndtype[v] throw(MOI.LowerBoundAlreadySet{MOI.GreaterThan{T}, MOI.EqualTo{T}}(v)) elseif MOI.EqualTo{T} ∈ m.var2bndtype[v] throw(MOI.UpperBoundAlreadySet{MOI.EqualTo{T}, MOI.EqualTo{T}}(v)) elseif MOI.Interval{T} ∈ m.var2bndtype[v] throw(MOI.UpperBoundAlreadySet{MOI.Interval{T}, MOI.EqualTo{T}}(v)) end # Update inner model j = m.var_indices[v] # inner index set_attribute(m.inner, VariableLowerBound(), j, s.value) set_attribute(m.inner, VariableUpperBound(), j, s.value) # Update bound tracking push!(m.var2bndtype[v], MOI.EqualTo{T}) return MOI.ConstraintIndex{MOI.VariableIndex, MOI.EqualTo{T}}(v.value) end function MOI.add_constraint( m::Optimizer{T}, v::MOI.VariableIndex, s::MOI.Interval{T} ) where{T} # Check that variable exists MOI.throw_if_not_valid(m, v) # Check if a bound already exists if MOI.LessThan{T} ∈ m.var2bndtype[v] throw(MOI.UpperBoundAlreadySet{MOI.LessThan{T}, MOI.Interval{T}}(v)) elseif MOI.GreaterThan{T} ∈ m.var2bndtype[v] throw(MOI.LowerBoundAlreadySet{MOI.GreaterThan{T}, MOI.Interval{T}}(v)) elseif MOI.EqualTo{T} ∈ m.var2bndtype[v] throw(MOI.UpperBoundAlreadySet{MOI.EqualTo{T}, MOI.Interval{T}}(v)) elseif MOI.Interval{T} ∈ m.var2bndtype[v] throw(MOI.UpperBoundAlreadySet{MOI.Interval{T}, MOI.Interval{T}}(v)) end # Update variable bounds j = m.var_indices[v] # inner index set_attribute(m.inner, VariableLowerBound(), j, s.lower) set_attribute(m.inner, VariableUpperBound(), j, s.upper) # Update bound tracking push!(m.var2bndtype[v], MOI.Interval{T}) return MOI.ConstraintIndex{MOI.VariableIndex, MOI.Interval{T}}(v.value) end # General linear constraints function MOI.add_constraint( m::Optimizer{T}, f::MOI.ScalarAffineFunction{T}, s::SCALAR_SETS{T} ) where{T} # Check that constant term is zero if !iszero(f.constant) throw(MOI.ScalarFunctionConstantNotZero{T, typeof(f), typeof(s)}(f.constant)) end # Convert to canonical form fc = MOI.Utilities.canonical(f) # Extract row nz = length(fc.terms) rind = Vector{Int}(undef, nz) rval = Vector{T}(undef, nz) lb, ub = _bounds(s) for (k, t) in enumerate(fc.terms) rind[k] = m.var_indices[t.variable] rval[k] = t.coefficient end # Update inner model i = add_constraint!(m.inner.pbdata, rind, rval, lb, ub) # Create MOI index m.con_counter += 1 cidx = MOI.ConstraintIndex{typeof(f), typeof(s)}(m.con_counter) # Update constraint tracking m.con_indices[cidx] = i push!(m.con_indices_moi, cidx) return cidx end # ============================================= # 3. Delete constraints # ============================================= function MOI.delete( m::Optimizer{T}, c::MOI.ConstraintIndex{MOI.VariableIndex, S} ) where{T, S<:SCALAR_SETS{T}} # Sanity check MOI.throw_if_not_valid(m, c) v = MOI.VariableIndex(c.value) # Update inner model j = m.var_indices[v] if S == MOI.LessThan{T} # Remove upper-bound set_attribute(m.inner, VariableUpperBound(), j, T(Inf)) elseif S == MOI.GreaterThan{T} # Remove lower bound set_attribute(m.inner, VariableLowerBound(), j, T(-Inf)) else # Set variable to free set_attribute(m.inner, VariableLowerBound(), j, T(-Inf)) set_attribute(m.inner, VariableUpperBound(), j, T(Inf)) end # Delete tracking of bounds delete!(m.var2bndtype[v], S) return nothing end function MOI.delete( m::Optimizer{T}, c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, S} ) where{T, S<:SCALAR_SETS{T}} MOI.throw_if_not_valid(m, c) # Update inner model i = m.con_indices[c] old_name = get_attribute(m.inner, ConstraintName(), i) delete_constraint!(m.inner.pbdata, i) # Update index tracking for c_ in m.con_indices_moi[i+1:end] m.con_indices[c_] -= 1 end deleteat!(m.con_indices_moi, i) delete!(m.con_indices, c) # Update name tracking if old_name != "" && haskey(m.name2con, old_name) s = m.name2con[old_name] delete!(s, c) length(s) == 0 && delete!(m.name2con, old_name) end return nothing end # ============================================= # 4. Modify constraints # ============================================= function MOI.modify( m::Optimizer{T}, c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, S}, chg::MOI.ScalarCoefficientChange{T} ) where{T, S<:SCALAR_SETS{T}} MOI.is_valid(m, c) || throw(MOI.InvalidIndex(c)) MOI.is_valid(m, chg.variable) || throw(MOI.InvalidIndex(chg.variable)) # Update inner problem i = m.con_indices[c] j = m.var_indices[chg.variable] v = chg.new_coefficient set_coefficient!(m.inner.pbdata, i, j, v) return nothing end # ============================================= # 5. Get/set constraint attributes # ============================================= # # ListOfConstraintIndices # function MOI.get( m::Optimizer{T}, ::MOI.ListOfConstraintIndices{MOI.VariableIndex, S} ) where{T, S<:SCALAR_SETS{T}} indices = MOI.ConstraintIndex{MOI.VariableIndex, S}[] for (var, bounds_set) in m.var2bndtype S ∈ bounds_set && push!(indices, MOI.ConstraintIndex{MOI.VariableIndex, S}(var.value)) end return sort!(indices, by = v -> v.value) end function MOI.get( m::Optimizer{T}, ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T}, S} ) where{T, S<:SCALAR_SETS{T}} indices = [ cidx for cidx in keys(m.con_indices) if isa(cidx, MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, S} ) ] return sort!(indices, by = v -> v.value) end # # NumberOfConstraints # function MOI.get( m::Optimizer{T}, ::MOI.NumberOfConstraints{MOI.VariableIndex, S} ) where{T, S<:SCALAR_SETS{T}} ncon = 0 for (v, bound_sets) in m.var2bndtype ncon += S ∈ bound_sets end return ncon end function MOI.get( m::Optimizer{T}, ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T}, S} ) where{T, S<:SCALAR_SETS{T}} ncon = 0 for cidx in keys(m.con_indices) ncon += isa(cidx, MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, S}) end return ncon end # # ConstraintName # function MOI.get( m::Optimizer{T}, ::MOI.ConstraintName, c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, S} ) where {T, S<:SCALAR_SETS{T}} MOI.throw_if_not_valid(m, c) # Get name from inner model i = m.con_indices[c] return get_attribute(m.inner, ConstraintName(), i) end function MOI.set(::Optimizer, ::MOI.ConstraintName, ::MOI.ConstraintIndex{<:MOI.VariableIndex}, ::String) throw(MOI.VariableIndexConstraintNameError()) end function MOI.set( m::Optimizer{T}, ::MOI.ConstraintName, c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, S}, name::String ) where{T, S<:SCALAR_SETS{T}} MOI.throw_if_not_valid(m, c) s = get!(m.name2con, name, Set{MOI.ConstraintIndex}()) # Update inner model i = m.con_indices[c] old_name = get_attribute(m.inner, ConstraintName(), i) set_attribute(m.inner, ConstraintName(), i, name) # Update constraint name tracking push!(s, c) # Delete old name s_old = get(m.name2con, old_name, Set{MOI.ConstraintIndex}()) if length(s_old) == 0 # Constraint previously didn't have name --> ignore elseif length(s_old) == 1 delete!(m.name2con, old_name) else delete!(s_old, c) end return nothing end function MOI.get(m::Optimizer, CIType::Type{<:MOI.ConstraintIndex}, name::String) s = get(m.name2con, name, Set{MOI.ConstraintIndex}()) if length(s) == 0 return nothing elseif length(s) == 1 c = first(s) return isa(c, CIType) ? c : nothing else error("Duplicate constraint name detected: $(name)") end return nothing end # # ConstraintFunction # function MOI.get( m::Optimizer{T}, ::MOI.ConstraintFunction, c::MOI.ConstraintIndex{MOI.VariableIndex, S} ) where {T, S<:SCALAR_SETS{T}} MOI.throw_if_not_valid(m, c) # Sanity check return MOI.VariableIndex(c.value) end function MOI.set( ::Optimizer{T}, ::MOI.ConstraintFunction, c::MOI.ConstraintIndex{MOI.VariableIndex, S}, ::MOI.VariableIndex, ) where {T, S<:SCALAR_SETS{T}} return throw(MOI.SettingVariableIndexNotAllowed()) end function MOI.get( m::Optimizer{T}, ::MOI.ConstraintFunction, c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, S} ) where {T, S<:SCALAR_SETS{T}} MOI.throw_if_not_valid(m, c) # Sanity check # Get row from inner model i = m.con_indices[c] row = m.inner.pbdata.arows[i] nz = length(row.nzind) # Map inner indices to MOI indices terms = Vector{MOI.ScalarAffineTerm{T}}(undef, nz) for (k, (j, v)) in enumerate(zip(row.nzind, row.nzval)) terms[k] = MOI.ScalarAffineTerm{T}(v, m.var_indices_moi[j]) end return MOI.ScalarAffineFunction(terms, zero(T)) end # TODO function MOI.set( m::Optimizer{T}, ::MOI.ConstraintFunction, c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, S}, f::MOI.ScalarAffineFunction{T} ) where{T, S<:SCALAR_SETS{T}} MOI.throw_if_not_valid(m, c) iszero(f.constant) || throw(MOI.ScalarFunctionConstantNotZero{T, typeof(f), S}(f.constant)) fc = MOI.Utilities.canonical(f) # Update inner model # TODO: use inner query i = m.con_indices[c] # Set old row to zero f_old = MOI.get(m, MOI.ConstraintFunction(), c) for term in f_old.terms j = m.var_indices[term.variable] set_coefficient!(m.inner.pbdata, i, j, zero(T)) end # Set new row coefficients for term in fc.terms j = m.var_indices[term.variable] set_coefficient!(m.inner.pbdata, i, j, term.coefficient) end # Done return nothing end # # ConstraintSet # function MOI.get( m::Optimizer{T}, ::MOI.ConstraintSet, c::MOI.ConstraintIndex{MOI.VariableIndex, MOI.LessThan{T}} ) where{T} # Sanity check MOI.throw_if_not_valid(m, c) v = MOI.VariableIndex(c.value) # Get inner bounds j = m.var_indices[v] ub = m.inner.pbdata.uvar[j] return MOI.LessThan(ub) end function MOI.get( m::Optimizer{T}, ::MOI.ConstraintSet, c::MOI.ConstraintIndex{MOI.VariableIndex, MOI.GreaterThan{T}} ) where{T} # Sanity check MOI.throw_if_not_valid(m, c) v = MOI.VariableIndex(c.value) # Get inner bounds j = m.var_indices[v] lb = m.inner.pbdata.lvar[j] return MOI.GreaterThan(lb) end function MOI.get( m::Optimizer{T}, ::MOI.ConstraintSet, c::MOI.ConstraintIndex{MOI.VariableIndex, MOI.EqualTo{T}} ) where{T} # Sanity check MOI.throw_if_not_valid(m, c) v = MOI.VariableIndex(c.value) # Get inner bounds j = m.var_indices[v] ub = m.inner.pbdata.uvar[j] return MOI.EqualTo(ub) end function MOI.get( m::Optimizer{T}, ::MOI.ConstraintSet, c::MOI.ConstraintIndex{MOI.VariableIndex, MOI.Interval{T}} ) where{T} # Sanity check MOI.throw_if_not_valid(m, c) v = MOI.VariableIndex(c.value) # Get inner bounds j = m.var_indices[v] lb = m.inner.pbdata.lvar[j] ub = m.inner.pbdata.uvar[j] return MOI.Interval(lb, ub) end function MOI.get( m::Optimizer{T}, ::MOI.ConstraintSet, c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, S} ) where{T, S<:SCALAR_SETS{T}} MOI.throw_if_not_valid(m, c) # Sanity check # Get inner bounds i = m.con_indices[c] lb = m.inner.pbdata.lcon[i] ub = m.inner.pbdata.ucon[i] if S == MOI.LessThan{T} return MOI.LessThan(ub) elseif S == MOI.GreaterThan{T} return MOI.GreaterThan(lb) elseif S == MOI.EqualTo{T} return MOI.EqualTo(lb) elseif S == MOI.Interval{T} return MOI.Interval(lb, ub) end end function MOI.set( m::Optimizer{T}, ::MOI.ConstraintSet, c::MOI.ConstraintIndex{MOI.VariableIndex, S}, s::S ) where{T, S<:SCALAR_SETS{T}} # Sanity check MOI.throw_if_not_valid(m, c) v = MOI.VariableIndex(c.value) # Update inner bounds # Bound key does not need to be updated j = m.var_indices[v] if S == MOI.LessThan{T} set_attribute(m.inner, VariableUpperBound(), j, s.upper) elseif S == MOI.GreaterThan{T} set_attribute(m.inner, VariableLowerBound(), j, s.lower) elseif S == MOI.EqualTo{T} set_attribute(m.inner, VariableLowerBound(), j, s.value) set_attribute(m.inner, VariableUpperBound(), j, s.value) elseif S == MOI.Interval{T} set_attribute(m.inner, VariableLowerBound(), j, s.lower) set_attribute(m.inner, VariableUpperBound(), j, s.upper) else error("Unknown type for ConstraintSet: $S.") end return nothing end function MOI.set( m::Optimizer{T}, ::MOI.ConstraintSet, c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, S}, s::S ) where{T, S<:SCALAR_SETS{T}} MOI.throw_if_not_valid(m, c) # Update inner bounds i = m.con_indices[c] if S == MOI.LessThan{T} set_attribute(m.inner, ConstraintUpperBound(), i, s.upper) elseif S == MOI.GreaterThan{T} set_attribute(m.inner, ConstraintLowerBound(), i, s.lower) elseif S == MOI.EqualTo{T} set_attribute(m.inner, ConstraintLowerBound(), i, s.value) set_attribute(m.inner, ConstraintUpperBound(), i, s.value) elseif S == MOI.Interval{T} set_attribute(m.inner, ConstraintLowerBound(), i, s.lower) set_attribute(m.inner, ConstraintUpperBound(), i, s.upper) else error("Unknown type for ConstraintSet: $S.") end return nothing end # # ConstraintPrimal # function MOI.get( m::Optimizer{T}, attr::MOI.ConstraintPrimal, c::MOI.ConstraintIndex{MOI.VariableIndex, S} ) where{T, S<:SCALAR_SETS{T}} MOI.throw_if_not_valid(m, c) MOI.check_result_index_bounds(m, attr) # Query row primal j = m.var_indices[MOI.VariableIndex(c.value)] return m.inner.solution.x[j] end function MOI.get( m::Optimizer{T}, attr::MOI.ConstraintPrimal, c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, S} ) where{T, S<:SCALAR_SETS{T}} MOI.throw_if_not_valid(m, c) MOI.check_result_index_bounds(m, attr) # Query from inner model i = m.con_indices[c] return m.inner.solution.Ax[i] end # # ConstraintDual # function MOI.get( m::Optimizer{T}, attr::MOI.ConstraintDual, c::MOI.ConstraintIndex{MOI.VariableIndex, S} ) where{T, S<:SCALAR_SETS{T}} MOI.throw_if_not_valid(m, c) MOI.check_result_index_bounds(m, attr) # Get variable index j = m.var_indices[MOI.VariableIndex(c.value)] # Extract reduced cost if S == MOI.LessThan{T} return -m.inner.solution.s_upper[j] elseif S == MOI.GreaterThan{T} return m.inner.solution.s_lower[j] else return m.inner.solution.s_lower[j] - m.inner.solution.s_upper[j] end end function MOI.get( m::Optimizer{T}, attr::MOI.ConstraintDual, c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, S} ) where{T, S<:SCALAR_SETS{T}} MOI.throw_if_not_valid(m, c) MOI.check_result_index_bounds(m, attr) # Get dual from inner model i = m.con_indices[c] if isa(S, MOI.LessThan) return -m.inner.solution.y_upper[i] elseif isa(S, MOI.GreaterThan) return m.inner.solution.y_lower[i] else return m.inner.solution.y_lower[i] - m.inner.solution.y_upper[i] end end ================================================ FILE: src/Interfaces/MOI/objective.jl ================================================ # ============================================= # 1. Supported objectives # ============================================= function MOI.supports( ::Optimizer{T}, ::MOI.ObjectiveFunction{F} ) where{T, F<:Union{MOI.VariableIndex, MOI.ScalarAffineFunction{T}}} return true end # ============================================= # 2. Get/set objective function # ============================================= function MOI.get( m::Optimizer{T}, ::MOI.ObjectiveFunction{F} ) where{T,F} obj = MOI.get(m, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}()) return convert(F, obj) end function MOI.get( m::Optimizer{T}, ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}} ) where{T} # Objective coeffs terms = MOI.ScalarAffineTerm{T}[] for (j, cj) in enumerate(m.inner.pbdata.obj) !iszero(cj) && push!(terms, MOI.ScalarAffineTerm(cj, m.var_indices_moi[j])) end # Constant term c0 = m.inner.pbdata.obj0 return MOI.ScalarAffineFunction(terms, c0) end # TODO: use inner API function MOI.set( m::Optimizer{T}, ::MOI.ObjectiveFunction{F}, f::F ) where{T, F <: MOI.VariableIndex} MOI.set( m, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}(), convert(MOI.ScalarAffineFunction{T}, f) ) m._obj_type = _SINGLE_VARIABLE return nothing end function MOI.set( m::Optimizer{T}, ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}, f::MOI.ScalarAffineFunction{T} ) where{T} # Sanity checks isfinite(f.constant) || error("Objective constant term must be finite") for t in f.terms MOI.throw_if_not_valid(m, t.variable) end # Update inner model m.inner.pbdata.obj .= zero(T) # Reset inner objective to zero for t in f.terms j = m.var_indices[t.variable] m.inner.pbdata.obj[j] += t.coefficient # there may be dupplicates end set_attribute(m.inner, ObjectiveConstant(), f.constant) # objective offset m._obj_type = _SCALAR_AFFINE return nothing end # ============================================= # 3. Modify objective # ============================================= function MOI.modify( m::Optimizer{T}, c::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}, chg::MOI.ScalarCoefficientChange{T} ) where{T} # Sanity checks v = chg.variable MOI.throw_if_not_valid(m, v) # Update inner model j = m.var_indices[v] m.inner.pbdata.obj[j] = chg.new_coefficient # TODO: use inner API m._obj_type = _SCALAR_AFFINE return nothing end function MOI.modify( m::Optimizer{T}, ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}, chg::MOI.ScalarConstantChange{T} ) where{T} isfinite(chg.new_constant) || error("Objective constant term must be finite") m.inner.pbdata.obj0 = chg.new_constant m._obj_type = _SCALAR_AFFINE return nothing end ================================================ FILE: src/Interfaces/MOI/variables.jl ================================================ # ============================================= # 1. Supported variable attributes # ============================================= """ SUPPORTED_VARIABLE_ATTR List of supported `MOI.VariableAttribute`. * `MOI.VariablePrimal` """ const SUPPORTED_VARIABLE_ATTR = Union{ MOI.VariableName, # MOI.VariablePrimalStart, MOI.VariablePrimal } MOI.supports(::Optimizer, ::MOI.VariableName, ::Type{MOI.VariableIndex}) = true # ============================================= # 2. Add variables # ============================================= function MOI.is_valid(m::Optimizer, x::MOI.VariableIndex) return haskey(m.var_indices, x) end function MOI.add_variable(m::Optimizer{T}) where{T} # TODO: dispatch a function call to m.inner instead of m.inner.pbdata m.var_counter += 1 x = MOI.VariableIndex(m.var_counter) j = Tulip.add_variable!(m.inner.pbdata, Int[], T[], zero(T), T(-Inf), T(Inf)) # Update tracking of variables m.var_indices[x] = j m.var2bndtype[x] = Set{Type{<:MOI.AbstractScalarSet}}() push!(m.var_indices_moi, x) return x end # ============================================= # 3. Delete variables # ============================================= function MOI.delete(m::Optimizer, v::MOI.VariableIndex) MOI.throw_if_not_valid(m, v) # Update inner model j = m.var_indices[v] old_name = get_attribute(m.inner, VariableName(), j) delete_variable!(m.inner.pbdata, j) # Remove bound tracking delete!(m.var2bndtype, v) # Name update if old_name != "" s = m.name2var[old_name] delete!(s, v) length(s) == 0 && delete!(m.name2var, old_name) end # Update indices correspondence deleteat!(m.var_indices_moi, j) delete!(m.var_indices, v) for v_ in m.var_indices_moi[j:end] m.var_indices[v_] -= 1 end return nothing end # ============================================= # 4. Get/set variable attributes # ============================================= function MOI.get(m::Optimizer, ::Type{MOI.VariableIndex}, name::String) s = get(m.name2var, name, Set{MOI.VariableIndex}()) if length(s) == 0 return nothing elseif length(s) == 1 return first(s) else error("Duplicate variable name detected: $(name)") end end function MOI.get(m::Optimizer, ::MOI.VariableName, v::MOI.VariableIndex) MOI.throw_if_not_valid(m, v) # Get name from inner model j = m.var_indices[v] return get_attribute(m.inner, VariableName(), j) end function MOI.set(m::Optimizer, ::MOI.VariableName, v::MOI.VariableIndex, name::String) # Check that variable does exist MOI.throw_if_not_valid(m, v) # Update inner model j = m.var_indices[v] old_name = get_attribute(m.inner, VariableName(), j) if name == old_name return # It's the same name! end set_attribute(m.inner, VariableName(), j, name) s = get!(m.name2var, name, Set{MOI.VariableIndex}()) # Update names mapping push!(s, v) # Delete old name s_old = get(m.name2var, old_name, Set{MOI.ConstraintIndex}()) if length(s_old) == 0 # Variable didn't have name before elseif length(s_old) == 1 # Delete this from mapping delete!(m.name2var, old_name) else delete!(s_old, v) end return nothing end function MOI.get(m::Optimizer{T}, attr::MOI.VariablePrimal, x::MOI.VariableIndex ) where{T} MOI.throw_if_not_valid(m, x) MOI.check_result_index_bounds(m, attr) # Query inner solution j = m.var_indices[x] return m.inner.solution.x[j] end ================================================ FILE: src/Interfaces/tulip_julia_api.jl ================================================ using QPSReader using TimerOutputs: tottime # TODO: user-facing API in Julia # Other APIs should wrap this one # TODO: docstrings # TODO: define Traits on attributes (e.g.: IsModifiable, IsNumeric, etc..) # for error messages """ load_problem!(m::Model{T}, fname::String) Read a model from file `fname` and load it into model `m`. Only free MPS files are currently supported. """ function load_problem!(m::Model{T}, fname::String) where{T} Base.empty!(m) dat = with_logger(Logging.NullLogger()) do _open(fname) do io readqps(io, mpsformat=:free) end end # TODO: avoid allocations when T is Float64 objsense = !(dat.objsense == :max) load_problem!(m.pbdata, dat.name, objsense, T.(dat.c), T(dat.c0), sparse(dat.arows, dat.acols, T.(dat.avals), dat.ncon, dat.nvar), T.(dat.lcon), T.(dat.ucon), T.(dat.lvar), T.(dat.uvar), dat.connames, dat.varnames ) return m end """ get_attribute(model::Model, ::ModelName) Query the `ModelName` attribute from `model` """ get_attribute(m::Model, ::ModelName) = m.pbdata.name """ set_attribute(model::Model, ::ModelName, name::String) Set the `ModelName` attribute in `model` """ set_attribute(m::Model, ::ModelName, name::String) = (m.pbdata.name = name; return nothing) """ get_attribute(model::Model, ::Status) Query the `Status` attribute from `model` """ function get_attribute(m::Model, ::Status) return m.status end """ get_attribute(model::Model, ::SolutionTime) Query the `SolutionTime` attribute from `model` """ function get_attribute(m::Model, ::SolutionTime) if isnothing(m.solver) return 0 else local ns = tottime(m.solver.timer) return ns * 1e-9 end end """ get_attribute(model::Model, ::BarrierIterations) Query the `BarrierIterations` attribute from `model` """ function get_attribute(m::Model, ::BarrierIterations) if isnothing(m.solver) return 0 else return m.solver.niter end end """ set_attribute(m::Model{T}, ::VariableLowerBound, j::Int, lb::T) Set the lower bound of variable `j` in model `m` to `lb`. """ function set_attribute(m::Model{T}, ::VariableLowerBound, j::Int, lb::T) where{T} # sanity checks 1 <= j <= m.pbdata.nvar || error("Invalid variable index $j") # Update bound m.pbdata.lvar[j] = lb return nothing end """ get_attribute(m::Model{T}, ::VariableLowerBound, j::Int) Query the lower bound of variable `j` in model `m`. """ function get_attribute(m::Model, ::VariableLowerBound, j::Int) # sanity checks 1 <= j <= m.pbdata.nvar || error("Invalid variable index $j") # Update bound return m.pbdata.lvar[j] end """ set_attribute(m::Model{T}, ::VariableUpperBound, j::Int, ub::T) Set the upper bound of variable `j` in model `m` to `ub`. """ function set_attribute(m::Model{T}, ::VariableUpperBound, j::Int, ub::T) where{T} # sanity checks 1 <= j <= m.pbdata.nvar || error("Invalid variable index $j") # Update bound m.pbdata.uvar[j] = ub return nothing end """ set_attribute(m::Model{T}, ::ConstraintLowerBound, i::Int, lb::T) Set the lower bound of constraint `i` in model `m` to `lb`. """ function set_attribute(m::Model{T}, ::ConstraintLowerBound, i::Int, lb::T) where{T} # sanity checks 1 <= i <= m.pbdata.ncon || error("Invalid constraint index $i") # Update bound m.pbdata.lcon[i] = lb return nothing end """ set_attribute(m::Model{T}, ::ConstraintUpperBound, i::Int, ub::T) Set the upper bound of constraint `i` in model `m` to `ub`. """ function set_attribute(m::Model{T}, ::ConstraintUpperBound, i::Int, ub::T) where{T} # sanity checks 1 <= i <= m.pbdata.ncon || error("Invalid constraint index $i") # Update bound m.pbdata.ucon[i] = ub return nothing end """ get_attribute(m::Model, ::VariableName, j::Int) Query the name of variable `j` in model `m` """ function get_attribute(m::Model, ::VariableName, j::Int) 1 <= j <= m.pbdata.nvar || error("Invalid variable index $j") return m.pbdata.var_names[j] end """ set_attribute(m::Model, ::VariableName, j::Int, name::String) Set the name of variable `j` in model `m` to `name`. """ function set_attribute(m::Model, ::VariableName, j::Int, name::String) 1 <= j <= m.pbdata.nvar || error("Invalid variable index $j") # TODO: ensure that no two variables have the same name m.pbdata.var_names[j] = name return nothing end """ get_attribute(m::Model, ::ConstraintName, i::Int) Query the name of constraint `i` in model `m` """ function get_attribute(m::Model, ::ConstraintName, i::Int) 1 <= i <= m.pbdata.ncon || error("Invalid constraint index $i") return m.pbdata.con_names[i] end """ set_attribute(m::Model, ::ConstraintName, i::Int, name::String) Set the name of constraint `i` in model `m` to `name`. """ function set_attribute(m::Model, ::ConstraintName, i::Int, name::String) 1 <= i <= m.pbdata.ncon || error("Invalid constraint index $i") m.pbdata.con_names[i] = name return nothing end # TODO: Set/get parameters """ get_parameter(m::Model, pname::String) Query the value of parameter `pname` in model `m`. """ function get_parameter(m::Model, pname::String) return getfield(m.params, Symbol(pname)) end """ set_parameter(m::Model, pname::String, val) Set the value of parameter `pname` in model `m` to `val` """ function set_parameter(m::Model, pname::String, val) if length(pname) > 4 && pname[1:4] == "IPM_" setfield!(m.params.IPM, Symbol(pname[5:end]), val) elseif length(pname) > 4 && pname[1:4] == "KKT_" setfield!(m.params.KKT, Symbol(pname[5:end]), val) elseif length(pname) > 10 && pname[1:9] == "Presolve_" setfield!(m.params.Presolve, Symbol(pname[10:end]), val) elseif hasfield(typeof(m.params), Symbol(pname)) setfield!(m.params, Symbol(pname), val) else error("Unknown option: $pname") end return nothing end # TODO: Query solution value get_attribute(m::Model, ::ObjectiveConstant) = m.pbdata.obj0 set_attribute(m::Model{T}, ::ObjectiveConstant, obj0::T) where{T} = (m.pbdata.obj0 = obj0; return nothing) """ get_attribute(model::Model, ::ObjectiveValue) Query the `ObjectiveValue` attribute from `model` """ function get_attribute(m::Model{T}, ::ObjectiveValue) where{T} if isnothing(m.solution) error("Model has no solution") end pst = m.solution.primal_status if pst != Sln_Unknown z = dot(m.solution.x, m.pbdata.obj) # If solution is a ray, ignore constant objective term is_ray = m.solution.is_primal_ray z0 = !is_ray * m.pbdata.obj0 return (z + z0) else # No solution, return zero return zero(T) end end """ get_attribute(model::Model, ::DualObjectiveValue) Query the `DualObjectiveValue` attribute from `model` """ function get_attribute(m::Model{T}, ::DualObjectiveValue) where{T} if isnothing(m.solution) error("Model has no solution") end dst = m.solution.dual_status if dst != Sln_Unknown yl = m.solution.y_lower yu = m.solution.y_upper sl = m.solution.s_lower su = m.solution.s_upper bl = m.pbdata.lcon bu = m.pbdata.ucon xl = m.pbdata.lvar xu = m.pbdata.uvar z = ( dot(yl, Diagonal(isfinite.(bl)), bl) - dot(yu, Diagonal(isfinite.(bu)), bu) + dot(sl, Diagonal(isfinite.(xl)), xl) - dot(su, Diagonal(isfinite.(xu)), xu) ) # If problem is maximization, we need to negate the dual value # to comply with MOI duality convention z = m.pbdata.objsense ? z : -z # If solution is a ray, ignore constant objective term is_ray = m.solution.is_dual_ray z0 = !is_ray * m.pbdata.obj0 return (z + z0) else # No solution, return zero return zero(T) end end ================================================ FILE: src/KKT/Cholmod/cholmod.jl ================================================ module TlpCholmod using LinearAlgebra using SparseArrays using SparseArrays.CHOLMOD using ..KKT: AbstractKKTBackend, AbstractKKTSolver using ..KKT: AbstractKKTSystem, K1, K2 import ..KKT: setup, update!, solve!, backend, linear_system """ Backend CHOLMOD backend for solving linear systems. See [`CholmodSolver`](@ref) for further details. """ struct Backend <: AbstractKKTBackend end """ CholmodSolver{T,S<:AbstractKKTSystem} CHOLMOD-based KKT solver. # Supported arithmetics * `Float64` # Supported systems * [`K2`](@ref) via ``LDLᵀ`` factorization * [`K1`](@ref) via Cholesky (``LLᵀ``) factorization # Examples * To solve the augmented system with CHOLMOD's ``LDL^{T}`` factorization: ```julia set_parameter(tlp_model, "KKT_Backend", Tulip.KKT.TlpCholmod.Backend()) set_parameter(tlp_model, "KKT_System", Tulip.KKT.K2()) ``` * To solve the normal equations system with CHOLMOD's Cholesky factorization: ```julia set_parameter(tlp_model, "KKT_Backend", Tulip.KKT.TlpCholmod.Backend()) set_parameter(tlp_model, "KKT_System", Tulip.KKT.K1()) ``` """ mutable struct CholmodSolver{T,S} <: AbstractKKTSolver{T} # Problem data m::Int n::Int A::SparseMatrixCSC{T,Int} # Workspace # TODO: store K as CHOLMOD.Sparse instead of SparseMatrixCSC θ::Vector{T} # Diagonal scaling regP::Vector{T} # Primal regularization regD::Vector{T} # Dual regularization K::SparseMatrixCSC{T,Int} # KKT matrix F::CHOLMOD.Factor{T} # Factorization ξ::Vector{T} # RHS of KKT system end backend(::CholmodSolver) = "CHOLMOD" # Convert to sparse matrix if other type is used setup(A, system, backend::Backend) = setup(convert(SparseMatrixCSC, A), system, backend) include("spd.jl") # Normal equations include("sqd.jl") # Augmented system end # module ================================================ FILE: src/KKT/Cholmod/spd.jl ================================================ const CholmodSPD = CholmodSolver{Float64,K1} linear_system(::CholmodSPD) = "Normal equations (K1)" function setup(A::SparseMatrixCSC{Float64}, ::K1, ::Backend) m, n = size(A) θ = ones(Float64, n) regP = ones(Float64, n) regD = ones(Float64, m) ξ = zeros(Float64, m) # TODO: analyze + in-place A*D*A' product K = sparse(A * A') + spdiagm(0 => regD) # TODO: PSD-ness checks F = cholesky(Symmetric(K)) return CholmodSolver{Float64,K1}(m, n, A, θ, regP, regD, K, F, ξ) end function update!(kkt::CholmodSPD, θ, regP, regD) m, n = kkt.m, kkt.n # Sanity checks length(θ) == n || throw(DimensionMismatch( "length(θ)=$(length(θ)) but KKT solver has n=$n." )) length(regP) == n || throw(DimensionMismatch( "length(regP)=$(length(regP)) but KKT solver has n=$n" )) length(regD) == m || throw(DimensionMismatch( "length(regD)=$(length(regD)) but KKT solver has m=$m" )) copyto!(kkt.θ, θ) copyto!(kkt.regP, regP) copyto!(kkt.regD, regD) # Form normal equations matrix # TODO: use in-place update of S D = inv(Diagonal(kkt.θ .+ kkt.regP)) kkt.K = (kkt.A * D * kkt.A') + spdiagm(0 => kkt.regD) # Update factorization cholesky!(kkt.F, Symmetric(kkt.K), check=false) issuccess(kkt.F) || throw(PosDefException(0)) return nothing end function solve!(dx, dy, kkt::CholmodSPD, ξp, ξd) m, n = kkt.m, kkt.n D = inv(Diagonal(kkt.θ .+ kkt.regP)) copyto!(kkt.ξ, ξp) mul!(kkt.ξ, kkt.A, D * ξd, true, true) # Solve normal equations # CHOLMOD doesn't have in-place solve, so this line will allocate dy .= kkt.F \ kkt.ξ # Recover dx copyto!(dx, ξd) mul!(dx, kkt.A', dy, 1.0, -1.0) lmul!(D, dx) # TODO: iterative refinement return nothing end ================================================ FILE: src/KKT/Cholmod/sqd.jl ================================================ const CholmodSQD = CholmodSolver{Float64,K2} linear_system(::CholmodSQD) = "Augmented system (K2)" function setup(A::SparseMatrixCSC{Float64,Int}, ::K2, ::Backend) m, n = size(A) θ = ones(Float64, n) regP = ones(Float64, n) regD = ones(Float64, m) ξ = zeros(Float64, m+n) K = [ spdiagm(0 => -θ) A'; spzeros(Float64, m, n) spdiagm(0 => ones(m)) ] # TODO: Symbolic factorization only F = ldlt(Symmetric(K)) return CholmodSolver{Float64,K2}(m, n, A, θ, regP, regD, K, F, ξ) end function update!(kkt::CholmodSQD, θ, regP, regD) m, n = kkt.m, kkt.n # Sanity checks length(θ) == n || throw(DimensionMismatch( "length(θ)=$(length(θ)) but KKT solver has n=$n." )) length(regP) == n || throw(DimensionMismatch( "length(regP)=$(length(regP)) but KKT solver has n=$n" )) length(regD) == m || throw(DimensionMismatch( "length(regD)=$(length(regD)) but KKT solver has m=$m" )) copyto!(kkt.θ, θ) copyto!(kkt.regP, regP) copyto!(kkt.regD, regD) # Update KKT matrix # K is stored as upper-triangular, and only its diagonal is changed @inbounds for j in 1:kkt.n k = kkt.K.colptr[1+j] - 1 kkt.K.nzval[k] = -kkt.θ[j] - regP[j] end @inbounds for i in 1:kkt.m k = kkt.K.colptr[1+kkt.n+i] - 1 kkt.K.nzval[k] = regD[i] end ldlt!(kkt.F, Symmetric(kkt.K)) return nothing end function solve!(dx, dy, kkt::CholmodSQD, ξp, ξd) m, n = kkt.m, kkt.n # Setup right-hand side @views copyto!(kkt.ξ[1:n], ξd) @views copyto!(kkt.ξ[(n+1):end], ξp) # Solve augmented system # CHOLMOD doesn't have in-place solve, so this line will allocate δ = kkt.F \ kkt.ξ # Recover dx, dy @views copyto!(dx, δ[1:n]) @views copyto!(dy, δ[(n+1):end]) # TODO: iterative refinement return nothing end ================================================ FILE: src/KKT/Dense/lapack.jl ================================================ module TlpDense using LinearAlgebra using LinearAlgebra:BlasReal using ..KKT: AbstractKKTBackend, AbstractKKTSolver using ..KKT: AbstractKKTSystem, K1, K2 import ..KKT: setup, update!, solve!, backend, linear_system """ Backend Dense linear algebra backend for solving linear systems. See [`DenseSolver`](@ref) for further details. """ struct Backend <: AbstractKKTBackend end """ DenseSolver{T} Dense linear algebra-based KKT solver. # Supported arithmetics All arithmetics are supported. BLAS/LAPACK routines are used automatically with `Float32` and `Float64` arithmetic. # Supported systems * [`K1`](@ref) via Cholesky factorization """ mutable struct DenseSolver{T} <: AbstractKKTSolver{T} # Problem data m::Int n::Int A::Matrix{T} # Workspace _A::Matrix{T} # Place-holder for scaled copy of A θ::Vector{T} # Diagonal scaling regP::Vector{T} # Primal regularization regD::Vector{T} # Dual regularization K::Matrix{T} # KKT matrix ξ::Vector{T} # RHS of KKT system end backend(::DenseSolver) = "Julia.LinearAlgebra" backend(::DenseSolver{<:BlasReal}) = "LAPACK $(LinearAlgebra.BLAS.vendor())" linear_system(::DenseSolver) = "Normal equations (K1)" function setup(A::Matrix{T}, ::K1, ::Backend) where{T} m, n = size(A) _A = Matrix{T}(undef, m, n) θ = ones(T, n) regP = ones(T, n) regD = ones(T, m) K = Matrix{T}(undef, m, m) ξ = zeros(T, m) return DenseSolver{T}(m, n, A, _A, θ, regP, regD, K, ξ) end function update!(kkt::DenseSolver, θ, regP, regD) m, n = kkt.m, kkt.n # Sanity checks length(θ) == n || throw(DimensionMismatch( "length(θ)=$(length(θ)) but KKT solver has n=$n." )) length(regP) == n || throw(DimensionMismatch( "length(regP)=$(length(regP)) but KKT solver has n=$n" )) length(regD) == m || throw(DimensionMismatch( "length(regD)=$(length(regD)) but KKT solver has m=$m" )) copyto!(kkt.θ, θ) copyto!(kkt.regP, regP) copyto!(kkt.regD, regD) # Re-compute normal equations matrix # There's no function that does S = A*D*A', so we cache a copy of A copyto!(kkt._A, kkt.A) D = sqrt(inv(Diagonal(kkt.θ .+ kkt.regP))) rmul!(kkt._A, D) # B = A * √D mul!(kkt.K, kkt._A, transpose(kkt._A), true, false) # Now K = A*D*A' # Finally, add dual regularizations to the diagonal @inbounds for i in 1:kkt.m kkt.K[i, i] += kkt.regD[i] end # In-place Cholesky factorization cholesky!(Symmetric(kkt.K)) return nothing end function solve!(dx, dy, kkt::DenseSolver{T}, ξp, ξd) where{T} m, n = kkt.m, kkt.n # Set-up right-hand side D = inv(Diagonal(kkt.θ .+ kkt.regP)) copyto!(dy, ξp) mul!(dy, kkt.A, D * ξd, true, true) # Solve normal equations ldiv!(UpperTriangular(kkt.K)', dy) ldiv!(UpperTriangular(kkt.K) , dy) # Recover dx copyto!(dx, ξd) mul!(dx, kkt.A', dy, one(T), -one(T)) lmul!(D, dx) # TODO: Iterative refinement return nothing end end # module ================================================ FILE: src/KKT/KKT.jl ================================================ module KKT using LinearAlgebra using SparseArrays using LinearOperators export AbstractKKTSystem, AbstractKKTBackend, AbstractKKTSolver export KKTOptions """ AbstractKKTSystem Abstract type for KKT systems """ abstract type AbstractKKTSystem end include("systems.jl") """ AbstractKKTBackend Abstract type for KKT backend, i.e., the actual linear solver. """ abstract type AbstractKKTBackend end """ DefaultKKTBackend Default setting for KKT backend. Currently defaults to [`TlpCholmod.Backend`](@ref) for `Float64` arithmetic, and [`TlpLDLFact.Backend`](@ref) otherwise. """ struct DefaultKKTBackend <: AbstractKKTBackend end """ AbstractKKTSolver{T} Abstract container for solving KKT systems in arithmetic `T`. """ abstract type AbstractKKTSolver{T} end """ KKTOptions{T} KKT solver options. """ Base.@kwdef mutable struct KKTOptions{T} Backend::AbstractKKTBackend = DefaultKKTBackend() System::AbstractKKTSystem = DefaultKKTSystem() end """ setup(A, system, backend; kwargs...) Instantiate a KKT solver object. """ function setup end # # Specialized implementations should extend the functions below # """ update!(kkt, θinv, regP, regD) Update internal data and factorization/pre-conditioner. After this call, `kkt` can be used to solve the augmented system ``` [-(Θ⁻¹ + Rp) Aᵀ] [dx] = [ξd] [ A Rd] [dy] [ξp] ``` for given right-hand sides `ξd` and `ξp`. # Arguments * `kkt::AbstractKKTSolver{T}`: the KKT solver object * `θinv::AbstractVector{T}`: ``θ⁻¹`` * `regP::AbstractVector{T}`: primal regularizations * `regD::AbstractVector{T}`: dual regularizations """ function update! end """ solve!(dx, dy, kkt, ξp, ξd) Solve the symmetric quasi-definite augmented system ``` [-(Θ⁻¹ + Rp) Aᵀ] [dx] = [ξd] [ A Rd] [dy] [ξp] ``` and over-write `dx`, `dy` with the result. # Arguments - `dx, dy`: Vectors of unknowns, modified in-place - `kkt`: Linear solver for the augmented system - `ξp, ξd`: Right-hand-side vectors """ function solve! end """ arithmetic(kkt::AbstractKKTSolver) Return the arithmetic used by the solver. """ arithmetic(::AbstractKKTSolver{T}) where T = T """ backend(kkt) Return the name of the solver's backend. """ backend(::AbstractKKTSolver) = "Unkown" """ linear_system(kkt) Return which system is solved by the kkt solver. """ linear_system(::AbstractKKTSolver) = "Unkown" # Generic tests include("Test/test.jl") # Custom linear solvers include("Dense/lapack.jl") include("Cholmod/cholmod.jl") include("LDLFactorizations/ldlfact.jl") const TlpLDLFact = TlpLDLFactorizations include("Krylov/krylov.jl") # Default backend and system choices function setup(A, ::DefaultKKTSystem, ::DefaultKKTBackend) T = eltype(A) if T == Float64 return setup(A, K2(), TlpCholmod.Backend()) else return setup(A, K2(), TlpLDLFact.Backend()) end end end # module ================================================ FILE: src/KKT/Krylov/defs.jl ================================================ const _KRYLOV_SPD = Union{ Krylov.CgWorkspace, Krylov.CrWorkspace, Krylov.CarWorkspace, } const _KRYLOV_SID = Union{ Krylov.MinresWorkspace, Krylov.MinaresWorkspace, Krylov.MinresQlpWorkspace, Krylov.SymmlqWorkspace } const _KRYLOV_SQD = Union{ Krylov.TricgWorkspace, Krylov.TrimrWorkspace, } const _KRYLOV_LN = Union{ Krylov.LnlqWorkspace, Krylov.CraigWorkspace, Krylov.CraigmrWorkspace, } const _KRYLOV_LS = Union{ Krylov.LslqWorkspace, Krylov.LsqrWorkspace, Krylov.LsmrWorkspace, } ================================================ FILE: src/KKT/Krylov/krylov.jl ================================================ module TlpKrylov using LinearAlgebra using Krylov using LinearOperators const LO = LinearOperators using ..KKT: AbstractKKTBackend, AbstractKKTSolver using ..KKT: AbstractKKTSystem, K1, K2 import ..KKT: arithmetic, backend, linear_system import ..KKT: setup, update!, solve! include("defs.jl") """ Backend{KS<:Krylov.KrylovWorkspace,V<:AbstractVector} [Krylov.jl](https://github.com/JuliaSmoothOptimizers/Krylov.jl)-based backend for solving linear systems. The type is parametrized by: * `KS<:Krylov.KrylovWorkspace`: workspace type for the Krylov method. Also defines the Krylov method to be used. * `V<:AbstractVector`: the vector storage type used within the Krylov method. This should be set to `Vector{T}` (for arithmetic `T`) unless, e.g., one uses a GPU. See the [Krylov.jl documentation](https://juliasmoothoptimizers.github.io/Krylov.jl/dev/inplace/) for further details. # Example usage All the following examples assume everything runs on a CPU in `Float64` arithmetic. * To use the conjugate gradient: ```julia backend = KKT.TlpKrylov.Backend(Krylov.CgWorkspace, Vector{Float64}) ``` * To use MINRES: ```julia backend = KKT.TlpKrylov.Backend(Krylov.MinresWorkspace, Vector{Float64}) ``` """ struct Backend{KW,V} <: AbstractKKTBackend krylov_workspace::Type{KW} vector_storage::Type{V} end """ AbstractKrylovSolver{T} Abstract type for Kyrlov-based linear solvers. """ abstract type AbstractKrylovSolver{T} <: AbstractKKTSolver{T} end include("spd.jl") include("sid.jl") include("sqd.jl") end # module ================================================ FILE: src/KKT/Krylov/sid.jl ================================================ """ SIDSolver """ mutable struct SIDSolver{T,V,Ta,KL,KW} <: AbstractKrylovSolver{T} # Problem data m::Int n::Int A::Ta # IPM-related workspace θ::Vector{T} regP::Vector{T} regD::Vector{T} # Krylov-related workspace Θp::Diagonal{T,V} Θd::Diagonal{T,V} ξ::V opK::KL # Krylov solver & related options atol::T rtol::T krylov_workspace::KW # TODO: preconditioner end backend(kkt::SIDSolver) = "$(typeof(kkt.krylov_workspace))" linear_system(kkt::SIDSolver) = "K2" function setup(A, ::K2, backend::Backend{KW,V}) where{KW<:_KRYLOV_SID,V} Ta = typeof(A) T = eltype(A) T == eltype(V) || error("eltype(A)=$T incompatible with eltype of Krylov vector storage $V.") m, n = size(A) # Workspace θ = ones(T, n) regP = ones(T, n) regD = ones(T, m) Θp = Diagonal(V(undef, n)) Θd = Diagonal(V(undef, m)) ξ = V(undef, m+n) # Define linear operator for the augmented system # This linear operator is symmetric indefinite opK = LO.LinearOperator(T, m+n, m+n, true, false, (u, w, α, β) -> begin @views u1 = u[1:n] @views u2 = u[(n+1):(m+n)] @views w1 = w[1:n] @views w2 = w[n+1:n+m] mul!(u1, Θp, w1, α, β) mul!(u1, A', w2, α, one(T)) mul!(u2, A , w1, α, β) mul!(u2, Θd, w2, α, one(T)) u end ) # Allocate Krylov solver's workspace atol = sqrt(eps(T)) rtol = sqrt(eps(T)) krylov_workspace = KW(m+n, m+n, V) return SIDSolver{T,V,Ta,typeof(opK),typeof(krylov_workspace)}( m, n, A, θ, regP, regD, Θp, Θd, ξ, opK, atol, rtol, krylov_workspace ) end function update!(kkt::SIDSolver, θ, regP, regD) copyto!(kkt.θ, θ) copyto!(kkt.regP, regP) copyto!(kkt.regD, regD) copyto!(kkt.Θd.diag, regD) copyto!(kkt.Θp.diag, -(kkt.θ .+ kkt.regP)) return nothing end function solve!(dx, dy, kkt::SIDSolver{T}, ξp, ξd) where{T} m, n = kkt.m, kkt.n @views copyto!(kkt.ξ[1:m], ξp) @views copyto!(kkt.ξ[(m+1):(m+n)], ξd) # Solve the augmented system krylov_solve!(kkt.krylov_workspace, kkt.opK, kkt.ξ; atol=kkt.atol, rtol=kkt.rtol) # Recover dx, dy copyto!(dx, kkt.krylov_workspace.x[1:n]) copyto!(dy, kkt.krylov_workspace.x[(n+1):(m+n)]) # TODO: iterative refinement (?) return nothing end ================================================ FILE: src/KKT/Krylov/spd.jl ================================================ """ SPDSolver """ mutable struct SPDSolver{T,V,Ta,KL,KS} <: AbstractKrylovSolver{T} # Problem data m::Int n::Int A::Ta # IPM-related workspace θ::Vector{T} regP::Vector{T} regD::Vector{T} # Krylov-related workspace D::Diagonal{T,V} Rd::Diagonal{T,V} ξ::V opK::KL # Krylov solver & related options atol::T rtol::T krylov_solver::KS # TODO: preconditioner end backend(kkt::SPDSolver) = "$(typeof(kkt.krylov_solver))" linear_system(kkt::SPDSolver) = "K1" function setup(A, ::K1, backend::Backend{KS,V}) where{KS<:Union{_KRYLOV_SPD,_KRYLOV_SID},V} Ta = typeof(A) T = eltype(A) T == eltype(V) || error("eltype(A)=$T incompatible with eltype of Krylov vector storage $V.") m, n = size(A) # Workspace θ = ones(T, n) regP = ones(T, n) regD = ones(T, m) D = Diagonal(V(undef, n)) Rd = Diagonal(V(undef, m)) ξ = V(undef, m) # Define linear operator for normal equations system # We need to allocate one temporary vector # This linear operator is symmetric definite positive, # so we only need to define # u ⟵ α (A×D×A'+Rd) × w + β u # i.e., u = α(ADA')×w + (αRd)×w + βu vtmp = V(undef, n) opK = LO.LinearOperator(T, m, m, true, true, (u, w, α, β) -> begin mul!(vtmp, A', w) lmul!(D, vtmp) mul!(u, A, vtmp, α, β) mul!(u, Rd, w, α, one(T)) u end ) # Allocate Krylov solver's workspace atol = sqrt(eps(T)) rtol = sqrt(eps(T)) krylov_solver = KS(m, m, V) return SPDSolver{T,V,Ta,typeof(opK),typeof(krylov_solver)}( m, n, A, θ, regP, regD, D, Rd, ξ, opK, atol, rtol, krylov_solver ) end function update!(kkt::SPDSolver, θ, regP, regD) copyto!(kkt.θ, θ) copyto!(kkt.regP, regP) copyto!(kkt.regD, regD) copyto!(kkt.Rd.diag, regD) copyto!(kkt.D.diag, inv.(kkt.θ .+ kkt.regP)) return nothing end function solve!(dx, dy, kkt::SPDSolver{T}, ξp, ξd) where{T} m, n = kkt.m, kkt.n copyto!(kkt.ξ, ξp) mul!(kkt.ξ, kkt.A, kkt.D * ξd, true, true) # Solve the normal equations krylov_solve!(kkt.krylov_solver, kkt.opK, kkt.ξ; atol=kkt.atol, rtol=kkt.rtol) copyto!(dy, kkt.krylov_solver.x) # Recover dx copyto!(dx, ξd) mul!(dx, kkt.A', dy, one(T), -one(T)) lmul!(kkt.D, dx) # TODO: iterative refinement (?) return nothing end ================================================ FILE: src/KKT/Krylov/sqd.jl ================================================ """ SQDSolver """ mutable struct SQDSolver{T,V,Ta,KS} <: AbstractKrylovSolver{T} # Problem data m::Int n::Int A::Ta # IPM-related workspace θ::Vector{T} regP::Vector{T} regD::Vector{T} # Krylov-related workspace Θp::Diagonal{T,V} Θp⁻¹::Diagonal{T,V} Θd::Diagonal{T,V} Θd⁻¹::Diagonal{T,V} ξp::V ξd::V # Krylov solver & related options atol::T rtol::T krylov_solver::KS # TODO: preconditioner end backend(kkt::SQDSolver) = "$(typeof(kkt.krylov_solver))" linear_system(kkt::SQDSolver) = "K2" function setup(A, ::K2, backend::Backend{KS,V}) where{KS<:_KRYLOV_SQD,V} Ta = typeof(A) T = eltype(A) T == eltype(V) || error("eltype(A)=$T incompatible with eltype of Krylov vector storage $V.") m, n = size(A) # Workspace θ = ones(T, n) regP = ones(T, n) regD = ones(T, m) Θp = Diagonal(V(undef, n)) Θp⁻¹ = Diagonal(V(undef, n)) Θd = Diagonal(V(undef, m)) Θd⁻¹ = Diagonal(V(undef, m)) ξp = V(undef, m) ξd = V(undef, n) # Allocate Krylov solver's workspace atol = sqrt(eps(T)) rtol = sqrt(eps(T)) krylov_solver = KS(m, n, V) return SQDSolver{T,V,Ta,typeof(krylov_solver)}( m, n, A, θ, regP, regD, Θp, Θp⁻¹, Θd, Θd⁻¹, ξp, ξd, atol, rtol, krylov_solver ) end function update!(kkt::SQDSolver, θ, regP, regD) copyto!(kkt.θ, θ) copyto!(kkt.regP, regP) copyto!(kkt.regD, regD) copyto!(kkt.Θp.diag, -(kkt.θ .+ kkt.regP)) copyto!(kkt.Θp⁻¹.diag, inv.(kkt.θ .+ kkt.regP)) # Θp⁻¹ will be negated by tricg/trimr copyto!(kkt.Θd.diag, kkt.regD) copyto!(kkt.Θd⁻¹.diag, inv.(kkt.regD)) return nothing end function solve!(dx, dy, kkt::SQDSolver{T}, ξp, ξd) where{T} copyto!(kkt.ξp, ξp) copyto!(kkt.ξd, ξd) # Solve the augmented system krylov_solve!(kkt.krylov_solver, kkt.A, kkt.ξp, kkt.ξd; M=kkt.Θd⁻¹, N=kkt.Θp⁻¹, atol=kkt.atol, rtol=kkt.rtol ) # Recover dx, dy copyto!(dx, kkt.krylov_solver.y) copyto!(dy, kkt.krylov_solver.x) # TODO: iterative refinement (?) return nothing end ================================================ FILE: src/KKT/LDLFactorizations/ldlfact.jl ================================================ module TlpLDLFactorizations using LinearAlgebra using SparseArrays using LDLFactorizations const LDLF = LDLFactorizations using ..KKT: AbstractKKTBackend, AbstractKKTSolver using ..KKT: AbstractKKTSystem, K1, K2 import ..KKT: setup, update!, solve!, backend, linear_system """ Backend LDLFactorizations backend for solving linear systems. See [`LDLFactSolver`](@ref) for further details. """ struct Backend <: AbstractKKTBackend end """ LDLFactSolver{T,S<:AbstractKKTSystem} [`LDLFactorizations.jl`](https://github.com/JuliaSmoothOptimizers/LDLFactorizations.jl)-based KKT solver. # Supported arithmetics * All arithmetics are supported # Supported systems * [`K2`](@ref) via ``LDLᵀ`` factorization # Examples To solve the augmented system with LDLFactorizations' ``LDL^{T}`` factorization: ```julia set_parameter(tlp_model, "KKT_Backend", Tulip.KKT.TlpLDLFact.Backend()) set_parameter(tlp_model, "KKT_System", Tulip.KKT.K2()) ``` """ mutable struct LDLFactSolver{T,S} <: AbstractKKTSolver{T} # Problem data m::Int n::Int A::SparseMatrixCSC{T,Int} # Workspace θ::Vector{T} # Diagonal scaling regP::Vector{T} # Primal regularization regD::Vector{T} # Dual regularization K::SparseMatrixCSC{T,Int} # KKT matrix F::LDLF.LDLFactorization{T,Int,Int,Int} # Factorization ξ::Vector{T} # RHS of KKT system end backend(::LDLFactSolver) = "LDLFactorizations" linear_system(::LDLFactSolver) = "Augmented system (K2)" # Convert A to sparse matrix if needed setup(A, system, backend::Backend) = setup(convert(SparseMatrixCSC, A), system, backend) function setup(A::SparseMatrixCSC{T,Int}, ::K2, ::Backend) where{T} m, n = size(A) θ = ones(T, n) regP = ones(T, n) regD = ones(T, m) ξ = zeros(T, m+n) K = [ spdiagm(0 => -θ) A'; spzeros(T, m, n) spdiagm(0 => ones(T, m)) ] # TODO: Symbolic factorization only F = LDLF.ldl_analyze(Symmetric(K)) return LDLFactSolver{T,K2}(m, n, A, θ, regP, regD, K, F, ξ) end function update!(kkt::LDLFactSolver{T,K2}, θ, regP, regD) where{T} m, n = kkt.m, kkt.n # Sanity checks length(θ) == n || throw(DimensionMismatch( "length(θ)=$(length(θ)) but KKT solver has n=$n." )) length(regP) == n || throw(DimensionMismatch( "length(regP)=$(length(regP)) but KKT solver has n=$n" )) length(regD) == m || throw(DimensionMismatch( "length(regD)=$(length(regD)) but KKT solver has m=$m" )) copyto!(kkt.θ, θ) copyto!(kkt.regP, regP) copyto!(kkt.regD, regD) # Update KKT matrix # K is stored as upper-triangular, and only its diagonal is changed @inbounds for j in 1:kkt.n k = kkt.K.colptr[1+j] - 1 kkt.K.nzval[k] = -kkt.θ[j] - regP[j] end @inbounds for i in 1:kkt.m k = kkt.K.colptr[1+kkt.n+i] - 1 kkt.K.nzval[k] = regD[i] end # Update factorization try LDLF.ldl_factorize!(Symmetric(kkt.K), kkt.F) catch err isa(err, LDLF.SQDException) && throw(PosDefException(-1)) rethrow(err) end return nothing end function solve!(dx, dy, kkt::LDLFactSolver{T,K2}, ξp, ξd) where{T} m, n = kkt.m, kkt.n # Setup right-hand side @views copyto!(kkt.ξ[1:n], ξd) @views copyto!(kkt.ξ[(n+1):end], ξp) # Solve augmented system # CHOLMOD doesn't have in-place solve, so this line will allocate LDLF.ldiv!(kkt.F, kkt.ξ) # Recover dx, dy @views copyto!(dx, kkt.ξ[1:n]) @views copyto!(dy, kkt.ξ[(n+1):end]) # TODO: iterative refinement return nothing end end # module ================================================ FILE: src/KKT/Test/test.jl ================================================ using Test using LinearAlgebra """ run_ls_tests(A, kkt; atol) """ function run_ls_tests( A::AbstractMatrix{T}, kkt::AbstractKKTSolver{T}; atol::T=sqrt(eps(T)) ) where{T} # Check that required methods are implemented @testset "Required methods" begin Tls = typeof(kkt) V = Vector{T} @test hasmethod(update!, Tuple{Tls, V, V, V}) @test hasmethod(solve!, Tuple{V, V, Tls, V, V}) end m, n = size(A) # Factorization/pre-conditionner update θ = ones(T, n) regP = ones(T, n) regD = ones(T, m) update!(kkt, θ, regP, regD) # Solve linear system ξp = ones(T, m) ξd = ones(T, n) dx = zeros(T, n) dy = zeros(T, m) solve!(dx, dy, kkt, ξp, ξd) # Check residuals rp = A * dx + regD .* dy - ξp rd = -dx .*(θ + regP) + A' * dy - ξd @testset "Residuals" begin @test norm(rp, Inf) <= atol @test norm(rd, Inf) <= atol end return nothing end ================================================ FILE: src/KKT/systems.jl ================================================ """ DefaultKKTSystem Default KKT system setting. Currently equivalent to [`K2`](@ref) """ struct DefaultKKTSystem <: AbstractKKTSystem end @doc raw""" K2 <: AbstractKKTSystem Augmented system ```math \begin{bmatrix} -(\Theta^{-1} + R_{p}) & A^{\top}\\ A & R_{d} \end{bmatrix} \begin{bmatrix} \Delta x\\ \Delta y \end{bmatrix} = \begin{bmatrix} \xi_d\\ \xi_p \end{bmatrix} ``` where * ``\Theta^{-1} = X^{-l}Z^{l} + X^{-u} Z^{u}`` * ``R_{p}, R_{d}`` are current primal and dual regularizations * ``\xi_{d}, \xi_{p}`` are given right-hand sides """ struct K2 <: AbstractKKTSystem end @doc raw""" K1 <: AbstractKKTSystem Normal equations system ```math \begin{array}{rl} \left( A (\Theta^{-1} + R_{p})^{-1} A^{\top} + R_{d} \right) \Delta y & = \xi_{p} + A (Θ^{-1} + R_{p})^{-1} \xi_{d}\\ \Delta x &= (Θ^{-1} + R_{p})^{-1} (A^{\top} \Delta y - \xi_{d}) \end{array} ``` where * ``\Theta^{-1} = X^{-l}Z^{l} + X^{-u} Z^{u}`` * ``R_{p}, R_{d}`` are current primal and dual regularizations * ``\xi_{d}, \xi_{p}`` are the augmented system's right-hand side """ struct K1 <: AbstractKKTSystem end ================================================ FILE: src/LinearAlgebra/LinearAlgebra.jl ================================================ module TLPLinearAlgebra using LinearAlgebra using SparseArrays export construct_matrix import ..Tulip.Factory """ construct_matrix(Ta, m, n, aI, aJ, aV) Construct matrix given matrix type `Ta`, size `m, n`, and data in COO format. """ function construct_matrix end function construct_matrix( ::Type{Matrix}, m::Int, n::Int, aI::Vector{Int}, aJ::Vector{Int}, aV::Vector{T} ) where{T} A = zeros(T, m, n) # TODO: may be more efficient to first sort indices so that # A is accessed in column-major order. for(i, j, v) in zip(aI, aJ, aV) A[i, j] = v end return A end construct_matrix( ::Type{SparseMatrixCSC}, m::Int, n::Int, aI::Vector{Int}, aJ::Vector{Int}, aV::Vector{T} ) where{T} = sparse(aI, aJ, aV, m, n) end # module ================================================ FILE: src/Presolve/Presolve.jl ================================================ Base.@kwdef mutable struct PresolveOptions{T} Level::Int = 1 # Presolve level TolerancePFeas::T = sqrt(eps(T)) # Primal feasibility tolerance ToleranceDFeas::T = sqrt(eps(T)) # Dual feasibility tolerance end """ PresolveTransformation{T} Abstract type for pre-solve transformations. """ abstract type PresolveTransformation{T} end """ PresolveData{T} Stores information about an LP in the form ``` min c'x + c0 s.t. lr ⩽ Ax ⩽ ur lc ⩽ x ⩽ uc ``` whose dual writes ``` max lr'y⁺ - ur'y⁻ + lc's⁺ - uc's⁻ s.t. A'y⁺ - A'y⁻ + s⁺ - s⁻ = c y⁺, y⁻, s⁺, s⁻ ⩾ 0 ``` """ mutable struct PresolveData{T} updated::Bool status::TerminationStatus options::PresolveOptions{T} # Original problem pb0::ProblemData{T} # Reduced problem # Nothing until the reduced problem is extracted pb_red::Union{Nothing, ProblemData{T}} solution::Solution{T} # only used if presolve solves the problem # Presolved data # Active rows and columns rowflag::Vector{Bool} colflag::Vector{Bool} # Non-zeros in rows and columns nzrow::Vector{Int} nzcol::Vector{Int} # Objective objsense::Bool obj::Vector{T} obj0::T # Current number of constraints/variables in presolved problem nrow::Int ncol::Int # Primal bounds lrow::Vector{T} urow::Vector{T} lcol::Vector{T} ucol::Vector{T} # Dual bounds ly::Vector{T} uy::Vector{T} ls::Vector{T} us::Vector{T} # Scaling row_scaling::Vector{T} col_scaling::Vector{T} # TODO: objective and RHS scaling # Old <-> new index mapping # Instantiated only after pre-solve is performed new_con_idx::Vector{Int} new_var_idx::Vector{Int} old_con_idx::Vector{Int} old_var_idx::Vector{Int} # Singletons row_singletons::Vector{Int} # Row singletons free_col_singletons::Vector{Int} # (implied) free column singletons # TODO: set of transformations for pre-post crush ops::Vector{PresolveTransformation{T}} function PresolveData( pb::ProblemData{T}, options::PresolveOptions{T}=PresolveOptions{T}() ) where{T} ps = new{T}() ps.updated = false ps.status = Trm_Unknown ps.options = options ps.pb0 = pb ps.pb_red = nothing ps.solution = Solution{T}(pb.ncon, pb.nvar) ps.nrow = pb.ncon ps.ncol = pb.nvar # All rows and columns are active ps.rowflag = trues(ps.nrow) ps.colflag = trues(ps.ncol) # Number of non-zeros in rows/columns ps.nzrow = zeros(Int, ps.nrow) ps.nzcol = zeros(Int, ps.ncol) for (j, col) in enumerate(pb.acols) for (i, aij) in zip(col.nzind, col.nzval) ps.nzcol[j] += !iszero(aij) ps.nzrow[i] += !iszero(aij) end end # Objective ps.objsense = pb.objsense if pb.objsense ps.obj = copy(pb.obj) ps.obj0 = pb.obj0 else # Maximization problem: negate the objective for pre-solve # This will be undone when extracting the reduced problem ps.obj = -copy(pb.obj) ps.obj0 = -pb.obj0 end # Copy primal bounds ps.lrow = copy(pb.lcon) ps.urow = copy(pb.ucon) ps.lcol = copy(pb.lvar) ps.ucol = copy(pb.uvar) # Set dual bounds ps.ly = Vector{T}(undef, ps.nrow) ps.uy = Vector{T}(undef, ps.nrow) ps.ls = Vector{T}(undef, ps.ncol) ps.us = Vector{T}(undef, ps.ncol) for (i, (lc, uc)) in enumerate(zip(ps.lrow, ps.urow)) ps.ly[i] = (uc == T( Inf)) ? zero(T) : T(-Inf) ps.uy[i] = (lc == T(-Inf)) ? zero(T) : T( Inf) end for (j, (lv, uv)) in enumerate(zip(ps.lcol, ps.ucol)) ps.ls[j] = (uv == T( Inf)) ? zero(T) : T(-Inf) ps.us[j] = (lv == T(-Inf)) ? zero(T) : T( Inf) end # Scalings ps.row_scaling = ones(T, ps.nrow) ps.col_scaling = ones(T, ps.ncol) # Index mappings ps.new_con_idx = Int[] ps.new_var_idx = Int[] ps.old_con_idx = Int[] ps.old_var_idx = Int[] # Singletons ps.row_singletons = Int[] ps.free_col_singletons = Int[] ps.ops = PresolveTransformation{T}[] return ps end end # Extract pre-solved problem data, to be passed to the IPM solver function extract_reduced_problem!(ps::PresolveData{T}) where{T} pb = ProblemData{T}() pb.ncon = sum(ps.rowflag) pb.nvar = sum(ps.colflag) pb.objsense = ps.objsense if pb.objsense pb.obj0 = ps.obj0 pb.obj = ps.obj[ps.colflag] else pb.obj0 = -ps.obj0 pb.obj = -ps.obj[ps.colflag] end # Primal bounds pb.lvar = ps.lcol[ps.colflag] pb.uvar = ps.ucol[ps.colflag] pb.lcon = ps.lrow[ps.rowflag] pb.ucon = ps.urow[ps.rowflag] # Extract new rows pb.arows = Vector{Row{T}}(undef, pb.ncon) inew = 0 for (iold, row) in enumerate(ps.pb0.arows) ps.rowflag[iold] || continue inew += 1 # Compute new row rind = Vector{Int}(undef, ps.nzrow[iold]) rval = Vector{T}(undef, ps.nzrow[iold]) k = 0 for (jold, aij) in zip(row.nzind, row.nzval) ps.colflag[jold] || continue iszero(aij) && continue # Set new coefficient k += 1 rind[k] = ps.new_var_idx[jold] rval[k] = aij end # Set new row pb.arows[inew] = Row{T}(rind, rval) end # Extract new columns pb.acols = Vector{Col{T}}(undef, pb.nvar) jnew = 0 for (jold, col) in enumerate(ps.pb0.acols) ps.colflag[jold] || continue jnew += 1 # Compute new row cind = Vector{Int}(undef, ps.nzcol[jold]) cval = Vector{T}(undef, ps.nzcol[jold]) k = 0 for (iold, aij) in zip(col.nzind, col.nzval) ps.rowflag[iold] || continue iszero(aij) && continue # Set new coefficient k += 1 cind[k] = ps.new_con_idx[iold] cval[k] = aij end # Set new column pb.acols[jnew] = Col{T}(cind, cval) end # Variable and constraint names # TODO: we don't need these pb.var_names = ps.pb0.var_names[ps.colflag] pb.con_names = ps.pb0.con_names[ps.rowflag] # Scaling rscale = zeros(T, ps.nrow) cscale = zeros(T, ps.ncol) # Compute norm of each row and column # TODO: use a parameter p and do norm(.., p) p = 2 for (i, row) in enumerate(pb.arows) r = norm(row.nzval, p) rscale[i] = r > zero(T) ? r : one(T) end for (j, col) in enumerate(pb.acols) r = norm(col.nzval, p) cscale[j] = r > zero(T) ? r : one(T) end map!(sqrt, cscale, cscale) map!(sqrt, rscale, rscale) # Rows for (i, row) in enumerate(pb.arows) # Scale row coefficients for (k, j) in enumerate(row.nzind) row.nzval[k] /= (rscale[i] * cscale[j]) end # Scale row bounds pb.lcon[i] /= rscale[i] pb.ucon[i] /= rscale[i] end # Columns for (j, col) in enumerate(pb.acols) # Scale column coefficients for (k, i) in enumerate(col.nzind) col.nzval[k] /= (rscale[i] * cscale[j]) end # Scale objective and variable bounds pb.obj[j] /= cscale[j] pb.lvar[j] *= cscale[j] pb.uvar[j] *= cscale[j] end # Record scaling @debug "Scaling info" extrema(rscale) extrema(cscale) ps.row_scaling = rscale ps.col_scaling = cscale # Done ps.pb_red = pb return nothing end include("empty_row.jl") include("empty_column.jl") include("fixed_variable.jl") include("row_singleton.jl") include("forcing_row.jl") include("free_column_singleton.jl") include("dominated_column.jl") """ postsolve! Perform post-solve. """ function postsolve!(sol::Solution{T}, sol_::Solution{T}, ps::PresolveData{T}) where{T} # Check dimensions (sol_.m, sol_.n) == (ps.nrow, ps.ncol) || error( "Inner solution has size $((sol_.m, sol_.n)) but presolved problem has size $((ps.nrow, ps.ncol))" ) (sol.m, sol.n) == (ps.pb0.ncon, ps.pb0.nvar) || error( "Solution has size $((sol.m, sol.n)) but original problem has size $((ps.pb0.ncon, ps.pb0.nvar))" ) # Copy solution status and objective values sol.primal_status = sol_.primal_status sol.dual_status = sol_.dual_status sol.is_primal_ray = sol_.is_primal_ray sol.is_dual_ray = sol_.is_dual_ray sol.z_primal = sol_.z_primal sol.z_dual = sol_.z_dual # Extract and un-scale inner solution components # TODO: create a PresolveTransformation for scaling for (j_, j) in enumerate(ps.old_var_idx) sol.x[j] = sol_.x[j_] / ps.col_scaling[j_] sol.s_lower[j] = sol_.s_lower[j_] * ps.col_scaling[j_] sol.s_upper[j] = sol_.s_upper[j_] * ps.col_scaling[j_] end for (i_, i) in enumerate(ps.old_con_idx) sol.y_lower[i] = sol_.y_lower[i_] / ps.row_scaling[i_] sol.y_upper[i] = sol_.y_upper[i_] / ps.row_scaling[i_] end # Reverse transformations for op in Iterators.reverse(ps.ops) postsolve!(sol, op) end # Compute row primals for (i, row) in enumerate(ps.pb0.arows) sol.Ax[i] = zero(T) for (j, aij) in zip(row.nzind, row.nzval) sol.Ax[i] += aij * sol.x[j] end end # Done return nothing end """ presolve(pb::ProblemData) Perform pre-solve. """ function presolve!(ps::PresolveData{T}) where{T} # Check bound consistency on all rows/columns st = bounds_consistency_checks!(ps) ps.status == Trm_PrimalInfeasible && return ps.status # I. Remove all fixed variables, empty rows and columns # remove_fixed_variables!(ps) remove_empty_rows!(ps) remove_empty_columns!(ps) # TODO: check status for potential early return ps.status == Trm_Unknown || return ps.status # Identify row singletons ps.row_singletons = [i for (i, nz) in enumerate(ps.nzrow) if ps.rowflag[i] && nz == 1] # II. Passes ps.updated = true npasses = 0 # TODO: maximum number of passes while ps.updated && ps.status == Trm_Unknown npasses += 1 ps.updated = false @debug "Presolve pass $npasses" ps.nrow ps.ncol bounds_consistency_checks!(ps) ps.status == Trm_Unknown || return ps.status remove_empty_columns!(ps) ps.status == Trm_Unknown || return ps.status # Remove all fixed variables # TODO: remove empty variables as well remove_row_singletons!(ps) ps.status == Trm_Unknown || return ps.status remove_fixed_variables!(ps) ps.status == Trm_Unknown || return ps.status # Remove forcing & dominated constraints remove_row_singletons!(ps) ps.status == Trm_Unknown || return ps.status remove_forcing_rows!(ps) ps.status == Trm_Unknown || return ps.status # Remove free and implied free column singletons remove_row_singletons!(ps) ps.status == Trm_Unknown || return ps.status remove_free_column_singletons!(ps) ps.status == Trm_Unknown || return ps.status # TODO: remove column singleton with doubleton equation # Dual reductions remove_row_singletons!(ps) ps.status == Trm_Unknown || return ps.status remove_dominated_columns!(ps) ps.status == Trm_Unknown || return ps.status end remove_empty_columns!(ps) @debug("Presolved problem info", ps.pb0.ncon, ps.nrow, ps.pb0.nvar, ps.ncol, sum(ps.nzcol[ps.colflag]), sum(ps.nzrow[ps.rowflag]) ) # TODO: check problem dimensions and declare optimality if problem is empty if ps.nrow == 0 && ps.ncol == 0 # Problem is empty: declare optimality now ps.status = Trm_Optimal # Resize solution resize!(ps.solution, 0, 0) ps.solution.primal_status = Sln_Optimal ps.solution.dual_status = Sln_Optimal ps.solution.is_primal_ray = false ps.solution.is_dual_ray = false ps.solution.z_primal = ps.obj0 ps.solution.z_dual = ps.obj0 end # Old <-> new index mapping compute_index_mapping!(ps) # TODO: extract reduced problem (?) # Done. return ps.status end function compute_index_mapping!(ps::PresolveData) ps.new_con_idx = Vector{Int}(undef, ps.pb0.ncon) ps.new_var_idx = Vector{Int}(undef, ps.pb0.nvar) ps.old_con_idx = Vector{Int}(undef, ps.nrow) ps.old_var_idx = Vector{Int}(undef, ps.ncol) inew = 0 for iold in 1:ps.pb0.ncon if ps.rowflag[iold] inew += 1 ps.new_con_idx[iold] = inew ps.old_con_idx[inew] = iold else ps.new_con_idx[iold] = 0 end end jnew = 0 for jold in 1:ps.pb0.nvar if ps.colflag[jold] jnew += 1 ps.new_var_idx[jold] = jnew ps.old_var_idx[jnew] = jold else ps.new_var_idx[jold] = 0 end end return nothing end """ bounds_consistency_checks(ps) Check that all primal & dual bounds are consistent. TODO: If not, declare primal/dual infeasibility and extract ray. """ function bounds_consistency_checks!(ps::PresolveData{T}) where{T} # Check primal bounds for (i, (l, u)) in enumerate(zip(ps.lrow, ps.urow)) if ps.rowflag[i] && l > u # Problem is primal infeasible @debug "Row $i is primal infeasible" ps.status = Trm_PrimalInfeasible ps.updated = true # Resize problem compute_index_mapping!(ps) resize!(ps.solution, ps.nrow, ps.ncol) ps.solution.x .= zero(T) ps.solution.y_lower .= zero(T) ps.solution.y_upper .= zero(T) ps.solution.s_lower .= zero(T) ps.solution.s_upper .= zero(T) # Farkas ray: y⁺_i = y⁻_i = 1 (any > 0 value works) ps.solution.primal_status = Sln_Unknown ps.solution.dual_status = Sln_InfeasibilityCertificate ps.solution.is_primal_ray = false ps.solution.is_dual_ray = true ps.solution.z_primal = ps.solution.z_dual = T(Inf) i_ = ps.new_con_idx[i] ps.solution.y_lower[i_] = one(T) ps.solution.y_upper[i_] = one(T) return end end for (j, (l, u)) in enumerate(zip(ps.lcol, ps.ucol)) if ps.colflag[j] && l > u # Primal is primal infeasible @debug "Column $j is primal infeasible" ps.status = Trm_PrimalInfeasible ps.updated = true # Resize problem compute_index_mapping!(ps) resize!(ps.solution, ps.nrow, ps.ncol) ps.solution.x .= zero(T) ps.solution.y_lower .= zero(T) ps.solution.y_upper .= zero(T) ps.solution.s_lower .= zero(T) ps.solution.s_upper .= zero(T) # Farkas ray: y⁺_i = y⁻_i = 1 (any > 0 value works) ps.solution.primal_status = Sln_Unknown ps.solution.dual_status = Sln_InfeasibilityCertificate ps.solution.is_primal_ray = false ps.solution.is_dual_ray = true ps.solution.z_primal = ps.solution.z_dual = T(Inf) j_ = ps.new_var_idx[j] ps.solution.s_lower[j_] = one(T) ps.solution.s_upper[j_] = one(T) return end end # TODO: Check dual bounds return nothing end """ remove_empty_rows!(ps::PresolveData) Remove all empty rows. Called once at the beginning of the presolve procedure. If an empty row is created later, it is removed on the spot. """ function remove_empty_rows!(ps::PresolveData{T}) where{T} nempty = 0 for i in 1:ps.pb0.ncon (ps.rowflag[i] && (ps.nzrow[i] == 0)) || continue @debug "Remove empty row $i" remove_empty_row!(ps, i) end return nothing end """ remove_empty_columns!(ps::PresolveData) Remove all empty columns. Called once at the beginning of the presolve procedure. If an empty column is created later, it is removed on the spot. """ function remove_empty_columns!(ps::PresolveData{T}) where{T} for j in 1:ps.pb0.nvar remove_empty_column!(ps, j) ps.status == Trm_Unknown || break end return nothing end """ remove_fixed_variables!(ps::PresolveData) Remove all fixed variables. """ function remove_fixed_variables!(ps::PresolveData{T}) where{T} for (j, flag) in enumerate(ps.colflag) flag || continue remove_fixed_variable!(ps, j) end return nothing end function remove_row_singletons!(ps::PresolveData{T}) where{T} nsingletons = 0 for i in ps.row_singletons remove_row_singleton!(ps, i) end ps.row_singletons = Int[] return nothing end """ remove_forcing_rows! Remove forcing and dominated row """ function remove_forcing_rows!(ps::PresolveData) for (i, flag) in enumerate(ps.rowflag) flag && remove_forcing_row!(ps, i) end return nothing end """ remove_free_column_singletons!(ps) """ function remove_free_column_singletons!(ps::PresolveData) for (j, flag) in enumerate(ps.colflag) remove_free_column_singleton!(ps, j) end return nothing end function remove_dominated_columns!(ps::PresolveData{T}) where{T} # Strengthen dual bounds with column singletons for (j, (l, u)) in enumerate(zip(ps.lcol, ps.ucol)) (ps.colflag[j] && ps.nzcol[j] == 1) || continue col = ps.pb0.acols[j] # Find non-zero index nz = 0 i, aij = 0, zero(T) for (i_, a_) in zip(col.nzind, col.nzval) if ps.rowflag[i_] && !iszero(a_) nz += 1; nz <= 1 || break i = i_ aij = a_ end end nz == 1 || (@error "Expected singleton but column $j has $nz non-zeros"; continue) iszero(aij) && continue # empty column # Strengthen dual bounds #= =# cj = ps.obj[j] y_ = cj / aij if !isfinite(l) && !isfinite(u) # Free variable. Should not happen but handle anyway # TODO elseif isfinite(l) && !isfinite(u) # Lower-bounded variable: `aij * yi ≤ cj` if aij > zero(T) # yi ≤ cj / aij @debug "Col $j forces y$i <= $y_" ps.uy[i] = min(ps.uy[i], y_) else # yi ≥ cj / aij @debug "Col $j forces y$i >= $y_" ps.ly[i] = max(ps.ly[i], y_) end elseif !isfinite(l) && isfinite(u) # Upper-bounded variable: `aij * yi ≥ cj` if aij > zero(T) # yi ≥ cj / aij @debug "Col $j forces y$i >= $y_" ps.ly[i] = max(ps.ly[i], y_) else # yi ≤ cj / aij @debug "Col $j forces y$i <= $y_" ps.uy[i] = min(ps.uy[i], y_) end end # TODO: dual feasibility check (?) end for (j, flag) in enumerate(ps.colflag) remove_dominated_column!(ps, j) ps.status == Trm_Unknown || break end return nothing end ================================================ FILE: src/Presolve/dominated_column.jl ================================================ struct DominatedColumn{T} <: PresolveTransformation{T} j::Int x::T # Primal value cj::T # Objective col::Col{T} # Column end function remove_dominated_column!(ps::PresolveData{T}, j::Int; tol::T=100*sqrt(eps(T))) where{T} ps.colflag[j] || return nothing # Compute implied bounds on reduced cost: `ls ≤ s ≤ us` ls = us = zero(T) col = ps.pb0.acols[j] for (i, aij) in zip(col.nzind, col.nzval) (ps.rowflag[i] && !iszero(aij)) || continue ls += aij * ( (aij >= zero(T)) ? ps.ly[i] : ps.uy[i] ) us += aij * ( (aij >= zero(T)) ? ps.uy[i] : ps.ly[i] ) end # Check if column is dominated cj = ps.obj[j] if cj - us > tol # Reduced cost is always positive => fix to lower bound (or problem is unbounded) lb = ps.lcol[j] @debug "Fixing dominated column $j to its lower bound $lb" if !isfinite(lb) # Problem is dual infeasible @debug "Column $j is (lower) unbounded" ps.status = Trm_DualInfeasible ps.updated = true # Resize problem compute_index_mapping!(ps) resize!(ps.solution, ps.nrow, ps.ncol) ps.solution.x .= zero(T) ps.solution.y_lower .= zero(T) ps.solution.y_upper .= zero(T) ps.solution.s_lower .= zero(T) ps.solution.s_upper .= zero(T) # Unbounded ray: xj = -1 ps.solution.primal_status = Sln_InfeasibilityCertificate ps.solution.dual_status = Sln_Unknown ps.solution.is_primal_ray = true ps.solution.is_dual_ray = false ps.solution.z_primal = ps.solution.z_dual = -T(Inf) j_ = ps.new_var_idx[j] ps.solution.x[j_] = -one(T) return nothing end # Update objective ps.obj0 += cj * lb # Extract column and update rows col_ = Col{T}(Int[], T[]) for (i, aij) in zip(col.nzind, col.nzval) ps.rowflag[i] || continue push!(col_.nzind, i) push!(col_.nzval, aij) # Update bounds and non-zeros ps.lrow[i] -= aij * lb ps.urow[i] -= aij * lb ps.nzrow[i] -= 1 ps.nzrow[i] == 1 && push!(ps.row_singletons, i) end # Remove variable from problem push!(ps.ops, DominatedColumn(j, lb, cj, col_)) ps.colflag[j] = false ps.ncol -= 1 ps.updated = true elseif cj - ls < -tol # Reduced cost is always negative => fix to upper bound (or problem is unbounded) ub = ps.ucol[j] if !isfinite(ub) # Problem is unbounded @debug "Column $j is (upper) unbounded" ps.status = Trm_DualInfeasible ps.updated = true # Resize solution compute_index_mapping!(ps) resize!(ps.solution, ps.nrow, ps.ncol) ps.solution.x .= zero(T) ps.solution.y_lower .= zero(T) ps.solution.y_upper .= zero(T) ps.solution.s_lower .= zero(T) ps.solution.s_upper .= zero(T) # Unbounded ray: xj = -1 ps.solution.primal_status = Sln_InfeasibilityCertificate ps.solution.dual_status = Sln_Unknown ps.solution.is_primal_ray = true ps.solution.is_dual_ray = false ps.solution.z_primal = ps.solution.z_dual = -T(Inf) j_ = ps.new_var_idx[j] ps.solution.x[j_] = one(T) return nothing end @debug "Fixing dominated column $j to its upper bound $ub" # Update objective ps.obj0 += cj * ub # Extract column and update rows col_ = Col{T}(Int[], T[]) for (i, aij) in zip(col.nzind, col.nzval) ps.rowflag[i] || continue push!(col_.nzind, i) push!(col_.nzval, aij) # Update bounds and non-zeros ps.lrow[i] -= aij * ub ps.urow[i] -= aij * ub ps.nzrow[i] -= 1 ps.nzrow[i] == 1 && push!(ps.row_singletons, i) end # Remove variable from problem push!(ps.ops, DominatedColumn(j, ub, cj, col_)) ps.colflag[j] = false ps.ncol -= 1 ps.updated = true end return nothing end function postsolve!(sol::Solution{T}, op::DominatedColumn{T}) where{T} # Primal value sol.x[op.j] = op.x # Reduced cost s = sol.is_dual_ray ? zero(T) : op.cj for (i, aij) in zip(op.col.nzind, op.col.nzval) s -= aij * (sol.y_lower[i] - sol.y_upper[i]) end sol.s_lower[op.j] = pos_part(s) sol.s_upper[op.j] = neg_part(s) return nothing end ================================================ FILE: src/Presolve/empty_column.jl ================================================ struct EmptyColumn{T} <: PresolveTransformation{T} j::Int # variable index x::T # Variable value s::T # Reduced cost end function remove_empty_column!(ps::PresolveData{T}, j::Int) where{T} # Sanity check (ps.colflag[j] && (ps.nzcol[j] == 0)) || return nothing # Remove column lb, ub = ps.lcol[j], ps.ucol[j] cj = ps.obj[j] @debug "Removing empty column $j" cj lb ub ϵ = ps.options.ToleranceDFeas if cj > ϵ if isfinite(lb) # Set variable to lower bound # Update objective constant ps.obj0 += lb * cj push!(ps.ops, EmptyColumn(j, lb, cj)) else # Problem is dual infeasible @debug "Column $j is (lower) unbounded" ps.status = Trm_DualInfeasible ps.updated = true # Resize problem compute_index_mapping!(ps) resize!(ps.solution, ps.nrow, ps.ncol) ps.solution.x .= zero(T) ps.solution.y_lower .= zero(T) ps.solution.y_upper .= zero(T) ps.solution.s_lower .= zero(T) ps.solution.s_upper .= zero(T) # Unbounded ray: xj = -1 ps.solution.primal_status = Sln_InfeasibilityCertificate ps.solution.dual_status = Sln_Unknown ps.solution.is_primal_ray = true ps.solution.is_dual_ray = false ps.solution.z_primal = ps.solution.z_dual = -T(Inf) j_ = ps.new_var_idx[j] ps.solution.x[j_] = -one(T) return nothing end elseif cj < -ϵ if isfinite(ub) # Set variable to upper bound # Update objective constant ps.obj0 += ub * cj push!(ps.ops, EmptyColumn(j, ub, cj)) else # Problem is dual infeasible @debug "Column $j is (upper) unbounded" ps.status = Trm_DualInfeasible ps.updated = true # Resize problem compute_index_mapping!(ps) resize!(ps.solution, ps.nrow, ps.ncol) ps.solution.x .= zero(T) ps.solution.y_lower .= zero(T) ps.solution.y_upper .= zero(T) ps.solution.s_lower .= zero(T) ps.solution.s_upper .= zero(T) # Unbounded ray: xj = 1 ps.solution.primal_status = Sln_InfeasibilityCertificate ps.solution.dual_status = Sln_Unknown ps.solution.is_primal_ray = true ps.solution.is_dual_ray = false ps.solution.z_primal = ps.solution.z_dual = -T(Inf) j_ = ps.new_var_idx[j] ps.solution.x[j_] = one(T) return end else # Any feasible value works if isfinite(lb) push!(ps.ops, EmptyColumn(j, lb, zero(T))) elseif isfinite(ub) push!(ps.ops, EmptyColumn(j, ub, zero(T))) else # Free variable with zero coefficient and empty column push!(ps.ops, EmptyColumn(j, zero(T), zero(T))) end end # Book=keeping ps.colflag[j] = false ps.updated = true ps.ncol -= 1 return nothing end function postsolve!(sol::Solution{T}, op::EmptyColumn{T}) where{T} sol.x[op.j] = op.x sol.s_lower[op.j] = pos_part(op.s) sol.s_upper[op.j] = neg_part(op.s) return nothing end ================================================ FILE: src/Presolve/empty_row.jl ================================================ # TODO: this is redundant with forcing constraint checks # => an empty row is automatically redundant or infeasible struct EmptyRow{T} <: PresolveTransformation{T} i::Int # row index y::T # dual multiplier end function remove_empty_row!(ps::PresolveData{T}, i::Int) where{T} # Sanity checks (ps.rowflag[i] && ps.nzrow[i] == 0) || return nothing # Check bounds lb, ub = ps.lrow[i], ps.urow[i] ϵ = ps.options.TolerancePFeas if ub < -ϵ # Infeasible @debug "Row $i is primal infeasible" ps.status = Trm_PrimalInfeasible ps.updated = true # Resize problem compute_index_mapping!(ps) resize!(ps.solution, ps.nrow, ps.ncol) ps.solution.x .= zero(T) ps.solution.y_lower .= zero(T) ps.solution.y_upper .= zero(T) ps.solution.s_lower .= zero(T) ps.solution.s_upper .= zero(T) # Farkas ray: y⁺_i = 1 (any > 0 value works) ps.solution.primal_status = Sln_Unknown ps.solution.dual_status = Sln_InfeasibilityCertificate ps.solution.is_primal_ray = false ps.solution.is_dual_ray = true ps.solution.z_primal = ps.solution.z_dual = T(Inf) i_ = ps.new_con_idx[i] ps.solution.y_upper[i_] = one(T) return elseif lb > ϵ @debug "Row $i is primal infeasible" ps.status = Trm_PrimalInfeasible ps.updated = true # Resize problem compute_index_mapping!(ps) resize!(ps.solution, ps.nrow, ps.ncol) ps.solution.x .= zero(T) ps.solution.y_lower .= zero(T) ps.solution.y_upper .= zero(T) ps.solution.s_lower .= zero(T) ps.solution.s_upper .= zero(T) # Farkas ray: y⁺_i = 1 (any > 0 value works) ps.solution.primal_status = Sln_Unknown ps.solution.dual_status = Sln_InfeasibilityCertificate ps.solution.is_primal_ray = false ps.solution.is_dual_ray = true ps.solution.z_primal = ps.solution.z_dual = T(Inf) i_ = ps.new_con_idx[i] ps.solution.y_lower[i_] = one(T) return else push!(ps.ops, EmptyRow(i, zero(T))) end # Book-keeping ps.updated = true ps.rowflag[i] = false ps.nrow -= 1 end function postsolve!(sol::Solution{T}, op::EmptyRow{T}) where{T} sol.y_lower[op.i] = pos_part(op.y) sol.y_upper[op.i] = neg_part(op.y) return nothing end ================================================ FILE: src/Presolve/fixed_variable.jl ================================================ struct FixedVariable{T} <: PresolveTransformation{T} j::Int # variable index x::T # primal value c::T # current objective coeff col::Col{T} # current column end function remove_fixed_variable!(ps::PresolveData, j::Int) ps.colflag[j] || return nothing # Column was already removed # Check bounds lb, ub = ps.lcol[j], ps.ucol[j] # TODO: use tolerance if lb == ub @debug "Fix variable $j to $lb" col = ps.pb0.acols[j] cj = ps.obj[j] # Remove column ps.colflag[j] = false ps.ncol -= 1 ps.updated = true # TODO: make this more efficient push!(ps.ops, FixedVariable(j, lb, cj, Col( [i for i in col.nzind if ps.rowflag[i]], [aij for (i, aij) in zip(col.nzind, col.nzval) if ps.rowflag[i]] ))) # Update objective constant ps.obj0 += cj * lb # Update rows for (i, aij) in zip(col.nzind, col.nzval) ps.rowflag[i] || continue # This row is no longer in the problem iszero(aij) && continue # Skip if coefficient is zero # Update row bounds ps.lrow[i] -= aij * lb ps.urow[i] -= aij * lb ps.nzrow[i] -= 1 # Check row if ps.nzrow[i] == 0 remove_empty_row!(ps, i) elseif ps.nzrow == 1 push!(ps.row_singletons, i) end end # row update end # Done return nothing end function postsolve!(sol::Solution{T}, op::FixedVariable{T}) where{T} sol.x[op.j] = op.x s = sol.is_dual_ray ? zero(T) : op.c for (i, aij) in zip(op.col.nzind, op.col.nzval) s -= aij * (sol.y_lower[i] - sol.y_upper[i]) end sol.s_lower[op.j] = pos_part(s) sol.s_upper[op.j] = neg_part(s) return nothing end ================================================ FILE: src/Presolve/forcing_row.jl ================================================ struct ForcingRow{T} <: PresolveTransformation{T} i::Int # Row index at_lower::Bool # Whether row is forced to its lower bound (false means upper) row::Row{T} # Row cols::Vector{Col{T}} # Columns of variables in forcing row xs::Vector{T} # Primal values of variables in the row (upper or lower bound) cs::Vector{T} # Objective coeffs of variables in forcing row end struct DominatedRow{T} <: PresolveTransformation{T} i::Int # Row index end function remove_forcing_row!(ps::PresolveData{T}, i::Int) where{T} ps.rowflag[i] || return ps.nzrow[i] == 1 && return # skip row singletons # Implied row bounds row = ps.pb0.arows[i] l_ = u_ = zero(T) for (j, aij) in zip(row.nzind, row.nzval) ps.colflag[j] || continue if aij < zero(T) l_ += aij * ps.ucol[j] u_ += aij * ps.lcol[j] else l_ += aij * ps.lcol[j] u_ += aij * ps.ucol[j] end isfinite(l_) || isfinite(u_) || break # infinite bounds end l, u = ps.lrow[i], ps.urow[i] if l <= l_ <= u_ <= u # Constraint is dominated @debug "Row $i is dominated" ps.rowflag[i] = false ps.updated = true ps.nrow -= 1 push!(ps.ops, DominatedRow{T}(i)) # Update column non-zeros for (j, aij) in zip(row.nzind, row.nzval) ps.colflag[j] || continue ps.nzcol[j] -= !iszero(aij) end elseif l_ == u # Row is forced to its upper bound @debug "Row $i is forced to its upper bound" (l, u) (l_, u_) # Record tranformation row_ = Row( [ j for (j, aij) in zip(row.nzind, row.nzval) if ps.colflag[j]], [aij for (j, aij) in zip(row.nzind, row.nzval) if ps.colflag[j]] ) cols_ = Col{T}[] xs = T[] cs = T[] for (j, aij) in zip(row.nzind, row.nzval) ps.colflag[j] || continue # Extract column j col = ps.pb0.acols[j] col_ = Col{T}(Int[], T[]) # Fix variable to its bound # TODO: put this in a function and mutualize with fixed variables if aij > 0 # Set xj to its lower bound xj_ = ps.lcol[j] else # Set xj to its upper bound xj_ = ps.ucol[j] end for (k, akj) in zip(col.nzind, col.nzval) ps.rowflag[k] || continue # Update column j push!(col_.nzind, k) push!(col_.nzval, akj) # Update row k ps.nzrow[k] -= 1 ps.lrow[k] -= akj * xj_ ps.urow[k] -= akj * xj_ # ps.nzrow[k] == 0 && remove_empty_row!(ps, k) ps.nzrow[k] == 1 && push!(ps.row_singletons, k) end cj = ps.obj[j] push!(cols_, col_) push!(xs, xj_) push!(cs, cj) # Remove variable from problem ps.colflag[j] = false ps.ncol -= 1 end # Record transformation push!(ps.ops, ForcingRow(i, true, row_, cols_, xs, cs)) # Book-keeping ps.rowflag[i] = false ps.nrow -= 1 ps.updated = true elseif u_ == l # Row is forced to its lower bound @debug "Row $i is forced to its lower bound" (l, u) (l_, u_) # Record tranformation row_ = Row( [ j for (j, aij) in zip(row.nzind, row.nzval) if ps.colflag[j]], [aij for (j, aij) in zip(row.nzind, row.nzval) if ps.colflag[j]] ) cols_ = Col{T}[] xs = T[] cs = T[] for (j, aij) in zip(row.nzind, row.nzval) ps.colflag[j] || continue # Extract column j col = ps.pb0.acols[j] col_ = Col{T}(Int[], T[]) # Fix variable to its bound # TODO: put this in a function and mutualize with fixed variables if aij > 0 # Set xj to its upper bound xj_ = ps.ucol[j] else # Set xj to its lower bound xj_ = ps.lcol[j] end for (k, akj) in zip(col.nzind, col.nzval) ps.rowflag[k] || continue # Update column j push!(col_.nzind, k) push!(col_.nzval, akj) # Update row k ps.nzrow[k] -= 1 ps.lrow[k] -= akj * xj_ ps.urow[k] -= akj * xj_ # ps.nzrow[k] == 0 && remove_empty_row!(ps, k) ps.nzrow[k] == 1 && push!(ps.row_singletons, k) end cj = ps.obj[j] push!(cols_, col_) push!(xs, xj_) push!(cs, cj) # Remove variable from problem ps.colflag[j] = false ps.ncol -= 1 end # Record transformation push!(ps.ops, ForcingRow(i, false, row_, cols_, xs, cs)) # Book-keeping ps.rowflag[i] = false ps.nrow -= 1 ps.updated = true end # TODO: handle infeasible row cases return nothing end function postsolve!(sol::Solution{T}, op::DominatedRow{T}) where{T} sol.y_lower[op.i] = zero(T) sol.y_upper[op.i] = zero(T) return nothing end # TODO: postsolve of forcing rows function postsolve!(sol::Solution{T}, op::ForcingRow{T}) where{T} # Primal for (j, xj) in zip(op.row.nzind, op.xs) sol.x[j] = xj end # Dual z = similar(op.cs) for (l, (j, cj, col)) in enumerate(zip(op.row.nzind, op.cs, op.cols)) z[l] = cj for (k, akj) in zip(col.nzind, col.nzval) z[l] -= akj * (sol.y_lower[k] - sol.y_upper[k]) end end # First, compute yi y = op.at_lower ? maximum(z ./ op.row.nzval) : minimum(z ./ op.row.nzval) sol.y_lower[op.i] = pos_part(y) sol.y_upper[op.i] = neg_part(y) # Extract reduced costs for (j, aij, zj) in zip(op.row.nzind, op.row.nzval, z) s = zj - aij * y sol.s_lower[j] = pos_part(s) sol.s_upper[j] = neg_part(s) end return nothing end ================================================ FILE: src/Presolve/free_column_singleton.jl ================================================ struct FreeColumnSingleton{T} <: PresolveTransformation{T} i::Int # Row index j::Int # Column index l::T # Row lower bound u::T # Row upper bound aij::T y::T # Dual variable row::Row{T} end function remove_free_column_singleton!(ps::PresolveData{T}, j::Int) where{T} ps.colflag[j] && ps.nzcol[j] == 1 || return nothing # only column singletons col = ps.pb0.acols[j] # Find non-zero index # TODO: put this in a function nz = 0 i, aij = 0, zero(T) for (i_, a_) in zip(col.nzind, col.nzval) if ps.rowflag[i_] nz += !iszero(a_); nz <= 1 || break i = i_ aij = a_ end end # Not a singleton nz == 1 || error("Expected singletons but column $j has $nz non-zeros") if iszero(aij) || iszero(i) return nothing # column is actually empty end row = ps.pb0.arows[i] lr, ur = ps.lrow[i], ps.urow[i] # Detect if xj is implied free l, u = ps.lcol[j], ps.ucol[j] if isfinite(l) || isfinite(u) # Not a free variable, compute implied bounds if aij > zero(T) l_, u_ = lr, ur for (k, aik) in zip(row.nzind, row.nzval) (ps.colflag[k] && k != j) || continue # Update bounds if aik > 0 l_ -= aik * ps.ucol[k] u_ -= aik * ps.lcol[k] else l_ -= aik * ps.lcol[k] u_ -= aik * ps.ucol[k] end end l_ /= aij u_ /= aij else l_, u_ = ur, lr for (k, aik) in zip(row.nzind, row.nzval) (ps.colflag[k] && k != j) || continue # Update bounds if aik > 0 l_ -= aik * ps.lcol[k] u_ -= aik * ps.ucol[k] else l_ -= aik * ps.ucol[k] u_ -= aik * ps.lcol[k] end end l_ /= aij u_ /= aij end @debug """Column singleton $j Original bounds: [$l, $u] Implied bounds: [$(l_), $(u_)] """ l <= l_ <= u_ <= u || return nothing # Not implied free end # Remove (implied) free column @debug "Removing (implied) free column singleton $j" y = ps.obj[j] / aij # dual of row i # Update objective ps.obj0 += (y >= zero(T)) ? y * lr : y * ur row_ = Row{T}(Int[], T[]) for (j_, aij_) in zip(row.nzind, row.nzval) ps.colflag[j_] && (j_ != j) || continue push!(row_.nzind, j_) push!(row_.nzval, aij_) ps.obj[j_] -= y * aij_ # Update number of non-zeros in column ps.nzcol[j_] -= 1 end # Book-keeping push!(ps.ops, FreeColumnSingleton(i, j, lr, ur, aij, y, row_)) ps.rowflag[i] = false # remove row ps.colflag[j] = false # remove column ps.nrow -= 1 ps.ncol -= 1 ps.updated = true return nothing end function postsolve!(sol::Solution{T}, op::FreeColumnSingleton{T}) where{T} # Dual y = op.y sol.y_lower[op.i] = pos_part(y) sol.y_upper[op.i] = neg_part(y) sol.s_lower[op.j] = zero(T) sol.s_upper[op.j] = zero(T) # Primal sol.x[op.j] = sol.is_primal_ray ? zero(T) : (y >= zero(T) ? op.l : op.u) for (k, aik) in zip(op.row.nzind, op.row.nzval) sol.x[op.j] -= aik * sol.x[k] end sol.x[op.j] /= op.aij return nothing end ================================================ FILE: src/Presolve/row_singleton.jl ================================================ struct RowSingleton{T} <: PresolveTransformation{T} i::Int # Row index j::Int # Column index aij::T # Row coefficient force_lower::Bool # Whether row was forcing the lower bound force_upper::Bool # Whether row was forcing the upper bound end function remove_row_singleton!(ps::PresolveData{T}, i::Int) where{T} # Sanity checks (ps.rowflag[i] && ps.nzrow[i] == 1) || return nothing row = ps.pb0.arows[i] # Find non-zero coefficient and its column index nz = 0 j, aij = 0, zero(T) for (j_, aij_) in zip(row.nzind, row.nzval) if ps.colflag[j_] && !iszero(aij_) nz += 1; nz <= 1 || break # not a row singleton j = j_ aij = aij_ end end if nz > 1 @error "Row $i was marked as row singleton but has $nz non-zeros" return nothing end if iszero(aij) # Row is actually empty # It will be removed at the next forcing constraints check return nothing end # Compute implied bounds if aij > zero(T) l = ps.lrow[i] / aij u = ps.urow[i] / aij else l = ps.urow[i] / aij u = ps.lrow[i] / aij end # Compare to variable bounds # TODO: what if bounds are incompatible? lb, ub = ps.lcol[j], ps.ucol[j] force_lower = (l >= lb) force_upper = (u <= ub) if l >= lb ps.lcol[j] = l end if u <= ub ps.ucol[j] = u end # Book-keeping push!(ps.ops, RowSingleton(i, j, aij, force_lower, force_upper)) ps.rowflag[i] = false ps.updated = true ps.nrow -= 1 ps.nzcol[j] -= 1 # Check if we created a fixed/empty column if ps.lcol[j] == ps.ucol[j] remove_fixed_variable!(ps, j) return nothing end if ps.nzcol[j] == 0 # TODO: remove empty column elseif ps.nzcol[j] == 1 # TODO: track column singleton end return nothing end function postsolve!(sol::Solution{T}, op::RowSingleton{T}) where{T} if op.force_lower if op.aij > zero(T) sol.y_lower[op.i] = sol.s_lower[op.j] / op.aij else sol.y_upper[op.i] = sol.s_lower[op.j] / abs(op.aij) end sol.s_lower[op.j] = zero(T) end if op.force_upper if op.aij > zero(T) sol.y_upper[op.i] = sol.s_upper[op.j] / op.aij else sol.y_lower[op.i] = sol.s_upper[op.j] / abs(op.aij) end sol.s_upper[op.j] = zero(T) end return nothing end ================================================ FILE: src/Tulip.jl ================================================ module Tulip using LinearAlgebra using Logging using Printf using SparseArrays using TOML using TimerOutputs # Read Tulip version from Project.toml file const TULIP_VERSION = let project = joinpath(@__DIR__, "..", "Project.toml") Base.include_dependency(project) VersionNumber(TOML.parsefile(project)["version"]) end version() = TULIP_VERSION include("utils.jl") # Linear algebra include("LinearAlgebra/LinearAlgebra.jl") import .TLPLinearAlgebra.construct_matrix const TLA = TLPLinearAlgebra # KKT solvers include("KKT/KKT.jl") using .KKT # Commons data structures # TODO: put this in a module include("status.jl") # Termination and solution statuses include("problemData.jl") include("solution.jl") include("attributes.jl") # Presolve module include("Presolve/Presolve.jl") # IPM solvers include("./IPM/IPM.jl") include("parameters.jl") # Model include("model.jl") # Interfaces include("Interfaces/tulip_julia_api.jl") include("Interfaces/MOI/MOI_wrapper.jl") end # module ================================================ FILE: src/attributes.jl ================================================ abstract type AbstractAttribute end # ============================================================================== # # Model attributes # # ============================================================================== abstract type AbstractModelAttribute <: AbstractAttribute end """ ModelName The name of the model. **Type:** `String` **Modifiable:** Yes ### Examples ```julia Tulip.set_attribute(model, Tulip.ModelName(), "lp_example") Tulip.get_attribute(model, Tulip.ModelName()) ``` """ struct ModelName <: AbstractModelAttribute end """ NumberOfConstraints Number of constraints in the model. **Type:** `Int` **Modifiable:** No ### Examples ```julia Tulip.get_attribute(model, Tulip.NumberOfConstraints()) ``` """ struct NumberOfConstraints <: AbstractModelAttribute end """ NumberOfVariables Number of variables in the model. **Type:** `Int` **Modifiable:** No ### Examples ```julia Tulip.get_attribute(model, Tulip.NumberOfVariables()) ``` """ struct NumberOfVariables <: AbstractModelAttribute end """ ObjectiveValue Objective value of the current primal solution. **Type:** `T` **Modifiable:** No ### Examples ```julia Tulip.get_attribute(model, Tulip.ObjectiveValue()) ``` """ struct ObjectiveValue <: AbstractModelAttribute end """ DualObjectiveValue Objective value of the current dual solution. **Type:** `T` **Modifiable:** No ### Examples ```julia Tulip.get_attribute(model, Tulip.DualObjectiveValue()) ``` """ struct DualObjectiveValue <: AbstractModelAttribute end """ ObjectiveConstant Constant objective offset, defaults to zero. **Type:** `T` **Modifiable:** Yes ### Examples ```julia Tulip.set_attribute(model, Tulip.ObjectiveConstant(), zero(T)) Tulip.get_attribute(model, Tulip.ObjectiveConstant()) ``` """ struct ObjectiveConstant <: AbstractModelAttribute end """ ObjectiveSense """ struct ObjectiveSense <: AbstractModelAttribute end """ Status Model status ### Type: **Modifiable:** No ### Examples ```julia Tulip.get(model, Tulip.Status()) ``` """ struct Status <: AbstractModelAttribute end """ BarrierIterations Number of iterations of the barrier algorithm in the last call. This number may be zero in the following cases: * the model has been solved yet * presolve solved the model * the initial solution was optimal **Type:** `Int` **Modifiable:** No ### Examples ```julia Tulip.get_attribute(model, Tulip.BarrierIterations()) ``` """ struct BarrierIterations <: AbstractModelAttribute end """ SolutionTime Total solution time, in seconds. **Type:** `Float64` **Modifiable:** No ### Examples ```julia Tulip.get_attribute(model, Tulip.SolutionTime()) ``` """ struct SolutionTime <: AbstractModelAttribute end # ============================================================================== # # Variable attributes # # ============================================================================== abstract type AbstractVariableAttribute <: AbstractAttribute end """ VariableLowerBound Variable lower bound. **Type:** `T` **Modifiable:** Yes ### Examples ```julia Tulip.set_attribute(model, Tulip.VariableLowerBound(), 1, zero(T)) Tulip.get_attribute(model, Tulip.VariableLowerBound(), 1) ``` """ struct VariableLowerBound <: AbstractVariableAttribute end """ VariableUpperBound Variable upper bound. **Type:** `T` **Modifiable:** Yes ### Examples ```julia Tulip.set_attribute(model, Tulip.VariableUpperBound(), 1, one(T)) Tulip.get_attribute(model, Tulip.VariableUpperBound(), 1) ``` """ struct VariableUpperBound <: AbstractVariableAttribute end """ VariableObjectiveCoeff Objective coefficient of the variable. **Type:** `T` **Modifiable:** Yes ### Examples ```julia Tulip.set_attribute(model, Tulip.VariableObjectiveCoeff(), 1, one(T)) Tulip.get_attribute(model, Tulip.VariableObjectiveCoeff(), 1) ``` """ struct VariableObjectiveCoeff <: AbstractVariableAttribute end """ VariableName Name of the variable. **Type:** `String` **Modifiable:** Yes ### Examples ```julia Tulip.set_attribute(model, Tulip.VariableName(), 1, "x1") Tulip.get_attribute(model, Tulip.VariableName(), 1) ``` """ struct VariableName <: AbstractVariableAttribute end # ============================================================================== # # Constraint attributes # # ============================================================================== abstract type AbstractConstraintAttribute <: AbstractAttribute end """ ConstraintLowerBound Constraint lower bound. **Type:** `T` **Modifiable:** Yes ### Examples ```julia Tulip.set_attribute(model, Tulip.ConstraintLowerBound(), 1, zero(T)) Tulip.get_attribute(model, Tulip.ConstraintLowerBound(), 1) ``` """ struct ConstraintLowerBound <: AbstractConstraintAttribute end """ ConstraintUpperBound Constraint upper bound. **Type:** `T` **Modifiable:** Yes ### Examples ```julia Tulip.set_attribute(model, Tulip.ConstraintUpperBound(), 1, one(T)) Tulip.get_attribute(model, Tulip.ConstraintUpperBound(), 1) ``` """ struct ConstraintUpperBound <: AbstractConstraintAttribute end """ ConstraintName Name of the constraint. **Type:** `String` **Modifiable:** Yes ### Examples ```julia Tulip.set_attribute(model, Tulip.ConstraintName(), 1, "c1") Tulip.get_attribute(model, Tulip.ConstraintName(), 1) ``` """ struct ConstraintName <: AbstractConstraintAttribute end ================================================ FILE: src/model.jl ================================================ mutable struct Model{T} # Parameters params::Parameters{T} # TODO: model status #= Use an enum * Empty * Modified * OptimizationInProgress (optimize! is being called) * Solved (optimize! was called and the problem has not been modified since) TODO: some modifications should not change the solution status, e.g.: * changing names * changing objective constant =# status::TerminationStatus # Problem data pbdata::ProblemData{T} # Presolved problem # If presolved is disabled, this will point to m.pbdata presolve_data::Union{Nothing, PresolveData{T}} # IPM solver # If required, the problem is transformed to standard form # when instantiating the IPMSolver object. solver::Union{Nothing, AbstractIPMOptimizer{T}} # Problem solution (in original space) solution::Union{Nothing, Solution{T}} Model{T}() where{T} = new{T}( Parameters{T}(), Trm_NotCalled, ProblemData{T}(), nothing, nothing, nothing ) end # TODO # Basic functionalities (e.g., copy, empty, reset) should go in this file # Interface-like should go in Interfaces #= * optimize! * empty! * querying/setting parameters & attributes * build/modify problem through Model object * solution query =# import Base.empty! function Base.empty!(m::Model{T}) where{T} m.pbdata = ProblemData{T}() m.status = Trm_NotCalled m.presolve_data = nothing m.solver = nothing m.solution = nothing return nothing end """ optimize!(model::Model{T}) Solve the optimization problem. """ function optimize!(model::Model{T}) where{T} # Set number of threads model.params.Threads >= 1 || error( "Number of threads must be > 0 (is $(model.params.Threads))" ) BLAS.set_num_threads(model.params.Threads) # Print initial stats if model.params.OutputLevel > 0 @printf "\nProblem info\n" @printf " Name : %s\n" model.pbdata.name @printf " Constraints : %d\n" model.pbdata.ncon @printf " Variables : %d\n" model.pbdata.nvar @printf " Non-zeros : %d\n" sum(length.([col.nzind for col in model.pbdata.acols])) end pb_ = model.pbdata # Presolve # TODO: improve the if-else ps_options = model.params.Presolve if ps_options.Level > 0 model.presolve_data = PresolveData(model.pbdata, ps_options) t_ = @elapsed st = presolve!(model.presolve_data) model.status = st if model.params.OutputLevel > 0 ps = model.presolve_data nz0 = mapreduce(col -> length(col.nzind), +, model.pbdata.acols) nz = sum(ps.nzrow[ps.rowflag]) @printf "\nReduced problem info\n" @printf " Constraints : %d (removed %d)\n" ps.nrow (ps.pb0.ncon - ps.nrow) @printf " Variables : %d (removed %d)\n" ps.ncol (ps.pb0.nvar - ps.ncol) @printf " Non-zeros : %d (removed %d)\n" nz (nz0 - nz) @printf "Presolve time : %.3fs\n" t_ end # Check presolve status if st == Trm_Optimal || st == Trm_PrimalInfeasible || st == Trm_DualInfeasible || st == Trm_PrimalDualInfeasible model.params.OutputLevel > 0 && println("Presolve solved the problem.") # Perform post-solve sol0 = Solution{T}(model.pbdata.ncon, model.pbdata.nvar) postsolve!(sol0, model.presolve_data.solution, model.presolve_data) model.solution = sol0 # Book-keeping # TODO: have a ModelStatus that indicates that model was solved by presolve model.solver = nothing # Done. return end # Presolve was not able to solve the problem extract_reduced_problem!(model.presolve_data) pb_ = model.presolve_data.pb_red end # Extract data in IPM form dat = IPMData(pb_, model.params.MatrixFactory) # Instantiate the IPM solver model.params.IPM.OutputLevel = model.params.OutputLevel model.solver = instantiate(model.params.IPM.Factory, dat, model.params.KKT) # Solve the problem # TODO: add a try-catch for error handling ipm_optimize!(model.solver, model.params.IPM) # Recover solution in original space sol_inner = Solution{T}(pb_.ncon, pb_.nvar) _extract_solution!(sol_inner, pb_, model.solver) # Post-solve if ps_options.Level > 0 sol_outer = Solution{T}(model.pbdata.ncon, model.pbdata.nvar) postsolve!(sol_outer, sol_inner, model.presolve_data) model.solution = sol_outer else model.solution = sol_inner end model.status = model.solver.solver_status # Done. return nothing end function _extract_solution!(sol::Solution{T}, pb::ProblemData{T}, ipm::AbstractIPMOptimizer{T} ) where{T} m, n = pb.ncon, pb.nvar # Extract column information # TODO: check for ray vs vertex sol.primal_status = ipm.primal_status sol.dual_status = ipm.dual_status is_primal_ray = (sol.primal_status == Sln_InfeasibilityCertificate) is_dual_ray = (sol.dual_status == Sln_InfeasibilityCertificate) sol.is_primal_ray = is_primal_ray sol.is_dual_ray = is_dual_ray τ_ = (is_primal_ray || is_dual_ray) ? one(T) : inv(ipm.pt.τ) @. sol.x = ipm.pt.x[1:n] * τ_ @. sol.s_lower = ipm.pt.zl[1:n] * τ_ @. sol.s_upper = ipm.pt.zu[1:n] * τ_ # Extract row information @. sol.y_lower = pos_part.(ipm.pt.y) * τ_ @. sol.y_upper = neg_part.(ipm.pt.y) * τ_ # Compute row primal for (i, row) in enumerate(pb.arows) ax = zero(T) for (j, aij) in zip(row.nzind, row.nzval) ax += aij * sol.x[j] end sol.Ax[i] = ax end # Primal and dual objectives if sol.primal_status == Sln_InfeasibilityCertificate # Unbounded ray sol.z_primal = -T(Inf) sol.z_dual = -T(Inf) elseif sol.primal_status == Sln_Optimal || sol.primal_status == Sln_FeasiblePoint sol.z_primal = ipm.primal_objective else # Unknown solution status sol.z_primal = NaN end if sol.dual_status == Sln_InfeasibilityCertificate # Farkas proof of infeasibility sol.z_primal = T(Inf) sol.z_dual = T(Inf) elseif sol.dual_status == Sln_Optimal || sol.dual_status == Sln_FeasiblePoint # Dual solution is feasible sol.z_dual = ipm.dual_objective else # Unknown solution status sol.z_dual = NaN end return nothing end ================================================ FILE: src/parameters.jl ================================================ """ Parameters{T} """ Base.@kwdef mutable struct Parameters{T} # Model-wise parameters Threads::Int = 1 OutputLevel::Int = 0 # Linear algebra (MatrixOptions) MatrixFactory::Factory{<:AbstractMatrix} = Factory(SparseMatrixCSC) # Presolve Presolve::PresolveOptions{T} = PresolveOptions{T}() # IPM IPM::IPMOptions{T} = IPMOptions{T}() # KKT KKT::KKTOptions{T} = KKTOptions{T}() end ================================================ FILE: src/problemData.jl ================================================ using SparseArrays mutable struct RowOrCol{T} nzind::Vector{Int} nzval::Vector{T} end const Row = RowOrCol const Col = RowOrCol """ ProblemData{T} Data structure for storing problem data in precision `T`. The LP is represented in canonical form ```math \\begin{array}{rl} \\displaystyle \\min_{x} \\ \\ \\ & c^{T} x + c_{0} \\\\ s.t. \\ \\ \\ & l_{r} \\leq A x \\leq u_{r} \\\\ & l_{c} \\leq x \\leq u_{c} \\end{array} ``` """ mutable struct ProblemData{T} name::String # Dimensions ncon::Int # Number of rows nvar::Int # Number of columns (i.e. variables) # Objective # TODO: objective sense objsense::Bool # true is min, false is max obj::Vector{T} obj0::T # Constant objective offset # Constraint matrix # We store both rows and columns. It is redundant but simplifies access. # TODO: put this in its own data structure? (would allow more flexibility in modelling) arows::Vector{Row{T}} acols::Vector{Col{T}} # TODO: Data structures for QP # qrows # qcols # Bounds lcon::Vector{T} ucon::Vector{T} lvar::Vector{T} uvar::Vector{T} # Names con_names::Vector{String} var_names::Vector{String} # Only allow empty problems to be instantiated for now ProblemData{T}(pbname::String="") where {T} = new{T}( pbname, 0, 0, true, T[], zero(T), Row{T}[], Col{T}[], T[], T[], T[], T[], String[], String[] ) end import Base.empty! function Base.empty!(pb::ProblemData{T}) where{T} pb.name = "" pb.ncon = 0 pb.nvar = 0 pb.objsense = true pb.obj = T[] pb.obj0 = zero(T) pb.arows = Row{T}[] pb.acols = Col{T}[] pb.lcon = T[] pb.ucon = T[] pb.lvar = T[] pb.uvar = T[] pb.con_names = String[] pb.var_names = String[] return pb end #= TODO: * Creation *[x] Add single constraint *[ ] Add multiple constraints *[x] Add single variable *[ ] Add multiple variables *[x] Load entire problem * Modification *[x] Empty model *[x] Delete single constraint *[x] Delete multiple constraints (fallback) *[x] Delete single variable *[x] Delete multiple variables (fallback) *[x] Change single coefficient *[ ] Change multiple coefficients * Attributes *[ ] Query model attributes *[ ] MOI-supported attributes *[ ] Other attributes =# # ============================= # Problem creation # ============================= """ add_constraint!(pb, rind, rval, l, u; [name, issorted]) Add one linear constraint to the problem. # Arguments * `pb::ProblemData{T}`: the problem to which the new row is added * `rind::Vector{Int}`: column indices in the new row * `rval::Vector{T}`: non-zero values in the new row * `l::T` * `u::T` * `name::String`: row name (defaults to `""`) * `issorted::Bool`: indicates whether the row indices are already issorted. """ function add_constraint!(pb::ProblemData{T}, rind::Vector{Int}, rval::Vector{T}, l::T, u::T, name::String=""; issorted::Bool=false )::Int where{T} # Sanity checks nz = length(rind) nz == length(rval) || throw(DimensionMismatch( "Cannot add a row with $nz indices but $(length(rval)) non-zeros" )) # Go through through rval to check all coeffs are finite and remove zeros. _rind = Vector{Int}(undef, nz) _rval = Vector{T}(undef, nz) _nz = 0 for (j, aij) in zip(rind, rval) if !iszero(aij) isfinite(aij) || error("Invalid row coefficient: $(aij)") _nz += 1 _rind[_nz] = j _rval[_nz] = aij end end resize!(_rind, _nz) resize!(_rval, _nz) # TODO: combine dupplicate indices # Increment row counter pb.ncon += 1 push!(pb.lcon, l) push!(pb.ucon, u) push!(pb.con_names, name) # Create new row if issorted row = Row{T}(_rind, _rval) else # Sort indices first p = sortperm(_rind) row = Row{T}(_rind[p], _rval[p]) end push!(pb.arows, row) # Update column coefficients for (j, aij) in zip(_rind, _rval) push!(pb.acols[j].nzind, pb.ncon) push!(pb.acols[j].nzval, aij) end # Done return pb.ncon end """ add_variable!(pb, cind, cval, obj, l, u, [name]) Add one variable to the problem. # Arguments * `pb::ProblemData{T}`: the problem to which the new column is added * `cind::Vector{Int}`: row indices in the new column * `cval::Vector{T}`: non-zero values in the new column * `obj::T`: objective coefficient * `l::T`: column lower bound * `u::T`: column upper bound * `name::String`: column name (defaults to `""`) * `issorted::Bool`: indicates whether the column indices are already issorted. """ function add_variable!(pb::ProblemData{T}, cind::Vector{Int}, cval::Vector{T}, obj::T, l::T, u::T, name::String=""; issorted::Bool=false )::Int where{T} # Sanity checks nz = length(cind) nz == length(cval) || throw(DimensionMismatch( "Cannot add a column with $nz indices but $(length(cval)) non-zeros" )) # Go through through cval to check all coeffs are finite and remove zeros. _cind = Vector{Int}(undef, nz) _cval = Vector{T}(undef, nz) _nz = 0 for (j, aij) in zip(cind, cval) if !iszero(aij) isfinite(aij) || error("Invalid column coefficient: $(aij)") _nz += 1 _cind[_nz] = j _cval[_nz] = aij end end resize!(_cind, _nz) resize!(_cval, _nz) # Increment column counter pb.nvar += 1 push!(pb.lvar, l) push!(pb.uvar, u) push!(pb.obj, obj) push!(pb.var_names, name) # TODO: combine dupplicate indices # Create a new column if issorted col = Col{T}(_cind, _cval) else # Sort indices p = sortperm(_cind) col = Col{T}(_cind[p], _cval[p]) end push!(pb.acols, col) # Update row coefficients for (i, aij) in zip(_cind, _cval) push!(pb.arows[i].nzind, pb.nvar) push!(pb.arows[i].nzval, aij) end # Done return pb.nvar end """ load_problem!(pb, ) Load entire problem. """ function load_problem!(pb::ProblemData{T}, name::String, objsense::Bool, obj::Vector{T}, obj0::T, A::SparseMatrixCSC, lcon::Vector{T}, ucon::Vector{T}, lvar::Vector{T}, uvar::Vector{T}, con_names::Vector{String}, var_names::Vector{String} ) where{T} empty!(pb) # Sanity checks ncon, nvar = size(A) ncon == length(lcon) || error("") ncon == length(ucon) || error("") ncon == length(con_names) || error("") nvar == length(obj) isfinite(obj0) || error("Objective offset $obj0 is not finite") nvar == length(lvar) || error("") nvar == length(uvar) || error("") # Copy data pb.name = name pb.ncon = ncon pb.nvar = nvar pb.objsense = objsense pb.obj = copy(obj) pb.obj0 = obj0 pb.lcon = copy(lcon) pb.ucon = copy(ucon) pb.lvar = copy(lvar) pb.uvar = copy(uvar) pb.con_names = copy(con_names) pb.var_names = copy(var_names) # Load coefficients pb.acols = Vector{Col{T}}(undef, nvar) pb.arows = Vector{Row{T}}(undef, ncon) for j in 1:nvar col = A[:, j] pb.acols[j] = Col{T}(col.nzind, col.nzval) end At = sparse(A') for i in 1:ncon row = At[:, i] pb.arows[i] = Row{T}(row.nzind, row.nzval) end return pb end # ============================= # Problem modification # ============================= """ delete_constraint!(pb::ProblemData, rind::Int) Delete a single constraint from problem `pb`. """ function delete_constraint!(pb::ProblemData{T}, rind::Int) where{T} # Sanity checks 1 <= rind <= pb.ncon || error("Invalid row index $rind") # Delete row name and bounds deleteat!(pb.con_names, rind) deleteat!(pb.lcon, rind) deleteat!(pb.ucon, rind) # Update columns for (j, col) in enumerate(pb.acols) # Search for row in that column rg = searchsorted(col.nzind, rind) if rg.start > length(col.nzind) # Nothing to do continue else if col.nzind[rg.start] == rind # Delete row from column deleteat!(col.nzind, rg.start) deleteat!(col.nzval, rg.start) end # Decrement subsequent row indices col.nzind[rg.start:end] .-= 1 end end # Delete row deleteat!(pb.arows, rind) # Update row counter pb.ncon -= 1 return nothing end """ delete_constraints!(pb::ProblemData, rinds) Delete rows in collection `rind` from problem `pb`. # Arguments * `pb::ProblemData` * `rinds`: collection of row indices to be removed """ function delete_constraints!(pb::ProblemData{T}, rinds) where{T} # TODO: don't use fallback for i in rinds delete_constraint!(pb, i) end return nothing end """ delete_variable!(pb, cind) Delete a single column from problem `pb`. """ function delete_variable!(pb::ProblemData{T}, cind::Int) where{T} # Sanity checks 1 <= cind <= pb.nvar || error("Invalid column index $cind") # Delete column name, objective and bounds deleteat!(pb.var_names, cind) deleteat!(pb.obj, cind) deleteat!(pb.lvar, cind) deleteat!(pb.uvar, cind) # Update rows for (i, row) in enumerate(pb.arows) # Search for column in that row rg = searchsorted(row.nzind, cind) if rg.start > length(row.nzind) # Nothing to do continue else if row.nzind[rg.start] == cind # Column appears in row deleteat!(row.nzind, rg.start) deleteat!(row.nzval, rg.start) end # Decrement subsequent column indices row.nzind[rg.start:end] .-= 1 end end # Delete column deleteat!(pb.acols, cind) # Update column counter pb.nvar -= 1 return nothing end """ delete_variables!(pb::ProblemData, cinds) Delete a collection of columns from problem `pb`. # Arguments * `pb::ProblemData` * `cinds`: collection of row indices to be removed """ function delete_variables!(pb::ProblemData{T}, cinds) where{T} # TODO: don't use fallback for j in cinds delete_variable!(pb, j) end return nothing end """ set_coefficient!(pb, i, j, v) Set the coefficient `(i, j)` to value `v`. # Arguments * `pb::ProblemData{T}`: the problem whose coefficient * `i::Int`: row index * `j::Int`: column index * `v::T`: coefficient value """ function set_coefficient!(pb::ProblemData{T}, i::Int, j::Int, v::T) where{T} # Sanity checks 1 <= i <= pb.ncon && 1 <= j <= pb.nvar || error( "Cannot access coeff $((i, j)) in a model of size ($(pb.ncon), $(pb.nvar))" ) # Update row and column _set_coefficient!(pb.arows[i], j, v) _set_coefficient!(pb.acols[j], i, v) return nothing end """ _set_coefficient!(roc::RowOrCol{T}, ind::Int, v::T) Set coefficient to value `v`. """ function _set_coefficient!(roc::RowOrCol{T}, ind::Int, v::T) where{T} # Check if index already exists k = searchsortedfirst(roc.nzind, ind) if (1 <= k <= length(roc.nzind)) && roc.nzind[k] == ind # This coefficient was a non-zero before if iszero(v) deleteat!(roc.nzind, k) deleteat!(roc.nzval, k) else roc.nzval[k] = v end else # Only add coeff if non-zero if !iszero(v) insert!(roc.nzind, k, ind) insert!(roc.nzval, k, v) end end return nothing end # ============================= # Problem queries # ============================= # TODO ================================================ FILE: src/solution.jl ================================================ mutable struct Solution{T} m::Int n::Int primal_status::SolutionStatus dual_status::SolutionStatus is_primal_ray::Bool is_dual_ray::Bool z_primal::T z_dual::T x::Vector{T} Ax::Vector{T} y_lower::Vector{T} y_upper::Vector{T} s_lower::Vector{T} s_upper::Vector{T} Solution{T}(m, n) where{T} = new{T}( m, n, Sln_Unknown, Sln_Unknown, false, false, zero(T), zero(T), zeros(T, n), zeros(T, m), zeros(T, m), zeros(T, m), zeros(T, n), zeros(T, n) ) end import Base.resize! function Base.resize!(sol::Solution, m::Int, n::Int) m >= 0 || throw(ArgumentError("m must be >= 0")) n >= 0 || throw(ArgumentError("n must be >= 0")) sol.m = m sol.n = n resize!(sol.x, n) resize!(sol.Ax, m) resize!(sol.y_lower, m) resize!(sol.y_upper, m) resize!(sol.s_lower, n) resize!(sol.s_upper, n) return sol end ================================================ FILE: src/status.jl ================================================ """ TerminationStatus - `Success`: No error occured - `PrimalInfeasibleNoResult`: Problem is proved to be primal infeasible, but no result (e.g. certificate of infeasibility) is available. - `DualInfeasibleNoResult`: Problem is proved to be primal infeasible, but no result (e.g. certificate of infeasibility) is available. - `IterationLimit`: Maximum number of iterations reached. - `TimeLimit`: Time limit reached. - `MemoryLimit`: Memory limit reached. - `NumericalProblem`: Numerical problem encountered, e.g. failure of the Cholesky decomposition. """ @enum(TerminationStatus, Trm_NotCalled, Trm_Unknown, # OK statuses Trm_Optimal, Trm_PrimalInfeasible, Trm_DualInfeasible, Trm_PrimalDualInfeasible, # Limits Trm_IterationLimit, Trm_TimeLimit, # Errors Trm_MemoryLimit, Trm_NumericalProblem ) """ SolutionStatus Solution Status code - `Sln_Unknown`: Unknown status - `Sln_Optimal`: The current solution is optimal. - `Sln_FeasiblePoint`: The current solution is feasible. - `Sln_InfeasiblePoint`: The current solution is not feasible. - `Sln_InfeasibilityCertificate`: The current solution is a certificate of infeasibility. The primal solution is a certificate of dual infeasibility, while the dual solution is a certificate of primal infeasibility. """ @enum(SolutionStatus, Sln_Unknown, Sln_Optimal, Sln_FeasiblePoint, Sln_InfeasiblePoint, Sln_InfeasibilityCertificate ) ================================================ FILE: src/utils.jl ================================================ using CodecBzip2 using CodecZlib """ _open(f, fname) Open a file with decompression stream as required. """ function _open(f::Function, fname::String) ext = Symbol(split(fname, ".")[end]) if ext == :gz return Base.open(f, CodecZlib.GzipDecompressorStream, fname, "r") elseif ext == :bz2 return Base.open(f, CodecBzip2.Bzip2DecompressorStream, fname, "r") else return Base.open(f, fname, "r") end end # Positive and negative part of a number pos_part(x::T) where{T} = x >= zero(T) ? x : zero(T) neg_part(x::T) where{T} = x >= zero(T) ? zero(T) : -x @inline tones(Tv, n) = fill!(Tv(undef, n), one(eltype(Tv))) @inline tzeros(Tv, n) = fill!(Tv(undef, n), zero(eltype(Tv))) """ Factory{T} Factory-like struct for passing options to lower-level components. """ struct Factory{T} T::Type{T} options::Base.Iterators.Pairs # Constructors Factory(::Type{T}; kwargs...) where{T} = new{T}(T, kwargs) Factory{T}(;kwargs...) where{T} = new{T}(T, kwargs) end instantiate(f::Factory{T}, args...; kwargs...) where{T} = T(args...; kwargs..., f.options...) ================================================ FILE: test/Core/problemData.jl ================================================ function run_tests_pbdata(::Type{T}) where{T} @testset "Creation" begin pb = TLP.ProblemData{T}("test") @test pb.name == "test" check_problem_size(pb, 0, 0) @test pb.objsense @test iszero(pb.obj0) # Add two columns #= min x1 + 2 x2 s.t. 0 ⩽ x1 ⩽ ∞ 1 ⩽ x2 ⩽ ∞ =# TLP.add_variable!(pb, Int[], T[], one(T), zero(T), T(Inf), "x1") TLP.add_variable!(pb, Int[], T[], 2 * one(T), one(T), T(Inf), "x2") check_problem_size(pb, 0, 2) col1, col2 = pb.acols[1], pb.acols[2] @test pb.obj == [one(T), 2*one(T)] @test pb.lvar == [zero(T), one(T)] @test pb.uvar == [T(Inf), T(Inf)] @test length(col1.nzind) == length(col1.nzval) == 0 @test length(col2.nzind) == length(col2.nzval) == 0 @test pb.var_names == ["x1", "x2"] # Add two constraints #= min x1 + 2 x2 s.t. -∞ ⩽ -x1 + x2 ⩽ 1 -1 ⩽ 2 x1 - 2 x2 ⩽ 0 0 ⩽ x1 ⩽ ∞ 1 ⩽ x2 ⩽ ∞ =# TLP.add_constraint!(pb, [1, 2], T.([-1, 1]), T(-Inf), one(T), "row1") TLP.add_constraint!(pb, [1, 2], T.([2, -2]), -one(T), zero(T), "row2") # Check dimensions check_problem_size(pb, 2, 2) # Check coefficients row1, row2 = pb.arows[1], pb.arows[2] @test row1.nzind == [1, 2] @test row1.nzval == T.([-1, 1]) @test row2.nzind == [1, 2] @test row2.nzval == T.([2, -2]) @test col1.nzind == [1, 2] @test col1.nzval == T.([-1, 2]) @test col2.nzind == [1, 2] @test col2.nzval == T.([1, -2]) # Check row bounds @test pb.lcon == [T(-Inf), -one(T)] @test pb.ucon == [one(T), zero(T)] # Check names @test pb.con_names == ["row1", "row2"] @test pb.var_names == ["x1", "x2"] empty!(pb) @test pb.name == "" @test iszero(pb.obj0) check_problem_size(pb, 0, 0) end @testset "Delete" begin pb = TLP.ProblemData{T}("test") #= min x1 + 2 x2 + 3 x3 s.t. 1 ⩽ 1 * x1 ⩽ 10 2 ⩽ 2 * x2 ⩽ 20 3 ⩽ 3 * x3 ⩽ 30 11 ⩽ x1 ⩽ 110 22 ⩽ x2 ⩽ 220 33 ⩽ x3 ⩽ 330 =# TLP.add_variable!(pb, Int[], T[], one(T), 11 * one(T), 110 * one(T), "x1") TLP.add_variable!(pb, Int[], T[], 2 * one(T), 22 * one(T), 220 * one(T), "x2") TLP.add_variable!(pb, Int[], T[], 3 * one(T), 33 * one(T), 330 * one(T), "x3") TLP.add_constraint!(pb, [1], T.([1]), 1 * one(T), 10 * one(T), "row1") TLP.add_constraint!(pb, [2], T.([2]), 2 * one(T), 20 * one(T), "row2") TLP.add_constraint!(pb, [3], T.([3]), 3 * one(T), 30 * one(T), "row3") # Delete row 1 and check remaining problem TLP.delete_constraint!(pb, 1) @test pb.ncon == 2 @test pb.nvar == 3 row2, row3 = pb.arows @test pb.con_names == ["row2", "row3"] @test pb.lcon == T.([2, 3]) @test pb.ucon == T.([20, 30]) @test row2.nzind == [2] @test row2.nzval == [T(2)] @test row3.nzind == [3] @test row3.nzval == [T(3)] # Delete variable 2 TLP.delete_variable!(pb, 2) @test pb.ncon == 2 @test pb.nvar == 2 col1, col3 = pb.acols @test pb.var_names == ["x1", "x3"] @test pb.lvar == T.([11, 33]) @test pb.uvar == T.([110, 330]) @test col1.nzind == [] @test col1.nzval == T[] @test col3.nzind == [2] @test col3.nzval == [T(3)] end return nothing end function check_problem_size(pb::TLP.ProblemData, ncon::Int, nvar::Int) @test pb.ncon == ncon @test pb.nvar == nvar @test length(pb.obj) == nvar @test length(pb.arows) == ncon @test length(pb.acols) == nvar @test length(pb.lcon) == ncon @test length(pb.ucon) == ncon @test length(pb.lvar) == nvar @test length(pb.uvar) == nvar @test length(pb.con_names) == ncon @test length(pb.var_names) == nvar return nothing end function test_pbdata_checkcoeff(::Type{T}) where{T} @testset "Zero in row" begin pb = TLP.ProblemData{T}("test") Tulip.add_variable!(pb, Int[], T[], T(1), zero(T), one(T), "x1") Tulip.add_variable!(pb, Int[], T[], T(2), zero(T), one(T), "x2") Tulip.add_variable!(pb, Int[], T[], T(3), zero(T), one(T), "x3") Tulip.add_constraint!(pb, [1, 2, 3], T[1, 0, 0], zero(T), one(T), "c1") Tulip.add_constraint!(pb, [1, 2, 3], T[0, 1, 2], zero(T), one(T), "c2") @test length(pb.arows) == 2 @test pb.arows[1].nzind == [1] @test pb.arows[1].nzval == T[1] @test pb.arows[2].nzind == [2, 3] @test pb.arows[2].nzval == T[1, 2] @test length(pb.acols) == 3 @test pb.acols[1].nzind == [1] @test pb.acols[1].nzval == T[1] @test pb.acols[2].nzind == [2] @test pb.acols[2].nzval == T[1] @test pb.acols[3].nzind == [2] @test pb.acols[3].nzval == T[2] end @testset "Zero in col" begin pb = TLP.ProblemData{T}("test") Tulip.add_constraint!(pb, Int[], T[], zero(T), one(T), "c1") Tulip.add_constraint!(pb, Int[], T[], zero(T), one(T), "c2") Tulip.add_variable!(pb, [1, 2], T[1, 0], T(1), zero(T), one(T), "x1") Tulip.add_variable!(pb, [1, 2], T[0, 1], T(2), zero(T), one(T), "x2") Tulip.add_variable!(pb, [1, 2], T[0, 2], T(3), zero(T), one(T), "x3") @test length(pb.arows) == 2 @test pb.arows[1].nzind == [1] @test pb.arows[1].nzval == T[1] @test pb.arows[2].nzind == [2, 3] @test pb.arows[2].nzval == T[1, 2] @test length(pb.acols) == 3 @test pb.acols[1].nzind == [1] @test pb.acols[1].nzval == T[1] @test pb.acols[2].nzind == [2] @test pb.acols[2].nzval == T[1] @test pb.acols[3].nzind == [2] @test pb.acols[3].nzval == T[2] end end @testset "ProblemData" begin for T in TvTYPES @testset "$T" begin run_tests_pbdata(T) test_pbdata_checkcoeff(T) end end end ================================================ FILE: test/IPM/HSD.jl ================================================ function run_tests_hsd(T::Type) Tv = Vector{T} params = TLP.IPMOptions{T}() kkt_options = TLP.KKTOptions{T}() @testset "step length" begin m, n, p = 2, 2, 1 pt = TLP.Point{T, Tv}(m, n, p, hflag=true) pt.x .= one(T) pt.xl .= one(T) pt.xu .= one(T) pt.y .= zero(T) pt.zl .= zero(T) pt.zu .= zero(T) pt.τ = one(T) pt.κ = one(T) pt.μ = one(T) d = TLP.Point{T, Tv}(m, n, p, hflag=true) d.x .= one(T) d.xl .= one(T) d.xu .= one(T) d.y .= zero(T) d.zl .= zero(T) d.zu .= zero(T) d.τ = one(T) d.κ = one(T) d.μ = one(T) # Max step length for a single (x, d) @inferred TLP.max_step_length(pt.x, d.x) @test TLP.max_step_length(ones(T, 1), ones(T, 1)) == T(Inf) @test TLP.max_step_length(ones(T, 1), -ones(T, 1)) ≈ one(T) @test TLP.max_step_length(zeros(T, 1), -ones(T, 1)) ≈ zero(T) @test TLP.max_step_length(zeros(T, 1), ones(T, 1)) == T(Inf) # Max step length for the whole primal-dual point @inferred TLP.max_step_length(pt, d) @test TLP.max_step_length(pt, d) ≈ one(T) end # Simple example: #= min x1 - x2 s.τ. x1 + x2 = 1 x1 - x2 = 0 0 <= x1 <= 2 0 <= x2 <= 2 =# m, n = 2, 2 p = 2 * n A = Matrix{T}([ [1 1]; [1 -1] ]) b = Vector{T}([1, 0]) c = Vector{T}([1, -1]) c0 = zero(T) l = Vector{T}([0, 0]) u = Vector{T}([2, 2]) dat = Tulip.IPMData(A, b, true, c, c0, l, u) hsd = TLP.HSD(dat, kkt_options) # Primal-dual optimal solution # x1 = x2 = 0.5; xl = 0.5; xu = 1.5; τ = 1 # y1 = 0, y2 = 1; zl = zu = 0; κ = 0 hsd.pt.x .= T.([1 // 2, 1 // 2]) hsd.pt.xl .= T.([1 // 2, 1 // 2]) hsd.pt.xu .= T.([3 // 2, 3 // 2]) hsd.pt.y .= T.([0, 1]) hsd.pt.zl .= T.([0, 0]) hsd.pt.zu .= T.([0, 0]) hsd.pt.τ = 1 hsd.pt.κ = 0 hsd.pt.μ = 0 ϵ = sqrt(eps(T)) TLP.compute_residuals!(hsd) @testset "Convergence" begin hsd.solver_status = TLP.Trm_Unknown TLP.update_solver_status!(hsd, ϵ, ϵ, ϵ, ϵ) @test hsd.solver_status == TLP.Trm_Optimal # TODO: dual infeasible # TODO: primal infeasible # TODO: ill-posed end end function test_hsd_residuals(T::Type) # Simple example: #= min x1 - x2 s.τ. x1 + x2 = 1 x1 - x2 = 0 0 <= x1 <= 2 0 <= x2 <= 2 =# kkt_options = TLP.KKTOptions{T}() A = Matrix{T}([ [1 1]; [1 -1] ]) b = Vector{T}([1, 0]) c = Vector{T}([1, -1]) c0 = zero(T) l = Vector{T}([0, 0]) u = Vector{T}([2, 2]) dat = Tulip.IPMData(A, b, true, c, c0, l, u) hsd = TLP.HSD(dat, kkt_options) pt = hsd.pt res = hsd.res # Primal-dual solution x = pt.x .= T[3, 5] xl = pt.xl .= T[1, 8] xu = pt.xu .= T[2, 1] y = pt.y .= T[10, -2] zl = pt.zl .= T[2, 1] zu = pt.zu .= T[5, 7] τ = pt.τ = T(1//2) κ = pt.κ = T(1//10) μ = pt.μ = 0 TLP.compute_residuals!(hsd) @test res.rp ≈ (τ .* b) - A * x @test res.rl ≈ (τ .* l) - (x - xl) @test res.ru ≈ (τ .* u) - (x + xu) @test res.rd ≈ (τ .* c) - A' * y - zl + zu @test res.rg ≈ c'x - (b'y + l'zl - u'zu) + κ @test res.rp_nrm == norm(res.rp, Inf) @test res.rl_nrm == norm(res.rl, Inf) @test res.ru_nrm == norm(res.ru, Inf) @test res.rd_nrm == norm(res.rd, Inf) @test res.rg_nrm == norm(res.rg, Inf) return nothing end @testset "HSD" begin @testset "$T" for T in TvTYPES run_tests_hsd(T) test_hsd_residuals(T) end end ================================================ FILE: test/IPM/MPC.jl ================================================ function run_tests_mpc(T::Type) Tv = Vector{T} params = TLP.IPMOptions{T}() kkt_options = TLP.KKTOptions{T}() @testset "step length" begin m, n, p = 2, 2, 1 pt = TLP.Point{T, Tv}(m, n, p, hflag=false) pt.x .= one(T) pt.xl .= one(T) pt.xu .= one(T) pt.y .= zero(T) pt.zl .= zero(T) pt.zu .= zero(T) pt.τ = one(T) pt.κ = one(T) pt.μ = one(T) d = TLP.Point{T, Tv}(m, n, p, hflag=false) d.x .= one(T) d.xl .= one(T) d.xu .= one(T) d.y .= zero(T) d.zl .= zero(T) d.zu .= zero(T) d.τ = one(T) d.κ = one(T) d.μ = one(T) # Max step length for a single (x, d) @inferred TLP.max_step_length(pt.x, d.x) @test TLP.max_step_length(ones(T, 1), ones(T, 1)) == T(Inf) @test TLP.max_step_length(ones(T, 1), -ones(T, 1)) ≈ one(T) @test TLP.max_step_length(zeros(T, 1), -ones(T, 1)) ≈ zero(T) @test TLP.max_step_length(zeros(T, 1), ones(T, 1)) == T(Inf) # Max step length for the whole primal-dual point @inferred TLP.max_step_length(pt, d) @test TLP.max_step_length(pt, d) ≈ one(T) end # Simple example: #= min x1 - x2 s.τ. x1 + x2 = 1 x1 - x2 = 0 0 <= x1 <= 2 0 <= x2 <= 2 =# m, n = 2, 2 p = 2 * n A = Matrix{T}([ [1 1]; [1 -1] ]) b = Vector{T}([1, 0]) c = Vector{T}([1, -1]) c0 = zero(T) l = Vector{T}([0, 0]) u = Vector{T}([2, 2]) dat = Tulip.IPMData(A, b, true, c, c0, l, u) ipm = TLP.MPC(dat, kkt_options) # Primal-dual optimal solution # x1 = x2 = 0.5; xl = 0.5; xu = 1.5; τ = 1 # y1 = 0, y2 = 1; zl = zu = 0; κ = 0 ipm.pt.x .= T.([1 // 2, 1 // 2]) ipm.pt.xl .= T.([1 // 2, 1 // 2]) ipm.pt.xu .= T.([3 // 2, 3 // 2]) ipm.pt.y .= T.([0, 1]) ipm.pt.zl .= T.([0, 0]) ipm.pt.zu .= T.([0, 0]) ipm.pt.τ = 1 ipm.pt.κ = 0 ipm.pt.μ = 0 ϵ = sqrt(eps(T)) TLP.compute_residuals!(ipm) @testset "Convergence" begin ipm.solver_status = TLP.Trm_Unknown TLP.update_solver_status!(ipm, ϵ, ϵ, ϵ, ϵ) @test ipm.solver_status == TLP.Trm_Optimal # TODO: dual infeasible # TODO: primal infeasible # TODO: ill-posed end end function test_mpc_residuals(T::Type) # Simple example: #= min x1 - x2 s.τ. x1 + x2 = 1 x1 - x2 = 0 0 <= x1 <= 2 0 <= x2 <= 2 =# kkt_options = TLP.KKTOptions{T}() A = Matrix{T}([ [1 1]; [1 -1] ]) b = Vector{T}([1, 0]) c = Vector{T}([1, -1]) c0 = zero(T) l = Vector{T}([0, 0]) u = Vector{T}([2, 2]) dat = Tulip.IPMData(A, b, true, c, c0, l, u) ipm = TLP.MPC(dat, kkt_options) pt = ipm.pt res = ipm.res # Primal-dual solution x = pt.x .= T[3, 5] xl = pt.xl .= T[1, 8] xu = pt.xu .= T[2, 1] y = pt.y .= T[10, -2] zl = pt.zl .= T[2, 1] zu = pt.zu .= T[5, 7] pt.τ = 1 pt.κ = 0 pt.μ = 0 TLP.compute_residuals!(ipm) @test res.rp ≈ b - A*x @test res.rl ≈ l - (x - xl) @test res.ru ≈ u - (x + xu) @test res.rd ≈ c - A' * y - zl + zu @test res.rg == 0 @test res.rp_nrm == norm(res.rp, Inf) @test res.rl_nrm == norm(res.rl, Inf) @test res.ru_nrm == norm(res.ru, Inf) @test res.rd_nrm == norm(res.rd, Inf) @test res.rg_nrm == norm(res.rg, Inf) return nothing end @testset "MPC" begin @testset "$T" for T in TvTYPES run_tests_mpc(T) test_mpc_residuals(T) end end ================================================ FILE: test/Interfaces/MOI_wrapper.jl ================================================ # Copyright 2018-2019: Mathieu Tanneau # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. using Test import MathOptInterface as MOI import Tulip @testset "Direct optimizer" begin model = Tulip.Optimizer() MOI.set(model, MOI.Silent(), true) MOI.Test.runtests( model, MOI.Test.Config( Float64; atol = 1e-6, rtol = 1e-6, exclude = Any[MOI.ConstraintBasisStatus, MOI.VariableBasisStatus], ), ) end @testset "MOI Bridged" begin model = MOI.Bridges.full_bridge_optimizer(Tulip.Optimizer(), Float64) MOI.set(model, MOI.Silent(), true) MOI.Test.runtests( model, MOI.Test.Config( Float64; atol = 1e-6, rtol = 1e-6, exclude = Any[MOI.ConstraintBasisStatus, MOI.VariableBasisStatus], ), exclude=[ r"^test_conic_NormInfinityCone_INFEASIBLE$", r"^test_conic_NormOneCone_INFEASIBLE$", ], ) end # Run the MOI tests with HSD and MPC algorithms @testset "MOI Linear tests - $ipm" for ipm in [Tulip.HSD, Tulip.MPC] model = Tulip.Optimizer() model.inner.params.IPM.Factory = Tulip.Factory(ipm) MOI.set(model, MOI.Silent(), true) MOI.Test.runtests( model, MOI.Test.Config( Float64; atol = 1e-6, rtol = 1e-6, exclude = Any[MOI.ConstraintBasisStatus, MOI.VariableBasisStatus], ), include=["linear"], ) end @testset "Cached optimizer" begin inner = MOI.Utilities.CachingOptimizer( MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), Tulip.Optimizer(), ) model = MOI.Bridges.full_bridge_optimizer(inner, Float64) MOI.set(model, MOI.Silent(), true) MOI.Test.runtests( model, MOI.Test.Config( Float64; atol = 1e-6, rtol = 1e-6, exclude = Any[MOI.ConstraintBasisStatus, MOI.VariableBasisStatus], ), exclude=[ r"^test_conic_NormInfinityCone_INFEASIBLE$", r"^test_conic_NormOneCone_INFEASIBLE$", ], ) end @testset "test_attribute_TimeLimitSec" begin model = Tulip.Optimizer() @test MOI.supports(model, MOI.TimeLimitSec()) @test MOI.get(model, MOI.TimeLimitSec()) === nothing MOI.set(model, MOI.TimeLimitSec(), 0.0) @test MOI.get(model, MOI.TimeLimitSec()) == 0.0 MOI.set(model, MOI.TimeLimitSec(), nothing) @test MOI.get(model, MOI.TimeLimitSec()) === nothing MOI.set(model, MOI.TimeLimitSec(), 1.0) @test MOI.get(model, MOI.TimeLimitSec()) == 1.0 end ================================================ FILE: test/Interfaces/julia_api.jl ================================================ import Tulip using Test function test_reader() lp = Tulip.Model{Float64}() Tulip.load_problem!(lp, joinpath(@__DIR__, "lp.mps")) check_data(lp) Tulip.load_problem!(lp, joinpath(@__DIR__, "lp.mps.gz")) check_data(lp) Tulip.load_problem!(lp, joinpath(@__DIR__, "lp.mps.bz2")) check_data(lp) end function check_data(lp) pb = lp.pbdata @test pb.name == "LP1" @test pb.ncon == 2 @test pb.nvar == 2 @test pb.objsense @test pb.obj0 == 0.0 @test pb.obj == [1.0, 2.0] @test pb.lvar == [0.0, 0.0] @test pb.uvar == [1.0, 1.0] @test pb.lcon == [1.0, 0.0] @test pb.ucon == [1.0, 0.0] @test pb.con_names == ["ROW1", "ROW2"] @test pb.var_names == ["X1", "X2"] col1, col2 = pb.acols[1], pb.acols[2] @test col1.nzind == [1, 2] @test col1.nzval == [1.0, 1.0] @test col2.nzind == [1, 2] @test col2.nzval == [1.0, -1.0] row1, row2 = pb.arows[1], pb.arows[2] @test row1.nzind == [1, 2] @test row1.nzval == [1.0, 1.0] @test row2.nzind == [1, 2] @test row2.nzval == [1.0, -1.0] end @testset "Reader" begin test_reader() end ================================================ FILE: test/Interfaces/lp.mps ================================================ NAME LP1 * Problem: * min x1 + 2*x2 * s.t. x1 + x2 = 1 * x1 - x2 = 0 * 0 <= x1, x2, <= 1 ROWS E ROW1 E ROW2 N COST COLUMNS X1 ROW1 1. ROW2 1. X1 COST 1. X2 ROW1 1. ROW2 -1. X2 COST 2. RHS B ROW1 1. ROW2 0. BOUNDS UP BND1 X1 1. UP BND1 X2 1. ENDATA ================================================ FILE: test/KKT/Cholmod/cholmod.jl ================================================ @testset "CHOLMOD" begin A = SparseMatrixCSC{Float64, Int}([ 1 0 1 0; 0 1 0 1 ]) @testset "LDL" begin kkt = KKT.setup(A, KKT.K2(), KKT.TlpCholmod.Backend()) KKT.run_ls_tests(A, kkt) end @testset "Cholesky" begin kkt = KKT.setup(A, KKT.K1(), KKT.TlpCholmod.Backend()) KKT.run_ls_tests(A, kkt) end end ================================================ FILE: test/KKT/Dense/lapack.jl ================================================ @testset "LAPACK" begin for T in TvTYPES @testset "$T" begin A = Matrix{T}([ 1 0 1 0; 0 1 0 1 ]) kkt = KKT.setup(A, KKT.K1(), KKT.TlpDense.Backend()) KKT.run_ls_tests(A, kkt) end end end ================================================ FILE: test/KKT/KKT.jl ================================================ const KKT = Tulip.KKT include("Dense/lapack.jl") include("Cholmod/cholmod.jl") include("LDLFactorizations/ldlfact.jl") include("Krylov/krylov.jl") ================================================ FILE: test/KKT/Krylov/krylov.jl ================================================ using Krylov @testset "Krylov" begin include("spd.jl") include("sid.jl") include("sqd.jl") end ================================================ FILE: test/KKT/Krylov/sid.jl ================================================ function test_krylov_sid(T, ksolver) A = SparseMatrixCSC{T, Int}([ 1 0 1 0; 0 1 0 1 ]) kkt = KKT.setup(A, KKT.K2(), KKT.TlpKrylov.Backend(ksolver, Vector{T})) KKT.run_ls_tests(A, kkt) return nothing end @testset "SID" begin for T in TvTYPES, ksolver in [MinresWorkspace, MinaresWorkspace, MinresQlpWorkspace, SymmlqWorkspace] @testset "$ksolver ($T)" begin test_krylov_sid(T, ksolver) end end end ================================================ FILE: test/KKT/Krylov/spd.jl ================================================ function test_krylov_spd(T, ksolver) A = SparseMatrixCSC{T, Int}([ 1 0 1 0; 0 1 0 1 ]) kkt = KKT.setup(A, KKT.K1(), KKT.TlpKrylov.Backend(ksolver, Vector{T})) KKT.run_ls_tests(A, kkt) return nothing end @testset "SPD" begin for T in TvTYPES, ksolver in [CgWorkspace, CarWorkspace] @testset "$ksolver ($T)" begin test_krylov_spd(T, ksolver) end end end ================================================ FILE: test/KKT/Krylov/sqd.jl ================================================ function test_krylov_sqd(T, ksolver) A = SparseMatrixCSC{T, Int}([ 1 0 1 0; 0 1 0 1 ]) kkt = KKT.setup(A, KKT.K2(), KKT.TlpKrylov.Backend(ksolver, Vector{T})) KKT.run_ls_tests(A, kkt) return nothing end @testset "SQD" begin for T in TvTYPES, ksolver in [TricgWorkspace, TrimrWorkspace] @testset "$ksolver ($T)" begin test_krylov_sqd(T, ksolver) end end end ================================================ FILE: test/KKT/LDLFactorizations/ldlfact.jl ================================================ @testset "LDLFact" begin for T in TvTYPES @testset "$T" begin A = SparseMatrixCSC{T, Int}([ 1 0 1 0; 0 1 0 1 ]) kkt = KKT.setup(A, KKT.K2(), KKT.TlpLDLFact.Backend()) KKT.run_ls_tests(A, kkt) end end end ================================================ FILE: test/Presolve/empty_column.jl ================================================ function emtpy_column_tests(T::Type) # We test all the following combinations: #= min c * x s.t. lb ≤ x ≤ ub ------------------------------ | c (lb, ub) | +1 | -1 | 0 ------------------------------ (-∞, u) | -∞ | u | u (-∞, +∞) | -∞ | +∞ | 0 ( l, u) | l | u | l ( l, +∞) | l | +∞ | l ------------------------------ =# function build_problem(l, u, c) pb = Tulip.ProblemData{T}() Tulip.load_problem!(pb, "Test", true, [c], zero(T), spzeros(T, 0, 1), T[], T[], [l], [u], String[], ["x"] ) return pb end L = T.([-Inf, -1]) U = T.([ 1, Inf]) C = T.([-1, 0, 1]) for l in L, u in U, c in C @testset "$((l, u, c))" begin pb = build_problem(l, u, c) ps = Tulip.PresolveData(pb) # Remove empty variable Tulip.remove_empty_column!(ps, 1) if c > 0 && !isfinite(l) @test ps.status == Tulip.Trm_DualInfeasible @test ps.colflag[1] @test ps.ncol == 1 sol = ps.solution @test sol.primal_status == Tulip.Sln_InfeasibilityCertificate @test sol.m == 0 && sol.n == 1 @test sol.x[1] < 0 elseif c < 0 && !isfinite(u) @test ps.status == Tulip.Trm_DualInfeasible @test ps.colflag[1] @test ps.ncol == 1 sol = ps.solution @test sol.primal_status == Tulip.Sln_InfeasibilityCertificate @test sol.m == 0 && sol.n == 1 @test sol.x[1] > 0 else @test ps.status == Tulip.Trm_Unknown @test !ps.colflag[1] @test ps.ncol == 0 @test ps.updated # Check that operation was recorded correctly @test length(ps.ops) == 1 op = ps.ops[1] @test isa(op, Tulip.EmptyColumn) @test op.j == 1 end end # testset end # loop return end @testset "Empty column" begin for T in TvTYPES @testset "$T" begin emtpy_column_tests(T) end end end ================================================ FILE: test/Presolve/empty_row.jl ================================================ function empty_row_tests(T::Type) # Build the following model #= min x + y s.t. -1 ⩽ 0 * x + 0 * y + 0 * z ⩽ 1 1 ⩽ 0 * x + 0 * y + 0 * z ⩽ 2 =# pb = Tulip.ProblemData{T}() m, n = 2, 3 A = spzeros(T, m, n) b = ones(T, m) c = ones(T, n) Tulip.load_problem!(pb, "test", true, c, zero(T), A, T.([-1, 1]), T.([1, 2]), zeros(T, n), fill(T(Inf), n), ["c1", "c2"], ["x", "y", "z"] ) ps = Tulip.PresolveData(pb) @test !ps.updated @test ps.nzrow[1] == ps.nzrow[2] == 0 # Remove first empty row Tulip.remove_empty_row!(ps, 1) @test ps.updated @test ps.status == Tulip.Trm_Unknown @test ps.nrow == 1 @test !ps.rowflag[1] && ps.rowflag[2] @test length(ps.ops) == 1 op = ps.ops[1] @test isa(op, Tulip.EmptyRow{T}) @test op.i == 1 @test iszero(op.y) # Remove second empty row # This should detect infeasibility Tulip.remove_empty_row!(ps, 2) @test ps.status == Tulip.Trm_PrimalInfeasible @test ps.nrow == 1 @test !ps.rowflag[1] && ps.rowflag[2] @test length(ps.ops) == 1 # Check solution status & objective value sol = ps.solution @test sol.dual_status == Tulip.Sln_InfeasibilityCertificate @test sol.z_primal == sol.z_dual == T(Inf) # Check Farkas ray # (current problem only has 1 row) @test sol.y_lower[1] > zero(T) return end function test_empty_row_1(T::Type) # Empty row with l > 0 #= min x s.t. 1 ⩽ 0 * x ⩽ 2 x >= 0 =# pb = Tulip.ProblemData{T}() m, n = 1, 1 A = spzeros(T, m, n) c = ones(T, n) Tulip.load_problem!(pb, "test", true, c, zero(T), A, T.([1]), T.([2]), zeros(T, n), fill(T(Inf), n), ["c1"], ["x"] ) ps = Tulip.PresolveData(pb) Tulip.remove_empty_row!(ps, 1) @test ps.status == Tulip.Trm_PrimalInfeasible @test ps.nrow == 1 @test ps.rowflag[1] @test length(ps.ops) == 0 # Check solution status & objective value sol = ps.solution @test sol.dual_status == Tulip.Sln_InfeasibilityCertificate @test sol.z_primal == sol.z_dual == T(Inf) # Check Farkas ray # (current problem only has 1 row) @test sol.y_lower[1] > zero(T) return nothing end function test_empty_row_2(T::Type) # Empty row with u < 0 #= min x s.t. -2 ⩽ 0 * x ⩽ -1 x >= 0 =# pb = Tulip.ProblemData{T}() m, n = 1, 1 A = spzeros(T, m, n) c = ones(T, n) Tulip.load_problem!(pb, "test", true, c, zero(T), A, T.([-2]), T.([-1]), zeros(T, n), fill(T(Inf), n), ["c1"], ["x"] ) ps = Tulip.PresolveData(pb) Tulip.remove_empty_row!(ps, 1) @test ps.status == Tulip.Trm_PrimalInfeasible @test ps.nrow == 1 @test ps.rowflag[1] @test length(ps.ops) == 0 # Check solution status & objective value sol = ps.solution @test sol.dual_status == Tulip.Sln_InfeasibilityCertificate @test sol.z_primal == sol.z_dual == T(Inf) # Check Farkas ray # (current problem only has 1 row) @test sol.y_upper[1] > zero(T) return nothing end function test_empty_row_tolerances(T::Type) # Adapted from https://github.com/ds4dm/Tulip.jl/issues/98 #= min x + y + z s.t. x + y + z == 1 x == ¹/₃ y == ¹/₃ z == ¹/₃ x, y, z, ≥ 0 In the absence of numerical tolerances, x, y, and z get eliminated, but rouding errors cause the first constraint to be 0 == ϵ ≈ 1e-16, thereby rendering the problem infeasible. =# pb = Tulip.ProblemData{T}() m, n = 4, 3 A = sparse( [1, 1, 1, 2, 3, 4], [1, 2, 3, 1, 2, 3], T[1, 1, 1, 1, 1, 1], m, n ) c = ones(T, n) Tulip.load_problem!(pb, "test", true, c, zero(T), A, T[1, 1//3, 1//3, 1//3], T[1, 1//3, 1//3, 1//3], zeros(T, n), fill(T(Inf), n), ["row1", "row2", "row3", "row4"], ["x", "y", "z"] ) ps = Tulip.PresolveData(pb) Tulip.presolve!(ps) @test ps.status == Tulip.Trm_Optimal @test ps.nrow == 0 @test ps.ncol == 0 # Check solution status & objective value sol = ps.solution @test sol.primal_status == Tulip.Sln_Optimal @test sol.dual_status == Tulip.Sln_Optimal @test sol.z_primal ≈ 1 @test sol.z_dual ≈ 1 return nothing end @testset "Empty row" begin for T in TvTYPES @testset "$T" begin empty_row_tests(T) test_empty_row_1(T) test_empty_row_2(T) test_empty_row_tolerances(T) end end end ================================================ FILE: test/Presolve/fixed_variable.jl ================================================ """ Remove fixed variables with explicit zeros. """ function test_fixed_variable_with_zeros(T::Type) pb = Tulip.ProblemData{T}() m, n = 3, 2 arows = [1, 1, 2, 2, 3, 3] acols = [1, 2, 1, 2, 1, 2] avals = T.([ 11, 0, 0, 21, 31, 0 ]) A = sparse(arows, acols, avals, m, n) Tulip.load_problem!(pb, "Test", true, T.(collect(1:n)), zero(T), A, zeros(T, m), ones(T, m), ones(T, n), ones(T, n), ["" for _ in 1:m], ["" for _ in 1:n] ) ps = Tulip.PresolveData(pb) @test ps.nzrow == [1, 1, 1] @test ps.nzcol == [2, 1] Tulip.remove_fixed_variable!(ps, 1) @test ps.colflag == [false, true] @test ps.obj0 == T(1) @test ps.nzrow == [0, 1, 0] Tulip.remove_fixed_variable!(ps, 2) @test ps.colflag == [false, false] @test ps.obj0 == T(3) @test ps.nzrow == [0, 0, 0] return nothing end @testset "Fixed variable" begin for T in TvTYPES @testset "$T" begin test_fixed_variable_with_zeros(T) end end end ================================================ FILE: test/Presolve/presolve.jl ================================================ include("./empty_column.jl") include("./empty_row.jl") include("fixed_variable.jl") ================================================ FILE: test/Project.toml ================================================ [deps] Krylov = "ba0b0d4f-ebba-5204-a429-3ac8c609bfb7" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] Krylov = "0.10" MathOptInterface= "1" ================================================ FILE: test/examples.jl ================================================ const examples_dir = joinpath(@__FILE__, "../../examples") @testset "Optimal" begin include(joinpath(examples_dir, "optimal.jl")) for T in TvTYPES @testset "$T" begin ex_optimal(T; OutputLevel=1, IPM_Factory=Tulip.Factory(Tulip.HSD)) ex_optimal(T; OutputLevel=1, IPM_Factory=Tulip.Factory(Tulip.MPC)) end end end @testset "Free vars" begin include(joinpath(examples_dir, "freevars.jl")) for T in TvTYPES @testset "$T" begin ex_freevars(T; OutputLevel=1, IPM_Factory=Tulip.Factory(Tulip.HSD)) ex_freevars(T; OutputLevel=1, IPM_Factory=Tulip.Factory(Tulip.MPC)) end end end @testset "PrimalInfeas" begin include(joinpath(examples_dir, "infeasible.jl")) for T in TvTYPES @testset "$T" begin ex_infeasible(T, OutputLevel=0) end end end @testset "DualInfeas" begin include(joinpath(examples_dir, "unbounded.jl")) for T in TvTYPES @testset "$T" begin ex_unbounded(T, OutputLevel=0) end end end @testset "Optimal Float32" begin include(joinpath(examples_dir, "optimal_other_type.jl")) end ================================================ FILE: test/runtests.jl ================================================ using LinearAlgebra using SparseArrays using Test using TOML using Tulip TLP = Tulip const TvTYPES = [Float32, Float64, BigFloat] # Check That Tulip.version() matches what's in the Project.toml tlp_ver = Tulip.version() toml_ver = VersionNumber(TOML.parsefile(joinpath(@__DIR__, "..", "Project.toml"))["version"]) @test tlp_ver == toml_ver @testset "Tulip" begin @testset "Unit tests" begin @testset "Core" begin include("Core/problemData.jl") end @testset "IPM" begin include("IPM/HSD.jl") include("IPM/MPC.jl") end @testset "KKT" begin include("KKT/KKT.jl") end @testset "Presolve" begin include("Presolve/presolve.jl") end end # UnitTest @testset "Examples" begin include("examples.jl") end @testset "Interfaces" begin include("Interfaces/julia_api.jl") end @testset "MOI" begin include("Interfaces/MOI_wrapper.jl") end # @testset "Convex Problem Depot tests" begin # for T in TvTYPES # @testset "$T" begin # Convex.ProblemDepot.run_tests(; exclude=[r"mip", r"exp", r"socp", r"sdp"], T = T) do problem # Convex.solve!(problem, () -> Tulip.Optimizer{T}()) # end # end # end # end end # Tulip tests