Source code for pyomo.contrib.latex_printer.latex_printer

#  ___________________________________________________________________________
#
#  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 math
import copy
import re
import io
import pyomo.environ as pyo
from pyomo.core.expr.visitor import StreamBasedExpressionVisitor
from pyomo.core.expr import (
    NegationExpression,
    ProductExpression,
    DivisionExpression,
    PowExpression,
    AbsExpression,
    UnaryFunctionExpression,
    MonomialTermExpression,
    LinearExpression,
    SumExpression,
    EqualityExpression,
    InequalityExpression,
    RangedExpression,
    Expr_ifExpression,
    ExternalFunctionExpression,
)

from pyomo.core.expr.visitor import identify_components
from pyomo.core.expr.base import ExpressionBase
from pyomo.core.base.expression import ScalarExpression, ExpressionData
from pyomo.core.base.objective import ScalarObjective, ObjectiveData
import pyomo.core.kernel as kernel
from pyomo.core.expr.template_expr import (
    GetItemExpression,
    GetAttrExpression,
    TemplateSumExpression,
    IndexTemplate,
    Numeric_GetItemExpression,
    templatize_constraint,
    resolve_template,
    templatize_rule,
)
from pyomo.core.base.var import ScalarVar, VarData, IndexedVar
from pyomo.core.base.param import ParamData, ScalarParam, IndexedParam
from pyomo.core.base.set import SetData, SetOperator
from pyomo.core.base.constraint import ScalarConstraint, IndexedConstraint
from pyomo.common.collections.component_map import ComponentMap
from pyomo.common.collections.component_set import ComponentSet
from pyomo.core.expr.template_expr import (
    NPV_Numeric_GetItemExpression,
    NPV_Structural_GetItemExpression,
    Numeric_GetAttrExpression,
)
from pyomo.core.expr.numeric_expr import NPV_SumExpression, NPV_DivisionExpression
from pyomo.core.base.block import IndexedBlock

from pyomo.core.base.external import _PythonCallbackFunctionID
from pyomo.core.base.enums import SortComponents

from pyomo.core.base.block import BlockData

from pyomo.repn.util import ExprType

from pyomo.common import DeveloperError

_CONSTANT = ExprType.CONSTANT
_MONOMIAL = ExprType.MONOMIAL
_GENERAL = ExprType.GENERAL

from pyomo.common.errors import InfeasibleConstraintException

from pyomo.common.dependencies import numpy as np, numpy_available


set_operator_map = {
    '|': r' \cup ',
    '&': r' \cap ',
    '*': r' \times ',
    '-': r' \setminus ',
    '^': r' \triangle ',
}

latex_reals = r'\mathds{R}'
latex_integers = r'\mathds{Z}'

domainMap = {
    'Reals': latex_reals,
    'PositiveReals': latex_reals + '_{> 0}',
    'NonPositiveReals': latex_reals + '_{\\leq 0}',
    'NegativeReals': latex_reals + '_{< 0}',
    'NonNegativeReals': latex_reals + '_{\\geq 0}',
    'Integers': latex_integers,
    'PositiveIntegers': latex_integers + '_{> 0}',
    'NonPositiveIntegers': latex_integers + '_{\\leq 0}',
    'NegativeIntegers': latex_integers + '_{< 0}',
    'NonNegativeIntegers': latex_integers + '_{\\geq 0}',
    'Boolean': '\\left\\{ \\text{True} , \\text{False} \\right \\}',
    'Binary': '\\left\\{ 0 , 1 \\right \\}',
    # 'Any': None,
    # 'AnyWithNone': None,
    'EmptySet': '\\varnothing',
    'UnitInterval': latex_reals,
    'PercentFraction': latex_reals,
    # 'RealInterval' :        None    ,
    # 'IntegerInterval' :     None    ,
}


def decoder(num, base):
    if int(num) != abs(num):
        # Requiring an integer is nice, but not strictly necessary;
        # the algorithm works for floating point
        raise ValueError("num should be a nonnegative integer")
    if int(base) != abs(base) or not base:
        raise ValueError("base should be a positive integer")
    ans = []
    while 1:
        ans.append(num % base)
        num //= base
        if not num:
            return list(reversed(ans))


def indexCorrector(ixs, base):
    for i in range(0, len(ixs)):
        ix = ixs[i]
        if i + 1 < len(ixs):
            if ixs[i + 1] == 0:
                ixs[i] -= 1
                ixs[i + 1] = base
                if ixs[i] == 0:
                    ixs = indexCorrector(ixs, base)
    return ixs


