Skip to content

Ipopt_v2: update options processing #3693

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion pyomo/common/envvar.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,39 @@
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________

"""Attributes describing the current platform and user configuration.

This module provides standardized attributes that other parts of Pyomo
can use to interrogate aspects of the current platform, and to find
information about the current user configuration (including where to
locate the main Pyomo configuration directory).

"""

import os
import platform

_platform = platform.system().lower()
#: bool : True if running in a "native" Windows environment
is_native_windows = _platform.startswith('windows')
#: bool : True if running on Windows (native or cygwin)
is_windows = is_native_windows or _platform.startswith('cygwin')
#: bool : True if running on Mac/OSX
is_osx = _platform.startswith('darwin')
#: bool: True if running under the PyPy interpreter
is_pypy = platform.python_implementation().lower().startswith('pypy')

#: str : Absolute path to the user's Pyomo Configuration Directory.
#:
#: By default, this is ``~/.pyomo`` on Linux and OSX and
#: ``%LOCALAPPDATA%/Pyomo`` on Windows. It can be overridden by
#: setting the ``PYOMO_CONFIG_DIR`` environment variable before
#: importing Pyomo.
PYOMO_CONFIG_DIR = None

if 'PYOMO_CONFIG_DIR' in os.environ:
PYOMO_CONFIG_DIR = os.path.abspath(os.environ['PYOMO_CONFIG_DIR'])
elif platform.system().lower().startswith(('windows', 'cygwin')):
elif is_windows:
PYOMO_CONFIG_DIR = os.path.abspath(
os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Pyomo')
)
Expand Down
44 changes: 44 additions & 0 deletions pyomo/common/fileutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,50 @@ def import_file(path, clear_cache=False, infer_package=True, module_name=None):
return module


def to_legal_filename(name, universal=False) -> str:
"""Convert a string to a legal filename on the current platform.

This converts a candidate file name (not a path) and converts it to
a legal file name on the current platform. This includes replacing
any unallowable characters (including the path separator) with
underscores (``_``), and on some platforms, enforcing restrictions
on the allowable final character.

Parameters
----------
name : str

The original (desired) file name

universal : bool

If True, this will attempt a form of "universal" standardization
that uses the most restrictive set of character translations and
rules. Currently, ``universal=True`` is equivalent to running
the Windows translations.

"""
if envvar.is_windows or universal:
tr = getattr(to_legal_filename, 'tr', None)
if tr is None:
# Windows illegal characters: 0-31, plus < > : " / \ | ? *
_illegal = r'<>:"/\|?*' + ''.join(map(chr, range(32)))
tr = to_legal_filename.tr = str.maketrans(_illegal, '_' * len(_illegal))
# Remove illegal characters
name = name.translate(tr)
if name:
# Windows allows filenames to end with space or dot, but the
# file explorer can't interact with them
if name[-1] in ' .':
name = name[:-1] + '_'
# Similarly, starting with a space is generally a bad idea
if name[0] == ' ':
name = '_' + name[1:]
else:
name = name.replace('/', '_').replace(chr(0), '_')
return name


class PathData(object):
"""An object for storing and managing a :py:class:`PathManager` path"""

Expand Down
33 changes: 33 additions & 0 deletions pyomo/common/tests/test_fileutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
_libExt,
ExecutableData,
import_file,
to_legal_filename,
)
from pyomo.common.download import FileDownloader

Expand Down Expand Up @@ -497,3 +498,35 @@ def test_PathManager(self):
Executable(f_in_path2).rehash()
self.assertTrue(Executable(f_in_path2).available())
self.assertEqual(Executable(f_in_path2).path(), f_loc)

def test_to_legal_filename(self):
self.assertEqual('abc', to_legal_filename('abc'))
self.assertEqual('', to_legal_filename(''))
if envvar.is_windows:
self.assertEqual('_abc', to_legal_filename(' abc'))
self.assertEqual('abc_', to_legal_filename('abc.'))
self.assertEqual('abc_', to_legal_filename('abc '))
self.assertEqual('abc_def', to_legal_filename('abc/def'))
self.assertEqual('abc_def', to_legal_filename('abc\\def'))
self.assertEqual(
'a_b_c', to_legal_filename(''.join(['a', chr(0), 'b', chr(7), 'c']))
)
else:
self.assertEqual(' abc', to_legal_filename(' abc'))
self.assertEqual('abc.', to_legal_filename('abc.'))
self.assertEqual('abc ', to_legal_filename('abc '))
self.assertEqual('abc_def', to_legal_filename('abc/def'))
self.assertEqual('abc\\def', to_legal_filename('abc\\def'))
self.assertEqual(
'a_b' + chr(7) + 'c',
to_legal_filename(''.join(['a', chr(0), 'b', chr(7), 'c'])),
)

