Showing preview only (310K chars total). Download the full file or copy to clipboard to get everything.
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 <mathieu.tanneau@gmail.com>"]
[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
[](https://zenodo.org/badge/latestdoi/131298750)
[](https://github.com/ds4dm/Tulip.jl/actions?query=workflow%3ACI)
[](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 <mathieu.tanneau@gmail.com>"]
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_<Param>"`.
## 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.colf
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
Condensed preview — 106 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (311K chars).
[
{
"path": ".github/workflows/CompatHelper.yml",
"chars": 456,
"preview": "name: CompatHelper\non:\n schedule:\n - cron: 0 0 * * *\n workflow_dispatch:\njobs:\n CompatHelper:\n runs-on: ubuntu-"
},
{
"path": ".github/workflows/Documentation.yml",
"chars": 729,
"preview": "name: Documentation\non:\n push:\n branches: [master]\n tags: '*'\n pull_request:\n types: [opened, synchronize, re"
},
{
"path": ".github/workflows/TagBot.yml",
"chars": 317,
"preview": "name: TagBot\non:\n issue_comment:\n types:\n - created\n workflow_dispatch:\njobs:\n TagBot:\n if: github.event_n"
},
{
"path": ".github/workflows/ci.yml",
"chars": 951,
"preview": "name: CI\non:\n push:\n branches:\n - master\n - /^release-.*$/\n pull_request:\n types: [opened, synchronize"
},
{
"path": ".gitignore",
"chars": 76,
"preview": "# Julia\n*.jl.cov\n*.jl.*.cov\n*.jl.mem\nManifest.toml\n\n/dat\n/exp\n/.vscode\n/*.jl"
},
{
"path": "CITATION.bib",
"chars": 321,
"preview": "@TechReport{Tulip.jl,\n title = {{Tulip}.jl: an open-source interior-point linear optimization\n solver with abstrac"
},
{
"path": "LICENSE.md",
"chars": 18036,
"preview": "Copyright (c) 2018-2019: Mathieu Tanneau\n\nTulip.jl is licensed under the [MPL version 2.0](https://www.mozilla.org/MPL/2"
},
{
"path": "NEWS.md",
"chars": 2218,
"preview": "# Tulip release notes\n\n# v0.7.1 (January 8, 2021)\n\n## Bug fixes\n* Fix a bug in `add_variable!` and the handling of zero "
},
{
"path": "Project.toml",
"chars": 1086,
"preview": "name = \"Tulip\"\nuuid = \"6dd1b50a-3aae-11e9-10b5-ef983d2400fa\"\nversion = \"0.9.8\"\nauthors = [\"Mathieu Tanneau <mathieu.tann"
},
{
"path": "README.md",
"chars": 4571,
"preview": "# Tulip\n\n[](https://zenodo.org/badge/latestdoi/131298750)\n[ interior-poin"
},
{
"path": "app/precompile_app.jl",
"chars": 595,
"preview": "using TulipCL\n\nconst EXDIR = joinpath(dirname(pathof(TulipCL.Tulip)), \"../examples/dat\")\n\n# Run all example problems\nfor"
},
{
"path": "app/src/TulipCL.jl",
"chars": 2195,
"preview": "module TulipCL\n\nusing Printf\n\nimport Tulip\nconst TLP = Tulip\n\nusing ArgParse\n\nfunction julia_main()::Cint\n try\n "
},
{
"path": "docs/.gitignore",
"chars": 20,
"preview": "build/\nManifest.toml"
},
{
"path": "docs/Project.toml",
"chars": 210,
"preview": "[deps]\nDocumenter = \"e30172f5-a6a5-5a46-863b-614d45cd2de4\"\nJuMP = \"4076af6c-e467-56ae-b986-b466b2749572\"\nMathOptInterfac"
},
{
"path": "docs/make.jl",
"chars": 1290,
"preview": "using Documenter, Tulip\n\nconst _FAST = findfirst(isequal(\"--fast\"), ARGS) !== nothing\n\nmakedocs(\n sitename = \"Tulip.j"
},
{
"path": "docs/src/index.md",
"chars": 862,
"preview": "```@meta\nCurrentModule = Tulip\n```\n\n# Tulip.jl\n\n## Overview\n\nTulip is an open-source interior-point solver for linear pr"
},
{
"path": "docs/src/manual/IPM/HSD.md",
"chars": 235,
"preview": "# Homogeneous Self-Dual algorithm\n\n```@docs\nTulip.HSD\n```\n\n## References\n\n* Anjos, M.F.; Lodi, A.; Tanneau, M.\n [Desi"
},
{
"path": "docs/src/manual/IPM/MPC.md",
"chars": 238,
"preview": "# Predictor-Corrector algorithm\n\n```@docs\nTulip.MPC\n```\n\n## References\n\n* Mehrotra, S.\n [On the Implementation of a P"
},
{
"path": "docs/src/manual/formulation.md",
"chars": 1118,
"preview": "# Problem formulation\n\n## Model input\n\nTulip takes as input LP problems of the form\n```math\n \\begin{array}{rrcll}\n "
},
{
"path": "docs/src/manual/options.md",
"chars": 312,
"preview": "```@meta\nCurrentModule = Tulip\n```\n\n# Options management\n\n!!! info\n This part of the documentation is under construct"
},
{
"path": "docs/src/reference/API.md",
"chars": 65,
"preview": "```@autodocs\nModules = [Tulip]\nPages = [\"tulip_julia_api.jl\"]\n```"
},
{
"path": "docs/src/reference/KKT/kkt.md",
"chars": 1080,
"preview": "```@meta\nCurrentModule = Tulip.KKT\n```\n\n# Overview\n\nThe `KKT` module provides a modular, customizable interface for deve"
},
{
"path": "docs/src/reference/KKT/tlp_cholmod.md",
"chars": 124,
"preview": "```@meta\nCurrentModule = Tulip.KKT\n```\n\n# TlpCholmod\n\n```@docs\nTlpCholmod.Backend\n```\n\n```@docs\nTlpCholmod.CholmodSolver"
},
{
"path": "docs/src/reference/KKT/tlp_dense.md",
"chars": 125,
"preview": "```@meta\nCurrentModule = Tulip.KKT.TlpDense\n```\n\n# TlpDense\n\n```@docs\nTlpDense.Backend\n```\n\n```@docs\nTlpDense.DenseSolve"
},
{
"path": "docs/src/reference/KKT/tlp_krylov.md",
"chars": 373,
"preview": "```@meta\nCurrentModule = Tulip.KKT.TlpKrylov\n```\n\n# TlpKrylov\n\n!!! warning\n Iterative methods are still an experiment"
},
{
"path": "docs/src/reference/KKT/tlp_ldlfact.md",
"chars": 175,
"preview": "```@meta\nCurrentModule = Tulip.KKT.TlpLDLFactorizations\n```\n\n# TlpLDLFactorizations\n\n```@docs\nTlpLDLFactorizations.Backe"
},
{
"path": "docs/src/reference/Presolve/presolve.md",
"chars": 0,
"preview": ""
},
{
"path": "docs/src/reference/attributes.md",
"chars": 2530,
"preview": "```@meta\nCurrentModule = Tulip\n```\n\n# Attribute reference\n\nAttributes are queried using [`get_attribute`](@ref) and set "
},
{
"path": "docs/src/reference/options.md",
"chars": 2553,
"preview": "```@meta\nCurrentModule = Tulip\n```\n\n# Options reference\n\nParameters can be queried and set through the [`get_parameter`]"
},
{
"path": "docs/src/tutorials/lp_example.md",
"chars": 4769,
"preview": "# Toy example\n\nTulip can be accessed in 3 ways:\nthrough [JuMP](https://github.com/jump-dev/JuMP.jl),\nthrough [MathOptInt"
},
{
"path": "examples/.gitignore",
"chars": 4,
"preview": "dat/"
},
{
"path": "examples/freevars.jl",
"chars": 1723,
"preview": "using LinearAlgebra\nusing SparseArrays\nusing Test\n\nimport Tulip\nTLP = Tulip\n\nINSTANCE_DIR = joinpath(@__DIR__, \"dat\")\n\nf"
},
{
"path": "examples/infeasible.jl",
"chars": 1644,
"preview": "using LinearAlgebra\nusing SparseArrays\nusing Test\n\nimport Tulip\nTLP = Tulip\n\nINSTANCE_DIR = joinpath(@__DIR__, \"dat\")\n\nf"
},
{
"path": "examples/optimal.jl",
"chars": 1832,
"preview": "using LinearAlgebra\nusing SparseArrays\nusing Test\n\nimport Tulip\nTLP = Tulip\n\nINSTANCE_DIR = joinpath(@__DIR__, \"dat\")\n\nf"
},
{
"path": "examples/optimal_other_type.jl",
"chars": 1253,
"preview": "using Tulip\nimport MathOptInterface\nconst MOI = MathOptInterface\nusing Test\n\nconst T = Float32\n\nlp = Tulip.Optimizer{T}("
},
{
"path": "examples/unbounded.jl",
"chars": 1333,
"preview": "using LinearAlgebra\nusing SparseArrays\nusing Test\n\nimport Tulip\nTLP = Tulip\n\nINSTANCE_DIR = joinpath(@__DIR__, \"dat\")\n\nf"
},
{
"path": "src/IPM/HSD/HSD.jl",
"chars": 10539,
"preview": "\"\"\"\n HSD\n\nSolver for the homogeneous self-dual algorithm.\n\"\"\"\nmutable struct HSD{T, Tv, Tb, Ta, Tk} <: AbstractIPMOpt"
},
{
"path": "src/IPM/HSD/step.jl",
"chars": 11165,
"preview": "\"\"\"\n compute_step!(hsd, params)\n\nCompute next IP iterate for the HSD formulation.\n\n# Arguments\n- `hsd`: The HSD optim"
},
{
"path": "src/IPM/IPM.jl",
"chars": 473,
"preview": "using Printf\n\n\"\"\"\n AbstractIPMOptimizer\n\nAbstraction layer for IPM solvers.\n\nAn IPM solver implements an interior-poi"
},
{
"path": "src/IPM/MPC/MPC.jl",
"chars": 12130,
"preview": "\"\"\"\n MPC\n\nImplements Mehrotra's Predictor-Corrector interior-point algorithm.\n\"\"\"\nmutable struct MPC{T, Tv, Tb, Ta, T"
},
{
"path": "src/IPM/MPC/step.jl",
"chars": 9494,
"preview": "\"\"\"\n compute_step!(ipm, params)\n\nCompute next IP iterate for the MPC formulation.\n\n# Arguments\n- `ipm`: The MPC optim"
},
{
"path": "src/IPM/ipmdata.jl",
"chars": 4467,
"preview": "\"\"\"\n IPMData{T, Tv, Ta}\n\nHolds data about an interior point method.\n\nThe problem is represented as\n```\nmin c'x + c0"
},
{
"path": "src/IPM/options.jl",
"chars": 837,
"preview": "Base.@kwdef mutable struct IPMOptions{T}\n\n OutputLevel::Int = 0\n\n # User limits\n IterationsLimit::Int = 100\n "
},
{
"path": "src/IPM/point.jl",
"chars": 1336,
"preview": "\"\"\"\n Point{T, Tv}\n\nPrimal-dual point.\n\"\"\"\nmutable struct Point{T, Tv}\n # Dimensions\n m::Int # Number of constr"
},
{
"path": "src/IPM/residuals.jl",
"chars": 471,
"preview": "\"\"\"\n Residuals{T, Tv}\n\nData structure for IPM residual vectors.\n\"\"\"\nmutable struct Residuals{T, Tv}\n # Primal resi"
},
{
"path": "src/Interfaces/MOI/MOI_wrapper.jl",
"chars": 6715,
"preview": "import MathOptInterface as MOI\n\n# ==============================================================================\n# "
},
{
"path": "src/Interfaces/MOI/attributes.jl",
"chars": 6434,
"preview": "# =============================================\n# Supported attributes\n# ============================================="
},
{
"path": "src/Interfaces/MOI/constraints.jl",
"chars": 20319,
"preview": "# =============================================\n# 1. Supported constraints and attributes\n# =========================="
},
{
"path": "src/Interfaces/MOI/objective.jl",
"chars": 2891,
"preview": "# =============================================\n# 1. Supported objectives\n# =========================================="
},
{
"path": "src/Interfaces/MOI/variables.jl",
"chars": 3618,
"preview": "# =============================================\n# 1. Supported variable attributes\n# ================================="
},
{
"path": "src/Interfaces/tulip_julia_api.jl",
"chars": 8048,
"preview": "using QPSReader\nusing TimerOutputs: tottime\n\n# TODO: user-facing API in Julia\n# Other APIs should wrap this one\n# TODO: "
},
{
"path": "src/KKT/Cholmod/cholmod.jl",
"chars": 1854,
"preview": "module TlpCholmod\n\nusing LinearAlgebra\nusing SparseArrays\nusing SparseArrays.CHOLMOD\n\nusing ..KKT: AbstractKKTBackend, A"
},
{
"path": "src/KKT/Cholmod/spd.jl",
"chars": 1822,
"preview": "const CholmodSPD = CholmodSolver{Float64,K1}\n\nlinear_system(::CholmodSPD) = \"Normal equations (K1)\"\n\nfunction setup(A::S"
},
{
"path": "src/KKT/Cholmod/sqd.jl",
"chars": 1899,
"preview": "const CholmodSQD = CholmodSolver{Float64,K2}\n\nlinear_system(::CholmodSQD) = \"Augmented system (K2)\"\n\nfunction setup(A::S"
},
{
"path": "src/KKT/Dense/lapack.jl",
"chars": 3065,
"preview": "module TlpDense\n\nusing LinearAlgebra\nusing LinearAlgebra:BlasReal\n\nusing ..KKT: AbstractKKTBackend, AbstractKKTSolver\nus"
},
{
"path": "src/KKT/KKT.jl",
"chars": 2935,
"preview": "module KKT\n\nusing LinearAlgebra\nusing SparseArrays\n\nusing LinearOperators\n\nexport AbstractKKTSystem, AbstractKKTBackend,"
},
{
"path": "src/KKT/Krylov/defs.jl",
"chars": 549,
"preview": "const _KRYLOV_SPD = Union{\n Krylov.CgWorkspace,\n Krylov.CrWorkspace,\n Krylov.CarWorkspace,\n}\n\nconst _KRYLOV_SID"
},
{
"path": "src/KKT/Krylov/krylov.jl",
"chars": 1553,
"preview": "module TlpKrylov\n\nusing LinearAlgebra\n\nusing Krylov\nusing LinearOperators\nconst LO = LinearOperators\n\nusing ..KKT: Abstr"
},
{
"path": "src/KKT/Krylov/sid.jl",
"chars": 2465,
"preview": "\"\"\"\n SIDSolver\n\"\"\"\nmutable struct SIDSolver{T,V,Ta,KL,KW} <: AbstractKrylovSolver{T}\n # Problem data\n m::Int\n "
},
{
"path": "src/KKT/Krylov/spd.jl",
"chars": 2502,
"preview": "\"\"\"\n SPDSolver\n\"\"\"\nmutable struct SPDSolver{T,V,Ta,KL,KS} <: AbstractKrylovSolver{T}\n # Problem data\n m::Int\n "
},
{
"path": "src/KKT/Krylov/sqd.jl",
"chars": 2195,
"preview": "\"\"\"\n SQDSolver\n\"\"\"\nmutable struct SQDSolver{T,V,Ta,KS} <: AbstractKrylovSolver{T}\n # Problem data\n m::Int\n n"
},
{
"path": "src/KKT/LDLFactorizations/ldlfact.jl",
"chars": 3734,
"preview": "module TlpLDLFactorizations\n\nusing LinearAlgebra\nusing SparseArrays\n\nusing LDLFactorizations\nconst LDLF = LDLFactorizati"
},
{
"path": "src/KKT/Test/test.jl",
"chars": 977,
"preview": "using Test\nusing LinearAlgebra\n\n\"\"\"\n run_ls_tests(A, kkt; atol)\n\n\n\"\"\"\nfunction run_ls_tests(\n A::AbstractMatrix{T}"
},
{
"path": "src/KKT/systems.jl",
"chars": 1219,
"preview": "\"\"\"\n DefaultKKTSystem\n\nDefault KKT system setting. Currently equivalent to [`K2`](@ref)\n\"\"\"\nstruct DefaultKKTSystem <"
},
{
"path": "src/LinearAlgebra/LinearAlgebra.jl",
"chars": 779,
"preview": "module TLPLinearAlgebra\n\nusing LinearAlgebra\nusing SparseArrays\nexport construct_matrix\n\nimport ..Tulip.Factory\n\n\"\"\"\n "
},
{
"path": "src/Presolve/Presolve.jl",
"chars": 19667,
"preview": "Base.@kwdef mutable struct PresolveOptions{T}\n Level::Int = 1 # Presolve level\n\n TolerancePFeas::T = sqrt(eps(T))"
},
{
"path": "src/Presolve/dominated_column.jl",
"chars": 4750,
"preview": "struct DominatedColumn{T} <: PresolveTransformation{T}\n j::Int\n x::T # Primal value\n cj::T # Objective\n co"
},
{
"path": "src/Presolve/empty_column.jl",
"chars": 3372,
"preview": "struct EmptyColumn{T} <: PresolveTransformation{T}\n j::Int # variable index\n x::T # Variable value\n s::T # R"
},
{
"path": "src/Presolve/empty_row.jl",
"chars": 2430,
"preview": "# TODO: this is redundant with forcing constraint checks\n# => an empty row is automatically redundant or infeasible\n\ns"
},
{
"path": "src/Presolve/fixed_variable.jl",
"chars": 1857,
"preview": "struct FixedVariable{T} <: PresolveTransformation{T}\n j::Int # variable index\n x::T # primal value\n c::T # c"
},
{
"path": "src/Presolve/forcing_row.jl",
"chars": 6264,
"preview": "struct ForcingRow{T} <: PresolveTransformation{T}\n i::Int # Row index\n at_lower::Bool # Whether row is forced to"
},
{
"path": "src/Presolve/free_column_singleton.jl",
"chars": 3528,
"preview": "struct FreeColumnSingleton{T} <: PresolveTransformation{T}\n i::Int # Row index\n j::Int # Column index\n l::T "
},
{
"path": "src/Presolve/row_singleton.jl",
"chars": 2524,
"preview": "struct RowSingleton{T} <: PresolveTransformation{T}\n i::Int # Row index\n j::Int # Column index\n aij::T # Row"
},
{
"path": "src/Tulip.jl",
"chars": 1001,
"preview": "module Tulip\n\nusing LinearAlgebra\nusing Logging\nusing Printf\nusing SparseArrays\nusing TOML\n\nusing TimerOutputs\n\n# Read T"
},
{
"path": "src/attributes.jl",
"chars": 5414,
"preview": "abstract type AbstractAttribute end\n\n# ==============================================================================\n#\n"
},
{
"path": "src/model.jl",
"chars": 6606,
"preview": "mutable struct Model{T}\n\n # Parameters\n params::Parameters{T}\n\n # TODO: model status\n #= Use an enum\n "
},
{
"path": "src/parameters.jl",
"chars": 435,
"preview": "\"\"\"\n Parameters{T}\n\n\"\"\"\nBase.@kwdef mutable struct Parameters{T}\n # Model-wise parameters\n Threads::Int = 1\n "
},
{
"path": "src/problemData.jl",
"chars": 11941,
"preview": "using SparseArrays\n\nmutable struct RowOrCol{T}\n nzind::Vector{Int}\n nzval::Vector{T}\nend\n\nconst Row = RowOrCol\ncon"
},
{
"path": "src/solution.jl",
"chars": 947,
"preview": "mutable struct Solution{T}\n m::Int\n n::Int\n\n primal_status::SolutionStatus\n dual_status::SolutionStatus\n "
},
{
"path": "src/status.jl",
"chars": 1479,
"preview": "\"\"\"\n TerminationStatus\n\n- `Success`: No error occured\n- `PrimalInfeasibleNoResult`: Problem is proved to be primal in"
},
{
"path": "src/utils.jl",
"chars": 1124,
"preview": "using CodecBzip2\nusing CodecZlib\n\n\"\"\"\n _open(f, fname)\n\nOpen a file with decompression stream as required.\n\"\"\"\nfuncti"
},
{
"path": "test/Core/problemData.jl",
"chars": 6359,
"preview": "function run_tests_pbdata(::Type{T}) where{T}\n\n @testset \"Creation\" begin\n pb = TLP.ProblemData{T}(\"test\")\n\n "
},
{
"path": "test/IPM/HSD.jl",
"chars": 3857,
"preview": "function run_tests_hsd(T::Type)\n\n Tv = Vector{T}\n\n params = TLP.IPMOptions{T}()\n kkt_options = TLP.KKTOptions{T"
},
{
"path": "test/IPM/MPC.jl",
"chars": 3774,
"preview": "function run_tests_mpc(T::Type)\n\n Tv = Vector{T}\n\n params = TLP.IPMOptions{T}()\n kkt_options = TLP.KKTOptions{T"
},
{
"path": "test/Interfaces/MOI_wrapper.jl",
"chars": 2800,
"preview": "# Copyright 2018-2019: Mathieu Tanneau\n# This Source Code Form is subject to the terms of the Mozilla Public\n# Licens"
},
{
"path": "test/Interfaces/julia_api.jl",
"chars": 1143,
"preview": "import Tulip\nusing Test\n\nfunction test_reader()\n lp = Tulip.Model{Float64}()\n\n Tulip.load_problem!(lp, joinpath(@_"
},
{
"path": "test/Interfaces/lp.mps",
"chars": 594,
"preview": "NAME LP1 \n\n* Problem:\n* min x1 + 2*x2\n* s"
},
{
"path": "test/KKT/Cholmod/cholmod.jl",
"chars": 375,
"preview": "@testset \"CHOLMOD\" begin\n\n A = SparseMatrixCSC{Float64, Int}([\n 1 0 1 0;\n 0 1 0 1\n ])\n\n @testset "
},
{
"path": "test/KKT/Dense/lapack.jl",
"chars": 291,
"preview": "@testset \"LAPACK\" begin\n for T in TvTYPES\n @testset \"$T\" begin\n A = Matrix{T}([\n 1 0"
},
{
"path": "test/KKT/KKT.jl",
"chars": 148,
"preview": "const KKT = Tulip.KKT\n\ninclude(\"Dense/lapack.jl\")\ninclude(\"Cholmod/cholmod.jl\")\ninclude(\"LDLFactorizations/ldlfact.jl\")\n"
},
{
"path": "test/KKT/Krylov/krylov.jl",
"chars": 108,
"preview": "using Krylov\n\n@testset \"Krylov\" begin\n include(\"spd.jl\")\n include(\"sid.jl\")\n include(\"sqd.jl\")\nend\n"
},
{
"path": "test/KKT/Krylov/sid.jl",
"chars": 472,
"preview": "function test_krylov_sid(T, ksolver)\n A = SparseMatrixCSC{T, Int}([\n 1 0 1 0;\n 0 1 0 1\n ])\n\n kkt "
},
{
"path": "test/KKT/Krylov/spd.jl",
"chars": 427,
"preview": "function test_krylov_spd(T, ksolver)\n A = SparseMatrixCSC{T, Int}([\n 1 0 1 0;\n 0 1 0 1\n ])\n\n kkt "
},
{
"path": "test/KKT/Krylov/sqd.jl",
"chars": 432,
"preview": "function test_krylov_sqd(T, ksolver)\n A = SparseMatrixCSC{T, Int}([\n 1 0 1 0;\n 0 1 0 1\n ])\n\n kkt "
},
{
"path": "test/KKT/LDLFactorizations/ldlfact.jl",
"chars": 308,
"preview": "@testset \"LDLFact\" begin\n for T in TvTYPES\n @testset \"$T\" begin\n A = SparseMatrixCSC{T, Int}([\n "
},
{
"path": "test/Presolve/empty_column.jl",
"chars": 2368,
"preview": "function emtpy_column_tests(T::Type)\n\n # We test all the following combinations:\n #=\n min c * x\n "
},
{
"path": "test/Presolve/empty_row.jl",
"chars": 4744,
"preview": "function empty_row_tests(T::Type)\n\n # Build the following model\n #=\n min x + y\n s.t. -1 ⩽ 0 * "
},
{
"path": "test/Presolve/fixed_variable.jl",
"chars": 1075,
"preview": "\"\"\"\nRemove fixed variables with explicit zeros.\n\"\"\"\nfunction test_fixed_variable_with_zeros(T::Type)\n\n pb = Tulip.Pro"
},
{
"path": "test/Presolve/presolve.jl",
"chars": 83,
"preview": "include(\"./empty_column.jl\")\ninclude(\"./empty_row.jl\")\ninclude(\"fixed_variable.jl\")"
},
{
"path": "test/Project.toml",
"chars": 362,
"preview": "[deps]\nKrylov = \"ba0b0d4f-ebba-5204-a429-3ac8c609bfb7\"\nLinearAlgebra = \"37e2e46d-f89d-539d-b4ee-838fcccc9c8e\"\nMathOptInt"
},
{
"path": "test/examples.jl",
"chars": 1132,
"preview": "const examples_dir = joinpath(@__FILE__, \"../../examples\")\n\n@testset \"Optimal\" begin\n include(joinpath(examples_dir, "
},
{
"path": "test/runtests.jl",
"chars": 1265,
"preview": "using LinearAlgebra\nusing SparseArrays\nusing Test\nusing TOML\n\nusing Tulip\nTLP = Tulip\n\nconst TvTYPES = [Float32, Float64"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the ds4dm/Tulip.jl GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 106 files (283.8 KB), approximately 91.9k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.