Skip to content

Commit b8e7a83

Browse files
committed
Support for loading HiGHS solutions + tweaks to CPLEX solution parsing
1 parent 4a69304 commit b8e7a83

2 files changed

Lines changed: 111 additions & 7 deletions

File tree

gridpath/common_functions.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
# Copyright 2016-2023 Blue Marble Analytics LLC.
1+
# Copyright 2016-2025 Blue Marble Analytics LLC.
2+
# Copyright 2026 Sylvan Energy Analytics LLC.
23
#
34
# Licensed under the Apache License, Version 2.0 (the "License");
45
# you may not use this file except in compliance with the License.
@@ -205,6 +206,12 @@ def get_run_scenario_parser():
205206
action="store_true",
206207
help="Skip solve and load results from a Gurobi solution file instead.",
207208
)
209+
parser.add_argument(
210+
"--load_highs_solution",
211+
default=False,
212+
action="store_true",
213+
help="Skip solve and load results from a HiGHS solution file instead.",
214+
)
208215
# Solver options
209216
parser.add_argument(
210217
"--solver",

gridpath/run_scenario.py

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
# Copyright 2016-2023 Blue Marble Analytics LLC.
1+
# Copyright 2016-2025 Blue Marble Analytics LLC.
2+
# Copyright 2026 Sylvan Energy Analytics LLC.
23
#
34
# Licensed under the Apache License, Version 2.0 (the "License");
45
# you may not use this file except in compliance with the License.
@@ -320,6 +321,11 @@ def run_optimization_for_subproblem_stage(
320321
prob_sol_files_directory=prob_sol_files_directory,
321322
solution_filename="gurobi_solution.json",
322323
)
324+
if parsed_arguments.load_highs_solution:
325+
solved_instance, results, dynamic_components = load_highs_xml_solution(
326+
prob_sol_files_directory=prob_sol_files_directory,
327+
solution_filename="highs_solution.sol",
328+
)
323329
else:
324330
dynamic_components, instance = create_problem(
325331
scenario_directory=scenario_directory,
@@ -357,7 +363,7 @@ def run_optimization_for_subproblem_stage(
357363
symbol_map = instance.solutions.symbol_map[smap_id]
358364

359365
symbol_cuid_pairs = tuple(
360-
(symbol, ComponentUID(var_weakref(), cuid_buffer={}))
366+
(symbol, ComponentUID(var_weakref, cuid_buffer={}))
361367
for symbol, var_weakref in symbol_map.bySymbol.items()
362368
)
363369

@@ -1638,8 +1644,9 @@ def load_cplex_xml_solution(
16381644
type_tag.get("index"),
16391645
type_tag.get("value"),
16401646
)
1641-
if not var_id == "ONE_VAR_CONSTANT":
1642-
symbol_map.bySymbol[var_id]().value = float(value)
1647+
# "x2" with value None added for CPLEXSolution version 1.2
1648+
if not var_id in ["ONE_VAR_CONSTANT", "x2"]:
1649+
symbol_map.bySymbol[var_id].value = float(value)
16431650

16441651
# Constraints
16451652
for type_tag in root.findall("linearConstraints/constraint"):
@@ -1649,8 +1656,9 @@ def load_cplex_xml_solution(
16491656
type_tag.get("dual"),
16501657
)
16511658
if not constraint_id_w_extra_symbols == "c_e_ONE_VAR_CONSTANT":
1652-
constraint_id = constraint_id_w_extra_symbols[4:-1]
1653-
instance.dual[symbol_map.bySymbol[constraint_id]()] = float(dual)
1659+
# constraint_id = constraint_id_w_extra_symbols[4:-1]
1660+
constraint_id = constraint_id_w_extra_symbols
1661+
instance.dual[symbol_map.bySymbol[constraint_id]] = float(dual)
16541662

16551663
# Solver status
16561664
header = root.findall("header")[0] # Need a check that there is only one element
@@ -1715,6 +1723,95 @@ def load_gurobi_json_solution(
17151723
return instance, results, dynamic_components
17161724

17171725

1726+
def load_highs_xml_solution(
1727+
prob_sol_files_directory, solution_filename="highs_solution.sol"
1728+
):
1729+
"""
1730+
:param prob_sol_files_directory:
1731+
:param solution_filename:
1732+
:return:
1733+
"""
1734+
print(
1735+
"Loading results from solution file {}...".format(
1736+
os.path.join(prob_sol_files_directory, solution_filename)
1737+
)
1738+
)
1739+
instance, dynamic_components, symbol_map = load_problem_info(
1740+
prob_sol_files_directory=prob_sol_files_directory
1741+
)
1742+
1743+
# Read HiGHS solution file
1744+
with open(os.path.join(prob_sol_files_directory, solution_filename), "r") as f:
1745+
lines = f.readlines()
1746+
1747+
# Model status is th second line
1748+
model_status = lines[1].strip() if len(lines) > 1 else "Unknown"
1749+
termination_condition = model_status.lower()
1750+
solver_status = "ok" if termination_condition == "optimal" else "unknown"
1751+
1752+
# Parse the HiGHS solution file
1753+
section = None
1754+
1755+
for line in lines:
1756+
line = line.strip()
1757+
1758+
# Skip empty lines and comments
1759+
if not line or line.startswith("#"):
1760+
# Check for section headers in comments
1761+
if line == "# Primal solution values":
1762+
section = "primal_start"
1763+
elif "# Columns" in line and section == "primal_start":
1764+
section = "primal_columns"
1765+
elif "# Rows" in line and section == "primal_columns":
1766+
section = "primal_rows"
1767+
elif line == "# Dual solution values":
1768+
section = "dual_start"
1769+
elif "# Columns" in line and section == "dual_start":
1770+
section = "dual_columns"
1771+
elif "# Rows" in line and section in ["dual_start", "dual_columns"]:
1772+
section = "dual_rows"
1773+
elif line == "# Basis":
1774+
# Stop parsing once we reach basis section
1775+
break
1776+
continue
1777+
1778+
# Skip non-data lines
1779+
if line in ["Model status", "Feasible", "Valid"]:
1780+
continue
1781+
if line.startswith("Objective "):
1782+
continue
1783+
1784+
# Parse primal variable values (x variables only)
1785+
if section == "primal_columns":
1786+
parts = line.split()
1787+
if len(parts) == 2:
1788+
var_id, value = parts[0], parts[1]
1789+
if (
1790+
var_id.startswith("x")
1791+
and var_id in symbol_map.bySymbol
1792+
and var_id not in ["ONE_VAR_CONSTANT", "x2"]
1793+
):
1794+
symbol_map.bySymbol[var_id].value = float(value)
1795+
1796+
# Parse constraint dual values (c_ constraints only) from dual rows
1797+
elif section == "dual_rows":
1798+
parts = line.split()
1799+
if len(parts) == 2:
1800+
constraint_id, dual = parts[0], parts[1]
1801+
if (
1802+
constraint_id.startswith("c_")
1803+
and constraint_id in symbol_map.bySymbol
1804+
and constraint_id != "c_e_ONE_VAR_CONSTANT"
1805+
):
1806+
instance.dual[symbol_map.bySymbol[constraint_id]] = float(dual)
1807+
1808+
results = Results(
1809+
solver_status=solver_status, termination_condition=termination_condition
1810+
)
1811+
1812+
return instance, results, dynamic_components
1813+
1814+
17181815
def load_problem_info(prob_sol_files_directory):
17191816
with open(
17201817
os.path.join(prob_sol_files_directory, "instance.pickle"), "rb"

0 commit comments

Comments
 (0)