self.assertEqual('_abc', to_legal_filename(' abc', True))
self.assertEqual('abc_', to_legal_filename('abc.', True))
self.assertEqual('abc_', to_legal_filename('abc ', True))
self.assertEqual('abc_def', to_legal_filename('abc/def', True))
self.assertEqual('abc_def', to_legal_filename('abc\\def', True))
self.assertEqual(
'a_b_c', to_legal_filename(''.join(['a', chr(0), 'b', chr(7), 'c']), True)
)
87 changes: 77 additions & 10 deletions pyomo/contrib/solver/common/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,90 @@


class SolverFactoryClass(Factory):
"""
Registers new interfaces in the legacy SolverFactory
"""
"""Factory class for generating instances of solver interfaces (API v2)"""

def register(self, name, legacy_name=None, doc=None):
"""Register a new solver with this solver factory

This will register the solver both with this
:attr:`SolverFactory` and with the original (legacy)
:attr:`~pyomo.opt.base.solvers.LegacySolverFactory`

Examples
--------

.. testcode::
:hide:

SolverFactory = SolverFactoryClass()

This method can either be called as a decorator on a solver
interface class definition, e.g.:

.. testcode::

from pyomo.contrib.solver.common.base import SolverBase

@SolverFactory.register("test_solver_1")
class TestSolver1(SolverBase):
pass

Or explicitly:

.. testcode::

class TestSolver2(SolverBase):
pass

SolverFactory.register("test_solver_2")(TestSolver2)

When called explicitly, you can pass a custom class to register
with the :attr:`LegacySolverFactory`:

.. testcode::

from pyomo.contrib.solver.common.base import LegacySolverWrapper

class LegacyTestSolver2(LegacySolverWrapper, TestSolver2):
pass

SolverFactory.register("test_solver_2a")(TestSolver2, LegacyTestSolver2)


Parameters
----------
name : str

The name used to register this solver interface class

legacy_name : str

The name to use to register the legacy interface wrapper to
this solver interface in the LegacySolverInterface. If
``None``, then ``name`` will be used.

doc : str

Extended description of this solver interface.

"""
if legacy_name is None:
legacy_name = name

def decorator(cls):
def decorator(cls, legacy_cls=None):
self._cls[name] = cls
self._doc[name] = doc

class LegacySolver(LegacySolverWrapper, cls):
pass
if legacy_cls is None:

class LegacySolver(LegacySolverWrapper, cls):
pass

legacy_cls = LegacySolver

LegacySolverFactory.register(legacy_name, doc + " (new interface)")(
LegacySolver
)
LegacySolverFactory.register(
legacy_name, doc + " (new interface)" if doc else doc
)(legacy_cls)

# Preserve the preferred name, as registered in the Factory
cls.name = name
Expand All @@ -42,4 +108,5 @@ class LegacySolver(LegacySolverWrapper, cls):
return decorator


SolverFactory = SolverFactoryClass()
#: Global registry/factory for "v2" solver interfaces.
SolverFactory: SolverFactoryClass = SolverFactoryClass()
2 changes: 1 addition & 1 deletion pyomo/contrib/solver/common/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class NoFeasibleSolutionError(PyomoException):
class NoOptimalSolutionError(PyomoException):
default_message = (
'Solver did not find the optimal solution. Set '
'opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.'
'opt.config.raise_exception_on_nonoptimal_result=False to bypass this error.'
)


Expand Down
4 changes: 2 additions & 2 deletions pyomo/contrib/solver/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@


from .common.factory import SolverFactory
from .solvers.ipopt import Ipopt
from .solvers.ipopt import Ipopt, LegacyIpoptSolver
from .solvers.gurobi_persistent import GurobiPersistent
from .solvers.gurobi_direct import GurobiDirect
from .solvers.highs import Highs
Expand All @@ -20,7 +20,7 @@
def load():
SolverFactory.register(
name='ipopt', legacy_name='ipopt_v2', doc='The IPOPT NLP solver'
)(Ipopt)
)(Ipopt, LegacyIpoptSolver)
SolverFactory.register(
name='gurobi_persistent',
legacy_name='gurobi_persistent_v2',
Expand Down
Loading