Running a Macro Model
MacroEnergy.InfeasibleModelMacroEnergy.PeriodOutcomeMacroEnergy.SolveFailedMacroEnergy.UnboundedModelMacroEnergy.assert_solvedMacroEnergy.run_caseMacroEnergy.run_status_failureMacroEnergy.run_status_runningMacroEnergy.run_status_successMacroEnergy.solution_outcomeMacroEnergy.write_run_status
run_case
MacroEnergy.run_case — Function
run_case(case_path; kwargs...) -> (case::Case, solution::Any)Load, solve, and write results for a Macro case. This is the main entry point for running a complete Macro workflow.
Arguments
case_path::AbstractString: Path to the case directory containingsystem_data.json. Defaults to@__DIR__(the directory of the calling script).
Keyword Arguments
Data Loading
lazy_load::Bool=true: Whether to delay loading the input data until needed.
Logging
log_level::LogLevel=Logging.Info: Logging verbosity level (e.g.,Logging.Debug,Logging.Info,Logging.Warn,Logging.Error). Note: you need to import theLoggingmodule to change this parameter.log_to_console::Bool=true: Whether to print log messages to the console.log_to_file::Bool=true: Whether to write log messages to a file.log_file_path::AbstractString: Path to the log file. Defaults to<case_path>/<case_name>.log.log_file_attribution::Bool=true: Whether to include source file attribution in log messages.
Run status
write_status::Bool=true: Whether to write a machine-readable JSON record of how the run ended, on both success and failure. Intended especially for external processes that need to tell an infeasible model from a failed input load without parsing log output.status_file_path::AbstractString: Path to the run status file. Defaults to<case_path>/run_status.json.
Optimizer (Monolithic/Myopic)
optimizer::DataType=HiGHS.Optimizer: Optimizer constructor for Monolithic or Myopic algorithms.optimizer_env::Any=nothing: Optional optimizer environment.optimizer_attributes::Tuple: Solver-specific settings. Default:("BarConvTol" => 1e-3, "Crossover" => 0, "Method" => 2).
Optimizer (Benders)
planning_optimizer::DataType=HiGHS.Optimizer: Optimizer constructor for the planning problem.subproblem_optimizer::DataType=HiGHS.Optimizer: Optimizer constructor for the subproblems.planning_optimizer_attributes::Tuple: Solver settings for the planning problem.subproblem_optimizer_attributes::Tuple: Solver settings for the subproblems.
Returns
case::Case: A case object containing a vector of solved system objects (one per period) and the case settingssolution: The solution object (type depends on the solution algorithm:Modelfor Monolithic,MyopicResultsfor Myopic (both Monolithic and Benders),BendersModelfor Perfect Foresight + Benders).MyopicResults.resultsholds aVectorof per-period results whenReturnModels=true, ornothingwhenReturnModels=false.
Examples
Basic usage with HiGHS (default)
using MacroEnergy
(case, solution) = run_case(@__DIR__);Using Gurobi optimizer
using MacroEnergy
using Gurobi
(case, solution) = run_case(
@__DIR__;
optimizer=Gurobi.Optimizer,
optimizer_attributes=("Method" => 2, "Crossover" => 0, "BarConvTol" => 1e-3)
);Benders decomposition with custom settings
using MacroEnergy
using Gurobi
(case, solution) = run_case(
@__DIR__;
planning_optimizer=Gurobi.Optimizer,
subproblem_optimizer=Gurobi.Optimizer,
planning_optimizer_attributes=("Method" => 2, "Crossover" => 0, "BarConvTol" => 1e-3),
subproblem_optimizer_attributes=("Method" => 2, "Crossover" => 0, "BarConvTol" => 1e-3)
);Suppressing console output when running a case
using MacroEnergy
using Logging
(case, solution) = run_case(
case_path;
log_to_console=false,
log_level=Logging.Warn
);Notes
- Unless
write_status=false, arun_status.jsonfile is written to the case directory recording the outcome of the run."status"is one of:"RUNNING"(written before the work starts, so a process killed by the operating system does not leave the previous run's result behind),"OK"(solved to optimality),"SUBOPTIMAL"(a usable solution that is not provably optimal, such as a limit reached with a feasible incumbent),"INFEASIBLE","INFEASIBLE_OR_UNBOUNDED"(the solver could not tell the two apart),"UNBOUNDED","SOLVE_FAILED", or"ERROR"for anything that is not a solve outcome, such as a failed input load. Failures also record"exception"and"message", and solver failures additionally record"termination_status"and"label"(which identifies the period for Myopic runs). Exceptions are still thrown as normal. - On success, a copy is also written into the results directory, as happens with the log file. The case-level file is the one to watch from outside, since it has a path known before the run starts and is written even when the run fails before any results directory exists; the copy gives each result set its own record, which the case-level file loses as soon as the next run overwrites it.
- The solution algorithm (Monolithic, Myopic, or Benders) is determined by the
SolutionAlgorithmsetting in the case'ssettings/case_settings.jsonfile. - For Myopic runs, results are written during iteration; no additional output writing occurs after solving.
- For Benders with distributed processing enabled, worker processes are automatically created and cleaned up.
- Logging is scoped to the run: the caller's logger is restored and the run's file log stream is closed on both success and failure.
Solve status
Checks applied to each solved model, and the exceptions they raise. See Run Status File for the file these outcomes are recorded in.
MacroEnergy.assert_solved — Function
assert_solved(model::Model; label::AbstractString = "")Check that model has been solved to (near-)optimality and holds a usable primal solution, and throw a descriptive exception otherwise.
Arguments
model::Model: The JuMP model to check, afteroptimize!.
Keyword Arguments
label::AbstractString="": Description of the model included in error messages, used to identify which model failed when several are solved in sequence (e.g."period 3").
Returns
nothingif the model has a usable solution.
Throws
InfeasibleModel: For any status inINFEASIBLE_STATUSES, i.e. the optimizer proved the model infeasible. This coversMOI.INFEASIBLE_OR_UNBOUNDED, which is what solvers commonly report when presolve detects infeasibility.UnboundedModel: For any status inUNBOUNDED_STATUSES.SolveFailed: For any other status without a usable primal solution, including numerical failures, an invalid model, and limits hit before a feasible point was found.
Notes
- Statuses close to optimal (
MOI.ALMOST_OPTIMALetc.) are accepted, since the default optimizer attributes solve to a loose barrier tolerance without crossover. - Anything else that still carries a feasible point is accepted with a warning, whatever the termination status.
- The result is that only two things throw: a model the solver proved has no solution, and a solve that left nothing usable behind.
assert_solved(bm::BendersModel; label::AbstractString = "")Check the outcome of a Benders solve and warn if the algorithm stopped before converging.
Notes
- The Benders algorithm stops on convergence, a CPU time limit, a maximum iteration count, or a negative gap, and each of these leaves a planning solution in place. Infeasibility of the underlying problem surfaces while solving the planning problem or a subproblem, not here, so this method warns rather than throwing.
MacroEnergy.solution_outcome — Function
solution_outcome(solution) -> NamedTuple{(:status, :termination_status)}Summarize how a solve ended, for the run status file written by run_case.
assert_solved throws when a solve produced nothing usable, so anything reaching this point has a solution; the question here is whether that solution is provably optimal ("OK") or merely usable ("SUBOPTIMAL", e.g. a time limit reached with a feasible incumbent, or a Benders solve that stopped on its iteration limit). Without this distinction the status file would report a run that ran out of time exactly like one that converged.
MacroEnergy.InfeasibleModel — Type
InfeasibleModel <: ExceptionThrown when the optimizer proves that a model has no feasible solution.
Fields
status::MOI.TerminationStatusCode: Termination status reported by the optimizer.label::String: Optional description of which model failed (e.g."period 3").
MacroEnergy.UnboundedModel — Type
UnboundedModel <: ExceptionThrown when the optimizer reports that a model's objective is unbounded.
Fields
status::MOI.TerminationStatusCode: Termination status reported by the optimizer.label::String: Optional description of which model failed (e.g."period 3").
MacroEnergy.SolveFailed — Type
SolveFailed <: ExceptionThrown when the optimizer terminates without a usable primal solution for a reason other than proven infeasibility (e.g. numerical failure, or a limit reached before any feasible point was found).
Fields
status::MOI.TerminationStatusCode: Termination status reported by the optimizer.primal::MOI.ResultStatusCode: Primal status reported by the optimizer.label::String: Optional description of which model failed (e.g."period 3").
MacroEnergy.PeriodOutcome — Type
PeriodOutcomeHow one period of a Myopic run ended.
Recorded as each period is solved, rather than derived from the stored models afterwards: MyopicSettings[:ReturnModels] is off by default, in which case no models are kept and there would be nothing left to inspect.
Fields
period::Int: Index of the period, which is not the same as the position in the vector when periods are skipped byRestartorStopAfterPeriod.status::String:"OK"if the period solved to optimality,"SUBOPTIMAL"otherwise.termination_status::String: The solver's own verdict for that period.
Run status file
MacroEnergy.run_status_running — Function
run_status_running(case_path) -> OrderedDict{String,Any}Build the run status payload written before the work starts.
A run killed by the operating system — a scheduler walltime, an out-of-memory kill — never reaches either the success or the failure path, so without this marker the previous run's file survives and a watcher reads a stale "OK" pointing at the previous results.
MacroEnergy.run_status_success — Function
run_status_success(case_path, elapsed, output_path, solution) -> OrderedDict{String,Any}Build the run status payload for a completed run. output_path is recorded so that a caller which did not choose the results directory can find it.
The status is "OK" only when the solution is provably optimal, and "SUBOPTIMAL" when it is merely usable — a limit reached with a feasible incumbent, or a Benders solve that stopped short of converging. termination_status carries the solver's own verdict; for a Myopic run it lists one entry per period.
MacroEnergy.run_status_failure — Function
run_status_failure(case_path, elapsed, e) -> OrderedDict{String,Any}Build the run status payload for a failed run. The status field is "INFEASIBLE", "UNBOUNDED", or "SOLVE_FAILED" for the corresponding solver outcomes, and "ERROR" for everything else (a failed input load, a bad setting, an unexpected error), so that a caller can branch on the solve outcome without inspecting the message.
MacroEnergy.write_run_status — Function
write_run_status(file_path, build_data) -> Nothing
write_run_status(file_path, data::AbstractDict) -> NothingWrite a run status payload as JSON. Failures are warned about but never raised: this runs on the error path, and must not replace the exception being reported.
Prefer the build_data form on the error path. It builds the payload inside the same guard as the write, so that an exception whose showerror fails, or a call made from a deleted working directory, still cannot displace the original error.
Notes
- The directory must already exist. The status file is not a reason to create directories: doing so would silently produce a case directory for a mistyped path.
- Always written as plain JSON, whatever the file extension.