2424 NonPositiveReals ,
2525 maximize ,
2626 minimize ,
27+ RangeSet ,
2728 Reals ,
2829)
30+ from pyomo .core .expr .numvalue import native_numeric_types
2931from pyomo .opt import WriterFactory
3032from pyomo .repn .standard_repn import isclose_const
3133from pyomo .util .config_domains import ComponentDataSet
@@ -110,6 +112,11 @@ def _take_dual(self, model, std_form):
110112 "Model '%s' has no objective or multiple active objectives. Can "
111113 "only take dual with exactly one active objective!" % model .name
112114 )
115+ if len (std_form .columns ) == 0 and std_form .c .shape [1 ] == 0 :
116+ raise ValueError (
117+ f"Model '{ model .name } ' has no variables in the active Constraints "
118+ f"or Objective."
119+ )
113120 primal_sense = std_form .objectives [0 ].sense
114121
115122 dual = ConcreteModel (name = "%s dual" % model .name )
@@ -121,7 +128,29 @@ def _take_dual(self, model, std_form):
121128 dual_cols = range (A .shape [0 ])
122129 dual .x = Var (dual_cols , domain = NonNegativeReals )
123130 trans_info = dual .private_data ()
131+ A_csr = A .tocsr ()
124132 for j , (primal_cons , ineq ) in enumerate (std_form .rows ):
133+ # We need to check this constraint isn't trivial due to the
134+ # parameterization, which we can detect if the row is all 0's.
135+ if A_csr .indptr [j ] == A_csr .indptr [j + 1 ]:
136+ # All 0's in the coefficient matrix: check what's on the RHS
137+ b = std_form .rhs [j ]
138+ if type (b ) not in native_numeric_types :
139+ # The parameterization made this trivial. I'm not sure what's
140+ # really expected here, so maybe we just scream? Or we leave
141+ # the constraint in the model as it is written...
142+ raise ValueError (
143+ f"The primal model contains a constraint that the "
144+ f"parameterization makes trivial: '{ primal_cons .name } '"
145+ f"\n Please deactivate it or declare it on another Block "
146+ f"before taking the dual."
147+ )
148+ else :
149+ # The whole constraint is trivial--it will already have been
150+ # checked compiling the standard form, so we can safely ignore
151+ # it.
152+ pass
153+
125154 # maximize is -1 and minimize is +1 and ineq is +1 for <= and -1 for
126155 # >=, so we need to change domain to NonPositiveReals if the product
127156 # of these is +1.
@@ -141,17 +170,28 @@ def _take_dual(self, model, std_form):
141170 primal_row = A .indices [j ]
142171 lhs += coef * dual .x [primal_row ]
143172
144- if primal .domain is Reals :
173+ domain = primal .domain
174+ lb , ub = domain .bounds ()
175+ # Note: the following checks the domain for continuity and compactness:
176+ if not domain == RangeSet (* domain .bounds (), 0 ):
177+ raise ValueError (
178+ f"The domain of the primal variable '{ primal .name } ' "
179+ f"is not continuous."
180+ )
181+ unrestricted = (lb is None or lb < 0 ) and (ub is None or ub > 0 )
182+ nonneg = (lb is not None ) and lb >= 0
183+
184+ if unrestricted :
145185 dual .constraints [i ] = lhs == c [i ]
146186 elif primal_sense is minimize :
147- if primal . domain is NonNegativeReals :
187+ if nonneg :
148188 dual .constraints [i ] = lhs <= c [i ]
149- else : # primal. domain is NonPositiveReals
189+ else : # primal domain is nonpositive
150190 dual .constraints [i ] = lhs >= c [i ]
151191 else :
152- if primal . domain is NonNegativeReals :
192+ if nonneg :
153193 dual .constraints [i ] = lhs >= c [i ]
154- else : # primal. domain is NonPositiveReals
194+ else : # primal domain is nonpositive
155195 dual .constraints [i ] = lhs <= c [i ]
156196 trans_info .dual_constraint [primal ] = dual .constraints [i ]
157197 trans_info .primal_var [dual .constraints [i ]] = primal
0 commit comments