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
31 changes: 15 additions & 16 deletions .github/workflows/python.yaml
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
name: Test Python
on: [push]
on:
push:
branches:
- master
pull_request:
types: [opened, synchronize, reopened]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.10']
defaults:
run:
shell: bash -el {0} # Essential for Conda environment activation
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
- uses: actions/checkout@v7
- uses: conda-incubator/setup-miniconda@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install jsonschema
- name: Test
run: |
cd python
python mof.py
activate-environment: mathoptformat
environment-file: python/environment.yml
- run: python -m pytest ./python/run_pulp.py
- run: python ./python/mof.py
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.vscode
python/__pycache__
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# MathOptFormat

This repository describes a file-format for mathematical optimization problems
called _MathOptFormat_ with the file extension `.mof.json`.
This [repository](https://github.com/jump-dev/MathOptFormat) describes a
file-format for mathematical optimization problems called _MathOptFormat_, with
the file extension `.mof.json`.

MathOptFormat is rigidly defined by the [JSON schema](http://json-schema.org/)
available at
Expand All @@ -23,10 +24,11 @@ structure for mathematical optimization problems. INFORMS Journal on Computing.

## Implementations

- Julia
- The [MathOptInterface.jl](https://github.com/jump-dev/MathOptInterface.jl)
package supports reading and writing MathOptFormat files.

- The [MathOptInterface.jl](https://github.com/jump-dev/MathOptInterface.jl) package
supports reading and writing MathOptFormat files.
- Pedagogical Python code for parsing MathOptFormat files into
[PuLP](https://coin-or.github.io/pulp/) is available in the [python directory](https://github.com/jump-dev/MathOptFormat/tree/master/python).

## Standard form

Expand Down
8 changes: 8 additions & 0 deletions python/environment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: mathoptformat
dependencies:
- python=3.10
- pytest
- pip
- pip:
- pulp==3.*
- jsonschema
13 changes: 9 additions & 4 deletions python/mof.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# Copyright (c) 2020: Oscar Dowson and contributors
#
# Use of this source code is governed by an MIT-style license that can be found
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.

import json
import jsonschema
import os


SCHEMA_FILENAME = '../schemas/mof.1.schema.json'
ROOT = os.path.dirname(os.path.abspath(__file__)) + "/../"
SCHEMA_FILENAME = ROOT + 'schemas/mof.1.schema.json'

def validate(filename):
with open(filename, 'r', encoding='utf-8') as io:
Expand Down Expand Up @@ -88,8 +93,8 @@ def summarize_nonlinear(schema):
### Validate all the files in the examples directory.
###

for filename in os.listdir('../examples'):
validate(os.path.join('../examples', filename))
for filename in os.listdir(ROOT + 'examples'):
validate(os.path.join(ROOT + 'examples', filename))

###
### Summarize the schema for the README table.
Expand Down
120 changes: 120 additions & 0 deletions python/run_pulp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Copyright (c) 2020: Oscar Dowson and contributors
#
# Use of this source code is governed by an MIT-style license that can be found
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.

import json
import pulp
import pytest
import os

ROOT = os.path.dirname(os.path.abspath(__file__)) + "/../"

class UnsupportedObjective(Exception):
def __init__(self, F):
self.F = F
return

class UnsupportedConstraint(Exception):
def __init__(self, F, S):
self.F, self.S = F, S
return

def parse_func(f, var_by_name):
match f["type"]:
case "Variable":
return var_by_name[f["name"]]
case "ScalarAffineFunction":
expr = pulp.LpAffineExpression()
for term in f.get("terms", []):
expr += term["coefficient"] * var_by_name[term["variable"]]
if "constant" in f:
expr += f["constant"]
return expr
case _:
return None

def add_variable_constraint(prob, f, s, var_by_name):
x = var_by_name[f["name"]]
match s["type"]:
case "LessThan":
x.upBound = s["upper"]
case "GreaterThan":
x.lowBound = s["lower"]
case "EqualTo":
x.lowBound = x.upBound = s["value"]
case "Interval":
x.lowBound, x.upBound = s["lower"], s["upper"]
case "ZeroOne":
x.cat = pulp.LpBinary
case "Integer":
x.cat = pulp.LpInteger
case _:
raise UnsupportedConstraint(f["type"], s["type"])
return

def add_constraint(prob, c, var_by_name):
f, s = c["function"], c["set"]
match f["type"]:
case "Variable":
add_variable_constraint(prob, f, s, var_by_name)
case _:
expr = parse_func(f, var_by_name)
if expr is None:
raise UnsupportedConstraint(f["type"], s["type"])
match s["type"]:
case "LessThan":
prob += (expr <= s["upper"])
case "GreaterThan":
prob += (expr >= s["lower"])
case "EqualTo":
prob += (expr == s["value"])
case _:
raise UnsupportedConstraint(f["type"], s["type"])
return

def read_from_file(filename):
with open(filename, "r") as f:
data = json.load(f)
return read_from_dict(data)

def read_from_dict(data):
name = data.get("name", "")
obj = data.get("objective", {})
is_max = obj.get("sense", "MIN_SENSE") == "MAX_SENSE"
sense = pulp.LpMaximize if is_max else pulp.LpMinimize
prob = pulp.LpProblem(name=name, sense=sense)
prob.var_by_name = {}
for x in data.get("variables", []):
name = x["name"]
prob.var_by_name[name] = prob.add_variable(name, None, None)
if "function" in obj:
obj_f = parse_func(obj["function"], prob.var_by_name)
if obj_f is None:
raise UnsupportedObjective(obj["function"]["type"])
prob += obj_f
for c in data.get("constraints", []):
add_constraint(prob, c, prob.var_by_name)
return prob

def solve_from_file(filename):
prob = read_from_file(filename)
print(prob)
prob.solve()
return {
"status": pulp.LpStatus[prob.status],
"primal": {k: v.value() for (k, v) in prob.var_by_name.items()}
}

# Usage:

def test_success():
ret = solve_from_file(ROOT + 'examples/milp.mof.json')
assert ret["status"] == "Optimal"
assert ret["primal"] == {'x': 0.0, 'y': 1.0}
return

def test_failure():
with pytest.raises(UnsupportedObjective):
solve_from_file(ROOT + 'examples/nlp.mof.json')
return
Loading