# ___________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2024
# National Technology and Engineering Solutions of Sandia, LLC
# Under the terms of Contract DE-NA0003525 with National Technology and
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
# rights in this software.
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________
import logging
import os
import subprocess
import re
import tempfile
from pyomo.common import Executable
from pyomo.common.collections import Bunch
from pyomo.common.tempfiles import TempfileManager
from pyomo.opt.base import ProblemFormat, ResultsFormat, OptSolver
from pyomo.opt.base.solvers import _extract_version, SolverFactory
from pyomo.opt.results import (
SolverResults,
Solution,
SolverStatus,
TerminationCondition,
SolutionStatus,
)
from pyomo.opt.solver import SystemCallSolver
logger = logging.getLogger('pyomo.solvers')
[docs]
@SolverFactory.register('baron', doc='The BARON MINLP solver')
class BARONSHELL(SystemCallSolver):
"""The BARON MINLP solver"""
_solver_info_cache = {}
[docs]
def __init__(self, **kwds):
#
# Call base class constructor
#
kwds['type'] = 'baron'
SystemCallSolver.__init__(self, **kwds)
self._tim_file = None
self._valid_problem_formats = [ProblemFormat.bar]
self._valid_result_formats = {}
self._valid_result_formats[ProblemFormat.bar] = [ResultsFormat.soln]
self.set_problem_format(ProblemFormat.bar)
self._capabilities = Bunch()
self._capabilities.linear = True
self._capabilities.quadratic_objective = True
self._capabilities.quadratic_constraint = True
self._capabilities.integer = True
self._capabilities.sos1 = False
self._capabilities.sos2 = False
# CLH: Copied from cpxlp.py, the cplex file writer.
# Keven Hunter made a nice point about using %.16g in his attachment
# to ticket #4319. I am adjusting this to %.17g as this mocks the
# behavior of using %r (i.e., float('%r'%<number>) == <number>) with
# the added benefit of outputting (+/-). The only case where this
# fails to mock the behavior of %r is for large (long) integers (L),
# which is a rare case to run into and is probably indicative of
# other issues with the model.
# *** NOTE ***: If you use 'r' or 's' here, it will break code that
# relies on using '%+' before the formatting character
# and you will need to go add extra logic to output
# the number's sign.
self._precision_string = '.17g'
def _get_dummy_input_files(self, check_license=False):
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
# For some reason, if results: 0 is added to the options
# section, it causes a file named fort.71 to appear.
# So point the ResName option to a temporary file that
# we will delete
with tempfile.NamedTemporaryFile(mode='w', delete=False) as fr:
pass
# Doing this for the remaining output files as well.
# Can't seem to reliably control the files created by
# Baron otherwise.
with tempfile.NamedTemporaryFile(mode='w', delete=False) as fs:
pass
with tempfile.NamedTemporaryFile(mode='w', delete=False) as ft:
pass
f.write(
"//This is a dummy .bar file created to "
"return the baron version//\n"
"OPTIONS {\n"
"results: 1;\n"
"ResName: \"" + fr.name + "\";\n"
"summary: 1;\n"
"SumName: \"" + fs.name + "\";\n"
"times: 1;\n"
"TimName: \"" + ft.name + "\";\n"
"}\n"
)
f.write("POSITIVE_VARIABLES ")
if check_license:
f.write(", ".join("x" + str(i) for i in range(11)))
else:
f.write("x1")
f.write(";\n")
f.write("OBJ: minimize x1;")
return (f.name, fr.name, fs.name, ft.name)
def _remove_dummy_input_files(self, fnames):
for name in fnames:
try:
os.remove(name)
except OSError:
pass
[docs]
def license_is_valid(self):
"""Runs a check for a valid Baron license using the
given executable (default is 'baron'). All output is
hidden. If the test fails for any reason (including
the executable being invalid), then this function
will return False."""
solver_exec = self.executable()
if (solver_exec, 'licensed') in self._solver_info_cache:
return self._solver_info_cache[(solver_exec, 'licensed')]
if not solver_exec:
licensed = False
else:
fnames = self._get_dummy_input_files(check_license=True)
try:
process = subprocess.Popen(
[solver_exec, fnames[0]],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
stdout, stderr = process.communicate()
assert stderr is None
rc = 0
if process.returncode:
rc = 1
else:
stdout = stdout.decode()
if "Continuing in demo mode" in stdout:
rc = 1
except OSError:
rc = 1
finally:
self._remove_dummy_input_files(fnames)
licensed = not rc
self._solver_info_cache[(solver_exec, 'licensed')] = licensed
return licensed
def _default_executable(self):
executable = Executable("baron")
if not executable:
logger.warning(
"Could not locate the 'baron' executable, "
"which is required for solver %s" % self.name
)
self.enable = False
return None
return executable.path()
def _get_version(self):
"""
Returns a tuple describing the solver executable version.
"""
solver_exec = self.executable()
if (solver_exec, 'version') in self._solver_info_cache:
return self._solver_info_cache[(solver_exec, 'version')]
if solver_exec is None:
ver = _extract_version('')
else:
fnames = self._get_dummy_input_files(check_license=False)
try:
results = subprocess.run(
[solver_exec, fnames[0]],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
)
ver = _extract_version(results.stdout)
finally:
self._remove_dummy_input_files(fnames)
self._solver_info_cache[(solver_exec, 'version')] = ver
return ver
[docs]
def create_command_line(self, executable, problem_files):
# The solution file is created in the _convert_problem function.
# The bar file needs the solution filename in the OPTIONS section, but
# this function is executed after the bar problem file writing.
# self._soln_file = pyomo.common.tempfiles.TempfileManager.create_tempfile(suffix = '.baron.sol')
cmd = [executable, problem_files[0]]
if self._timer:
cmd.insert(0, self._timer)
return Bunch(cmd=cmd, log_file=self._log_file, env=None)
#
# Assuming the variable values stored in the model will
# automatically be included in the Baron input file
# (returning True implies the opposite and requires another function)
[docs]
def warm_start_capable(self):
return False
def _convert_problem(self, args, problem_format, valid_problem_formats, **kwds):
# Baron needs all solver options and file redirections
# inside the input file, so we need to input those
# here through io_options before calling the baron writer
#
# Define log file
#
if self._log_file is None:
self._log_file = TempfileManager.create_tempfile(suffix='.baron.log')
#
# Define solution file
#
if self._soln_file is None:
self._soln_file = TempfileManager.create_tempfile(suffix='.baron.soln')
self._tim_file = TempfileManager.create_tempfile(suffix='.baron.tim')
#
# Create options to send through as io_options
# containing all relevant info needed in the Baron file
#
solver_options = {}
solver_options['ResName'] = self._soln_file
solver_options['TimName'] = self._tim_file
for key in self.options:
lower_key = key.lower()
if lower_key == 'resname':
logger.warning(
'Ignoring user-specified option "%s=%s". This '
'option is set to %s, and can be overridden using '
'the "solnfile" argument to the solve() method.'
% (key, self.options[key], self._soln_file)
)
elif lower_key == 'timname':
logger.warning(
'Ignoring user-specified option "%s=%s". This '
'option is set to %s.' % (key, self.options[key], self._tim_file)
)
else:
solver_options[key] = self.options[key]
for suffix in self._suffixes:
if re.match(suffix, 'dual') or re.match(suffix, 'rc'):
solver_options['WantDual'] = 1
break
if 'solver_options' in kwds:
raise ValueError(
"Baron solver options should be set "
"using the options object on this "
"solver plugin. The solver_options "
"I/O options dict for the Baron writer "
"will be populated by this plugin's "
"options object"
)
kwds['solver_options'] = solver_options
return OptSolver._convert_problem(
self, args, problem_format, valid_problem_formats, **kwds
)
[docs]
def process_logfile(self):
results = SolverResults()
#
# Process logfile
#
cuts = ['Bilinear', 'LD-Envelopes', 'Multilinears', 'Convexity', 'Integrality']
# Collect cut-generation statistics from the log file
with open(self._log_file) as OUTPUT:
for line in OUTPUT:
for field in cuts:
if field in line:
try:
results.solver.statistics[field + '_cuts'] = int(
line.split()[1]
)
except:
pass
return results
[docs]
def process_soln_file(self, results):
# check for existence of the solution and time file. Not sure why we
# just return - would think that we would want to indicate
# some sort of error
if not os.path.exists(self._soln_file):
logger.warning("Solution file does not exist: %s" % (self._soln_file))
return
if not os.path.exists(self._tim_file):
logger.warning("Time file does not exist: %s" % (self._tim_file))
return
with open(self._tim_file, "r") as TimFile:
with open(self._soln_file, "r") as INPUT:
self._process_soln_file(results, TimFile, INPUT)
def _process_soln_file(self, results, TimFile, INPUT):
#
# **NOTE: This solution parser assumes the baron input file
# was generated by the Pyomo baron_writer plugin, and
# that a dummy constraint named c_e_FIX_ONE_VAR_CONST__
# was added as the initial constraint in order to
# support trivial constraint equations arrising from
# fixing pyomo variables. Thus, the dual price solution
# information for the first constraint in the solution
# file will be excluded from the results object.
#
# TODO: Is there a way to handle non-zero return values from baron?
# Example: the "NonLinearity Error if POW expression"
# (caused by x ^ y) when both x and y are variables
# causes an ugly python error and the solver log has a single
# line to display the error, hard to pick out of the list
# Check for suffixes to send back to pyomo
extract_marginals = False
extract_price = False
for suffix in self._suffixes:
flag = False
if re.match(suffix, "rc"): # baron_marginal
extract_marginals = True
flag = True
if re.match(suffix, "dual"): # baron_price
extract_price = True
flag = True
if not flag:
raise RuntimeError(
"***The BARON solver plugin cannot"
"extract solution suffix=" + suffix
)
soln = Solution()
#
# Process model and solver status from the Baron tim file
#
line = TimFile.readline().split()
try:
# The list of information in the tim file depends on the
# BARON version. As we extract things in order, older
# versions of BARON will just result in an IndexError AFTER
# we have grabbed all the data that it returns - and we can
# safely silently ignore the exception.
results.problem.name = line[0]
results.problem.number_of_constraints = int(line[1])
results.problem.number_of_variables = int(line[2])
try:
results.problem.lower_bound = float(line[5])
except ValueError:
results.problem.lower_bound = float("-inf")
try:
results.problem.upper_bound = float(line[6])
except ValueError:
results.problem.upper_bound = float("inf")
results.problem.missing_bounds = line[9]
results.problem.iterations = line[10]
results.problem.node_opt = line[11]
results.problem.node_memmax = line[12]
results.problem.cpu_time = float(line[13])
results.problem.wall_time = float(line[14])
except IndexError:
pass
soln.gap = results.problem.upper_bound - results.problem.lower_bound
solver_status = line[7]
model_status = line[8]
objective = None
##try:
## objective = symbol_map.getObject("__default_objective__")
## objective_label = symbol_map_byObjects[id(objective)]
##except:
## objective_label = "__default_objective__"
# [JDS 17/Feb/15] I am not sure why this is needed, but all
# other solvers (in particular the ASL solver and CPLEX) always
# return the objective value in the __default_objective__ label,
# and not by the Pyomo object name. For consistency, we will
# do the same here.
objective_label = "__default_objective__"
soln.objective[objective_label] = {'Value': None}
results.problem.number_of_objectives = 1
if objective is not None:
results.problem.sense = (
'minimizing' if objective.is_minimizing() else 'maximizing'
)
if solver_status == '1':
results.solver.status = SolverStatus.ok
elif solver_status == '2':
results.solver.status = SolverStatus.error
results.solver.termination_condition = TerminationCondition.error
# CLH: I wasn't sure if this was double reporting errors. I
# just filled in one termination_message for now
results.solver.termination_message = (
"Insufficient memory to store the number of nodes required "
"for this search tree. Increase physical memory or change "
"algorithmic options"
)
elif solver_status == '3':
results.solver.status = SolverStatus.ok
results.solver.termination_condition = TerminationCondition.maxIterations
elif solver_status == '4':
results.solver.status = SolverStatus.ok
results.solver.termination_condition = TerminationCondition.maxTimeLimit
elif solver_status == '5':
results.solver.status = SolverStatus.warning
results.solver.termination_condition = TerminationCondition.other
elif solver_status == '6':
results.solver.status = SolverStatus.aborted
results.solver.termination_condition = TerminationCondition.userInterrupt
elif solver_status == '7':
results.solver.status = SolverStatus.error
results.solver.termination_condition = TerminationCondition.error
elif solver_status == '8':
results.solver.status = SolverStatus.unknown
results.solver.termination_condition = TerminationCondition.unknown
elif solver_status == '9':
results.solver.status = SolverStatus.error
results.solver.termination_condition = TerminationCondition.solverFailure
elif solver_status == '10':
results.solver.status = SolverStatus.error
results.solver.termination_condition = TerminationCondition.error
elif solver_status == '11':
results.solver.status = SolverStatus.aborted
results.solver.termination_condition = (
TerminationCondition.licensingProblems
)
results.solver.termination_message = (
'Run terminated because of a licensing error.'
)
if model_status == '1':
soln.status = SolutionStatus.optimal
results.solver.termination_condition = TerminationCondition.optimal
elif model_status == '2':
soln.status = SolutionStatus.infeasible
results.solver.termination_condition = TerminationCondition.infeasible
elif model_status == '3':
soln.status = SolutionStatus.unbounded
results.solver.termination_condition = TerminationCondition.unbounded
elif model_status == '4':
soln.status = SolutionStatus.feasible
elif model_status == '5':
soln.status = SolutionStatus.unknown
#
# Process BARON results file
#
# Solutions that were preprocessed infeasible, were aborted,
# or gave error will not have filled in res.lst files
if results.solver.status not in [SolverStatus.error, SolverStatus.aborted]:
#
# Extract the solution vector and objective value from BARON
#
var_value = []
var_name = []
var_marginal = []
con_price = []
SolvedDuringPreprocessing = False
#############
#
# Scan through the first part of the solution file, until the
# termination message '*** Normal completion ***'
line = '\n'
while line and '***' not in line:
line = INPUT.readline()
if 'Problem solved during preprocessing' in line:
SolvedDuringPreprocessing = True
INPUT.readline()
INPUT.readline()
try:
objective_value = float(INPUT.readline().split()[4])
except IndexError:
# No objective value, so no solution to return
if solver_status == '1' and model_status in ('1', '4'):
logger.error(
"""Failed to process BARON solution file: could not extract the final
objective value, but BARON completed normally. This is indicative of a
bug in Pyomo's BARON solution parser. Please report this (along with
the Pyomo model and BARON version) to the Pyomo Developers."""
)
return
INPUT.readline()
INPUT.readline()
# Scan through the solution variable values
line = INPUT.readline()
while line.strip() != '':
var_value.append(float(line.split()[2]))
line = INPUT.readline()
# Only scan through the marginal and price values if baron
# found that information.
has_dual_info = False
if 'Corresponding dual solution vector is' in INPUT.readline():
has_dual_info = True
INPUT.readline()
line = INPUT.readline()
while 'Price' not in line and line.strip() != '':
var_marginal.append(float(line.split()[2]))
line = INPUT.readline()
if 'Price' in line:
line = INPUT.readline()
#
# Assume the baron_writer added the dummy
# c_e_FIX_ONE_VAR_CONST__ constraint as the first
#
line = INPUT.readline()
while line.strip() != '':
con_price.append(float(line.split()[2]))
line = INPUT.readline()
# Skip either a few blank lines or an empty block of useless
# marginal and price values (if 'No dual information is available')
while 'The best solution found is' not in INPUT.readline():
pass
# Collect the variable names, which are given in the same
# order as the lists for values already read
INPUT.readline()
INPUT.readline()
line = INPUT.readline()
while line.strip() != '':
var_name.append(line.split()[0])
line = INPUT.readline()
assert len(var_name) == len(var_value)
#
#
################
#
# Plug gathered information into pyomo soln
#
soln_variable = soln.variable
# After collecting solution information, the soln is
# filled with variable name, number, and value. Also,
# optionally fill the baron_marginal suffix
for i, (label, val) in enumerate(zip(var_name, var_value)):
soln_variable[label] = {"Value": val}
# Only adds the baron_marginal key it is requested and exists
if extract_marginals and has_dual_info:
soln_variable[label]["rc"] = var_marginal[i]
# Fill in the constraint 'price' information
if extract_price and has_dual_info:
soln_constraint = soln.constraint
#
# Assume the baron_writer added the dummy
# c_e_FIX_ONE_VAR_CONST__ constraint as the first,
# so constraint aliases start at 1
#
for i, price_val in enumerate(con_price, 1):
# use the alias made by the Baron writer
con_label = ".c" + str(i)
soln_constraint[con_label] = {"dual": price_val}
# This check is necessary because solutions that are
# preprocessed infeasible have ok solver status, but no
# objective value located in the res.lst file
if not (
SolvedDuringPreprocessing and soln.status == SolutionStatus.infeasible
):
soln.objective[objective_label] = {'Value': objective_value}
# Fill the solution for most cases, except errors
results.solution.insert(soln)