From 3d5cb6c8fc7eae46f91d85bc4bf2cc71aaac9dc9 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 27 Feb 2024 20:22:14 -0500 Subject: [PATCH 01/12] Fix PyROS discrete separation iteration log --- .../contrib/pyros/pyros_algorithm_methods.py | 2 +- .../pyros/separation_problem_methods.py | 1 + pyomo/contrib/pyros/solve_data.py | 29 +++++++++++++++++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index 45b652447ff..f0e32a284bb 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -805,7 +805,7 @@ def ROSolver_iterative_solve(model_data, config): len(scaled_violations) == len(separation_model.util.performance_constraints) and not separation_results.subsolver_error and not separation_results.time_out - ) + ) or separation_results.all_discrete_scenarios_exhausted iter_log_record = IterationLogRecord( iteration=k, diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index 084b0442ae6..b5939ff5b19 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -649,6 +649,7 @@ def perform_separation_loop(model_data, config, solve_globally): solver_call_results=ComponentMap(), solved_globally=solve_globally, worst_case_perf_con=None, + all_discrete_scenarios_exhausted=True, ) perf_con_to_maximize = sorted_priority_groups[ diff --git a/pyomo/contrib/pyros/solve_data.py b/pyomo/contrib/pyros/solve_data.py index bc6c071c9a3..c31eb8e5d3f 100644 --- a/pyomo/contrib/pyros/solve_data.py +++ b/pyomo/contrib/pyros/solve_data.py @@ -347,16 +347,23 @@ class SeparationLoopResults: solver_call_results : ComponentMap Mapping from performance constraints to corresponding ``SeparationSolveCallResults`` objects. - worst_case_perf_con : None or int, optional + worst_case_perf_con : None or Constraint Performance constraint mapped to ``SeparationSolveCallResults`` object in `self` corresponding to maximally violating separation problem solution. + all_discrete_scenarios_exhausted : bool, optional + For problems with discrete uncertainty sets, + True if all scenarios were explicitly accounted for in master + (which occurs if there have been + as many PyROS iterations as there are scenarios in the set) + False otherwise. Attributes ---------- solver_call_results solved_globally worst_case_perf_con + all_discrete_scenarios_exhausted found_violation violating_param_realization scaled_violations @@ -365,11 +372,18 @@ class SeparationLoopResults: time_out """ - def __init__(self, solved_globally, solver_call_results, worst_case_perf_con): + def __init__( + self, + solved_globally, + solver_call_results, + worst_case_perf_con, + all_discrete_scenarios_exhausted=False, + ): """Initialize self (see class docstring).""" self.solver_call_results = solver_call_results self.solved_globally = solved_globally self.worst_case_perf_con = worst_case_perf_con + self.all_discrete_scenarios_exhausted = all_discrete_scenarios_exhausted @property def found_violation(self): @@ -599,6 +613,17 @@ def get_violating_attr(self, attr_name): """ return getattr(self.main_loop_results, attr_name, None) + @property + def all_discrete_scenarios_exhausted(self): + """ + bool : For problems where the uncertainty set is of type + DiscreteScenarioSet, + True if last master problem solved explicitly + accounts for all scenarios in the uncertainty set, + False otherwise. + """ + return self.get_violating_attr("all_discrete_scenarios_exhausted") + @property def worst_case_perf_con(self): """ From 5b5f0046ab59accedee601deb51cfe14939298ec Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 28 Feb 2024 15:24:45 -0500 Subject: [PATCH 02/12] Fix indentation typo --- pyomo/contrib/pyros/solve_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/solve_data.py b/pyomo/contrib/pyros/solve_data.py index c31eb8e5d3f..73eee5202aa 100644 --- a/pyomo/contrib/pyros/solve_data.py +++ b/pyomo/contrib/pyros/solve_data.py @@ -347,7 +347,7 @@ class SeparationLoopResults: solver_call_results : ComponentMap Mapping from performance constraints to corresponding ``SeparationSolveCallResults`` objects. - worst_case_perf_con : None or Constraint + worst_case_perf_con : None or Constraint Performance constraint mapped to ``SeparationSolveCallResults`` object in `self` corresponding to maximally violating separation problem solution. From 4e5bbbf2f073911ed89d39002ea96b652e807e16 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 2 Mar 2024 22:58:51 -0700 Subject: [PATCH 03/12] NFC: fix copyright header --- pyomo/contrib/latex_printer/latex_printer.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 110df7cd5ca..a986f5d6b81 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -9,17 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2023 -# 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 From 0e673b663ad339e00ca93a65cf414bd0f79ca012 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 2 Mar 2024 23:01:38 -0700 Subject: [PATCH 04/12] performance: avoid duplication, linear searches --- pyomo/contrib/latex_printer/latex_printer.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index a986f5d6b81..41cff29ad80 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -275,11 +275,11 @@ def handle_functionID_node(visitor, node, *args): def handle_indexTemplate_node(visitor, node, *args): - if node._set in ComponentSet(visitor.setMap.keys()): + if node._set in visitor.setMap: # already detected set, do nothing pass else: - visitor.setMap[node._set] = 'SET%d' % (len(visitor.setMap.keys()) + 1) + visitor.setMap[node._set] = 'SET%d' % (len(visitor.setMap) + 1) return '__I_PLACEHOLDER_8675309_GROUP_%s_%s__' % ( node._group, @@ -616,15 +616,15 @@ def latex_printer( # Cody's backdoor because he got outvoted if latex_component_map is not None: - if 'use_short_descriptors' in list(latex_component_map.keys()): + 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([]) + existing_components = ComponentSet() else: - existing_components = ComponentSet(list(latex_component_map.keys())) + existing_components = ComponentSet(latex_component_map) isSingle = False @@ -1225,14 +1225,14 @@ def latex_printer( ) for ky, vl in new_variableMap.items(): - if ky not in ComponentSet(latex_component_map.keys()): + if ky not in latex_component_map: latex_component_map[ky] = vl for ky, vl in new_parameterMap.items(): - if ky not in ComponentSet(latex_component_map.keys()): + if ky not in latex_component_map: latex_component_map[ky] = vl rep_dict = {} - for ky in ComponentSet(list(reversed(list(latex_component_map.keys())))): + for ky in reversed(list(latex_component_map)): if isinstance(ky, (pyo.Var, _GeneralVarData)): overwrite_value = latex_component_map[ky] if ky not in existing_components: From ff111df42ac8aa4ff87377ab3fd16612a91b0eda Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 2 Mar 2024 23:04:18 -0700 Subject: [PATCH 05/12] resolve indextemplate naming for multidimensional sets --- pyomo/contrib/latex_printer/latex_printer.py | 153 +++++++++++-------- 1 file changed, 88 insertions(+), 65 deletions(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 41cff29ad80..90a5da0d9c1 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -281,8 +281,9 @@ def handle_indexTemplate_node(visitor, node, *args): else: visitor.setMap[node._set] = 'SET%d' % (len(visitor.setMap) + 1) - return '__I_PLACEHOLDER_8675309_GROUP_%s_%s__' % ( + return '__I_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' % ( node._group, + node._id, visitor.setMap[node._set], ) @@ -304,8 +305,9 @@ def handle_numericGetItemExpression_node(visitor, node, *args): def handle_templateSumExpression_node(visitor, node, *args): pstr = '' for i in range(0, len(node._iters)): - pstr += '\\sum_{__S_PLACEHOLDER_8675309_GROUP_%s_%s__} ' % ( + 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], ) @@ -904,24 +906,33 @@ def latex_printer( # setMap = visitor.setMap # Multiple constraints are generated using a set if len(indices) > 0: - if indices[0]._set in ComponentSet(visitor.setMap.keys()): - # already detected set, do nothing - pass - else: - visitor.setMap[indices[0]._set] = 'SET%d' % ( - len(visitor.setMap.keys()) + 1 + 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 ) - idxTag = '__I_PLACEHOLDER_8675309_GROUP_%s_%s__' % ( - indices[0]._group, - visitor.setMap[indices[0]._set], - ) - setTag = '__S_PLACEHOLDER_8675309_GROUP_%s_%s__' % ( - indices[0]._group, - visitor.setMap[indices[0]._set], - ) + 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 += ' \\qquad \\forall %s \\in %s ' % (idxTag, setTag) + conLine += ' %s \\in %s ' % (idxTag, setTag) pstr += conLine # Add labels as needed @@ -1070,8 +1081,8 @@ def latex_printer( for word in splitLatex: if "PLACEHOLDER_8675309_GROUP_" in word: ifo = word.split("PLACEHOLDER_8675309_GROUP_")[1] - gpNum, stName = ifo.split('_') - if gpNum not in groupMap.keys(): + gpNum, idNum, stName = ifo.split('_') + if gpNum not in groupMap: groupMap[gpNum] = [stName] if stName not in ComponentSet(uniqueSets): uniqueSets.append(stName) @@ -1088,10 +1099,7 @@ def latex_printer( ix = int(ky[3:]) - 1 setInfo[ky]['setObject'] = setMap_inverse[ky] # setList[ix] setInfo[ky]['setRegEx'] = ( - r'__S_PLACEHOLDER_8675309_GROUP_([0-9*])_%s__' % (ky) - ) - setInfo[ky]['sumSetRegEx'] = ( - r'sum_{__S_PLACEHOLDER_8675309_GROUP_([0-9*])_%s__}' % (ky) + r'__S_PLACEHOLDER_8675309_GROUP_([0-9]+)_([0-9,]+)_%s__' % (ky,) ) # setInfo[ky]['idxRegEx'] = r'__I_PLACEHOLDER_8675309_GROUP_[0-9*]_%s__'%(ky) @@ -1116,27 +1124,41 @@ def latex_printer( ed = stData[-1] replacement = ( - r'sum_{ __I_PLACEHOLDER_8675309_GROUP_\1_%s__ = %d }^{%d}' + r'sum_{ __I_PLACEHOLDER_8675309_GROUP_\1_\2_%s__ = %d }^{%d}' % (ky, bgn, ed) ) - ln = re.sub(setInfo[ky]['sumSetRegEx'], replacement, ln) + ln = re.sub( + 'sum_{' + setInfo[ky]['setRegEx'] + '}', replacement, ln + ) else: # if the set is not continuous or the flag has not been set - replacement = ( - r'sum_{ __I_PLACEHOLDER_8675309_GROUP_\1_%s__ \\in __S_PLACEHOLDER_8675309_GROUP_\1_%s__ }' - % (ky, ky) - ) - ln = re.sub(setInfo[ky]['sumSetRegEx'], replacement, ln) + 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*]_SET([0-9]*)__', ln + r'__I_PLACEHOLDER_8675309_GROUP_[0-9]+_[0-9]+_SET([0-9]+)__', ln ) - groupSetPairs = re.findall( - r'__I_PLACEHOLDER_8675309_GROUP_([0-9*])_SET([0-9]*)__', ln + groupIdSetTuples = re.findall( + r'__I_PLACEHOLDER_8675309_GROUP_([0-9]+)_([0-9]+)_SET([0-9]+)__', ln ) groupInfo = {} @@ -1146,43 +1168,44 @@ def latex_printer( 'indices': [], } - for gp in groupSetPairs: - if gp[0] not in groupInfo['SET' + gp[1]]['indices']: - groupInfo['SET' + gp[1]]['indices'].append(gp[0]) + 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(): - if vl['setObject'] in ComponentSet(latex_component_map.keys()): - indexNames = latex_component_map[vl['setObject']][1] - if len(indexNames) != 0: - if len(indexNames) < len(vl['indices']): - raise ValueError( - 'Insufficient number of indices provided to the overwrite dictionary for set %s' - % (vl['setObject'].name) - ) - for i in range(0, len(vl['indices'])): - ln = ln.replace( - '__I_PLACEHOLDER_8675309_GROUP_%s_%s__' - % (vl['indices'][i], ky), - indexNames[i], - ) - else: - for i in range(0, len(vl['indices'])): - ln = ln.replace( - '__I_PLACEHOLDER_8675309_GROUP_%s_%s__' - % (vl['indices'][i], ky), - alphabetStringGenerator(indexCounter), - ) - indexCounter += 1 - else: - for i in range(0, len(vl['indices'])): - ln = ln.replace( - '__I_PLACEHOLDER_8675309_GROUP_%s_%s__' - % (vl['indices'][i], ky), - alphabetStringGenerator(indexCounter), + 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) From e2e8165b731f24564a689a0f035797b621e78942 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 2 Mar 2024 23:04:53 -0700 Subject: [PATCH 06/12] make it easier to switch mathds/mathbb --- pyomo/contrib/latex_printer/latex_printer.py | 27 +++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 90a5da0d9c1..f3ffe2e5982 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -406,25 +406,28 @@ def exitNode(self, node, data): ) +mathbb = r'\mathbb' + + def analyze_variable(vr): domainMap = { - 'Reals': '\\mathds{R}', - 'PositiveReals': '\\mathds{R}_{> 0}', - 'NonPositiveReals': '\\mathds{R}_{\\leq 0}', - 'NegativeReals': '\\mathds{R}_{< 0}', - 'NonNegativeReals': '\\mathds{R}_{\\geq 0}', - 'Integers': '\\mathds{Z}', - 'PositiveIntegers': '\\mathds{Z}_{> 0}', - 'NonPositiveIntegers': '\\mathds{Z}_{\\leq 0}', - 'NegativeIntegers': '\\mathds{Z}_{< 0}', - 'NonNegativeIntegers': '\\mathds{Z}_{\\geq 0}', + 'Reals': mathbb + '{R}', + 'PositiveReals': mathbb + '{R}_{> 0}', + 'NonPositiveReals': mathbb + '{R}_{\\leq 0}', + 'NegativeReals': mathbb + '{R}_{< 0}', + 'NonNegativeReals': mathbb + '{R}_{\\geq 0}', + 'Integers': mathbb + '{Z}', + 'PositiveIntegers': mathbb + '{Z}_{> 0}', + 'NonPositiveIntegers': mathbb + '{Z}_{\\leq 0}', + 'NegativeIntegers': mathbb + '{Z}_{< 0}', + 'NonNegativeIntegers': mathbb + '{Z}_{\\geq 0}', 'Boolean': '\\left\\{ \\text{True} , \\text{False} \\right \\}', 'Binary': '\\left\\{ 0 , 1 \\right \\}', # 'Any': None, # 'AnyWithNone': None, 'EmptySet': '\\varnothing', - 'UnitInterval': '\\mathds{R}', - 'PercentFraction': '\\mathds{R}', + 'UnitInterval': mathbb + '{R}', + 'PercentFraction': mathbb + '{R}', # 'RealInterval' : None , # 'IntegerInterval' : None , } From 99c1bc319f281215fe42260af3da0296719dc650 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 2 Mar 2024 23:05:59 -0700 Subject: [PATCH 07/12] Resolve issue with ambiguous field codes (when >10 vars or params) --- pyomo/contrib/latex_printer/latex_printer.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index f3ffe2e5982..77afeb8f849 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -776,12 +776,12 @@ def latex_printer( for vr in variableList: vrIdx += 1 if isinstance(vr, ScalarVar): - variableMap[vr] = 'x_' + str(vrIdx) + variableMap[vr] = 'x_' + str(vrIdx) + '_' elif isinstance(vr, IndexedVar): - variableMap[vr] = 'x_' + str(vrIdx) + variableMap[vr] = 'x_' + str(vrIdx) + '_' for sd in vr.index_set().data(): vrIdx += 1 - variableMap[vr[sd]] = 'x_' + str(vrIdx) + variableMap[vr[sd]] = 'x_' + str(vrIdx) + '_' else: raise DeveloperError( 'Variable is not a variable. Should not happen. Contact developers' @@ -793,12 +793,12 @@ def latex_printer( for vr in parameterList: pmIdx += 1 if isinstance(vr, ScalarParam): - parameterMap[vr] = 'p_' + str(pmIdx) + parameterMap[vr] = 'p_' + str(pmIdx) + '_' elif isinstance(vr, IndexedParam): - parameterMap[vr] = 'p_' + str(pmIdx) + parameterMap[vr] = 'p_' + str(pmIdx) + '_' for sd in vr.index_set().data(): pmIdx += 1 - parameterMap[vr[sd]] = 'p_' + str(pmIdx) + parameterMap[vr[sd]] = 'p_' + str(pmIdx) + '_' else: raise DeveloperError( 'Parameter is not a parameter. Should not happen. Contact developers' From 28db0387d398c96af331741820d1715f210d0cdc Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 2 Mar 2024 23:25:07 -0700 Subject: [PATCH 08/12] Support name generation for set expressions --- pyomo/contrib/latex_printer/latex_printer.py | 81 ++++++++++++-------- 1 file changed, 48 insertions(+), 33 deletions(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 77afeb8f849..e41cbeac51e 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -49,7 +49,7 @@ ) from pyomo.core.base.var import ScalarVar, _GeneralVarData, IndexedVar from pyomo.core.base.param import _ParamData, ScalarParam, IndexedParam -from pyomo.core.base.set import _SetData +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 @@ -79,6 +79,39 @@ 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; @@ -406,32 +439,7 @@ def exitNode(self, node, data): ) -mathbb = r'\mathbb' - - def analyze_variable(vr): - domainMap = { - 'Reals': mathbb + '{R}', - 'PositiveReals': mathbb + '{R}_{> 0}', - 'NonPositiveReals': mathbb + '{R}_{\\leq 0}', - 'NegativeReals': mathbb + '{R}_{< 0}', - 'NonNegativeReals': mathbb + '{R}_{\\geq 0}', - 'Integers': mathbb + '{Z}', - 'PositiveIntegers': mathbb + '{Z}_{> 0}', - 'NonPositiveIntegers': mathbb + '{Z}_{\\leq 0}', - 'NegativeIntegers': mathbb + '{Z}_{< 0}', - 'NonNegativeIntegers': mathbb + '{Z}_{\\geq 0}', - 'Boolean': '\\left\\{ \\text{True} , \\text{False} \\right \\}', - 'Binary': '\\left\\{ 0 , 1 \\right \\}', - # 'Any': None, - # 'AnyWithNone': None, - 'EmptySet': '\\varnothing', - 'UnitInterval': mathbb + '{R}', - 'PercentFraction': mathbb + '{R}', - # 'RealInterval' : None , - # 'IntegerInterval' : None , - } - domainName = vr.domain.name varBounds = vr.bounds lowerBoundValue = varBounds[0] @@ -1062,15 +1070,22 @@ def latex_printer( 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_op_map[st._operator.strip()].join( + generate_set_name(s, lcm) for s in st.subsets(False) + ) + else: + return str(st).replace('_', r'\_').replace('{', '\{').replace('}', '\}') + # Handling the iterator indices defaultSetLatexNames = ComponentMap() - for ky, vl in setMap.items(): - st = ky - defaultSetLatexNames[st] = st.name.replace('_', '\\_') - if st in ComponentSet(latex_component_map.keys()): - defaultSetLatexNames[st] = latex_component_map[st][ - 0 - ] # .replace('_', '\\_') + for ky in setMap: + defaultSetLatexNames[ky] = generate_set_name(ky, latex_component_map) latexLines = pstr.split('\n') for jj in range(0, len(latexLines)): From b1444017c3fd536b0630dda91f9290b6e3043798 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 3 Mar 2024 09:07:15 -0700 Subject: [PATCH 09/12] NFC: apply black --- pyomo/contrib/latex_printer/latex_printer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index e41cbeac51e..c2cbfd6b2e1 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -112,6 +112,7 @@ # 'IntegerInterval' : None , } + def decoder(num, base): if int(num) != abs(num): # Requiring an integer is nice, but not strictly necessary; From 8ee01941a433eab987e6fa11ffa74a6b7ea5f2bb Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 12:38:32 -0700 Subject: [PATCH 10/12] Add tests for set products --- pyomo/contrib/latex_printer/latex_printer.py | 2 +- .../latex_printer/tests/test_latex_printer.py | 63 +++++++++++++---- pyomo/core/tests/examples/pmedian_concrete.py | 70 +++++++++++++++++++ 3 files changed, 120 insertions(+), 15 deletions(-) create mode 100644 pyomo/core/tests/examples/pmedian_concrete.py diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index c2cbfd6b2e1..1d5279e984a 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -1077,7 +1077,7 @@ def generate_set_name(st, lcm): if st.parent_block().component(st.name) is st: return st.name.replace('_', r'\_') if isinstance(st, SetOperator): - return _set_op_map[st._operator.strip()].join( + return set_operator_map[st._operator.strip()].join( generate_set_name(s, lcm) for s in st.subsets(False) ) else: diff --git a/pyomo/contrib/latex_printer/tests/test_latex_printer.py b/pyomo/contrib/latex_printer/tests/test_latex_printer.py index 2d7dd69dba8..f09a14b8b00 100644 --- a/pyomo/contrib/latex_printer/tests/test_latex_printer.py +++ b/pyomo/contrib/latex_printer/tests/test_latex_printer.py @@ -9,25 +9,16 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2023 -# 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 io +from textwrap import dedent + import pyomo.common.unittest as unittest -from pyomo.contrib.latex_printer import latex_printer +import pyomo.core.tests.examples.pmedian_concrete as pmedian_concrete import pyomo.environ as pyo -from textwrap import dedent + +from pyomo.contrib.latex_printer import latex_printer from pyomo.common.tempfiles import TempfileManager from pyomo.common.collections.component_map import ComponentMap - from pyomo.environ import ( Reals, PositiveReals, @@ -797,6 +788,50 @@ def ruleMaker_2(m, i): self.assertEqual('\n' + pstr + '\n', bstr) + def test_latexPrinter_pmedian_verbose(self): + m = pmedian_concrete.create_model() + self.assertEqual( + latex_printer(m).strip(), + r""" +\begin{align} + & \min + & & \sum_{ i \in Locations } \sum_{ j \in Customers } cost_{i,j} serve\_customer\_from\_location_{i,j} & \label{obj:M1_obj} \\ + & \text{s.t.} + & & \sum_{ i \in Locations } serve\_customer\_from\_location_{i,j} = 1 & \qquad \forall j \in Customers \label{con:M1_single_x} \\ + &&& serve\_customer\_from\_location_{i,j} \leq select\_location_{i} & \qquad \forall i,j \in Locations \times Customers \label{con:M1_bound_y} \\ + &&& \sum_{ i \in Locations } select\_location_{i} = P & \label{con:M1_num_facilities} \\ + & \text{w.b.} + & & 0.0 \leq serve\_customer\_from\_location \leq 1.0 & \qquad \in \mathds{R} \label{con:M1_serve_customer_from_location_bound} \\ + &&& select\_location & \qquad \in \left\{ 0 , 1 \right \} \label{con:M1_select_location_bound} +\end{align} + """.strip() + ) + + def test_latexPrinter_pmedian_concise(self): + m = pmedian_concrete.create_model() + lcm = ComponentMap() + lcm[m.Locations] = ['L', ['n']] + lcm[m.Customers] = ['C', ['m']] + lcm[m.cost] = 'd' + lcm[m.serve_customer_from_location] = 'x' + lcm[m.select_location] = 'y' + self.assertEqual( + latex_printer(m, latex_component_map=lcm).strip(), + r""" +\begin{align} + & \min + & & \sum_{ n \in L } \sum_{ m \in C } d_{n,m} x_{n,m} & \label{obj:M1_obj} \\ + & \text{s.t.} + & & \sum_{ n \in L } x_{n,m} = 1 & \qquad \forall m \in C \label{con:M1_single_x} \\ + &&& x_{n,m} \leq y_{n} & \qquad \forall n,m \in L \times C \label{con:M1_bound_y} \\ + &&& \sum_{ n \in L } y_{n} = P & \label{con:M1_num_facilities} \\ + & \text{w.b.} + & & 0.0 \leq x \leq 1.0 & \qquad \in \mathds{R} \label{con:M1_x_bound} \\ + &&& y & \qquad \in \left\{ 0 , 1 \right \} \label{con:M1_y_bound} +\end{align} + """.strip() + ) + if __name__ == '__main__': unittest.main() diff --git a/pyomo/core/tests/examples/pmedian_concrete.py b/pyomo/core/tests/examples/pmedian_concrete.py new file mode 100644 index 00000000000..a6a1859df23 --- /dev/null +++ b/pyomo/core/tests/examples/pmedian_concrete.py @@ -0,0 +1,70 @@ +# ___________________________________________________________________________ +# +# 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 +from pyomo.environ import ( + ConcreteModel, + Param, + RangeSet, + Var, + Reals, + Binary, + PositiveIntegers, +) + + +def _cost_rule(model, n, m): + # We will assume costs are an arbitrary function of the indices + return math.sin(n * 2.33333 + m * 7.99999) + + +def create_model(n=3, m=3, p=2): + model = ConcreteModel(name="M1") + + model.N = Param(initialize=n, within=PositiveIntegers) + model.M = Param(initialize=m, within=PositiveIntegers) + model.P = Param(initialize=p, within=RangeSet(1, model.N), mutable=True) + + model.Locations = RangeSet(1, model.N) + model.Customers = RangeSet(1, model.M) + + model.cost = Param( + model.Locations, model.Customers, initialize=_cost_rule, within=Reals + ) + model.serve_customer_from_location = Var( + model.Locations, model.Customers, bounds=(0.0, 1.0) + ) + model.select_location = Var(model.Locations, within=Binary) + + @model.Objective() + def obj(model): + return sum( + model.cost[n, m] * model.serve_customer_from_location[n, m] + for n in model.Locations + for m in model.Customers + ) + + @model.Constraint(model.Customers) + def single_x(model, m): + return ( + sum(model.serve_customer_from_location[n, m] for n in model.Locations) + == 1.0 + ) + + @model.Constraint(model.Locations, model.Customers) + def bound_y(model, n, m): + return model.serve_customer_from_location[n, m] <= model.select_location[n] + + @model.Constraint() + def num_facilities(model): + return sum(model.select_location[n] for n in model.Locations) == model.P + + return model From 30cb7e4b6daa82430eab415ae5cb603cd849ccf1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 12:49:29 -0700 Subject: [PATCH 11/12] NFC: apply black --- pyomo/contrib/latex_printer/tests/test_latex_printer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/latex_printer/tests/test_latex_printer.py b/pyomo/contrib/latex_printer/tests/test_latex_printer.py index f09a14b8b00..b0ada97a5fe 100644 --- a/pyomo/contrib/latex_printer/tests/test_latex_printer.py +++ b/pyomo/contrib/latex_printer/tests/test_latex_printer.py @@ -13,7 +13,7 @@ from textwrap import dedent import pyomo.common.unittest as unittest -import pyomo.core.tests.examples.pmedian_concrete as pmedian_concrete +import pyomo.core.tests.examples.pmedian_concrete as pmedian_concrete import pyomo.environ as pyo from pyomo.contrib.latex_printer import latex_printer @@ -804,7 +804,7 @@ def test_latexPrinter_pmedian_verbose(self): & & 0.0 \leq serve\_customer\_from\_location \leq 1.0 & \qquad \in \mathds{R} \label{con:M1_serve_customer_from_location_bound} \\ &&& select\_location & \qquad \in \left\{ 0 , 1 \right \} \label{con:M1_select_location_bound} \end{align} - """.strip() + """.strip(), ) def test_latexPrinter_pmedian_concise(self): @@ -829,7 +829,7 @@ def test_latexPrinter_pmedian_concise(self): & & 0.0 \leq x \leq 1.0 & \qquad \in \mathds{R} \label{con:M1_x_bound} \\ &&& y & \qquad \in \left\{ 0 , 1 \right \} \label{con:M1_y_bound} \end{align} - """.strip() + """.strip(), ) From 44b7ef2fca90843d807ff818ce36efea78a09713 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 21:32:05 -0700 Subject: [PATCH 12/12] Fix raw string escaping --- pyomo/contrib/latex_printer/latex_printer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 1d5279e984a..0a595dd8e1b 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -1081,7 +1081,7 @@ def generate_set_name(st, lcm): generate_set_name(s, lcm) for s in st.subsets(False) ) else: - return str(st).replace('_', r'\_').replace('{', '\{').replace('}', '\}') + return str(st).replace('_', r'\_').replace('{', r'\{').replace('}', r'\}') # Handling the iterator indices defaultSetLatexNames = ComponentMap()