6363TEMPLATIZE_CONSTRAINTS = False
6464
6565_inf = float ('inf' )
66- _nonfinite_values = {_inf , - _inf }
66+ _ninf = - _inf
67+ _nonfinite_values = {_inf , _ninf }
6768_known_relational_expression_types = {
6869 EqualityExpression ,
6970 InequalityExpression ,
@@ -246,21 +247,31 @@ def to_bounded_expression(self, evaluate_bounds=False):
246247
247248 if evaluate_bounds :
248249 lb , body , ub = ans
249- return self ._evaluate_bound (lb , True ), body , self ._evaluate_bound (ub , False )
250+ return self ._evaluate_bound (lb , _ninf ), body , self ._evaluate_bound (ub , _inf )
250251 return ans
251252
252- def _evaluate_bound (self , bound , is_lb ):
253+ def _evaluate_bound (self , bound , unbounded ):
253254 if bound is None :
254255 return None
255256 if bound .__class__ not in native_numeric_types :
256- bound = float (value (bound ))
257+ bound = value (bound )
258+ if bound .__class__ not in native_numeric_types :
259+ # Starting in numpy 1.25, casting 1-element ndarray to
260+ # float is deprecated. We still want to support
261+ # that... but without enforcing a hard numpy dependence
262+ for cls in bound .__class__ .__mro__ :
263+ if cls .__name__ == 'ndarray' and cls .__module__ == 'numpy' :
264+ if len (bound ) == 1 :
265+ bound = bound [0 ]
266+ break
267+ bound = float (bound )
257268 # Note that "bound != bound" catches float('nan')
258269 if bound in _nonfinite_values or bound != bound :
259- if bound == ( - _inf if is_lb else _inf ) :
270+ if bound == unbounded :
260271 return None
261272 raise ValueError (
262273 f"Constraint '{ self .name } ' created with an invalid non-finite "
263- f"{ 'lower ' if is_lb else 'upper ' } bound ({ bound } )."
274+ f"{ 'upper ' if unbounded == _inf else 'lower ' } bound ({ bound } )."
264275 )
265276 return bound
266277
@@ -333,12 +344,12 @@ def upper(self):
333344 @property
334345 def lb (self ):
335346 """float : the value of the lower bound of a constraint expression."""
336- return self ._evaluate_bound (self .to_bounded_expression ()[0 ], True )
347+ return self ._evaluate_bound (self .to_bounded_expression ()[0 ], _ninf )
337348
338349 @property
339350 def ub (self ):
340351 """float : the value of the upper bound of a constraint expression."""
341- return self ._evaluate_bound (self .to_bounded_expression ()[2 ], False )
352+ return self ._evaluate_bound (self .to_bounded_expression ()[2 ], _inf )
342353
343354 @property
344355 def equality (self ):
@@ -527,20 +538,9 @@ class _GeneralConstraintData(metaclass=RenamedClass):
527538 __renamed__version__ = '6.7.2'
528539
529540
530- class TemplateConstraintData ( ConstraintData ):
541+ class TemplateDataMixin ( object ):
531542 __slots__ = ()
532543
533- def __init__ (self , template_info , component , index ):
534- # These lines represent in-lining of the
535- # following constructors:
536- # - ConstraintData,
537- # - ActiveComponentData
538- # - ComponentData
539- self ._component = component
540- self ._active = True
541- self ._index = index
542- self ._expr = template_info
543-
544544 @property
545545 def expr (self ):
546546 # Note that it is faster to just generate the expression from
@@ -552,17 +552,41 @@ def template_expr(self):
552552 return self ._expr
553553
554554 def set_value (self , expr ):
555- self .__class__ = ConstraintData
555+ # Setting a value will convert this instance from a templatized
556+ # type to the original Data type (and call the original set_value()).
557+ #
558+ # Note: We assume that the templatized type is created by
559+ # inheriting (TemplateDataMixin, <original data class>), and
560+ # that this instance doesn't have additional multiple
561+ # inheritance that could re-order the MRO.
562+ self .__class__ = self .__class__ .__mro__ [
563+ self .__class__ .__mro__ .index (TemplateDataMixin ) + 1
564+ ]
556565 return self .set_value (expr )
557566
558- def to_bounded_expression (self ):
567+ def to_bounded_expression (self , evaluate_bounds = False ):
559568 tmp , self ._expr = self ._expr , self ._expr [0 ]
560569 try :
561- return super ().to_bounded_expression ()
570+ return super ().to_bounded_expression (evaluate_bounds )
562571 finally :
563572 self ._expr = tmp
564573
565574
575+ class TemplateConstraintData (TemplateDataMixin , ConstraintData ):
576+ __slots__ = ()
577+
578+ def __init__ (self , template_info , component , index ):
579+ # These lines represent in-lining of the
580+ # following constructors:
581+ # - ConstraintData,
582+ # - ActiveComponentData
583+ # - ComponentData
584+ self ._component = component
585+ self ._active = True
586+ self ._index = index
587+ self ._expr = template_info
588+
589+
566590@ModelComponentFactory .register ("General constraint expressions." )
567591class Constraint (ActiveIndexedComponent ):
568592 """
@@ -695,11 +719,17 @@ def construct(self, data=None):
695719 if TEMPLATIZE_CONSTRAINTS :
696720 try :
697721 template_info = templatize_constraint (self )
698- comp = weakref_ref (self )
699- self ._data = {
700- idx : TemplateConstraintData (template_info , comp , idx )
701- for idx in self .index_set ()
702- }
722+ if self .is_indexed ():
723+ comp = weakref_ref (self )
724+ self ._data = {
725+ idx : TemplateConstraintData (template_info , comp , idx )
726+ for idx in self .index_set ()
727+ }
728+ else :
729+ assert self .__class__ is ScalarConstraint
730+ self .__class__ = TemplateScalarConstraint
731+ self ._expr = template_info
732+ self ._data = {None : self }
703733 return
704734 except TemplateExpressionError :
705735 pass
@@ -926,6 +956,7 @@ class SimpleConstraint(metaclass=RenamedClass):
926956
927957@disable_methods (
928958 {
959+ '__call__' ,
929960 'add' ,
930961 'set_value' ,
931962 'to_bounded_expression' ,
@@ -947,6 +978,10 @@ class AbstractSimpleConstraint(metaclass=RenamedClass):
947978 __renamed__version__ = '6.0'
948979
949980
981+ class TemplateScalarConstraint (TemplateDataMixin , ScalarConstraint ):
982+ pass
983+
984+
950985class IndexedConstraint (Constraint ):
951986 #
952987 # Leaving this method for backward compatibility reasons
0 commit comments