def alphabetStringGenerator(num):
    alphabet = ['.', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r']

    ixs = decoder(num + 1, len(alphabet) - 1)
    pstr = ''
    ixs = indexCorrector(ixs, len(alphabet) - 1)
    for i in range(0, len(ixs)):
        ix = ixs[i]
        pstr += alphabet[ix]
        pstr = pstr.replace('.', '')
    return pstr


def templatize_expression(expr):
    expr, indices = templatize_rule(expr.parent_block(), expr._rule, expr.index_set())
    return (expr, indices)


def templatize_passthrough(con):
    return (con, [])


def precedenceChecker(node, arg1, arg2=None):
    childPrecedence = []
    for a in node.args:
        if hasattr(a, 'PRECEDENCE'):
            if a.PRECEDENCE is None:
                childPrecedence.append(-1)
            else:
                childPrecedence.append(a.PRECEDENCE)
        else:
            childPrecedence.append(-1)

    if hasattr(node, 'PRECEDENCE'):
        precedence = node.PRECEDENCE
    else:
        # Should never hit this
        raise DeveloperError(
            'This error should never be thrown, node does not have a precedence.  Report to developers'
        )

    if childPrecedence[0] > precedence:
        arg1 = ' \\left( ' + arg1 + ' \\right) '

    if arg2 is not None:
        if childPrecedence[1] > precedence:
            arg2 = ' \\left( ' + arg2 + ' \\right) '

    return arg1, arg2


def handle_negation_node(visitor, node, arg1):
    arg1, tsh = precedenceChecker(node, arg1)
    return '-' + arg1


def handle_product_node(visitor, node, arg1, arg2):
    arg1, arg2 = precedenceChecker(node, arg1, arg2)
    return ' '.join([arg1, arg2])


def handle_pow_node(visitor, node, arg1, arg2):
    arg1, arg2 = precedenceChecker(node, arg1, arg2)
    return "%s^{%s}" % (arg1, arg2)


def handle_division_node(visitor, node, arg1, arg2):
    return '\\frac{%s}{%s}' % (arg1, arg2)


def handle_abs_node(visitor, node, arg1):
    return ' \\left| ' + arg1 + ' \\right| '


def handle_unary_node(visitor, node, arg1):
    fcn_handle = node.getname()
    if fcn_handle == 'log10':
        fcn_handle = 'log_{10}'

    if fcn_handle == 'sqrt':
        return '\\sqrt { ' + arg1 + ' }'
    else:
        return '\\' + fcn_handle + ' \\left( ' + arg1 + ' \\right) '


def handle_equality_node(visitor, node, arg1, arg2):
    return arg1 + ' = ' + arg2


def handle_inequality_node(visitor, node, arg1, arg2):
    return arg1 + ' \\leq ' + arg2


def handle_var_node(visitor, node):
    return visitor.variableMap[node]


def handle_num_node(visitor, node):
    if isinstance(node, float):
        if node.is_integer():
            node = int(node)
    return str(node)


def handle_sumExpression_node(visitor, node, *args):
    rstr = args[0]
    for i in range(1, len(args)):
        if args[i][0] == '-':
            rstr += ' - ' + args[i][1:]
        else:
            rstr += ' + ' + args[i]
    return rstr


def handle_monomialTermExpression_node(visitor, node, arg1, arg2):
    if arg1 == '1':
        return arg2
    elif arg1 == '-1':
        return '-' + arg2
    else:
        return arg1 + ' ' + arg2


def handle_named_expression_node(visitor, node, arg1):
    # needed to preserve consistency with the exitNode function call
    # prevents the need to type check in the exitNode function
    return arg1


def handle_ranged_inequality_node(visitor, node, arg1, arg2, arg3):
    return arg1 + ' \\leq ' + arg2 + ' \\leq ' + arg3


def handle_exprif_node(visitor, node, arg1, arg2, arg3):
    return 'f_{\\text{exprIf}}(' + arg1 + ',' + arg2 + ',' + arg3 + ')'

    ## Could be handled in the future using cases or similar

    ## Raises not implemented error
    # raise NotImplementedError('Expr_if objects not supported by the Latex Printer')

    ## Puts cases in a bracketed matrix
    # pstr = ''
    # pstr += '\\begin{Bmatrix} '
    # pstr += arg2 + ' , & ' + arg1 + '\\\\ '
    # pstr += arg3 + ' , & \\text{otherwise}' + '\\\\ '
    # pstr += '\\end{Bmatrix}'
    # return pstr


def handle_external_function_node(visitor, node, *args):
    pstr = ''
    visitor.externalFunctionCounter += 1
    pstr += 'f\\_' + str(visitor.externalFunctionCounter) + '('
    for i in range(0, len(args) - 1):
        pstr += args[i]
        if i <= len(args) - 3:
            pstr += ','
        else:
            pstr += ')'
    return pstr


def handle_functionID_node(visitor, node, *args):
    # seems to just be a placeholder empty wrapper object
    return ''


def handle_indexTemplate_node(visitor, node, *args):
    if node._set in visitor.setMap:
        # already detected set, do nothing
        pass
    else:
        visitor.setMap[node._set] = 'SET%d' % (len(visitor.setMap) + 1)

    return '__I_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' % (
        node._group,
        node._id,
        visitor.setMap[node._set],
    )


def handle_numericGetItemExpression_node(visitor, node, *args):
    joinedName = args[0]

    pstr = ''
    pstr += joinedName + '_{'
    for i in range(1, len(args)):
        pstr += args[i]
        if i <= len(args) - 2:
            pstr += ','
        else:
            pstr += '}'
    return pstr


def handle_templateSumExpression_node(visitor, node, *args):
    pstr = ''
    for i in range(0, len(node._iters)):
        pstr += '\\sum_{__S_PLACEHOLDER_8675309_GROUP_%s_%s_%s__} ' % (
            node._iters[i][0]._group,
            ','.join(str(it._id) for it in node._iters[i]),
            visitor.setMap[node._iters[i][0]._set],
        )

    pstr += args[0]

    return pstr


def handle_param_node(visitor, node):
    return visitor.parameterMap[node]


def handle_str_node(visitor, node):
    return "\\mathtt{'" + node.replace('_', '\\_') + "'}"


def handle_npv_structuralGetItemExpression_node(visitor, node, *args):
    joinedName = args[0]

    pstr = ''
    pstr += joinedName + '['
    for i in range(1, len(args)):
        pstr += args[i]
        if i <= len(args) - 2:
            pstr += ','
        else:
            pstr += ']'
    return pstr


def handle_indexedBlock_node(visitor, node, *args):
    return str(node)


def handle_numericGetAttrExpression_node(visitor, node, *args):
    return args[0] + '.' + args[1]


class _LatexVisitor(StreamBasedExpressionVisitor):
    def __init__(self):
        super().__init__()
        self.externalFunctionCounter = 0

        self._operator_handles = {
            ScalarVar: handle_var_node,
            int: handle_num_node,
            float: handle_num_node,
            NegationExpression: handle_negation_node,
            ProductExpression: handle_product_node,
            DivisionExpression: handle_division_node,
            PowExpression: handle_pow_node,
            AbsExpression: handle_abs_node,
            UnaryFunctionExpression: handle_unary_node,
            Expr_ifExpression: handle_exprif_node,
            EqualityExpression: handle_equality_node,
            InequalityExpression: handle_inequality_node,
            RangedExpression: handle_ranged_inequality_node,
            ExpressionData: handle_named_expression_node,
            ScalarExpression: handle_named_expression_node,
            kernel.expression.expression: handle_named_expression_node,
            kernel.expression.noclone: handle_named_expression_node,
            ObjectiveData: handle_named_expression_node,
            VarData: handle_var_node,
            ScalarObjective: handle_named_expression_node,
            kernel.objective.objective: handle_named_expression_node,
            ExternalFunctionExpression: handle_external_function_node,
            _PythonCallbackFunctionID: handle_functionID_node,
            LinearExpression: handle_sumExpression_node,
            SumExpression: handle_sumExpression_node,
            MonomialTermExpression: handle_monomialTermExpression_node,
            IndexedVar: handle_var_node,
            IndexTemplate: handle_indexTemplate_node,
            Numeric_GetItemExpression: handle_numericGetItemExpression_node,
            TemplateSumExpression: handle_templateSumExpression_node,
            ScalarParam: handle_param_node,
            ParamData: handle_param_node,
            IndexedParam: handle_param_node,
            NPV_Numeric_GetItemExpression: handle_numericGetItemExpression_node,
            IndexedBlock: handle_indexedBlock_node,
            NPV_Structural_GetItemExpression: handle_npv_structuralGetItemExpression_node,
            str: handle_str_node,
            Numeric_GetAttrExpression: handle_numericGetAttrExpression_node,
            NPV_SumExpression: handle_sumExpression_node,
            NPV_DivisionExpression: handle_division_node,
        }
        if numpy_available:
            self._operator_handles[np.float64] = handle_num_node

    def exitNode(self, node, data):
        try:
            return self._operator_handles[node.__class__](self, node, *data)
        except:
            raise DeveloperError(
                'Latex printer encountered an error when processing type %s, contact the developers'
                % (node.__class__)
            )


def analyze_variable(vr):
    domainName = vr.domain.name
    varBounds = vr.bounds
    lowerBoundValue = varBounds[0]
    upperBoundValue = varBounds[1]

    if domainName in ['Reals', 'Integers']:
        if lowerBoundValue is not None:
            lowerBound = str(lowerBoundValue) + ' \\leq '
        else:
            lowerBound = ''

        if upperBoundValue is not None:
            upperBound = ' \\leq ' + str(upperBoundValue)
        else:
            upperBound = ''

    elif domainName in ['PositiveReals', 'PositiveIntegers']:
        if lowerBoundValue > 0:
            lowerBound = str(lowerBoundValue) + ' \\leq '
        else:
            lowerBound = ' 0 < '

        if upperBoundValue is not None:
            if upperBoundValue <= 0:
                raise InfeasibleConstraintException(
                    'Formulation is infeasible due to bounds on variable %s' % (vr.name)
                )
            else:
                upperBound = ' \\leq ' + str(upperBoundValue)
        else:
            upperBound = ''

    elif domainName in ['NonPositiveReals', 'NonPositiveIntegers']:
        if lowerBoundValue is not None:
            if lowerBoundValue > 0:
                raise InfeasibleConstraintException(
                    'Formulation is infeasible due to bounds on variable %s' % (vr.name)
                )
            elif lowerBoundValue == 0:
                lowerBound = ' 0 = '
            else:
                lowerBound = str(lowerBoundValue) + ' \\leq '
        else:
            lowerBound = ''

        if upperBoundValue >= 0:
            upperBound = ' \\leq 0 '
        else:
            upperBound = ' \\leq ' + str(upperBoundValue)

    elif domainName in ['NegativeReals', 'NegativeIntegers']:
        if lowerBoundValue is not None:
            if lowerBoundValue >= 0:
                raise InfeasibleConstraintException(
                    'Formulation is infeasible due to bounds on variable %s' % (vr.name)
                )
            else:
                lowerBound = str(lowerBoundValue) + ' \\leq '
        else:
            lowerBound = ''

        if upperBoundValue >= 0:
            upperBound = ' < 0 '
        else:
            upperBound = ' \\leq ' + str(upperBoundValue)

    elif domainName in ['NonNegativeReals', 'NonNegativeIntegers']:
        if lowerBoundValue > 0:
            lowerBound = str(lowerBoundValue) + ' \\leq '
        else:
            lowerBound = ' 0 \\leq '

        if upperBoundValue is not None:
            if upperBoundValue < 0:
                raise InfeasibleConstraintException(
                    'Formulation is infeasible due to bounds on variable %s' % (vr.name)
                )
            elif upperBoundValue == 0:
                upperBound = ' = 0 '
            else:
                upperBound = ' \\leq ' + str(upperBoundValue)
        else:
            upperBound = ''

    elif domainName in ['Boolean', 'Binary', 'Any', 'AnyWithNone', 'EmptySet']:
        lowerBound = ''
        upperBound = ''

    elif domainName in ['UnitInterval', 'PercentFraction']:
        if lowerBoundValue > 1:
            raise InfeasibleConstraintException(
                'Formulation is infeasible due to bounds on variable %s' % (vr.name)
            )
        elif lowerBoundValue == 1:
            lowerBound = ' = 1 '
        elif lowerBoundValue > 0:
            lowerBound = str(lowerBoundValue) + ' \\leq '
        else:
            lowerBound = ' 0 \\leq '

        if upperBoundValue < 0:
            raise InfeasibleConstraintException(
                'Formulation is infeasible due to bounds on variable %s' % (vr.name)
            )
        elif upperBoundValue == 0:
            upperBound = ' = 0 '
        elif upperBoundValue < 1:
            upperBound = ' \\leq ' + str(upperBoundValue)
        else:
            upperBound = ' \\leq 1 '

    else:
        raise NotImplementedError(
            'Invalid domain encountered, will be supported in a future update'
        )

    varBoundData = {
        'variable': vr,
        'lowerBound': lowerBound,
        'upperBound': upperBound,
        'domainName': domainName,
        'domainLatex': domainMap[domainName],
    }

    return varBoundData


def multiple_replace(pstr, rep_dict):
    pattern = re.compile("|".join(rep_dict.keys()), flags=re.DOTALL)
    return pattern.sub(lambda x: rep_dict[x.group(0)], pstr)


[docs] def latex_printer( pyomo_component, latex_component_map=None, ostream=None, use_equation_environment=False, explicit_set_summation=False, throw_templatization_error=False, ): """This function produces a string that can be rendered as LaTeX Prints a Pyomo component (Block, Model, Objective, Constraint, or Expression) to a LaTeX compatible string Parameters ---------- pyomo_component: BlockData or Model or Objective or Constraint or Expression The Pyomo component to be printed latex_component_map: pyomo.common.collections.component_map.ComponentMap A map keyed by Pyomo component, values become the LaTeX representation in the printer ostream: io.TextIOWrapper or io.StringIO or str The object to print the LaTeX string to. Can be an open file object, string I/O object, or a string for a filename to write to use_equation_environment: bool If False, the equation/aligned construction is used to create a single LaTeX equation. If True, then the align environment is used in LaTeX and each constraint and objective will be given an individual equation number explicit_set_summation: bool If False, all sums will be done over 'index in set' or similar. If True, sums will be done over 'i=1' to 'N' or similar if the set is a continuous set throw_templatization_error: bool Option to throw an error on templatization failure rather than printing each constraint individually, useful for very large models Returns ------- str A LaTeX string of the pyomo_component """ # Various setup things # is Single implies Objective, constraint, or expression # these objects require a slight modification of behavior # isSingle==False means a model or block use_short_descriptors = True # Cody's backdoor because he got outvoted if latex_component_map is not None: if 'use_short_descriptors' in latex_component_map: if latex_component_map['use_short_descriptors'] == False: use_short_descriptors = False if latex_component_map is None: latex_component_map = ComponentMap() existing_components = ComponentSet() else: existing_components = ComponentSet(latex_component_map) isSingle = False if isinstance(pyomo_component, pyo.Objective): objectives = [pyomo_component] constraints = [] expressions = [] templatize_fcn = templatize_constraint use_equation_environment = True isSingle = True elif isinstance(pyomo_component, pyo.Constraint): objectives = [] constraints = [pyomo_component] expressions = [] templatize_fcn = templatize_constraint use_equation_environment = True isSingle = True elif isinstance(pyomo_component, pyo.Expression): objectives = [] constraints = [] expressions = [pyomo_component] templatize_fcn = templatize_expression use_equation_environment = True isSingle = True elif isinstance(pyomo_component, (ExpressionBase, pyo.Var)): objectives = [] constraints = [] expressions = [pyomo_component] templatize_fcn = templatize_passthrough use_equation_environment = True isSingle = True elif isinstance(pyomo_component, BlockData): objectives = [ obj for obj in pyomo_component.component_data_objects( pyo.Objective, descend_into=True, active=True, sort=SortComponents.deterministic, ) ] constraints = [ con for con in pyomo_component.component_objects( pyo.Constraint, descend_into=True, active=True, sort=SortComponents.deterministic, ) ] expressions = [] templatize_fcn = templatize_constraint else: raise ValueError( "Invalid type %s passed into the latex printer" % (str(type(pyomo_component))) ) if isSingle: temp_comp, temp_indexes = templatize_fcn(pyomo_component) variableList = [] for v in identify_components(temp_comp, [ScalarVar, VarData, IndexedVar]): if isinstance(v, VarData): v_write = v.parent_component() if v_write not in ComponentSet(variableList): variableList.append(v_write) else: if v not in ComponentSet(variableList): variableList.append(v) parameterList = [] for p in identify_components(temp_comp, [ScalarParam, ParamData, IndexedParam]): if isinstance(p, ParamData): p_write = p.parent_component() if p_write not in ComponentSet(parameterList): parameterList.append(p_write) else: if p not in ComponentSet(parameterList): parameterList.append(p) # Will grab the sets as the expression is walked setList = [] else: variableList = [ vr for vr in pyomo_component.component_objects( pyo.Var, descend_into=True, active=True, sort=SortComponents.deterministic, ) ] parameterList = [ pm for pm in pyomo_component.component_objects( pyo.Param, descend_into=True, active=True, sort=SortComponents.deterministic, ) ] setList = [ st for st in pyomo_component.component_objects( pyo.Set, descend_into=True, active=True, sort=SortComponents.deterministic, ) ] descriptorDict = {} if use_short_descriptors: descriptorDict['minimize'] = '\\min' descriptorDict['maximize'] = '\\max' descriptorDict['subject to'] = '\\text{s.t.}' descriptorDict['with bounds'] = '\\text{w.b.}' else: descriptorDict['minimize'] = '\\text{minimize}' descriptorDict['maximize'] = '\\text{maximize}' descriptorDict['subject to'] = '\\text{subject to}' descriptorDict['with bounds'] = '\\text{with bounds}' # In the case where just a single expression is passed, add this to the constraint list for printing constraints = constraints + expressions # Declare a visitor/walker visitor = _LatexVisitor() variableMap = ComponentMap() vrIdx = 0 for vr in variableList: vrIdx += 1 if isinstance(vr, ScalarVar): variableMap[vr] = 'x_' + str(vrIdx) + '_' elif isinstance(vr, IndexedVar): variableMap[vr] = 'x_' + str(vrIdx) + '_' for sd in vr.index_set().data(): vrIdx += 1 variableMap[vr[sd]] = 'x_' + str(vrIdx) + '_' else: raise DeveloperError( 'Variable is not a variable. Should not happen. Contact developers' ) visitor.variableMap = variableMap parameterMap = ComponentMap() pmIdx = 0 for vr in parameterList: pmIdx += 1 if isinstance(vr, ScalarParam): parameterMap[vr] = 'p_' + str(pmIdx) + '_' elif isinstance(vr, IndexedParam): parameterMap[vr] = 'p_' + str(pmIdx) + '_' for sd in vr.index_set().data(): pmIdx += 1 parameterMap[vr[sd]] = 'p_' + str(pmIdx) + '_' else: raise DeveloperError( 'Parameter is not a parameter. Should not happen. Contact developers' ) visitor.parameterMap = parameterMap setMap = ComponentMap() for i in range(0, len(setList)): st = setList[i] setMap[st] = 'SET' + str(i + 1) visitor.setMap = setMap # starts building the output string pstr = '' if not use_equation_environment: pstr += '\\begin{align} \n' tbSpc = 4 trailingAligner = '& ' else: pstr += '\\begin{equation} \n' if not isSingle: pstr += ' \\begin{aligned} \n' tbSpc = 8 else: tbSpc = 4 trailingAligner = '' # Iterate over the objectives and print for obj in objectives: try: obj_template, obj_indices = templatize_fcn(obj) except: if throw_templatization_error: raise RuntimeError( "An objective named '%s' has been constructed that cannot be templatized" % (obj.__str__()) ) else: obj_template = obj if obj.sense == pyo.minimize: # or == 1 pstr += ' ' * tbSpc + '& %s \n' % (descriptorDict['minimize']) else: pstr += ' ' * tbSpc + '& %s \n' % (descriptorDict['maximize']) pstr += ' ' * tbSpc + '& & %s %s' % ( visitor.walk_expression(obj_template), trailingAligner, ) if not use_equation_environment: pstr += '\\label{obj:' + pyomo_component.name + '_' + obj.name + '} ' if not isSingle: pstr += '\\\\ \n' else: pstr += '\n' # Iterate over the constraints if len(constraints) > 0: # only print this if printing a full formulation if not isSingle: pstr += ' ' * tbSpc + '& %s \n' % (descriptorDict['subject to']) # first constraint needs different alignment because of the 'subject to': # & minimize & & [Objective] # & subject to & & [Constraint 1] # & & & [Constraint 2] # & & & [Constraint N] # The double '& &' renders better for some reason for i, con in enumerate(constraints): if not isSingle: if i == 0: algn = '& &' else: algn = '&&&' else: algn = '' if not isSingle: tail = '\\\\ \n' else: tail = '\n' # grab the constraint and templatize try: con_template, indices = templatize_fcn(con) con_template_list = [con_template] except: if throw_templatization_error: raise RuntimeError( "A constraint named '%s' has been constructed that cannot be templatized" % (con.__str__()) ) else: con_template_list = [c.expr for c in con.values()] indices = [] for con_template in con_template_list: # Walk the constraint conLine = ( ' ' * tbSpc + algn + ' %s %s' % (visitor.walk_expression(con_template), trailingAligner) ) # setMap = visitor.setMap # Multiple constraints are generated using a set if len(indices) > 0: conLine += ' \\qquad \\forall' _bygroups = {} for idx in indices: _bygroups.setdefault(idx._group, []).append(idx) for _group, idxs in _bygroups.items(): if idxs[0]._set in visitor.setMap: # already detected set, do nothing pass else: visitor.setMap[idxs[0]._set] = 'SET%d' % ( len(visitor.setMap) + 1 ) idxTag = ','.join( '__I_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' % (idx._group, idx._id, visitor.setMap[idx._set]) for idx in idxs ) setTag = '__S_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' % ( indices[0]._group, ','.join(str(it._id) for it in idxs), visitor.setMap[indices[0]._set], ) conLine += ' %s \\in %s ' % (idxTag, setTag) pstr += conLine # Add labels as needed if not use_equation_environment: pstr += ( '\\label{con:' + pyomo_component.name + '_' + con.name + '} ' ) pstr += tail # Print bounds and sets if not isSingle: varBoundData = [] for i in range(0, len(variableList)): vr = variableList[i] if isinstance(vr, ScalarVar): varBoundDataEntry = analyze_variable(vr) varBoundData.append(varBoundDataEntry) elif isinstance(vr, IndexedVar): varBoundData_indexedVar = [] setData = vr.index_set().data() for sd in setData: varBoundDataEntry = analyze_variable(vr[sd]) varBoundData_indexedVar.append(varBoundDataEntry) globIndexedVariables = True for j in range(0, len(varBoundData_indexedVar) - 1): chks = [] chks.append( varBoundData_indexedVar[j]['lowerBound'] == varBoundData_indexedVar[j + 1]['lowerBound'] ) chks.append( varBoundData_indexedVar[j]['upperBound'] == varBoundData_indexedVar[j + 1]['upperBound'] ) chks.append( varBoundData_indexedVar[j]['domainName'] == varBoundData_indexedVar[j + 1]['domainName'] ) if not all(chks): globIndexedVariables = False break if globIndexedVariables: varBoundData.append( { 'variable': vr, 'lowerBound': varBoundData_indexedVar[0]['lowerBound'], 'upperBound': varBoundData_indexedVar[0]['upperBound'], 'domainName': varBoundData_indexedVar[0]['domainName'], 'domainLatex': varBoundData_indexedVar[0]['domainLatex'], } ) else: varBoundData += varBoundData_indexedVar else: raise DeveloperError( 'Variable is not a variable. Should not happen. Contact developers' ) # print the accumulated data to the string bstr = '' appendBoundString = False useThreeAlgn = False for i, vbd in enumerate(varBoundData): if ( vbd['lowerBound'] == '' and vbd['upperBound'] == '' and vbd['domainName'] == 'Reals' ): # unbounded all real, do not print if i == len(varBoundData) - 1: bstr = bstr[0:-4] else: if not useThreeAlgn: algn = '& &' useThreeAlgn = True else: algn = '&&&' if use_equation_environment: conLabel = '' else: conLabel = ( ' \\label{con:' + pyomo_component.name + '_' + variableMap[vbd['variable']] + '_bound' + '} ' ) appendBoundString = True coreString = ( vbd['lowerBound'] + variableMap[vbd['variable']] + vbd['upperBound'] + ' ' + trailingAligner + '\\qquad \\in ' + vbd['domainLatex'] + conLabel ) bstr += ' ' * tbSpc + algn + ' %s' % (coreString) if i <= len(varBoundData) - 2: bstr += '\\\\ \n' else: bstr += '\n' if appendBoundString: pstr += ' ' * tbSpc + '& %s \n' % (descriptorDict['with bounds']) pstr += bstr + '\n' else: pstr = pstr[0:-4] + '\n' # close off the print string if not use_equation_environment: pstr += '\\end{align} \n' else: if not isSingle: pstr += ' \\end{aligned} \n' pstr += ' \\label{%s} \n' % (pyomo_component.name) pstr += '\\end{equation} \n' setMap = visitor.setMap setMap_inverse = {vl: ky for ky, vl in setMap.items()} def generate_set_name(st, lcm): if st in lcm: return lcm[st][0] if st.parent_block().component(st.name) is st: return st.name.replace('_', r'\_') if isinstance(st, SetOperator): return set_operator_map[st._operator.strip()].join( generate_set_name(s, lcm) for s in st.subsets(False) ) else: return str(st).replace('_', r'\_').replace('{', r'\{').replace('}', r'\}') # Handling the iterator indices defaultSetLatexNames = ComponentMap() for ky in setMap: defaultSetLatexNames[ky] = generate_set_name(ky, latex_component_map) latexLines = pstr.split('\n') for jj in range(0, len(latexLines)): groupMap = {} uniqueSets = [] ln = latexLines[jj] # only modify if there is a placeholder in the line if "PLACEHOLDER_8675309_GROUP_" in ln: splitLatex = ln.split('__') # Find the unique combinations of group numbers and set names for word in splitLatex: if "PLACEHOLDER_8675309_GROUP_" in word: ifo = word.split("PLACEHOLDER_8675309_GROUP_")[1] gpNum, idNum, stName = ifo.split('_') if gpNum not in groupMap: groupMap[gpNum] = [stName] if stName not in ComponentSet(uniqueSets): uniqueSets.append(stName) # Determine if the set is continuous setInfo = dict( zip( uniqueSets, [{'continuous': False} for i in range(0, len(uniqueSets))], ) ) for ky, vl in setInfo.items(): ix = int(ky[3:]) - 1 setInfo[ky]['setObject'] = setMap_inverse[ky] # setList[ix] setInfo[ky]['setRegEx'] = ( r'__S_PLACEHOLDER_8675309_GROUP_([0-9]+)_([0-9,]+)_%s__' % (ky,) ) # setInfo[ky]['idxRegEx'] = r'__I_PLACEHOLDER_8675309_GROUP_[0-9*]_%s__'%(ky) if explicit_set_summation: for ky, vl in setInfo.items(): st = vl['setObject'] stData = st.data() stCont = True for ii in range(0, len(stData)): if ii + stData[0] != stData[ii]: stCont = False break setInfo[ky]['continuous'] = stCont # replace the sets for ky, vl in setInfo.items(): # if the set is continuous and the flag has been set if explicit_set_summation and setInfo[ky]['continuous']: st = setInfo[ky]['setObject'] stData = st.data() bgn = stData[0] ed = stData[-1] replacement = ( r'sum_{ __I_PLACEHOLDER_8675309_GROUP_\1_\2_%s__ = %d }^{%d}' % (ky, bgn, ed) ) ln = re.sub( 'sum_{' + setInfo[ky]['setRegEx'] + '}', replacement, ln ) else: # if the set is not continuous or the flag has not been set for _grp, _id in re.findall( 'sum_{' + setInfo[ky]['setRegEx'] + '}', ln ): set_placeholder = '__S_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' % ( _grp, _id, ky, ) i_placeholder = ','.join( '__I_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' % (_grp, _, ky) for _ in _id.split(',') ) replacement = r'sum_{ %s \in %s }' % ( i_placeholder, set_placeholder, ) ln = ln.replace('sum_{' + set_placeholder + '}', replacement) replacement = repr(defaultSetLatexNames[setInfo[ky]['setObject']])[1:-1] ln = re.sub(setInfo[ky]['setRegEx'], replacement, ln) # groupNumbers = re.findall(r'__I_PLACEHOLDER_8675309_GROUP_([0-9*])_SET[0-9]*__',ln) setNumbers = re.findall( r'__I_PLACEHOLDER_8675309_GROUP_[0-9]+_[0-9]+_SET([0-9]+)__', ln ) groupIdSetTuples = re.findall( r'__I_PLACEHOLDER_8675309_GROUP_([0-9]+)_([0-9]+)_SET([0-9]+)__', ln ) groupInfo = {} for vl in setNumbers: groupInfo['SET' + vl] = { 'setObject': setInfo['SET' + vl]['setObject'], 'indices': [], } for _gp, _id, _set in groupIdSetTuples: if (_gp, _id) not in groupInfo['SET' + _set]['indices']: groupInfo['SET' + _set]['indices'].append((_gp, _id)) def get_index_names(st, lcm): if st in lcm: return lcm[st][1] elif isinstance(st, SetOperator): return sum( (get_index_names(s, lcm) for s in st.subsets(False)), start=[] ) elif st.dimen is not None: return [None] * st.dimen else: return [Ellipsis] indexCounter = 0 for ky, vl in groupInfo.items(): indexNames = get_index_names(vl['setObject'], latex_component_map) nonNone = list(filter(None, indexNames)) if nonNone: if len(nonNone) < len(vl['indices']): raise ValueError( 'Insufficient number of indices provided to the ' 'overwrite dictionary for set %s (expected %s, but got %s)' % (vl['setObject'].name, len(vl['indices']), indexNames) ) else: indexNames = [] for i in vl['indices']: indexNames.append(alphabetStringGenerator(indexCounter)) indexCounter += 1 for i in range(0, len(vl['indices'])): ln = ln.replace( '__I_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' % (*vl['indices'][i], ky), indexNames[i], ) latexLines[jj] = ln pstr = '\n'.join(latexLines) new_variableMap = ComponentMap() for i, vr in enumerate(variableList): if isinstance(vr, ScalarVar): new_variableMap[vr] = vr.name elif isinstance(vr, IndexedVar): new_variableMap[vr] = vr.name for sd in vr.index_set().data(): sdString = str(sd) if sdString[0] == '(': sdString = sdString[1:] if sdString[-1] == ')': sdString = sdString[0:-1] new_variableMap[vr[sd]] = vr[sd].name else: raise DeveloperError( 'Variable is not a variable. Should not happen. Contact developers' ) new_parameterMap = ComponentMap() for i, pm in enumerate(parameterList): pm = parameterList[i] if isinstance(pm, ScalarParam): new_parameterMap[pm] = pm.name elif isinstance(pm, IndexedParam): new_parameterMap[pm] = pm.name for sd in pm.index_set().data(): sdString = str(sd) if sdString[0] == '(': sdString = sdString[1:] if sdString[-1] == ')': sdString = sdString[0:-1] new_parameterMap[pm[sd]] = str(pm[sd]) # .name else: raise DeveloperError( 'Parameter is not a parameter. Should not happen. Contact developers' ) for ky, vl in new_variableMap.items(): if ky not in latex_component_map: latex_component_map[ky] = vl for ky, vl in new_parameterMap.items(): if ky not in latex_component_map: latex_component_map[ky] = vl rep_dict = {} for ky in reversed(list(latex_component_map)): if isinstance(ky, (pyo.Var, VarData)): overwrite_value = latex_component_map[ky] if ky not in existing_components: overwrite_value = overwrite_value.replace('_', '\\_') rep_dict[variableMap[ky]] = overwrite_value elif isinstance(ky, (pyo.Param, ParamData)): overwrite_value = latex_component_map[ky] if ky not in existing_components: overwrite_value = overwrite_value.replace('_', '\\_') rep_dict[parameterMap[ky]] = overwrite_value elif isinstance(ky, SetData): # already handled pass elif isinstance(ky, (float, int)): # happens when immutable parameters are used, do nothing pass else: raise ValueError( 'The latex_component_map object has a key of invalid type: %s' % (str(ky)) ) label_rep_dict = copy.deepcopy(rep_dict) for ky, vl in label_rep_dict.items(): label_rep_dict[ky] = vl.replace('{', '').replace('}', '').replace('\\', '') splitLines = pstr.split('\n') for i in range(0, len(splitLines)): if use_equation_environment: splitLines[i] = multiple_replace(splitLines[i], rep_dict) else: if '\\label{' in splitLines[i]: epr, lbl = splitLines[i].split('\\label{') epr = multiple_replace(epr, rep_dict) # rep_dict[ky] = vl.replace('_', '\\_') lbl = multiple_replace(lbl, label_rep_dict) splitLines[i] = epr + '\\label{' + lbl pstr = '\n'.join(splitLines) pattern = r'_{([^{]*)}_{([^{]*)}' replacement = r'_{\1_{\2}}' pstr = re.sub(pattern, replacement, pstr) pattern = r'_(.)_{([^}]*)}' replacement = r'_{\1_{\2}}' pstr = re.sub(pattern, replacement, pstr) splitLines = pstr.split('\n') finalLines = [] for sl in splitLines: if sl != '': finalLines.append(sl) pstr = '\n'.join(finalLines) if ostream is not None: fstr = '' fstr += '\\documentclass{article} \n' fstr += '\\usepackage{amsmath} \n' fstr += '\\usepackage{amssymb} \n' fstr += '\\usepackage{dsfont} \n' fstr += '\\usepackage[paperheight=11in, paperwidth=8.5in, left=1in, right=1in, top=1in, bottom=1in]{geometry} \n' fstr += '\\allowdisplaybreaks \n' fstr += '\\begin{document} \n' fstr += '\\normalsize \n' fstr += pstr + '\n' fstr += '\\end{document} \n' # optional write to output file if isinstance(ostream, (io.TextIOWrapper, io.StringIO)): ostream.write(fstr) elif isinstance(ostream, str): f = open(ostream, 'w') f.write(fstr) f.close() else: raise ValueError( 'Invalid type %s encountered when parsing the ostream. Must be a StringIO, FileIO, or valid filename string' ) # return the latex string return pstr