-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add tests for gams transport tutorial
* Exclude resulting files from version control * Adapt code to make tests pass * Copy required code temporarily until location is finalized
- Loading branch information
1 parent
58c8705
commit 9bf74bc
Showing
8 changed files
with
392 additions
and
91 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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], | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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%'; |
Oops, something went wrong.