Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: CI
on:
push:
branches: [master]
tags: ["*"]
pull_request:
branches: [master]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }}

jobs:
test:
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
version:
- "1.11"
- nightly
os:
- ubuntu-latest
- windows-latest
- macos-latest
arch:
- x64
steps:
- uses: actions/checkout@v4
- uses: julia-actions/setup-julia@v2
with:
version: ${{ matrix.version }}
arch: ${{ matrix.arch }}
- uses: julia-actions/cache@v2
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
- uses: julia-actions/julia-processcoverage@v1
if: matrix.version == '1.11' && matrix.os == 'ubuntu-latest'
- uses: codecov/codecov-action@v5
if: matrix.version == '1.11' && matrix.os == 'ubuntu-latest'
with:
files: lcov.info
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

docs:
name: Documentation
runs-on: ubuntu-latest
permissions:
contents: write
statuses: write
steps:
- uses: actions/checkout@v4
- uses: julia-actions/setup-julia@v2
with:
version: "1.11"
- uses: julia-actions/cache@v2
- 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 }}
run: julia --project=docs docs/make.jl
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/compile/builddir/
compile/builddir/
/compile/builddir/*.*
/compile/builddir/*.*
docs/build/
docs/Manifest.toml
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024-2026 PSR / rafabench

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
6 changes: 6 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@ ArgParse = "1"
MathOptInterface = "1"
ProgressMeter = "1"
julia = "1.11"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test"]
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## ModelCompare

[![CI](https://github.com/rafabench/ModelCompare/actions/workflows/CI.yml/badge.svg)](https://github.com/rafabench/ModelCompare/actions/workflows/CI.yml)
[![codecov](https://codecov.io/gh/rafabench/ModelCompare/branch/master/graph/badge.svg)](https://codecov.io/gh/rafabench/ModelCompare)
[![docs-dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://rafabench.github.io/ModelCompare/dev/)

The purpose of this project is to compare two optimization models in two different files.

### Running
Expand All @@ -9,9 +13,9 @@ You can compare them by running:

```julia
compare_models(
file1 = "test/models/model1.lp",
file2 = "test/models/model2.lp",
outfile = "test/models/compare_lp.txt",
"test/models/model1.lp",
"test/models/model2.lp",
outfile = "test/models/compare_lp.txt",
tol = 1e-3
)
```
Expand Down
3 changes: 3 additions & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[deps]
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
ModelCompare = "1a876020-4912-11ea-1b46-d176d33439c0"
17 changes: 17 additions & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Documenter
using ModelCompare

makedocs(
sitename = "ModelCompare.jl",
modules = [ModelCompare],
warnonly = [:missing_docs],
pages = [
"Home" => "index.md",
"API Reference" => "api.md",
],
)

deploydocs(
repo = "github.com/rafabench/ModelCompare.git",
devbranch = "master",
)
51 changes: 51 additions & 0 deletions docs/src/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# API Reference

## Main Entry Point

```@docs
compare_models
```

## Variables

```@docs
VariablesDiff
compare_variables
```

## Bounds

```@docs
BoundsDiff
compare_bounds
```

## Objective

```@docs
ObjectiveDiff
compare_objective
```

## Expressions

```@docs
ExpressionDiff
compare_expressions
```

## Constraints

```@docs
ConstraintNamesDiff
ConstraintElementsDiff
compare_constraints
```

## Utilities

```@docs
readmodel
partition
sort_model
```
64 changes: 64 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# ModelCompare.jl

A Julia package for comparing two optimization models (LP/MPS format) and producing a detailed diff report covering variables, bounds, objective function, and constraints.

## Installation

```julia
using Pkg
Pkg.add(url="https://github.com/rafabench/ModelCompare")
```

## Quick Start

```julia
using ModelCompare

result = compare_models(
"model1.lp",
"model2.lp",
outfile = "comparison.txt",
tol = 1e-3,
)
```

The result is a `NamedTuple` with four fields:

- `variables` — a [`VariablesDiff`](@ref) showing which variables are unique to each model vs shared
- `bounds` — a [`BoundsDiff`](@ref) showing variable bound differences
- `objective` — an [`ObjectiveDiff`](@ref) showing objective sense and coefficient differences
- `constraints` — a [`ConstraintElementsDiff`](@ref) showing constraint coefficient and bound differences

## Output Sections

### Variable Names

Lists variables that belong only to Model 1, only to Model 2, or both.

### Variable Bounds

Shows bound differences for shared variables and bounds for variables unique to each model.

### Objective Function

Shows coefficient differences in the objective function. If the optimization senses differ (min vs max), that is reported as well.

### Constraints

Compares constraints **with matching names** across both models. Unmatched constraint names are not reported. For each matched constraint, coefficient and set (bound) differences are shown.

## Sorting Models

You can canonicalize a model file by sorting its variables and constraints alphabetically:

```julia
sort_model("model.lp") # creates model.lp.sorted
```

## CLI Usage

After compilation (see `compile/compile.jl`):

```bash
ModelCompare --file1 model1.lp --file2 model2.lp -o result.txt -t 0.001
```
9 changes: 6 additions & 3 deletions src/ModelCompare.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
module ModelCompare

export read_from_file, compare_variables, compare_expressions, compare_objective, compare_bounds, compare_constraints, compare_models
export readmodel, partition,
compare_variables, compare_expressions, compare_objective,
compare_bounds, compare_constraints, compare_models,
sort_model,
VariablesDiff, BoundsDiff, ExpressionDiff, ObjectiveDiff,
ConstraintNamesDiff, ConstraintElementsDiff

using MathOptInterface
using ArgParse
Expand All @@ -20,6 +25,4 @@ include("args.jl")
include("lp_write_moi.jl")
include("sort.jl")

export compare_models, sort_model

end
7 changes: 7 additions & 0 deletions src/bounds.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ struct BoundsDiff
second :: Dict{String, Tuple{Float64, Float64}}
end

"""
compare_bounds(model1, model2, vardiff; tol) -> BoundsDiff
compare_bounds(model1, model2; tol) -> BoundsDiff

Compare variable bounds between two MOI models. The 2-argument form computes
the [`VariablesDiff`](@ref) automatically. Returns a [`BoundsDiff`](@ref).
"""
function compare_bounds(model1::MOI.ModelLike, model2::MOI.ModelLike, vardiff::VariablesDiff; tol::Float64)
indices1 = index_for_name(model1)
indices2 = index_for_name(model2)
Expand Down
14 changes: 14 additions & 0 deletions src/compare.jl
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,20 @@ function print_compare(outfile::String,
end
end

"""
compare_models(file1, file2; tol, outfile, verbose=true, one_by_one=true, separate_files=false)

Compare two optimization model files (LP or MPS format) and write a detailed
diff report to `outfile`. Returns a `NamedTuple` with fields `variables`,
`bounds`, `objective`, and `constraints`.

# Arguments
- `file1::String`: path to the first model file.
- `file2::String`: path to the second model file.
- `tol::Float64`: absolute tolerance for numerical comparisons.
- `outfile::String`: path to the output report file.
- `separate_files::Bool`: if `true`, write each section to a separate file.
"""
function compare_models(file1, file2;
tol :: Float64,
outfile :: String,
Expand Down
29 changes: 27 additions & 2 deletions src/constraints.jl
Original file line number Diff line number Diff line change
@@ -1,18 +1,43 @@
"""
ConstraintNamesDiff

Result of comparing constraint names between two models.

# Fields
- `in_both::Vector{String}`: constraint names present in both models.
- `only_one::Vector{String}`: constraint names unique to the first model.
- `only_two::Vector{String}`: constraint names unique to the second model.
"""
struct ConstraintNamesDiff
in_both :: Vector{String}
only_one :: Vector{String}
only_two :: Vector{String}
end

# Compare expression of constraints and objective function
# Then, compare bounds of constraints
"""
ConstraintElementsDiff

Result of comparing constraint coefficients and bounds between two models.

# Fields
- `equal::Vector{String}`: constraints that are identical (within tolerance).
- `both::Dict`: constraints present in both models with differences — maps name to `(ExpressionDiff, (set1, set2))`.
- `first::Dict`: constraints unique to the first model — maps name to `(coefficients, bounds)`.
- `second::Dict`: constraints unique to the second model — maps name to `(coefficients, bounds)`.
"""
struct ConstraintElementsDiff
equal::Vector{String}
both::Dict{String, Tuple{ExpressionDiff, Tuple{Tuple{Float64, Float64}, Tuple{Float64, Float64}}}}
first::Dict{String, Tuple{Dict{String, Float64}, Tuple{Float64, Float64}}}
second::Dict{String, Tuple{Dict{String, Float64}, Tuple{Float64, Float64}}}
end

"""
compare_constraints(model1, model2; tol) -> ConstraintElementsDiff

Compare the constraints of two MOI models. Only constraints with matching names
are compared; unmatched constraint names are reported separately.
"""
function compare_constraints(model1::MOI.ModelLike, model2::MOI.ModelLike; tol::Float64)
condiff = ConstraintNamesDiff(partition(constraint_names(model1), constraint_names(model2))...)
ctrindices1 = ctr_index_for_name(model1)
Expand Down
17 changes: 17 additions & 0 deletions src/expression.jl
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
"""
ExpressionDiff

Result of comparing two linear expressions (objective or constraint).

# Fields
- `equal::Vector{String}`: variable names with matching coefficients (within tolerance).
- `both::Dict{String, Tuple{Float64, Float64}}`: variables present in both expressions with differing coefficients `(coef1, coef2)`.
- `first::Dict{String, Float64}`: variables only in the first expression.
- `second::Dict{String, Float64}`: variables only in the second expression.
"""
struct ExpressionDiff
equal :: Vector{String}
both :: Dict{String, Tuple{Float64, Float64}}
first :: Dict{String, Float64}
second :: Dict{String, Float64}
end

"""
compare_expressions(expr1, expr2, model1, model2; tol) -> ExpressionDiff

Compare two `MOI.AbstractScalarFunction` expressions, returning an [`ExpressionDiff`](@ref)
that partitions variables into equal, differing, and unique-to-each-model categories.
"""
function compare_expressions(expr1::MOI.AbstractScalarFunction, expr2::MOI.AbstractScalarFunction, model1::MOI.ModelLike, model2::MOI.ModelLike; tol::Float64)
coefs1 = Dict(model1.var_to_name[t.variable] => t.coefficient for t in expr1.terms)
coefs2 = Dict(model2.var_to_name[t.variable] => t.coefficient for t in expr2.terms)
Expand Down
Loading
Loading