-
Notifications
You must be signed in to change notification settings - Fork 314
Expand file tree
/
Copy pathscaling.py
More file actions
1732 lines (1494 loc) · 64.9 KB
/
Copy pathscaling.py
File metadata and controls
1732 lines (1494 loc) · 64.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#################################################################################
# The Institute for the Design of Advanced Energy Systems Integrated Platform
# Framework (IDAES IP) was produced under the DOE Institute for the
# Design of Advanced Energy Systems (IDAES).
#
# Copyright (c) 2018-2026 by the software owners: The Regents of the
# University of California, through Lawrence Berkeley National Laboratory,
# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon
# University, West Virginia University Research Corporation, et al.
# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md
# for full copyright and license information.
#################################################################################
"""
This module contains utilities to provide variable and expression scaling
factors by providing an expression to calculate them via a suffix.
The main purpose of this code is to use the calculate_scaling_factors function
to calculate scaling factors to be used with the Pyomo scaling transformation or
with solvers. A user can provide a scaling_expression suffix to calculate scale
factors from existing variable scaling factors. This allows scaling factors from
a small set of fundamental variables to be propagated to the rest of the model.
The scaling_expression suffix contains Pyomo expressions with model variables.
The expressions can be evaluated with variable scaling factors in place of
variables to calculate additional scaling factors.
"""
__author__ = "John Eslick, Tim Bartholomew, Robert Parker, Andrew Lee"
import math
import sys
import scipy.sparse.linalg as spla
import scipy.linalg as la
import pyomo.environ as pyo
from pyomo.core.base.var import VarData
from pyomo.core.base.param import ParamData
from pyomo.core.expr.visitor import identify_variables
from pyomo.network import Arc
from pyomo.contrib.pynumero.interfaces.pyomo_nlp import PyomoNLP
from pyomo.contrib.pynumero.asl import AmplInterface
from pyomo.common.modeling import unique_component_name
from pyomo.core.base.constraint import ConstraintData
from pyomo.common.collections import ComponentMap, ComponentSet
from pyomo.dae import DerivativeVar
from pyomo.dae.flatten import slice_component_along_sets
from pyomo.util.calc_var_value import calculate_variable_from_constraint
from pyomo.core import expr as EXPR
from pyomo.common.numeric_types import native_types
from pyomo.core.base.units_container import _PyomoUnit
import idaes.logger as idaeslog
_log = idaeslog.getLogger(__name__)
def __none_left_mult(x, y):
"""PRIVATE FUNCTION, If x is None return None, else return x * y"""
if x is not None:
return x * y
return None
def __scale_constraint(c, v):
"""PRIVATE FUNCTION, scale Constraint c to value v"""
if c.equality:
c.set_value((c.lower * v, c.body * v))
else:
c.set_value(
(__none_left_mult(c.lower, v), c.body * v, __none_left_mult(c.upper, v))
)
def scale_arc_constraints(blk):
"""Find Arc constraints in a block and its subblocks. Then scale them based
on the minimum scaling factor of the variables in the constraint.
Args:
blk: Block in which to look for Arc constraints to scale.
Returns:
None
"""
for arc in blk.component_data_objects(Arc, descend_into=True):
arc_block = arc.expanded_block
if arc_block is None: # arc not expanded or port empty?
_log.warning(
f"{arc} has no constraints. Has the Arc expansion transform "
"been applied?"
)
continue
warning = (
"Automatic scaling for arc constraints is supported for "
"only the Equality rule. Variable {name} on Port {port} was "
"created with a different rule, so the corresponding constraint "
"on {arc_name} will not be scaled."
)
port1 = arc.ports[0]
port2 = arc.ports[1]
for name in port1.vars.keys():
if not port1.is_equality(name):
_log.warning(
warning.format(name=name, port=port1.name, arc_name=arc.name)
)
continue
if not port2.is_equality(name):
_log.warning(
warning.format(name=name, port=port2.name, arc_name=arc.name)
)
continue
con = getattr(arc_block, name + "_equality")
for i, c in con.items():
if i is None:
sf = min_scaling_factor([port1.vars[name], port2.vars[name]])
else:
sf = min_scaling_factor([port1.vars[name][i], port2.vars[name][i]])
constraint_scaling_transform(c, sf)
def map_scaling_factor(components, default=1, warning=False, func=min, hint=None):
"""Map get_scaling_factor to an iterable of Pyomo components, and call func
on the result. This could be use, for example, to get the minimum or
maximum scaling factor of a set of components.
Args:
components: Iterable yielding Pyomo components
default: The default value used when a scaling factor is missing. The
default is default=1.
warning: Log a warning for missing scaling factors
func: The function to call on the resulting iterable of scaling factors.
The default is min().
hint: Paired with warning=True, this is a string to indicate where the
missing scaling factor was being accessed, to easier diagnose issues.
Returns:
The result of func on the set of scaling factors
"""
return func(
map(
lambda x: get_scaling_factor(
x, default=default, warning=warning, hint=hint
),
components,
)
)
def min_scaling_factor(components, default=1, warning=True, hint=None):
"""Map get_scaling_factor to an iterable of Pyomo components, and get the
minimum scaling factor.
Args:
iter: Iterable yielding Pyomo components
default: The default value used when a scaling factor is missing. If
None, this will raise an exception when scaling factors are missing.
The default is default=1.
warning: Log a warning for missing scaling factors
hint: Paired with warning=True, this is a string to indicate where the
missing scaling factor was being accessed, to easier diagnose issues.
Returns:
Minimum scaling factor of the components in iter
"""
return map_scaling_factor(
components, default=default, warning=warning, func=min, hint=hint
)
def propagate_indexed_component_scaling_factors(
blk, typ=None, overwrite=False, descend_into=True
):
"""Use the parent component scaling factor to set all component data object
scaling factors.
Args:
blk: The block on which to search for components
typ: Component type(s) (default=(Var, Constraint, Expression, Param))
overwrite: if a data object already has a scaling factor should it be
overwritten (default=False)
descend_into: descend into child blocks (default=True)
"""
if typ is None:
typ = (pyo.Var, pyo.Constraint, pyo.Expression)
for c in blk.component_objects(typ, descend_into=descend_into):
if get_scaling_factor(c) is not None and c.is_indexed():
for cdat in c.values():
if overwrite or get_scaling_factor(cdat) is None:
set_scaling_factor(cdat, get_scaling_factor(c))
def calculate_scaling_factors(blk):
"""Look for calculate_scaling_factors methods and run them. This uses a
recursive function to execute the subblock calculate_scaling_factors
methods first.
"""
def cs(blk2):
"""Recursive function for to do subblocks first"""
for b in blk2.component_data_objects(pyo.Block, descend_into=False):
cs(b)
if hasattr(blk2, "calculate_scaling_factors"):
blk2.calculate_scaling_factors()
# Call recursive function to run calculate_scaling_factors on blocks from
# the bottom up.
cs(blk)
# If a scale factor is set for an indexed component, propagate it to the
# component data if a scale factor hasn't already been explicitly set
propagate_indexed_component_scaling_factors(blk)
# Use the variable scaling factors to scale the arc constraints.
scale_arc_constraints(blk)
def set_scaling_factor(c, v, data_objects=True, overwrite=True):
"""Set a scaling factor for a model component. This function creates the
scaling_factor suffix if needed.
Args:
c: component to supply scaling factor for
v: scaling factor
data_objects: set scaling factors for indexed data objects (default=True)
overwrite: whether to overwrite an existing scaling factor
Returns:
None
"""
if isinstance(c, (float, int)):
# property packages can return 0 for material balance terms on components
# doesn't exist. This handles the case where you get a constant 0 and
# need its scale factor to scale the mass balance.
return 1
# Cast scaling factor to float to catch garbage input
v = float(v)
try:
suf = c.parent_block().scaling_factor
except AttributeError:
c.parent_block().scaling_factor = pyo.Suffix(direction=pyo.Suffix.EXPORT)
suf = c.parent_block().scaling_factor
if not overwrite:
try:
tmp = suf[c] # pylint: disable=unused-variable
# Able to access suffix value for c, so return without setting scaling factor
return
except KeyError:
# No value exists yet for c, go ahead and set it
pass
suf[c] = v
if data_objects and c.is_indexed():
for cdat in c.values():
if not overwrite:
try:
tmp = suf[cdat]
continue
except KeyError:
pass
suf[cdat] = v
def get_scaling_factor(c, default=None, warning=False, exception=False, hint=None):
"""Get a component scale factor.
Args:
c: component
default: value to return if no scale factor exists (default=None)
warning: whether to log a warning if a scaling factor is not found
(default=False)
exception: whether to raise an Exception if a scaling factor is not
found (default=False)
hint: (str) a string to add to the warning or exception message to help
locate the source.
Returns:
scaling factor (float)
"""
try:
sf = c.parent_block().scaling_factor[c]
except (AttributeError, KeyError):
if not isinstance(c, (pyo.Param, ParamData)):
if hint is None:
h = ""
else:
h = f", {hint}"
if warning:
if hasattr(c, "is_component_type") and c.is_component_type():
_log.warning(f"Missing scaling factor for {c}{h}")
else:
_log.warning(f"Trying to get scaling factor for unnamed expr {h}")
if exception and default is None:
if hasattr(c, "is_component_type") and c.is_component_type():
_log.error(f"Missing scaling factor for {c}{h}")
else:
_log.error(f"Trying to get scaling factor for unnamed expr {h}")
raise
sf = default
else:
# Params can just use current value (as long it is not 0)
val = pyo.value(c)
if not val == 0:
sf = abs(1 / pyo.value(c))
else:
sf = 1
return sf
def set_and_get_scaling_factor(c, default, warning=False, exception=False, hint=None):
"""Checks whether a scaling factor exists for a component, sets the scaling factor
if it doesn't exist, then returns the scaling factor on the component (which is
the default value if it wasn't set originally).
Args:
c: component
default: default value to use for scaling factor of c if there is none
warning: whether to log a warning if a scaling factor is not found
(default=False)
exception: whether to raise an Exception if a scaling factor is not
found (default=False)
hint: (str) a string to add to the warning or exception message to help
locate the source.
Returns:
scaling factor (float)
"""
if c.is_indexed():
raise AttributeError(
f"Ambiguous which scaling factor to return for indexed component {c.name}."
)
sf = get_scaling_factor(c, warning=warning, exception=exception, hint=hint)
if sf is None:
sf = default
set_scaling_factor(c, sf, data_objects=False)
return sf
def unset_scaling_factor(c, data_objects=True):
"""Delete a component scaling factor.
Args:
c: component
Returns:
None
"""
try:
del c.parent_block().scaling_factor[c]
except (AttributeError, KeyError):
pass # no scaling factor suffix, is fine
try:
if data_objects and c.is_indexed():
for cdat in c.values():
del cdat.parent_block().scaling_factor[cdat]
except (AttributeError, KeyError):
pass # no scaling factor suffix, is fine
def populate_default_scaling_factors(c):
"""
Method to set default scaling factors for a number of common quantities
based of typical values expressed in SI units. Values are converted to
those used by the property package using Pyomo's unit conversion tools.
"""
units = c.get_metadata().derived_units
si_scale = {
"temperature": (100 * pyo.units.K, "temperature"),
"pressure": (1e5 * pyo.units.Pa, "pressure"),
"dens_mol_phase": (100 * pyo.units.mol / pyo.units.m**3, "density_mole"),
"enth_mol": (1e4 * pyo.units.J / pyo.units.mol, "energy_mole"),
"entr_mol": (100 * pyo.units.J / pyo.units.mol / pyo.units.K, "entropy_mole"),
"fug_phase_comp": (1e4 * pyo.units.Pa, "pressure"),
"fug_coeff_phase_comp": (1 * pyo.units.dimensionless, None),
"gibbs_mol": (1e4 * pyo.units.J / pyo.units.mol, "energy_mole"),
"mole_frac_comp": (0.001 * pyo.units.dimensionless, None),
"mole_frac_phase_comp": (0.001 * pyo.units.dimensionless, None),
"mw": (1e-3 * pyo.units.kg / pyo.units.mol, "molecular_weight"),
"mw_comp": (1e-3 * pyo.units.kg / pyo.units.mol, "molecular_weight"),
"mw_phase": (1e-3 * pyo.units.kg / pyo.units.mol, "molecular_weight"),
}
for p, f in si_scale.items():
# If a default scaling factor exists, do not over write it
if p not in c.default_scaling_factor.keys():
if f[1] is not None:
v = pyo.units.convert(f[0], to_units=units[f[1]])
else:
v = f[0]
sf = 1 / (10 ** round(math.log10(pyo.value(v))))
c.set_default_scaling(p, sf)
def __set_constraint_transform_applied_scaling_factor(c, v):
"""PRIVATE FUNCTION Set the scaling factor used to transform a constraint.
This is used to keep track of scaling transformations that have been applied
to constraints.
Args:
c: component to supply scaling factor for
v: scaling factor
Returns:
None
"""
try:
c.parent_block().constraint_transformed_scaling_factor[c] = v
except AttributeError:
c.parent_block().constraint_transformed_scaling_factor = pyo.Suffix(
direction=pyo.Suffix.LOCAL
)
c.parent_block().constraint_transformed_scaling_factor[c] = v
def get_constraint_transform_applied_scaling_factor(c, default=None):
"""Get a the scale factor that was used to transform a
constraint.
Args:
c: constraint data object
default: value to return if no scaling factor exists (default=None)
Returns:
The scaling factor that has been used to transform the constraint or the
default.
"""
try:
sf = c.parent_block().constraint_transformed_scaling_factor.get(c, default)
except AttributeError:
sf = default # when there is no suffix
return sf
def __unset_constraint_transform_applied_scaling_factor(c):
"""PRIVATE FUNCTION: Delete the recorded scale factor that has been used
to transform constraint c. This is used when undoing a constraint
transformation.
"""
try:
del c.parent_block().constraint_transformed_scaling_factor[c]
except AttributeError:
pass # no scaling factor suffix, is fine
except KeyError:
pass # no scaling factor is fine
def constraint_scaling_transform(c, s, overwrite=True):
"""This transforms a constraint by the argument s. The scaling factor
applies to original constraint (e.g. if one where to call this twice in a row
for a constraint with a scaling factor of 2, the original constraint would
still, only be scaled by a factor of 2.)
Args:
c: Pyomo constraint
s: scale factor applied to the constraint as originally written
overwrite: overwrite existing scaling factors if present (default=True)
Returns:
None
"""
# Want to clear away any units that may have incidentally become attached to s
s = pyo.value(s)
if not isinstance(c, ConstraintData):
raise TypeError(f"{c} is not a constraint or is an indexed constraint")
st = get_constraint_transform_applied_scaling_factor(c, default=None)
if not overwrite and st is not None:
# Existing scaling factor and overwrite False, do nothing
return
if st is None:
# If no existing scaling factor, use value of 1
st = 1
v = s / st
__scale_constraint(c, v)
__set_constraint_transform_applied_scaling_factor(c, s)
def constraint_scaling_transform_undo(c):
"""The undoes the scaling transforms previously applied to a constraint.
Args:
c: Pyomo constraint
Returns:
None
"""
if not isinstance(c, ConstraintData):
raise TypeError(f"{c} is not a constraint or is an indexed constraint")
v = get_constraint_transform_applied_scaling_factor(c)
if v is None:
return # hasn't been transformed, so nothing to do.
__scale_constraint(c, 1 / v)
__unset_constraint_transform_applied_scaling_factor(c)
def unscaled_variables_generator(blk, descend_into=True, include_fixed=False):
"""Generator for unscaled variables
Args:
block
Yields:
variables with no scale factor
"""
for v in blk.component_data_objects(pyo.Var, descend_into=descend_into):
if v.fixed and not include_fixed:
continue
if get_scaling_factor(v) is None:
yield v
def list_unscaled_variables(
blk: pyo.Block, descend_into: bool = True, include_fixed: bool = False
):
"""
Return a list of variables which do not have a scaling factor assigned
Args:
blk: block to check for unscaled variables
descend_into: bool indicating whether to check variables in sub-blocks
include_fixed: bool indicating whether to include fixed Vars in list
Returns:
list of unscaled variable data objects
"""
return [c for c in unscaled_variables_generator(blk, descend_into, include_fixed)]
def unscaled_constraints_generator(blk, descend_into=True):
"""Generator for unscaled constraints
Args:
block
Yields:
constraints with no scale factor
"""
for c in blk.component_data_objects(
pyo.Constraint, active=True, descend_into=descend_into
):
if (
get_scaling_factor(c) is None
and get_constraint_transform_applied_scaling_factor(c) is None
):
yield c
def list_unscaled_constraints(blk: pyo.Block, descend_into: bool = True):
"""
Return a list of constraints which do not have a scaling factor assigned
Args:
blk: block to check for unscaled constraints
descend_into: bool indicating whether to check constraints in sub-blocks
Returns:
list of unscaled constraint data objects
"""
return [c for c in unscaled_constraints_generator(blk, descend_into)]
def constraints_with_scale_factor_generator(blk, descend_into=True):
"""Generator for constraints scaled by a scaling factor, may or not have
been transformed.
Args:
block
Yields:
constraint with a scale factor, scale factor
"""
for c in blk.component_data_objects(
pyo.Constraint, active=True, descend_into=descend_into
):
s = get_scaling_factor(c)
if s is not None:
yield c, s
def badly_scaled_var_generator(
blk, large=1e4, small=1e-3, zero=1e-10, descend_into=True, include_fixed=False
):
"""This provides a rough check for variables with poor scaling based on
their current scale factors and values. For each potentially poorly scaled
variable it returns the var and its current scaled value.
Note that while this method is a reasonable heuristic for non-negative
variables like (absolute) temperature and pressure, molar flows, etc., it
can be misleading for variables like enthalpies and fluxes.
Args:
blk: pyomo block
large: Magnitude that is considered to be too large
small: Magnitude that is considered to be too small
zero: Magnitude that is considered to be zero, variables with a value of
zero are okay, and not reported.
Yields:
variable data object, current absolute value of scaled value
"""
for v in blk.component_data_objects(pyo.Var, descend_into=descend_into):
if v.fixed and not include_fixed:
continue
val = pyo.value(v, exception=False)
if val is None:
continue
sf = get_scaling_factor(v, default=1)
sv = abs(val * sf) # scaled value
if sv > large:
yield v, sv
elif sv < zero:
continue
elif sv < small:
yield v, sv
def list_badly_scaled_variables(
blk,
large: float = 1e4,
small: float = 1e-3,
zero: float = 1e-10,
descend_into: bool = True,
include_fixed: bool = False,
):
"""Return a list of variables with poor scaling based on
their current scale factors and values. For each potentially poorly scaled
variable it returns the var and its current scaled value.
Note that while this method is a reasonable heuristic for non-negative
variables like (absolute) temperature and pressure, molar flows, etc., it
can be misleading for variables like enthalpies and fluxes.
Args:
blk: pyomo block
large: Magnitude that is considered to be too large
small: Magnitude that is considered to be too small
zero: Magnitude that is considered to be zero, variables with a value of
zero are okay, and not reported.
descend_into: bool indicating whether to check constraints in sub-blocks
include_fixed: bool indicating whether to include fixed Vars in list
Returns:
list of tuples containing (variable data object, current absolute value of scaled value)
"""
return [
c
for c in badly_scaled_var_generator(
blk, large, small, zero, descend_into, include_fixed
)
]
def constraint_autoscale_large_jac(
m,
ignore_constraint_scaling=False,
ignore_variable_scaling=False,
max_grad=100,
min_scale=1e-6,
no_scale=False,
equality_constraints_only=False,
):
"""Automatically scale constraints based on the Jacobian. This function
imitates Ipopt's default constraint scaling. This scales constraints down
to avoid extremely large values in the Jacobian. This function also returns
the unscaled and scaled Jacobian matrixes and the Pynumero NLP which can be
used to identify the constraints and variables corresponding to the rows and
comlumns.
Args:
m: model to scale
ignore_constraint_scaling: ignore existing constraint scaling
ignore_variable_scaling: ignore existing variable scaling
max_grad: maximum value in Jacobian after scaling, subject to minimum
scaling factor restriction.
min_scale: minimum scaling factor allowed, keeps constraints from being
scaled too much.
no_scale: just calculate the Jacobian and scaled Jacobian, don't scale
anything
equality_constraints_only: Include only the equality constraints in the
Jacobian
Returns:
unscaled Jacobian CSR from, scaled Jacobian CSR from, Pynumero NLP
"""
# Pynumero requires an objective, but I don't, so let's see if we have one
n_obj = 0
for c in m.component_data_objects(pyo.Objective, active=True):
n_obj += 1
# Add an objective if there isn't one
if n_obj == 0:
dummy_objective_name = unique_component_name(m, "objective")
setattr(m, dummy_objective_name, pyo.Objective(expr=0))
# Create NLP and calculate the objective
if not AmplInterface.available():
raise RuntimeError("Pynumero not available.")
nlp = PyomoNLP(m)
if equality_constraints_only:
jac = nlp.evaluate_jacobian_eq().tocsr()
else:
jac = nlp.evaluate_jacobian().tocsr()
# Get lists of variables and constraints to translate Jacobian indexes
# save them on the NLP for later, since generating them seems to take a while
if equality_constraints_only:
nlp.clist = clist = nlp.get_pyomo_equality_constraints()
else:
nlp.clist = clist = nlp.get_pyomo_constraints()
nlp.vlist = vlist = nlp.get_pyomo_variables()
# Create a scaled Jacobian to account for variable scaling, for now ignore
# constraint scaling
jac_scaled = jac.copy()
for i, c in enumerate(clist):
for j in jac_scaled[i].indices:
v = vlist[j]
if ignore_variable_scaling:
sv = 1
else:
sv = get_scaling_factor(v, default=1)
jac_scaled[i, j] = jac_scaled[i, j] / sv
# calculate constraint scale factors
for i, c in enumerate(clist):
sc = get_scaling_factor(c, default=1)
if not no_scale:
if ignore_constraint_scaling or get_scaling_factor(c) is None:
sc = 1
row = jac_scaled[i]
for d in row.indices:
row[0, d] = abs(row[0, d])
mg = row.max()
if mg > max_grad:
sc = max(min_scale, max_grad / mg)
set_scaling_factor(c, sc)
for j in jac_scaled[i].indices:
# update the scaled jacobian
jac_scaled[i, j] = jac_scaled[i, j] * sc
# delete dummy objective
if n_obj == 0:
delattr(m, dummy_objective_name)
return jac, jac_scaled, nlp
def get_jacobian(m, scaled=True, equality_constraints_only=False):
"""
Get the Jacobian matrix at the current model values. This function also
returns the Pynumero NLP which can be used to identify the constraints and
variables corresponding to the rows and columns.
Args:
m: model to get Jacobian from
scaled: if True return scaled Jacobian, else get unscaled
equality_constraints_only: Only include equality constraints in the
Jacobian calculated and scaled
Returns:
Jacobian matrix in Scipy CSR format, Pynumero nlp
"""
jac, jac_scaled, nlp = constraint_autoscale_large_jac(
m, no_scale=True, equality_constraints_only=equality_constraints_only
)
if scaled:
return jac_scaled, nlp
else:
return jac, nlp
def extreme_jacobian_entries(
m=None, scaled=True, large=1e4, small=1e-4, zero=1e-10, jac=None, nlp=None
):
"""
Show very large and very small Jacobian entries.
Args:
m: model
scaled: if true use scaled Jacobian
large: >= to this value is considered large
small: <= to this and >= zero is considered small
Returns:
(list of tuples), Jacobian entry, Constraint, Variable
"""
if jac is None or nlp is None:
jac, nlp = get_jacobian(m, scaled)
el = []
for i, c in enumerate(nlp.clist):
for j in jac[i].indices:
v = nlp.vlist[j]
e = abs(jac[i, j])
if (e <= small and e > zero) or e >= large:
el.append((e, c, v))
return el
def extreme_jacobian_rows(
m=None, scaled=True, large=1e4, small=1e-4, jac=None, nlp=None
):
"""
Show very large and very small Jacobian rows. Typically indicates a badly-
scaled constraint.
Args:
m: model
scaled: if true use scaled Jacobian
large: >= to this value is considered large
small: <= to this is considered small
Returns:
(list of tuples), Row norm, Constraint
"""
# Need both jac for the linear algebra and nlp for constraint names
if jac is None or nlp is None:
jac, nlp = get_jacobian(m, scaled)
el = []
for i, c in enumerate(nlp.clist):
norm = 0
# Calculate L2 norm
for j in jac[i].indices:
norm += jac[i, j] ** 2
norm = norm**0.5
if norm <= small or norm >= large:
el.append((norm, c))
return el
def extreme_jacobian_columns(
m=None, scaled=True, large=1e4, small=1e-4, jac=None, nlp=None
):
"""
Show very large and very small Jacobian columns. A more reliable indicator
of a badly-scaled variable than badly_scaled_var_generator.
Args:
m: model
scaled: if true use scaled Jacobian
large: >= to this value is considered large
small: <= to this is considered small
Returns:
(list of tuples), Column norm, Variable
"""
# Need both jac for the linear algebra and nlp for variable names
if jac is None or nlp is None:
jac, nlp = get_jacobian(m, scaled)
jac = jac.tocsc()
el = []
for j, v in enumerate(nlp.vlist):
norm = 0
# Calculate L2 norm
for i in jac.getcol(j).indices:
norm += jac[i, j] ** 2
norm = norm**0.5
if norm <= small or norm >= large:
el.append((norm, v))
return el
def jacobian_cond(m=None, scaled=True, order=None, pinv=False, jac=None):
"""
Get the condition number of the scaled or unscaled Jacobian matrix of a model.
Args:
m: calculate the condition number of the Jacobian from this model.
scaled: if True use scaled Jacobian, else use unscaled
order: norm order, None = Frobenius, see scipy.sparse.linalg.norm for more
pinv: Use pseudoinverse, works for non-square matrices
jac: (optional) previously calculated Jacobian
Returns:
(float) Condition number
"""
if jac is None:
jac, _ = get_jacobian(m, scaled)
jac = jac.tocsc()
if jac.shape[0] != jac.shape[1] and not pinv:
_log.warning("Nonsquare Jacobian using pseudo inverse")
pinv = True
if not pinv:
jac_inv = spla.inv(jac)
return spla.norm(jac, order) * spla.norm(jac_inv, order)
else:
jac_inv = la.pinv(jac.toarray())
return spla.norm(jac, order) * la.norm(jac_inv, order)
def scale_time_discretization_equations(blk, time_set, time_scaling_factor):
"""
Scales time discretization equations generated via a Pyomo discretization
transformation. Also scales continuity equations for collocation methods
of discretization that require them.
Args:
blk: Block whose time discretization equations are being scaled
time_set: Time set object. For an IDAES flowsheet object fs, this is fs.time.
time_scaling_factor: Scaling factor to use for time
Returns:
None
"""
tname = time_set.local_name
# Copy and pasted from solvers.petsc.find_discretization_equations then modified
for var in blk.component_objects(pyo.Var):
if isinstance(var, DerivativeVar):
cont_set_set = ComponentSet(var.get_continuousset_list())
if time_set in cont_set_set:
if len(cont_set_set) > 1:
_log.warning(
"IDAES presently does not support automatically scaling discretization equations for "
f"second order or higher derivatives like {var.name} that are differentiated at least once with "
"respect to time. Please scale the corresponding discretization equation yourself."
)
continue
state_var = var.get_state_var()
parent_block = var.parent_block()
disc_eq = getattr(parent_block, var.local_name + "_disc_eq")
# Look for continuity equation, which exists only for collocation with certain sets of polynomials
try:
cont_eq = getattr(
parent_block, state_var.local_name + "_" + tname + "_cont_eq"
)
except AttributeError:
cont_eq = None
deriv_dict = dict(
(key, pyo.Reference(slc))
for key, slc in slice_component_along_sets(var, (time_set,))
)
state_dict = dict(
(key, pyo.Reference(slc))
for key, slc in slice_component_along_sets(state_var, (time_set,))
)
disc_dict = dict(
(key, pyo.Reference(slc))
for key, slc in slice_component_along_sets(disc_eq, (time_set,))
)
if cont_eq is not None:
cont_dict = dict(
(key, pyo.Reference(slc))
for key, slc in slice_component_along_sets(cont_eq, (time_set,))
)
for key, deriv in deriv_dict.items():
state = state_dict[key]
disc = disc_dict[key]
if cont_eq is not None:
cont = cont_dict[key]
for t in time_set:
s_state = get_scaling_factor(state[t], default=1, warning=True)
set_scaling_factor(
deriv[t], s_state / time_scaling_factor, overwrite=False
)
s_deriv = get_scaling_factor(deriv[t])
# Check time index to decide what constraints to scale
if cont_eq is None:
if t == time_set.first() or t == time_set.last():
try:
constraint_scaling_transform(
disc[t], s_deriv, overwrite=False
)
except KeyError:
# Discretization and continuity equations may or may not exist at the first or last time
# points depending on the method. Backwards skips first, forwards skips last, central skips
# both (which means the user needs to provide additional equations)
pass
else:
constraint_scaling_transform(
disc[t], s_deriv, overwrite=False
)
else:
# Lagrange-Legendre is a pain, because it has continuity equations on the edges of finite
# instead of discretization equations, but no intermediate continuity equations, so we have
# to look for both at every timepoint
try:
constraint_scaling_transform(
disc[t], s_deriv, overwrite=False
)
except KeyError:
if t != time_set.first():
constraint_scaling_transform(
# pylint: disable-next=possibly-used-before-assignment
cont[t],
s_state,
overwrite=False,
)
class CacheVars(object):
"""
A class for saving the values of variables then reloading them,
usually after they have been used to perform some solve or calculation.
"""
def __init__(self, vardata_list):
self.vars = vardata_list
self.cache = [None for var in self.vars]
def __enter__(self):
for i, var in enumerate(self.vars):