├── lib ├── highs │ ├── version.rb │ ├── array.rb │ ├── model.rb │ ├── methods.rb │ └── ffi.rb └── highs.rb ├── Gemfile ├── .gitignore ├── test ├── support │ └── lp.mps ├── test_helper.rb └── highs_test.rb ├── highs.gemspec ├── .github └── workflows │ └── build.yml ├── CHANGELOG.md ├── vendor ├── LICENSE.txt └── LICENSE-THIRD-PARTY.txt ├── LICENSE.txt ├── Rakefile └── README.md /lib/highs/version.rb: -------------------------------------------------------------------------------- 1 | module Highs 2 | VERSION = "0.2.6" 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rake" 6 | gem "minitest" 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | *.lock 10 | *.a 11 | *.dll 12 | *.dylib 13 | *.so 14 | -------------------------------------------------------------------------------- /test/support/lp.mps: -------------------------------------------------------------------------------- 1 | NAME 2 | ROWS 3 | N COST 4 | G R0 5 | G R1 6 | G R2 7 | COLUMNS 8 | C0 COST 8 9 | C0 R0 2 10 | C0 R1 3 11 | C0 R2 2 12 | C1 COST 10 13 | C1 R0 2 14 | C1 R1 4 15 | C1 R2 1 16 | RHS 17 | RHS_V R0 7 18 | RHS_V R1 12 19 | RHS_V R2 6 20 | ENDATA 21 | -------------------------------------------------------------------------------- /highs.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/highs/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "highs" 5 | spec.version = Highs::VERSION 6 | spec.summary = "Linear optimization for Ruby" 7 | spec.homepage = "https://github.com/ankane/highs-ruby" 8 | spec.license = "MIT" 9 | 10 | spec.author = "Andrew Kane" 11 | spec.email = "andrew@ankane.org" 12 | 13 | spec.files = Dir["*.{md,txt}", "{lib,vendor}/**/*"] 14 | spec.require_path = "lib" 15 | 16 | spec.required_ruby_version = ">= 3.1" 17 | 18 | spec.add_dependency "fiddle" 19 | end 20 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | Bundler.require(:default) 3 | require "minitest/autorun" 4 | require "minitest/pride" 5 | 6 | class Minitest::Test 7 | def setup 8 | # autoload before GC.stress 9 | Highs::FFI if stress? 10 | 11 | GC.stress = true if stress? 12 | end 13 | 14 | def teardown 15 | GC.stress = false if stress? 16 | end 17 | 18 | def stress? 19 | ENV["STRESS"] 20 | end 21 | 22 | def assert_elements_in_delta(expected, actual) 23 | assert_equal expected.size, actual.size 24 | expected.zip(actual) do |exp, act| 25 | assert_in_delta exp, act 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | include: 9 | - ruby: 3.4 10 | os: ubuntu-24.04 11 | - ruby: 3.3 12 | os: ubuntu-22.04 13 | - ruby: 3.2 14 | os: macos-15 15 | - ruby: 3.1 16 | os: macos-15-intel 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: ${{ matrix.ruby }} 23 | bundler-cache: true 24 | - run: bundle exec rake vendor:platform 25 | - run: bundle exec rake test 26 | -------------------------------------------------------------------------------- /lib/highs/array.rb: -------------------------------------------------------------------------------- 1 | module Highs 2 | class BaseArray 3 | NOT_SET = Object.new 4 | 5 | def initialize(size, value = NOT_SET) 6 | @size = size 7 | @ptr = 8 | if value == NOT_SET 9 | Fiddle::Pointer.malloc(size * self.class::SIZE, Fiddle::RUBY_FREE) 10 | else 11 | if value.size != size 12 | # TODO add variable name to message 13 | raise ArgumentError, "wrong size (given #{value.size}, expected #{size})" 14 | end 15 | Fiddle::Pointer[value.pack("#{self.class::FORMAT}#{size}")] 16 | end 17 | end 18 | 19 | def to_a 20 | @ptr[0, @size * self.class::SIZE].unpack("#{self.class::FORMAT}#{@size}") 21 | end 22 | 23 | def to_ptr 24 | @ptr 25 | end 26 | end 27 | 28 | class DoubleArray < BaseArray 29 | FORMAT = "d" 30 | SIZE = Fiddle::SIZEOF_DOUBLE 31 | end 32 | 33 | class IntArray < BaseArray 34 | FORMAT = "i!" 35 | SIZE = Fiddle::SIZEOF_INT 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/highs.rb: -------------------------------------------------------------------------------- 1 | # stdlib 2 | require "fiddle/import" 3 | 4 | # modules 5 | require_relative "highs/array" 6 | require_relative "highs/methods" 7 | require_relative "highs/model" 8 | require_relative "highs/version" 9 | 10 | module Highs 11 | class Error < StandardError; end 12 | 13 | extend Methods 14 | 15 | class << self 16 | attr_accessor :ffi_lib 17 | end 18 | lib_name = 19 | if Gem.win_platform? 20 | # uses lib prefix for Windows 21 | "libhighs.dll" 22 | elsif RbConfig::CONFIG["host_os"] =~ /darwin/i 23 | if RbConfig::CONFIG["host_cpu"] =~ /arm|aarch64/i 24 | "libhighs.arm64.dylib" 25 | else 26 | "libhighs.dylib" 27 | end 28 | else 29 | if RbConfig::CONFIG["host_cpu"] =~ /arm|aarch64/i 30 | "libhighs.arm64.so" 31 | else 32 | "libhighs.so" 33 | end 34 | end 35 | vendor_lib = File.expand_path("../vendor/#{lib_name}", __dir__) 36 | self.ffi_lib = [vendor_lib] 37 | 38 | # friendlier error message 39 | autoload :FFI, "highs/ffi" 40 | end 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.6 (2025-06-07) 2 | 3 | - Updated HiGHS to 1.11.0 4 | 5 | ## 0.2.5 (2025-05-04) 6 | 7 | - Fixed memory leak 8 | 9 | ## 0.2.4 (2025-03-20) 10 | 11 | - Updated HiGHS to 1.10.0 12 | 13 | ## 0.2.3 (2025-01-05) 14 | 15 | - Updated HiGHS to 1.9.0 16 | 17 | ## 0.2.2 (2024-12-29) 18 | 19 | - Fixed warning with Ruby 3.4 20 | 21 | ## 0.2.1 (2024-10-19) 22 | 23 | - Updated HiGHS to 1.8.0 24 | 25 | ## 0.2.0 (2024-07-31) 26 | 27 | - Updated HiGHS to 1.7.2 28 | - Dropped support for Ruby < 3.1 29 | 30 | ## 0.1.5 (2023-06-07) 31 | 32 | - Updated HiGHS to 1.5.1 33 | - Fixed error with `dup` and `clone` 34 | 35 | ## 0.1.4 (2022-12-08) 36 | 37 | - Updated HiGHS to 1.4.1 38 | 39 | ## 0.1.3 (2022-11-05) 40 | 41 | - Updated HiGHS to 1.3.0 42 | 43 | ## 0.1.2 (2022-04-14) 44 | 45 | - Added `verbose` and `time_limit` options to `solve` method 46 | - Added support for symbol integrality 47 | 48 | ## 0.1.1 (2022-04-10) 49 | 50 | - Added objective value 51 | - Added support for reading and writing models 52 | 53 | ## 0.1.0 (2022-04-05) 54 | 55 | - First release 56 | -------------------------------------------------------------------------------- /vendor/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 HiGHS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2024 HiGHS 4 | Copyright (c) 2022-2024 Andrew Kane 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/highs/model.rb: -------------------------------------------------------------------------------- 1 | module Highs 2 | class Model 3 | def initialize 4 | @ptr = FFI.Highs_create 5 | @ptr.free = FFI["Highs_destroy"] 6 | 7 | check_status FFI.Highs_setBoolOptionValue(@ptr, +"output_flag", 0) 8 | end 9 | 10 | def solve(verbose: false, time_limit: nil) 11 | num_col = FFI.Highs_getNumCol(@ptr) 12 | num_row = FFI.Highs_getNumRow(@ptr) 13 | 14 | col_value = DoubleArray.new(num_col) 15 | col_dual = DoubleArray.new(num_col) 16 | row_value = DoubleArray.new(num_row) 17 | row_dual = DoubleArray.new(num_row) 18 | col_basis = IntArray.new(num_col) 19 | row_basis = IntArray.new(num_row) 20 | 21 | with_options(verbose: verbose, time_limit: time_limit) do 22 | check_status FFI.Highs_run(@ptr) 23 | end 24 | check_status FFI.Highs_getSolution(@ptr, col_value, col_dual, row_value, row_dual) 25 | check_status FFI.Highs_getBasis(@ptr, col_basis, row_basis) 26 | model_status = FFI.Highs_getModelStatus(@ptr) 27 | 28 | { 29 | status: FFI::MODEL_STATUS[model_status], 30 | obj_value: FFI.Highs_getObjectiveValue(@ptr), 31 | col_value: col_value.to_a, 32 | col_dual: col_dual.to_a, 33 | row_value: row_value.to_a, 34 | row_dual: row_dual.to_a, 35 | col_basis: col_basis.to_a.map { |v| FFI::BASIS_STATUS[v] }, 36 | row_basis: row_basis.to_a.map { |v| FFI::BASIS_STATUS[v] } 37 | } 38 | end 39 | 40 | def write(filename) 41 | check_status FFI.Highs_writeModel(@ptr, +filename) 42 | end 43 | 44 | def to_ptr 45 | @ptr 46 | end 47 | 48 | private 49 | 50 | def check_status(status) 51 | Highs.send(:check_status, status) 52 | end 53 | 54 | def with_options(verbose:, time_limit:) 55 | check_status(FFI.Highs_setBoolOptionValue(@ptr, +"output_flag", 1)) if verbose 56 | check_status(FFI.Highs_setDoubleOptionValue(@ptr, +"time_limit", time_limit)) if time_limit 57 | yield 58 | ensure 59 | check_status(FFI.Highs_setBoolOptionValue(@ptr, +"output_flag", 0)) if verbose 60 | check_status(FFI.Highs_setDoubleOptionValue(@ptr, +"time_limit", Float::INFINITY)) if time_limit 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | task default: :test 5 | Rake::TestTask.new do |t| 6 | t.libs << "test" 7 | t.pattern = "test/**/*_test.rb" 8 | end 9 | 10 | shared_libraries = %w(libhighs.so libhighs.arm64.so libhighs.dylib libhighs.arm64.dylib) 11 | 12 | # ensure vendor files exist 13 | task :ensure_vendor do 14 | shared_libraries.each do |file| 15 | raise "Missing file: #{file}" unless File.exist?("vendor/#{file}") 16 | end 17 | end 18 | 19 | Rake::Task["build"].enhance [:ensure_vendor] 20 | 21 | def version 22 | "1.11.0" 23 | end 24 | 25 | def download_file(library, remote_lib, file, sha256) 26 | require "fileutils" 27 | require "open-uri" 28 | require "tmpdir" 29 | 30 | url = "https://github.com/JuliaBinaryWrappers/HiGHS_jll.jl/releases/download/HiGHS-v#{version}%2B0/#{file}" 31 | puts "Downloading #{file}..." 32 | contents = URI.parse(url).read 33 | 34 | computed_sha256 = Digest::SHA256.hexdigest(contents) 35 | raise "Bad hash: #{computed_sha256}" if computed_sha256 != sha256 36 | 37 | Dir.chdir(Dir.mktmpdir) do 38 | File.binwrite(file, contents) 39 | command = "tar xzf" 40 | system "#{command} #{file}" 41 | dest = File.expand_path("vendor", __dir__) 42 | 43 | FileUtils.cp(remote_lib, "#{dest}/#{library}") 44 | puts "Saved vendor/#{library}" 45 | 46 | if library.end_with?(".so") 47 | license_path = "share/licenses/HiGHS/LICENSE.txt" 48 | raise "Unexpected licenses" unless Dir["share/licenses/**/*"] != [license_path] 49 | FileUtils.cp(license_path, "#{dest}/LICENSE.txt") 50 | puts "Saved vendor/LICENSE.txt" 51 | end 52 | end 53 | end 54 | 55 | # https://github.com/JuliaBinaryWrappers/HiGHS_jll.jl/releases 56 | namespace :vendor do 57 | task :linux do 58 | download_file("libhighs.so", "lib/libhighs.so", "HiGHS.v#{version}.x86_64-linux-gnu-cxx11.tar.gz", "2e93fc61565295e67cc4e0e902f4cd3f2d3c6984799be66710159078c0ec3a4c") 59 | download_file("libhighs.arm64.so", "lib/libhighs.so", "HiGHS.v#{version}.aarch64-linux-gnu-cxx11.tar.gz", "aab4feb8dbdf13706ed2b9d46af2625245d212ed7e1c370ff30076c9b4043bd2") 60 | end 61 | 62 | task :mac do 63 | download_file("libhighs.dylib", "lib/libhighs.dylib", "HiGHS.v#{version}.x86_64-apple-darwin.tar.gz", "e35c969a7f62c762a5c6e2379b8d7d85914dbb786513c558c5e3a3cf340790ff") 64 | download_file("libhighs.arm64.dylib", "lib/libhighs.dylib", "HiGHS.v#{version}.aarch64-apple-darwin.tar.gz", "f329379db8ab2e14f652b15af6fe0fabfa17674d289057367cd83469eff0dd8b") 65 | end 66 | 67 | task :windows do 68 | download_file("libhighs.dll", "bin/libhighs.dll", "HiGHS.v#{version}.x86_64-w64-mingw32-cxx11.tar.gz", "8c59525eda77c981e2ce9e513282abe429d1cc2959fddaa67d36904953894a56") 69 | end 70 | 71 | task all: [:linux, :mac, :windows] 72 | 73 | task :platform do 74 | if Gem.win_platform? 75 | Rake::Task["vendor:windows"].invoke 76 | elsif RbConfig::CONFIG["host_os"] =~ /darwin/i 77 | Rake::Task["vendor:mac"].invoke 78 | else 79 | Rake::Task["vendor:linux"].invoke 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HiGHS Ruby 2 | 3 | [HiGHS](https://www.maths.ed.ac.uk/hall/HiGHS/) - linear optimization software - for Ruby 4 | 5 | Check out [Opt](https://github.com/ankane/opt) for a high-level interface 6 | 7 | [![Build Status](https://github.com/ankane/highs-ruby/actions/workflows/build.yml/badge.svg)](https://github.com/ankane/highs-ruby/actions) 8 | 9 | ## Installation 10 | 11 | Add this line to your application’s Gemfile: 12 | 13 | ```ruby 14 | gem "highs" 15 | ``` 16 | 17 | ## Getting Started 18 | 19 | *The API is fairly low-level at the moment* 20 | 21 | Load a linear program 22 | 23 | ```ruby 24 | model = 25 | Highs.lp( 26 | sense: :minimize, 27 | col_cost: [8, 10], 28 | col_lower: [0, 0], 29 | col_upper: [1e30, 1e30], 30 | row_lower: [7, 12, 6], 31 | row_upper: [1e30, 1e30, 1e30], 32 | a_format: :colwise, 33 | a_start: [0, 3], 34 | a_index: [0, 1, 2, 0, 1, 2], 35 | a_value: [2, 3, 2, 2, 4, 1] 36 | ) 37 | ``` 38 | 39 | Load a mixed-integer program 40 | 41 | ```ruby 42 | model = 43 | Highs.mip( 44 | sense: :minimize, 45 | col_cost: [8, 10], 46 | col_lower: [0, 0], 47 | col_upper: [1e30, 1e30], 48 | row_lower: [7, 12, 6], 49 | row_upper: [1e30, 1e30, 1e30], 50 | a_format: :colwise, 51 | a_start: [0, 3], 52 | a_index: [0, 1, 2, 0, 1, 2], 53 | a_value: [2, 3, 2, 2, 4, 1], 54 | integrality: [:integer, :continuous] 55 | ) 56 | ``` 57 | 58 | Load a quadratic program 59 | 60 | ```ruby 61 | model = 62 | Highs.qp( 63 | sense: :minimize, 64 | col_cost: [0, -1, 0], 65 | col_lower: [0, 0, 0], 66 | col_upper: [1e30, 1e30, 1e30], 67 | row_lower: [1, -1e30], 68 | row_upper: [1e30, 1e30], 69 | a_format: :colwise, 70 | a_start: [0, 1, 2], 71 | a_index: [0, 0, 0], 72 | a_value: [1, 1, 1], 73 | q_format: :colwise, 74 | q_start: [0, 2, 3], 75 | q_index: [0, 2, 1, 0, 2], 76 | q_value: [2, -1, 0.2, -1, 2] 77 | ) 78 | ``` 79 | 80 | Solve 81 | 82 | ```ruby 83 | model.solve 84 | ``` 85 | 86 | Write the program to an MPS file 87 | 88 | ```ruby 89 | model.write("model.mps") 90 | ``` 91 | 92 | Read a program from an MPS file 93 | 94 | ```ruby 95 | model = Highs.read("model.mps") 96 | ``` 97 | 98 | ## Reference 99 | 100 | Enable verbose logging 101 | 102 | ```ruby 103 | model.solve(verbose: true) 104 | ``` 105 | 106 | Set the time limit in seconds 107 | 108 | ```ruby 109 | model.solve(time_limit: 30) 110 | ``` 111 | 112 | ## History 113 | 114 | View the [changelog](https://github.com/ankane/highs-ruby/blob/master/CHANGELOG.md) 115 | 116 | ## Contributing 117 | 118 | Everyone is encouraged to help improve this project. Here are a few ways you can help: 119 | 120 | - [Report bugs](https://github.com/ankane/highs-ruby/issues) 121 | - Fix bugs and [submit pull requests](https://github.com/ankane/highs-ruby/pulls) 122 | - Write, clarify, or fix documentation 123 | - Suggest or add new features 124 | 125 | To get started with development: 126 | 127 | ```sh 128 | git clone https://github.com/ankane/highs-ruby.git 129 | cd highs-ruby 130 | bundle install 131 | bundle exec rake vendor:all 132 | bundle exec rake test 133 | ``` 134 | -------------------------------------------------------------------------------- /lib/highs/methods.rb: -------------------------------------------------------------------------------- 1 | module Highs 2 | module Methods 3 | def lp(sense:, offset: 0, col_cost:, col_lower:, col_upper:, row_lower:, row_upper:, a_format:, a_start:, a_index:, a_value:) 4 | num_col = col_cost.size 5 | num_row = row_lower.size 6 | num_nz = a_index.size 7 | a_format = FFI::MATRIX_FORMAT.fetch(a_format) 8 | sense = FFI::OBJ_SENSE.fetch(sense) 9 | 10 | model = Model.new 11 | check_status FFI.Highs_passLp( 12 | model, num_col, num_row, num_nz, a_format, sense, offset, 13 | DoubleArray.new(num_col, col_cost), DoubleArray.new(num_col, col_lower), DoubleArray.new(num_col, col_upper), 14 | DoubleArray.new(num_row, row_lower), DoubleArray.new(num_row, row_upper), 15 | IntArray.new(a_start.size, a_start), IntArray.new(num_nz, a_index), DoubleArray.new(num_nz, a_value), 16 | ) 17 | model 18 | end 19 | 20 | def lp_call(**options) 21 | lp(**options).solve 22 | end 23 | 24 | def mip(sense:, offset: 0, col_cost:, col_lower:, col_upper:, row_lower:, row_upper:, a_format:, a_start:, a_index:, a_value:, integrality:) 25 | num_col = col_cost.size 26 | num_row = row_lower.size 27 | num_nz = a_index.size 28 | a_format = FFI::MATRIX_FORMAT.fetch(a_format) 29 | sense = FFI::OBJ_SENSE.fetch(sense) 30 | integrality = integrality.map { |v| FFI::VAR_TYPE[v] || v } 31 | 32 | model = Model.new 33 | check_status FFI.Highs_passMip( 34 | model, num_col, num_row, num_nz, a_format, sense, offset, 35 | DoubleArray.new(num_col, col_cost), DoubleArray.new(num_col, col_lower), DoubleArray.new(num_col, col_upper), 36 | DoubleArray.new(num_row, row_lower), DoubleArray.new(num_row, row_upper), 37 | IntArray.new(a_start.size, a_start), IntArray.new(num_nz, a_index), DoubleArray.new(num_nz, a_value), 38 | IntArray.new(num_col, integrality) 39 | ) 40 | model 41 | end 42 | 43 | def mip_call(**options) 44 | mip(**options).solve.slice(:status, :obj_value, :col_value, :row_value) 45 | end 46 | 47 | def qp(sense:, offset: 0, col_cost:, col_lower:, col_upper:, row_lower:, row_upper:, a_format:, a_start:, a_index:, a_value:, q_format:, q_start:, q_index:, q_value:) 48 | num_col = col_cost.size 49 | num_row = row_lower.size 50 | num_nz = a_index.size 51 | q_num_nz = q_index.size 52 | a_format = FFI::MATRIX_FORMAT.fetch(a_format) 53 | q_format = FFI::MATRIX_FORMAT.fetch(q_format) 54 | sense = FFI::OBJ_SENSE.fetch(sense) 55 | 56 | model = Model.new 57 | check_status FFI.Highs_passModel( 58 | model, num_col, num_row, num_nz, q_num_nz, a_format, q_format, sense, offset, 59 | DoubleArray.new(num_col, col_cost), DoubleArray.new(num_col, col_lower), DoubleArray.new(num_col, col_upper), 60 | DoubleArray.new(num_row, row_lower), DoubleArray.new(num_row, row_upper), 61 | IntArray.new(a_start.size, a_start), IntArray.new(num_nz, a_index), DoubleArray.new(num_nz, a_value), 62 | IntArray.new(q_start.size, q_start), IntArray.new(q_num_nz, q_index), DoubleArray.new(q_num_nz, q_value), nil 63 | ) 64 | model 65 | end 66 | 67 | def qp_call(**options) 68 | qp(**options).solve 69 | end 70 | 71 | def read(filename) 72 | model = Model.new 73 | check_status FFI.Highs_readModel(model, +filename) 74 | model 75 | end 76 | 77 | private 78 | 79 | def check_status(status) 80 | # TODO handle warnings (status = 1) 81 | if status == -1 82 | raise Error, "Bad status" 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /test/highs_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | class HighsTest < Minitest::Test 4 | def test_lp 5 | model = 6 | Highs.lp( 7 | sense: :minimize, 8 | col_cost: [8, 10], 9 | col_lower: [0, 0], 10 | col_upper: [1e30, 1e30], 11 | row_lower: [7, 12, 6], 12 | row_upper: [1e30, 1e30, 1e30], 13 | a_format: :colwise, 14 | a_start: [0, 3], 15 | a_index: [0, 1, 2, 0, 1, 2], 16 | a_value: [2, 3, 2, 2, 4, 1] 17 | ) 18 | 19 | res = model.solve 20 | assert_equal :optimal, res[:status] 21 | assert_in_delta 31.2, res[:obj_value] 22 | assert_elements_in_delta [2.4, 1.2], res[:col_value] 23 | assert_elements_in_delta [0, 0], res[:col_dual] 24 | assert_elements_in_delta [7.2, 12, 6], res[:row_value] 25 | assert_elements_in_delta [0, 2.4, 0.4], res[:row_dual] 26 | assert_equal [:basic, :basic], res[:col_basis] 27 | assert_equal [:basic, :lower, :lower], res[:row_basis] 28 | 29 | path = "/tmp/lp.mps" 30 | model.write(path) 31 | model = Highs.read(path) 32 | 33 | res = model.solve 34 | assert_equal :optimal, res[:status] 35 | assert_in_delta 31.2, res[:obj_value] 36 | assert_elements_in_delta [2.4, 1.2], res[:col_value] 37 | assert_elements_in_delta [0, 0], res[:col_dual] 38 | assert_elements_in_delta [7.2, 12, 6], res[:row_value] 39 | assert_elements_in_delta [0, 2.4, 0.4], res[:row_dual] 40 | assert_equal [:basic, :basic], res[:col_basis] 41 | assert_equal [:basic, :lower, :lower], res[:row_basis] 42 | end 43 | 44 | def test_mip 45 | model = 46 | Highs.mip( 47 | sense: :minimize, 48 | col_cost: [8, 10], 49 | col_lower: [0, 0], 50 | col_upper: [1e30, 1e30], 51 | row_lower: [7, 12, 6], 52 | row_upper: [1e30, 1e30, 1e30], 53 | a_format: :colwise, 54 | a_start: [0, 3], 55 | a_index: [0, 1, 2, 0, 1, 2], 56 | a_value: [2, 3, 2, 2, 4, 1], 57 | integrality: [:integer, :integer] 58 | ) 59 | 60 | res = model.solve 61 | assert_equal :optimal, res[:status] 62 | assert_in_delta 32, res[:obj_value] 63 | assert_elements_in_delta [4, 0], res[:col_value] 64 | assert_elements_in_delta [8, 12, 8], res[:row_value] 65 | 66 | path = "/tmp/mip.mps" 67 | model.write(path) 68 | model = Highs.read(path) 69 | 70 | res = model.solve 71 | assert_equal :optimal, res[:status] 72 | assert_in_delta 32, res[:obj_value] 73 | assert_elements_in_delta [4, 0], res[:col_value] 74 | assert_elements_in_delta [8, 12, 8], res[:row_value] 75 | end 76 | 77 | def test_qp 78 | model = 79 | Highs.qp( 80 | sense: :minimize, 81 | col_cost: [0, -1, 0], 82 | col_lower: [0, 0, 0], 83 | col_upper: [1e30, 1e30, 1e30], 84 | row_lower: [1, -1e30], 85 | row_upper: [1e30, 1e30], 86 | a_format: :colwise, 87 | a_start: [0, 1, 2], 88 | a_index: [0, 0, 0], 89 | a_value: [1, 1, 1], 90 | q_format: :colwise, 91 | q_start: [0, 2, 3], 92 | q_index: [0, 2, 1, 0, 2], 93 | q_value: [2, -1, 0.2, -1, 2] 94 | ) 95 | 96 | res = model.solve 97 | assert_equal :optimal, res[:status] 98 | assert_in_delta(-2.5, res[:obj_value]) 99 | assert_elements_in_delta [0, 5, 0], res[:col_value] 100 | assert_elements_in_delta [0, 0, 0], res[:col_dual] 101 | assert_elements_in_delta [5, 0], res[:row_value] 102 | assert_elements_in_delta [0, 0], res[:row_dual] 103 | assert_equal [:lower, :basic, :lower], res[:col_basis] 104 | assert_equal [:nonbasic, :basic], res[:row_basis] 105 | 106 | path = "/tmp/qp.mps" 107 | model.write(path) 108 | model = Highs.read(path) 109 | 110 | res = model.solve 111 | assert_equal :optimal, res[:status] 112 | assert_in_delta(-2.5, res[:obj_value]) 113 | assert_elements_in_delta [0, 5, 0], res[:col_value] 114 | assert_elements_in_delta [0, 0, 0], res[:col_dual] 115 | # second row dropped since -infinity to infinity 116 | assert_elements_in_delta [5], res[:row_value] 117 | assert_elements_in_delta [0], res[:row_dual] 118 | assert_equal [:lower, :basic, :lower], res[:col_basis] 119 | assert_equal [:nonbasic], res[:row_basis] 120 | end 121 | 122 | def test_time_limit 123 | model = Highs.read("test/support/lp.mps") 124 | res = model.solve(time_limit: 0.000001) 125 | assert_equal :time_limit, res[:status] 126 | end 127 | 128 | def test_copy 129 | model = Highs.read("test/support/lp.mps") 130 | model.dup 131 | model.clone 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/highs/ffi.rb: -------------------------------------------------------------------------------- 1 | module Highs 2 | module FFI 3 | extend Fiddle::Importer 4 | 5 | libs = Array(Highs.ffi_lib).dup 6 | begin 7 | dlload Fiddle.dlopen(libs.shift) 8 | rescue Fiddle::DLError => e 9 | retry if libs.any? 10 | raise e 11 | end 12 | 13 | # https://github.com/ERGO-Code/HiGHS/blob/master/src/interfaces/highs_c_api.h 14 | 15 | VAR_TYPE = { 16 | continuous: 0, 17 | integer: 1, 18 | semi_continuous: 2, 19 | semi_integer: 3, 20 | implicit_integer: 4 21 | } 22 | 23 | OBJ_SENSE = { 24 | minimize: 1, 25 | maximize: -1 26 | } 27 | 28 | MATRIX_FORMAT = { 29 | colwise: 1, 30 | rowwise: 2 31 | } 32 | 33 | MODEL_STATUS = [ 34 | :not_set, :load_error, :model_error, :presolve_error, :solve_error, :postsolve_error, 35 | :model_empty, :optimal, :infeasible, :unbounded_or_infeasible, :unbounded, 36 | :objective_bound, :objective_target, :time_limit, :iteration_limit, :unknown 37 | ] 38 | 39 | BASIS_STATUS = [:lower, :basic, :upper, :zero, :nonbasic] 40 | 41 | typealias "HighsInt", "int" 42 | typealias "HighsUInt", "unsigned int" 43 | 44 | extern "HighsInt Highs_lpCall(HighsInt num_col, HighsInt num_row, HighsInt num_nz, HighsInt a_format, HighsInt sense, double offset, double* col_cost, double* col_lower, double* col_upper, double* row_lower, double* row_upper, HighsInt* a_start, HighsInt* a_index, double* a_value, double* col_value, double* col_dual, double* row_value, double* row_dual, HighsInt* col_basis_status, HighsInt* row_basis_status, HighsInt* model_status)" 45 | extern "HighsInt Highs_mipCall(HighsInt num_col, HighsInt num_row, HighsInt num_nz, HighsInt a_format, HighsInt sense, double offset, double* col_cost, double* col_lower, double* col_upper, double* row_lower, double* row_upper, HighsInt* a_start, HighsInt* a_index, double* a_value, HighsInt* integrality, double* col_value, double* row_value, HighsInt* model_status)" 46 | extern "HighsInt Highs_qpCall(HighsInt num_col, HighsInt num_row, HighsInt num_nz, HighsInt q_num_nz, HighsInt a_format, HighsInt q_format, HighsInt sense, double offset, double* col_cost, double* col_lower, double* col_upper, double* row_lower, double* row_upper, HighsInt* a_start, HighsInt* a_index, double* a_value, HighsInt* q_start, HighsInt* q_index, double* q_value, double* col_value, double* col_dual, double* row_value, double* row_dual, HighsInt* col_basis_status, HighsInt* row_basis_status, HighsInt* model_status)" 47 | 48 | extern "void* Highs_create(void)" 49 | extern "void Highs_destroy(void* highs)" 50 | extern "HighsInt Highs_readModel(void* highs, char* filename)" 51 | extern "HighsInt Highs_writeModel(void* highs, char* filename)" 52 | extern "HighsInt Highs_clearModel(void* highs)" 53 | extern "HighsInt Highs_run(void* highs)" 54 | extern "HighsInt Highs_writeSolution(void* highs, char* filename)" 55 | extern "HighsInt Highs_writeSolutionPretty(void* highs, char* filename)" 56 | extern "HighsInt Highs_passLp(void* highs, HighsInt num_col, HighsInt num_row, HighsInt num_nz, HighsInt a_format, HighsInt sense, double offset, double* col_cost, double* col_lower, double* col_upper, double* row_lower, double* row_upper, HighsInt* a_start, HighsInt* a_index, double* a_value)" 57 | extern "HighsInt Highs_passMip(void* highs, HighsInt num_col, HighsInt num_row, HighsInt num_nz, HighsInt a_format, HighsInt sense, double offset, double* col_cost, double* col_lower, double* col_upper, double* row_lower, double* row_upper, HighsInt* a_start, HighsInt* a_index, double* a_value, HighsInt* integrality)" 58 | extern "HighsInt Highs_passModel(void* highs, HighsInt num_col, HighsInt num_row, HighsInt num_nz, HighsInt q_num_nz, HighsInt a_format, HighsInt q_format, HighsInt sense, double offset, double* col_cost, double* col_lower, double* col_upper, double* row_lower, double* row_upper, HighsInt* a_start, HighsInt* a_index, double* a_value, HighsInt* q_start, HighsInt* q_index, double* q_value, HighsInt* integrality)" 59 | extern "HighsInt Highs_passHessian(void* highs, HighsInt dim, HighsInt num_nz, HighsInt format, HighsInt* start, HighsInt* index, double* value)" 60 | extern "HighsInt Highs_setBoolOptionValue(void* highs, char* option, HighsInt value)" 61 | extern "HighsInt Highs_setIntOptionValue(void* highs, char* option, HighsInt value)" 62 | extern "HighsInt Highs_setDoubleOptionValue(void* highs, char* option, double value)" 63 | extern "HighsInt Highs_setStringOptionValue(void* highs, char* option, char* value)" 64 | extern "HighsInt Highs_getBoolOptionValue(void* highs, char* option, HighsInt* value)" 65 | extern "HighsInt Highs_getIntOptionValue(void* highs, char* option, HighsInt* value)" 66 | extern "HighsInt Highs_getDoubleOptionValue(void* highs, char* option, double* value)" 67 | extern "HighsInt Highs_getStringOptionValue(void* highs, char* option, char* value)" 68 | extern "HighsInt Highs_getOptionType(void* highs, char* option, HighsInt* type)" 69 | extern "HighsInt Highs_getSolution(void* highs, double* col_value, double* col_dual, double* row_value, double* row_dual)" 70 | extern "HighsInt Highs_getBasis(void* highs, HighsInt* col_status, HighsInt* row_status)" 71 | extern "HighsInt Highs_getModelStatus(void* highs)" 72 | extern "HighsInt Highs_getDualRay(void* highs, HighsInt* has_dual_ray, double* dual_ray_value)" 73 | extern "HighsInt Highs_getPrimalRay(void* highs, HighsInt* has_primal_ray, double* primal_ray_value)" 74 | extern "double Highs_getObjectiveValue(void* highs)" 75 | extern "HighsInt Highs_getNumCol(void* highs)" 76 | extern "HighsInt Highs_getNumRow(void* highs)" 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /vendor/LICENSE-THIRD-PARTY.txt: -------------------------------------------------------------------------------- 1 | ================================================================================ 2 | CLI11 2.5.0 3 | ================================================================================ 4 | 5 | CLI11 2.5.0 Copyright (c) 2017-2025 University of Cincinnati, developed by Henry 6 | Schreiner under NSF AWARD 1414736. All rights reserved. 7 | 8 | Redistribution and use in source and binary forms of CLI11, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | 1. Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 2. Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 3. Neither the name of the copyright holder nor the names of its contributors 17 | may be used to endorse or promote products derived from this software without 18 | specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 24 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 27 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | ================================================================================ 32 | FilereaderLP 33 | ================================================================================ 34 | 35 | Copyright (c) 2020 Michael Feldmeier 36 | 37 | Permission is hereby granted, free of charge, to any person obtaining a copy 38 | of this software and associated documentation files (the "Software"), to deal 39 | in the Software without restriction, including without limitation the rights 40 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 41 | copies of the Software, and to permit persons to whom the Software is 42 | furnished to do so, subject to the following conditions: 43 | 44 | The above copyright notice and this permission notice shall be included in all 45 | copies or substantial portions of the Software. 46 | 47 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 48 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 49 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 50 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 51 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 52 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 53 | SOFTWARE. 54 | 55 | ================================================================================ 56 | pdqsort 57 | ================================================================================ 58 | 59 | Copyright (c) 2021 Orson Peters 60 | 61 | This software is provided 'as-is', without any express or implied warranty. In no event will the 62 | authors be held liable for any damages arising from the use of this software. 63 | 64 | Permission is granted to anyone to use this software for any purpose, including commercial 65 | applications, and to alter it and redistribute it freely, subject to the following restrictions: 66 | 67 | 1. The origin of this software must not be misrepresented; you must not claim that you wrote the 68 | original software. If you use this software in a product, an acknowledgment in the product 69 | documentation would be appreciated but is not required. 70 | 71 | 2. Altered source versions must be plainly marked as such, and must not be misrepresented as 72 | being the original software. 73 | 74 | 3. This notice may not be removed or altered from any source distribution. 75 | 76 | ================================================================================ 77 | zstr 1.0.5 78 | ================================================================================ 79 | 80 | The MIT License (MIT) 81 | 82 | Copyright (c) 2015 Matei David, Ontario Institute for Cancer Research 83 | 84 | Permission is hereby granted, free of charge, to any person obtaining a copy 85 | of this software and associated documentation files (the "Software"), to deal 86 | in the Software without restriction, including without limitation the rights 87 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 88 | copies of the Software, and to permit persons to whom the Software is 89 | furnished to do so, subject to the following conditions: 90 | 91 | The above copyright notice and this permission notice shall be included in all 92 | copies or substantial portions of the Software. 93 | 94 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 95 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 96 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 97 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 98 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 99 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 100 | SOFTWARE. 101 | --------------------------------------------------------------------------------