# ___________________________________________________________________________
#
# 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 re
from pyomo.common.deprecation import deprecated
from pyomo.core.base.componentuid import ComponentUID
# This module provides some basic functionality for generating labels
# from pyomo names, which often contain characters such as "[" and "]"
# (e.g., in my_var[1]). These characters generally cause issues with
# optimization input file formats, e.g., CPLEX LP files. The purpose of
# this module is to provide a simple remap function, that will satisfy
# broadly problematic symbols. if solver-specific remaps are required,
# they should be handled in the corresponding solver plugin.
class _CharMapper(object):
def __init__(self, preserve, translate, other):
"""
Arguments::
preserve: a string of characters to preserve
translate: a dict or key/value list of characters to translate
other: the character to return for all characters not in
preserve or translate
"""
self.table = {
k if isinstance(k, int) else ord(k): v for k, v in dict(translate).items()
}
for c in preserve:
_c = ord(c)
if _c in self.table and self.table[_c] != c:
raise RuntimeError(
"Duplicate character '%s' appears in both "
"translate table and preserve list" % (c,)
)
self.table[_c] = c
self.other = other
def __getitem__(self, c):
# Most of the time c should be known. For the rare cases we
# encounter a new character, remember it by adding a new entry
# into the translation table and return the default character
try:
return self.table[c]
except:
self.table[c] = self.other
return self.other
def make_table(self):
return ''.join(self[i] for i in range(256))
_alpha = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJLKMNOPQRSTUVWXYZ'
_digit = '1234567890'
_cpxlp_translation_table = _CharMapper(
preserve=_alpha + _digit + '()_', translate=zip('[]{}', '()()'), other='_'
).make_table()
[docs]
def cpxlp_label_from_name(name):
if name is None:
raise RuntimeError(
"Illegal name=None supplied to cpxlp_label_from_name function"
)
return str.translate(name, _cpxlp_translation_table)
_alphanum_translation_table = _CharMapper(
preserve=_alpha + _digit + '_', translate={}, other='_'
).make_table()
[docs]
def alphanum_label_from_name(name):
if name is None:
raise RuntimeError(
"Illegal name=None supplied to alphanum_label_from_name function"
)
return str.translate(name, _alphanum_translation_table)
[docs]
class CuidLabeler(object):
def __call__(self, obj=None):
return ComponentUID(obj)
[docs]
class CounterLabeler(object):
[docs]
def __init__(self, start=0):
self._id = start
def __call__(self, obj=None):
self._id += 1
return self._id
[docs]
class NumericLabeler(object):
[docs]
def __init__(self, prefix, start=0):
self.id = start
self.prefix = prefix
def __call__(self, obj=None):
self.id += 1
return self.prefix + str(self.id)
[docs]
@deprecated(
"The 'remove_obj' method is no longer "
"necessary now that 'getname' does not "
"support the use of a name buffer",
version="6.4.1",
)
def remove_obj(self, obj):
pass
#
# TODO: [JDS] I would like to rename TextLabeler to LPFileLabeler - as it
# generated LP-file-compliant labels - and make the CNameLabeler the
# TextLabeler. This makes sense as the name() is the closest thing we
# have to a human-readable canonical text naming convention (the
# ComponentUID strings are actually unique, but not meant to be human
# readable). Unfortunately, the TextLabeler is used all over the place
# (particularly PySP), and I don't know how much depends on the labels
# actually being LP-compliant.
#
[docs]
class CNameLabeler(object):
def __call__(self, obj):
return obj.getname(True)
[docs]
class LPFileLabeler(object):
def __call__(self, obj):
return cpxlp_label_from_name(obj.getname(True))
[docs]
@deprecated(
"The 'remove_obj' method is no longer "
"necessary now that 'getname' does not "
"support the use of a name buffer",
version="6.4.1",
)
def remove_obj(self, obj):
pass
TextLabeler = LPFileLabeler
[docs]
class AlphaNumericTextLabeler(object):
def __call__(self, obj):
return alphanum_label_from_name(obj.getname(True))
[docs]
class NameLabeler(object):
def __call__(self, obj):
return obj.getname(True)
[docs]
class ShortNameLabeler(object):
[docs]
def __init__(
self,
limit,
suffix,
start=0,
labeler=None,
prefix="",
caseInsensitive=False,
legalRegex=None,
):
self.id = start
self.prefix = prefix
self.suffix = suffix
self.limit = limit
if labeler is not None:
self.labeler = labeler
else:
self.labeler = AlphaNumericTextLabeler()
self.known_labels = set()
self.caseInsensitive = caseInsensitive
if isinstance(legalRegex, str):
self.legalRegex = re.compile(legalRegex)
else:
self.legalRegex = legalRegex
def __call__(self, obj=None):
lbl = self.labeler(obj)
lbl_len = len(lbl)
shorten = False
if lbl_len > self.limit:
shorten = True
elif (
lbl_len == self.limit
and lbl.startswith(self.prefix)
and lbl.endswith(self.suffix)
):
shorten = True
elif (lbl.upper() if self.caseInsensitive else lbl) in self.known_labels:
shorten = True
elif self.legalRegex and not self.legalRegex.match(lbl):
shorten = True
if shorten:
self.id += 1
suffix = "%s%d%s" % (self.suffix, self.id, self.suffix)
tail = -self.limit + len(suffix) + len(self.prefix)
if tail >= 0:
raise RuntimeError(
"Too many identifiers.\n\t"
"The ShortNameLabeler cannot generate a guaranteed unique "
"label limited to %d characters" % (self.limit,)
)
lbl = self.prefix + lbl[tail:] + suffix
if self.known_labels is not None:
self.known_labels.add(lbl.upper() if self.caseInsensitive else lbl)
return lbl