Skip to content

Commit

Permalink
Add tests for gams transport tutorial
Browse files Browse the repository at this point in the history
* Exclude resulting files from version control
* Adapt code to make tests pass
* Copy required code temporarily until location is finalized
  • Loading branch information
glatterf42 committed Aug 6, 2024
1 parent 58c8705 commit 9bf74bc
Show file tree
Hide file tree
Showing 8 changed files with 392 additions and 91 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,6 @@ PR.md

# tutorial/gams
tutorial/transport/*.gdx
tutorial/transport/*.lst
tutorial/transport/*.lst
tests/tutorial/transport/*.gdx
tests/tutorial/transport/*.lst
84 changes: 84 additions & 0 deletions tests/tutorial/transport/dantzig_model_gams.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import copy
from pathlib import Path

import gams.transfer as gt
import pandas as pd
from gams import GamsWorkspace

from ixmp4.core import Run


def write_run_to_gams(run: Run) -> gt.Container:
"""Writes scenario data from the Run to a GAMS container."""
m = gt.Container()
indexsets = [
gt.Set(
container=m,
name=indexset.name,
records=indexset.elements,
description=indexset.docs
if indexset.docs
else "", # description is "optional", but must be str
)
for indexset in run.optimization.indexsets.list()
]

for scalar in run.optimization.scalars.list():
gt.Parameter(
container=m,
name=scalar.name,
records=scalar.value,
description=scalar.docs if scalar.docs else "",
)

for parameter in run.optimization.parameters.list():
domains = [
indexset
for indexset in indexsets
if indexset.name in parameter.constrained_to_indexsets
]
records = copy.deepcopy(parameter.data)
del records[
"units"
] # all parameters must have units, but GAMS doesn't work on them
gt.Parameter(
container=m,
name=parameter.name,
domain=domains,
records=records,
description=parameter.docs if parameter.docs else "",
)

return m


# TODO I'd like to have proper Paths here, but gams can only handle str
def solve(model_file: Path, data_file: Path, result_file: Path | None = None) -> None:
ws = GamsWorkspace(working_directory=Path(__file__).parent.absolute())
ws.add_database_from_gdx(gdx_file_name=str(data_file))
gams_options = ws.add_options()
gams_options.defines["in"] = str(data_file)
if result_file:
gams_options.defines["out"] = str(result_file)
job = ws.add_job_from_file(file_name=str(model_file))
job.run(gams_options=gams_options)


def read_solution_to_run(run: Run, result_file: Path) -> None:
m = gt.Container(load_from=result_file)
for variable in run.optimization.variables.list():
# DF also includes lower, upper, scale
variable_data: pd.DataFrame = (
m.data[variable.name]
.records[["level", "marginal"]]
.rename(columns={"level": "levels", "marginal": "marginals"})
)
run.optimization.variables.get(variable.name).add(data=variable_data)

for equation in run.optimization.equations.list():
equation_data: pd.DataFrame = (
m.data[equation.name]
.records[["level", "marginal"]]
.rename(columns={"level": "levels", "marginal": "marginals"})
)
run.optimization.equations.get(equation.name).add(data=equation_data)
131 changes: 131 additions & 0 deletions tests/tutorial/transport/test_dantzig_model_gams.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import copy
import shutil
from pathlib import Path

from gams.transfer import Set

from ixmp4 import Platform

from ...utils import all_platforms, create_dantzig_run
from .dantzig_model_gams import read_solution_to_run, solve, write_run_to_gams


@all_platforms
class TestTransportTutorialLinopy:
# NOTE The function could be expanded by tables, equations, variables, none of which
# are tested here.
def test_write_run_to_gams(self, test_mp, request):
test_mp: Platform = request.getfixturevalue(test_mp) # type: ignore
run = create_dantzig_run(test_mp)
gams_container = write_run_to_gams(run=run)

# Should include exactly i, j, f, a, b, d
assert len(gams_container.data.keys()) == 6

gams_indexsets: list[Set] = [] # type: ignore
for indexset in run.optimization.indexsets.list():
gams_indexset = gams_container.data[indexset.name]
assert gams_indexset.name == indexset.name
assert gams_indexset.records["uni"].to_list() == indexset.elements
gams_indexsets.append(gams_indexset)

for scalar in run.optimization.scalars.list():
gams_scalar = gams_container.data[scalar.name]
assert gams_scalar.name == scalar.name
# Should only have one value
assert len(gams_scalar.records["value"]) == 1
assert gams_scalar.records["value"].values[0] == scalar.value

for parameter in run.optimization.parameters.list():
gams_parameter = gams_container.data[parameter.name]
assert gams_parameter.name == parameter.name

expected_domains = [
indexset
for indexset in gams_indexsets
if indexset.name in parameter.constrained_to_indexsets
]
assert gams_parameter.domain == expected_domains

expected_records = copy.deepcopy(parameter.data)
del expected_records[
"units"
] # all parameters must have units, but GAMS doesn't work on them
assert (
gams_parameter.records.rename(columns={"value": "values"}).to_dict(
orient="list"
)
== expected_records
)

def test_solve(self, test_mp, request, tmp_path):
test_mp: Platform = request.getfixturevalue(test_mp) # type: ignore
run = create_dantzig_run(test_mp)
gams_container = write_run_to_gams(run=run)
data_file = tmp_path / "transport_data.gdx"
# TODO once we know where the tests land, figure out how to navigate paths
# Same below.
model_file = shutil.copy(
src=Path(__file__).parent.absolute() / "transport_ixmp4.gms",
dst=tmp_path / "transport_ixmp4.gms",
)
gams_container.write(write_to=data_file)

# Test writing to default location
solve(model_file=model_file, data_file=data_file)
default_result_file = Path(__file__).parent.absolute() / "transport_results.gdx"
assert default_result_file.is_file()

# Test writing to specified location
result_file: Path = tmp_path / "different_transport_results.gdx" # type: ignore
solve(model_file=model_file, data_file=data_file, result_file=result_file)
assert result_file.is_file()

# TODO Maybe this test could be made more performant by receiving a run where the
# scenario has already been solved. However, this would make the test scenario less
# isolated. Also, solving the dantzig model only takes a few seconds.
def test_read_solution_to_run(self, test_mp, request, tmp_path):
test_mp: Platform = request.getfixturevalue(test_mp) # type: ignore
run = create_dantzig_run(test_mp)
gams_container = write_run_to_gams(run=run)
data_file = tmp_path / "transport_data.gdx"
model_file = shutil.copy(
src=Path(__file__).parent.absolute() / "transport_ixmp4.gms",
dst=tmp_path / "transport_ixmp4.gms",
)
gams_container.write(write_to=data_file)
solve(model_file=model_file, data_file=data_file)
read_solution_to_run(
run=run,
result_file=Path(__file__).parent.absolute() / "transport_results.gdx",
)

# Test objective value
z = run.optimization.variables.get("z")
assert z.levels == [153.675]
assert z.marginals == [0.0]

# Test shipment quantities
assert run.optimization.variables.get("x").data == {
"levels": [50.0, 300.0, 0.0, 275.0, 0.0, 275.0],
"marginals": [
0.0,
0.0,
0.036000000000000004,
0.0,
0.009000000000000008,
0.0,
],
}

# Test demand equation
assert run.optimization.equations.get("demand").data == {
"levels": [325.0, 300.0, 275.0],
"marginals": [0.225, 0.153, 0.126],
}

# Test supply equation
assert run.optimization.equations.get("supply").data == {
"levels": [350.0, 550.0],
"marginals": [-0.0, 0.0],
}
63 changes: 1 addition & 62 deletions tests/tutorial/transport/test_dantzig_model_linopy.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,75 +3,14 @@
import xarray as xr

from ixmp4 import Platform
from ixmp4.core import Run, Unit

from ...utils import all_platforms
from ...utils import all_platforms, create_dantzig_run
from .dantzig_model_linopy import (
create_dantzig_model,
read_dantzig_solution,
)


def create_dantzig_run(mp: Platform) -> Run:
"""Create a Run for the transport tutorial.
Please see the tutorial file for explanation.
"""
# Only needed once for each mp
try:
cases = mp.units.get("cases")
km = mp.units.get("km")
unit_cost_per_case = mp.units.get("USD/km")
except Unit.NotFound:
cases = mp.units.create("cases")
km = mp.units.create("km")
unit_cost_per_case = mp.units.create("USD/km")

# Create run and all data sets
run = mp.runs.create(model="transport problem", scenario="standard")
a_data = {
"i": ["seattle", "san-diego"],
"values": [350, 600],
"units": [cases.name, cases.name],
}
b_data = pd.DataFrame(
[
["new-york", 325, cases.name],
["chicago", 300, cases.name],
["topeka", 275, cases.name],
],
columns=["j", "values", "units"],
)
d_data = {
"i": ["seattle", "seattle", "seattle", "san-diego", "san-diego", "san-diego"],
"j": ["new-york", "chicago", "topeka", "new-york", "chicago", "topeka"],
"values": [2.5, 1.7, 1.8, 2.5, 1.8, 1.4],
"units": [km.name] * 6,
}

# Add all data to the run
run.optimization.indexsets.create("i").add(["seattle", "san-diego"])
run.optimization.indexsets.create("j").add(["new-york", "chicago", "topeka"])
run.optimization.parameters.create(name="a", constrained_to_indexsets=["i"]).add(
data=a_data
)
run.optimization.parameters.create("b", constrained_to_indexsets=["j"]).add(
data=b_data
)
run.optimization.parameters.create("d", constrained_to_indexsets=["i", "j"]).add(
data=d_data
)
run.optimization.scalars.create(name="f", value=90, unit=unit_cost_per_case)

# Create further optimization items to store solution data
run.optimization.variables.create("z")
run.optimization.variables.create("x", constrained_to_indexsets=["i", "j"])
run.optimization.equations.create("supply", constrained_to_indexsets=["i"])
run.optimization.equations.create("demand", constrained_to_indexsets=["j"])

return run


@all_platforms
class TestTransportTutorialLinopy:
def test_create_dantzig_model(self, test_mp, request):
Expand Down
70 changes: 70 additions & 0 deletions tests/tutorial/transport/transport_ixmp4.gms
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
*Basic example of transport model from GAMS model library

$Title A Transportation Problem (TRNSPORT,SEQ=1)
$Ontext

This problem finds a least cost shipping schedule that meets
requirements at markets and supplies at factories.

Dantzig, G B, Chapter 3.3. In Linear Programming and Extensions.
Princeton University Press, Princeton, New Jersey, 1963.

This formulation is described in detail in:
Rosenthal, R E, Chapter 2: A GAMS Tutorial. In GAMS: A User's Guide.
The Scientific Press, Redwood City, California, 1988.

The line numbers will not match those in the book because of these
comments.

$Offtext

Sets
i canning plants
j markets
;

Parameters
a(i) capacity of plant i in cases
b(j) demand at market j in cases
d(i,j) distance in thousands of miles
f freight in dollars per case per thousand miles
;

* This file will read in data from a gdx file and write results to another gdx file.
* The name of the input and output file can either be set directly from the command line,
* e.g. `gams transport_ixmp.gms --in\=\"<name>\" --out\=\"<name>\"`.
* If no command line parameters are given, the input and output files are set as specific below.

$IF NOT set in $SETGLOBAL in 'transport_data.gdx'
$IF NOT set out $SETGLOBAL out 'transport_results.gdx'

$GDXIN '%in%'
$LOAD i, j, a, b, d, f
$GDXIN

Parameter c(i,j) transport cost in thousands of dollars per case ;
c(i,j) = f * d(i,j) / 1000 ;
Variables
x(i,j) shipment quantities in cases
z total transportation costs in thousands of dollars ;

Positive Variable x ;

Equations
cost define objective function
supply(i) observe supply limit at plant i
demand(j) satisfy demand at market j ;

cost .. z =e= sum((i,j), c(i,j)*x(i,j)) ;

supply(i) .. sum(j, x(i,j)) =l= a(i) ;

demand(j) .. sum(i, x(i,j)) =g= b(j) ;

Model transport /all/ ;

Solve transport using lp minimizing z ;

Display x.l, x.m ;

Execute_unload '%out%';
Loading

0 comments on commit 9bf74bc

Please sign in to comment.