-
Notifications
You must be signed in to change notification settings - Fork 314
Expand file tree
/
Copy pathutil.py
More file actions
1461 lines (1240 loc) · 53.5 KB
/
Copy pathutil.py
File metadata and controls
1461 lines (1240 loc) · 53.5 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.
#################################################################################
"""
Utility functions for scaling.
Authors: Andrew Lee, Douglas Allan
"""
from copy import deepcopy
import math
import sys
import json
import numpy as np
from scipy.linalg import norm, pinv
from scipy.sparse import diags
from scipy.sparse.linalg import inv as spinv, norm as spnorm, svds
from pyomo.environ import (
Binary,
Block,
Boolean,
Constraint,
Expression,
NegativeIntegers,
NegativeReals,
NonNegativeIntegers,
NonNegativeReals,
NonPositiveIntegers,
NonPositiveReals,
Objective,
PositiveIntegers,
PositiveReals,
Reference,
Suffix,
value,
Var,
)
from pyomo.core.base.block import BlockData
from pyomo.core.base.var import VarData
from pyomo.core.base.constraint import ConstraintData
from pyomo.core.base.expression import ExpressionData
from pyomo.core.base.param import ParamData
from pyomo.core import expr as EXPR
from pyomo.core.base.suffix import SuffixFinder
from pyomo.core.base.units_container import _PyomoUnit
from pyomo.common.collections import ComponentSet
from pyomo.common.modeling import unique_component_name
from pyomo.common.numeric_types import native_types
from pyomo.dae import DerivativeVar
from pyomo.dae.flatten import slice_component_along_sets
from pyomo.contrib.pynumero.interfaces.pyomo_nlp import PyomoNLP
from pyomo.contrib.pynumero.asl import AmplInterface
from idaes.core.util.exceptions import BurntToast
from idaes.core.util.linalg import svd_rayleigh_ritz
from idaes.core.util.misc import StrEnum
import idaes.logger as idaeslog
_log = idaeslog.getLogger(__name__)
TAB = " " * 4
class MatrixNorm(StrEnum):
"""
Types of norm available for calculating the matrix condition number.
Let A be an m by n matrix and let sigma be a vector of the singular
values of A. Then the matrix norms can be expressed as follows:
twoNorm ("2"): max(sigma)
frobeniusNorm ("fro"): sqrt(sum(a**2 for a in row for row in A))
= sqrt(sum(s**2 for s in sigma))
So the Frobenius norm is the square root of the sum of squares of
all the matrix entries, while the matrix two norm is the largest
singular value of A. For the matrix two norm, we have
norm(A @ v, 2) <= norm(A, 2) * norm(v, 2) for all vectors v with
a length of n.
"""
twoNorm = "2"
frobeniusNorm = "fro"
def _filter_unknown(block_data):
# It can be confusing to users to see a block named "unknown" appear in
# an error message, but that's, unfortunately, what Pyomo uses as the
# default name of ConcreteModels. Therefore, filter out that case.
block_name = block_data.name
if block_name == "unknown" and block_data.model() is block_data:
return "model"
return f"block {block_name}"
def get_scaling_factor_suffix(blk: BlockData):
"""
Get scaling suffix from block.
Args:
blk: component to get scaling factor suffix for
Returns:
Pyomo scaling Suffix
Raises:
TypeError if component is an IndexedBlock
"""
if isinstance(blk, BlockData):
pass
elif isinstance(blk, Block):
raise TypeError(
"IndexedBlocks cannot have scaling factors attached to them. "
"Please assign scaling factors to the elements of the IndexedBlock."
)
else:
raise TypeError(
f"Component {blk.name} was not a BlockData, instead it was a {type(blk)}"
)
try:
sfx = blk.scaling_factor
except AttributeError:
# No existing suffix, create one
_log.debug(f"Created new scaling suffix for {_filter_unknown(blk)}")
sfx = blk.scaling_factor = Suffix(direction=Suffix.EXPORT)
if not sfx.active:
raise RuntimeError(
f"The scaling suffix on {_filter_unknown(blk)} has been deactivated. "
"Typically, this means that the user has performed a scaling transformation "
"on the model."
)
return sfx
def get_scaling_hint_suffix(blk: BlockData):
"""
Get scaling hint suffix from block.
Creates a new suffix if one is not found.
Args:
blk: block to get suffix for
Returns:
Pyomo scaling hint Suffix
Raises:
TypeError if component is an IndexedBlock or non-block.
"""
if isinstance(blk, BlockData):
pass
elif isinstance(blk, Block):
raise TypeError(
"IndexedBlocks cannot have scaling hints attached to them. "
"Please assign scaling hints to the elements of the IndexedBlock."
)
else:
raise TypeError(
f"Component {blk.name} was not a BlockData, instead it was a {type(blk)}"
)
try:
sfx = blk.scaling_hint
except AttributeError:
# No existing suffix, create one
_log.debug(f"Created new scaling hint suffix for {_filter_unknown(blk)}")
sfx = blk.scaling_hint = Suffix()
return sfx
def get_component_scaling_suffix(component):
"""
Get scaling suffix appropriate to component type from parent block.
Creates a new suffix if one is not found.
Args:
component: component to get suffix for
Returns:
Pyomo scaling factor Suffix (for VarData and ConstraintData)
or Pyomo scaling hint Suffix (for ExpressionData)
Raises:
TypeError if component isn't a VarData, ConstraintData, or ExpressionData.
"""
blk = component.parent_block()
if isinstance(component, (VarData, ConstraintData)):
return get_scaling_factor_suffix(blk)
elif isinstance(component, ExpressionData):
return get_scaling_hint_suffix(blk)
else:
raise TypeError(
"Can get scaling factors for only VarData, ConstraintData, and (hints from) ExpressionData. "
f"Component {component.name} is instead {type(component)}."
)
def scaling_factors_to_dict(blk_or_suffix, descend_into: bool = True):
"""
Write scaling factor and/or scaling hint suffixes to a serializable
json dict. If a Block, indexed or otherwise is passed, this function
collects both scaling factors and hints. If a suffix is passed
directly, it serializes only that suffix (factors or hints) and leaves
the other suffix (hints or factors) out of the resulting dict.
Component objects are replaced by their local names so they can be
serialized.
Args:
blk_or_suffix: Pyomo Block or Suffix object to covert to dict
descend_into: for Blocks, whether to descend into any child blocks
Returns
dict containing scaling factors and/or scaling hints indexed by
component names
Raises:
TypeError if blk_or_suffix is not an instance of Block or Suffix
"""
# First, determine what type of component we have
if isinstance(blk_or_suffix, Suffix):
out_dict = {"suffix": _suffix_to_dict(blk_or_suffix)}
blk = blk_or_suffix.parent_block()
elif isinstance(blk_or_suffix, BlockData):
# Scalar block or element of indexed block
out_dict = _collect_block_suffixes(blk_or_suffix, descend_into=descend_into)
blk = blk_or_suffix
elif isinstance(blk_or_suffix, Block):
# Indexed block
blk = blk_or_suffix
out_dict = {"block_data": {}}
for bd in blk_or_suffix.values():
out_dict["block_data"][bd.name] = _collect_block_suffixes(
bd, descend_into=descend_into
)
else:
# Not a block or suffix
raise TypeError(
f"{blk_or_suffix.name} is not an instance of a Block of Suffix."
)
# Attach block name for future verification
out_dict["block_name"] = blk.name
return out_dict
def scaling_factors_from_dict(
blk_or_suffix,
json_dict: dict,
overwrite: bool = False,
verify_names: bool = True,
):
"""
Set scaling factors and/or scaling hints based on values in a serializable json dict.
This method expects components to be referenced by their local names.
Args:
blk_or_suffix: Pyomo Block or Suffix object to set scaling factors on
json_dict: dict of scaling factors and/or scaling hints to load into model
overwrite: (bool) whether to overwrite existing scaling factors/hints or not
verify_names: (bool) whether to verify that all names in dict exist on block
Returns
None
Raises:
TypeError if blk_or_suffix is not an instance of Block or Suffix
"""
# First, copy json_dict so we do not mutate original
sdict = deepcopy(json_dict)
# Pop block name for verification
block_name = sdict.pop("block_name")
# Next, determine what type of component we have
if isinstance(blk_or_suffix, Suffix):
# Suffix
if verify_names and block_name != blk_or_suffix.parent_block().name:
raise ValueError(
f"Name of parent block ({blk_or_suffix.parent_block().name}) does "
f"not match that recorded in json_dict ({block_name})"
)
_suffix_from_dict(
blk_or_suffix,
sdict["suffix"],
overwrite=overwrite,
verify_names=verify_names,
)
elif isinstance(blk_or_suffix, BlockData):
# Scalar block or element of indexed block
if verify_names and block_name != blk_or_suffix.name:
raise ValueError(
f"Block name ({blk_or_suffix.name}) does "
f"not match that recorded in json_dict ({block_name})"
)
_set_block_suffixes_from_dict(
blk_or_suffix, sdict, overwrite=overwrite, verify_names=verify_names
)
elif isinstance(blk_or_suffix, Block):
# Indexed block
if verify_names and block_name != blk_or_suffix.name:
raise ValueError(
f"Block name ({blk_or_suffix.name}) does "
f"not match that recorded in json_dict ({block_name})"
)
for bd_name, bd_dict in sdict["block_data"].items():
bd = blk_or_suffix.parent_block().find_component(bd_name)
_set_block_suffixes_from_dict(
bd, bd_dict, overwrite=overwrite, verify_names=verify_names
)
else:
# Not a block or suffix
raise TypeError(
f"{blk_or_suffix.name} is not an instance of a Block of Suffix."
)
def scaling_factors_to_json_file(blk_or_suffix, filename: str):
"""
Serialize scaling factors to file in json format.
Args:
blk_of_suffix: Block or Suffix to save scaling factors for
filename: name of file to write to as string
Returns:
None
Raises:
TypeError if blk_or_suffix is not an instance of Block or Suffix
"""
with open(filename, "w") as fd:
json.dump(scaling_factors_to_dict(blk_or_suffix), fd, indent=3)
def scaling_factors_from_json_file(
blk_or_suffix, filename: str, overwrite: bool = False, verify_names: bool = True
):
"""
Load scaling factors from json file.
Args:
blk_of_suffix: Block or Suffix to load scaling factors for
filename: name of file to load as string
overwrite: (bool) whether to overwrite existing scaling factors or not
verify_names: (bool) whether to verify that all names in dict exist on block
Returns:
None
Raises:
TypeError if blk_or_suffix is not an instance of Block or Suffix
"""
with open(filename, "r") as f:
scaling_factors_from_dict(
blk_or_suffix, json.load(f), overwrite=overwrite, verify_names=verify_names
)
f.close()
def _collect_block_suffixes(block_data, descend_into=True):
sf_suffix = get_scaling_factor_suffix(block_data)
sh_suffix = get_scaling_hint_suffix(block_data)
out_dict = {
"scaling_factor_suffix": _suffix_to_dict(sf_suffix),
"scaling_hint_suffix": _suffix_to_dict(sh_suffix),
}
if descend_into:
out_dict["subblock_suffixes"] = {}
for sb in block_data.component_data_objects(Block, descend_into=False):
out_dict["subblock_suffixes"][sb.local_name] = _collect_block_suffixes(
sb, descend_into
)
return out_dict
def _set_block_suffixes_from_dict(
block_data, json_dict, verify_names=True, overwrite=False
):
# First, copy dict so we can take it apart
sdict = deepcopy(json_dict)
# Pop any subblock suffixes
sb_dict = sdict.pop("subblock_suffixes", None)
sf_dict = sdict.pop("scaling_factor_suffix", None)
sh_dict = sdict.pop("scaling_hint_suffix", None)
# Set local suffix values
sf_suffix = get_scaling_factor_suffix(block_data)
sh_suffix = get_scaling_hint_suffix(block_data)
if sf_dict is not None:
_suffix_from_dict(
sf_suffix,
sf_dict,
verify_names=verify_names,
overwrite=overwrite,
valid_types=[VarData, ConstraintData],
)
elif verify_names:
raise KeyError(
f"Missing scaling factor dictionary for {_filter_unknown(block_data)}."
)
if sh_dict is not None:
_suffix_from_dict(
sh_suffix,
sh_dict,
verify_names=verify_names,
overwrite=overwrite,
valid_types=[ExpressionData],
)
elif verify_names:
raise KeyError(
f"Missing scaling hint dictionary for {_filter_unknown(block_data)}."
)
if sb_dict is not None:
# Get each subblock and apply function recursively
for sb, sb_dict_value in sb_dict.items():
subblock = block_data.find_component(sb)
if subblock is not None:
_set_block_suffixes_from_dict(
subblock,
sb_dict_value,
verify_names=verify_names,
overwrite=overwrite,
)
elif verify_names:
raise AttributeError(
f"{_filter_unknown(block_data)} does not have a subblock named {sb}.".capitalize()
)
def _suffix_to_dict(suffix):
sdict = {}
for k, v in suffix.items():
# Record components by their local name so we can use
# find_Component to retrieve them later
sdict[k.local_name] = v
return sdict
def _suffix_from_dict(
suffix, json_dict, verify_names=True, overwrite=False, valid_types=None
):
parent_block = suffix.parent_block()
for k, v in json_dict.items():
comp = parent_block.find_component(k)
if comp is not None:
if valid_types is not None and not any(
[isinstance(comp, cls) for cls in valid_types]
):
raise TypeError(
f"Expected {comp.name} to be a subclass of {valid_types}, "
f"but it was instead {type(comp)}"
)
if overwrite or comp not in suffix:
suffix[comp] = v
elif verify_names:
raise ValueError(
f"Could not find component {k} on {_filter_unknown(parent_block)}."
)
def get_scaling_factor(component, default: float = None, warning: bool = False):
"""
Get scaling factor for component.
Args:
component: component to get scaling factor for
default: scaling factor to return if no scaling factor
exists for component
warning: Bool to determine whether a warning should be
returned if no scaling factor is found
Returns:
float scaling factor
Raises:
TypeError if component is not VarData, ConstraintData, or ExpressionData
"""
if component.is_indexed():
raise TypeError(
f"Component {component.name} is indexed. It is ambiguous which scaling factor to return."
)
if component.is_expression_type() and not component.is_named_expression_type():
raise TypeError(
"Can only get scaling hints for named expressions, but component was an unnamed expression."
)
if isinstance(component, (VarData, ConstraintData)):
sfx_finder = SuffixFinder("scaling_factor")
elif isinstance(component, ExpressionData):
sfx_finder = SuffixFinder("scaling_hint")
else:
raise TypeError(
f"Can get scaling factors for only VarData, ConstraintData, and (hints from) ExpressionData. "
f"Component {component.name} is instead {type(component)}."
)
sf = sfx_finder.find(component_data=component)
if sf is None:
if warning:
_log.warning(f"Missing scaling factor for {component.name}")
return default
else:
return sf
def set_scaling_factor(component, scaling_factor: float, overwrite: bool = False):
"""
Set scaling factor for component.
Scaling factors must be positive, non-zero floats.
Args:
component: component to set scaling factor for
scaling_factor: scaling factor to assign
overwrite: (bool) whether to overwrite existing scaling factor
Returns:
None
Raises:
ValueError is scaling_factor is 0 or negative
"""
# Cast to float to catch any garbage inputs
scaling_factor = float(scaling_factor)
# Check for negative or 0 scaling factors
if scaling_factor < 0:
raise ValueError(
f"Scaling factor for {component.name} is negative ({scaling_factor}). "
"Scaling factors must be strictly positive."
)
elif scaling_factor == 0:
raise ValueError(
f"Scaling factor for {component.name} is zero. "
"Scaling factors must be strictly positive."
)
elif scaling_factor == float("inf"):
raise ValueError(
f"Scaling factor for {component.name} is infinity. "
"Scaling factors must be finite."
)
elif math.isnan(scaling_factor):
raise ValueError(f"Scaling factor for {component.name} is NaN.")
if component.is_indexed():
# What if a scaling factor already exists for the indexed component?
# for idx in component:
# set_scaling_factor(component[idx], scaling_factor=scaling_factor, overwrite=overwrite)
raise TypeError(
f"Component {component.name} is indexed. Set scaling factors for individual indices instead."
)
try:
sfx = get_component_scaling_suffix(component)
except RuntimeError as err:
raise RuntimeError(
f"Cannot set a scaling factor for {component.name} because the scaling_factor "
"suffix has been deactivated."
) from err
if not overwrite and component in sfx:
_log.debug(
f"Existing scaling factor for {component.name} found and overwrite=False. "
"Scaling factor unchanged."
)
else:
sfx[component] = scaling_factor
def del_scaling_factor(component, delete_empty_suffix: bool = False):
"""
Delete scaling factor for component.
Args:
component: component to delete scaling factor for
delete_empty_suffix: (bool) whether to delete scaling Suffix if it is
empty after deletion.
"""
if component.is_indexed():
raise TypeError(
f"Component {component.name} is indexed. It is ambiguous which scaling factor to delete."
)
# Get suffix
parent = component.parent_block()
# TODO what if a scaling factor exists in a non-standard place?
sfx = get_component_scaling_suffix(component)
# Delete entry for component if it exists
# Pyomo handles case where value does not exist in suffix with a no-op
sfx.clear_value(component)
if delete_empty_suffix:
# Check if Suffix is empty (i.e. length 0)
if len(sfx) == 0:
# If so, delete suffix from parent block of component
if sfx.name == "scaling_factor":
_log.debug(f"Deleting empty scaling suffix from {parent.name}")
elif sfx.name == "scaling_hint":
_log.debug(f"Deleting empty scaling hint suffix from {parent.name}")
else:
raise BurntToast(
"This branch should be inaccessible, please report this issue "
"to the IDAES developers."
)
parent.del_component(sfx)
def report_scaling_factors(
blk: Block, ctype=None, descend_into: bool = False, stream=None
):
"""
Write the scaling factors for all components in a Block to a stream.
Args:
blk: Block to get scaling factors and/or scaling hints from.
ctype: None, Var, Constraint, or Expression. Type of component to show scaling factors for
(if None, shows all elements).
descend_into: whether to show scaling factors for components in sub-blocks.
stream: StringIO object to write results to. If not provided, writes to stdout.
Raises:
TypeError if blk is not a Pyomo Block.
ValueError is ctype is not None, Var or Constraint.
"""
if stream is None:
stream = sys.stdout
if ctype not in [None, Var, Constraint, Expression]:
raise ValueError(
f"report_scaling_factors only supports None, Var, Constraint, or Expression for argument ctype: "
f"received {ctype}."
)
if not isinstance(blk, (Block, BlockData)):
raise TypeError(
"report_scaling_factors: blk must be an instance of a Pyomo Block."
)
stream.write(f"Scaling Factors for {_filter_unknown(blk)}\n")
# We will report Vars and Constraint is separate sections for clarity - iterate separately
if ctype == Var or ctype is None:
# Collect Vars
vdict = {}
for blkdata in blk.values():
for vardata in blkdata.component_data_objects(
Var, descend_into=descend_into
):
val = vardata.value
sf = get_scaling_factor(vardata)
if sf is not None:
sfstr = "{:.3E}".format(sf)
else:
sfstr = "None "
if val is not None:
vstr = "{:.3E}".format(val)
if sf is not None:
sval = "{:.3E}".format(value(vardata * sf))
else:
sval = vstr
else:
vstr = "None "
sval = "None"
vdict[vardata.name] = (sfstr, vstr, sval)
# Write Var section - skip if no Vars
if len(vdict) > 0:
# Get longest var name
header = "Variable"
maxname = len(max(vdict.keys(), key=len))
if maxname < len(header):
maxname = len(header)
stream.write(
f"\n{header}{' '*(maxname-len(header))}{TAB}Scaling Factor{TAB}Value{' '*4}{TAB}Scaled Value\n"
)
for n, i in vdict.items():
# Pad name to length
stream.write(
f"{n + ' '*(maxname-len(n))}{TAB}{i[0]}{' '*5}{TAB}{i[1]}{TAB}{i[2]}\n"
)
if ctype == Constraint or ctype is None:
# Collect Constraints
cdict = {}
for blkdata in blk.values():
for condata in blkdata.component_data_objects(
Constraint, descend_into=descend_into
):
sf = get_scaling_factor(condata)
if sf is not None:
sfstr = "{:.3E}".format(sf)
else:
sfstr = "None"
cdict[condata.name] = sfstr
# Write Constraint section - skip if no Constraints
if len(cdict) > 0:
# Get longest con name
header = "Constraint"
maxname = len(max(cdict.keys(), key=len))
if maxname < len(header):
maxname = len(header)
stream.write(
f"\n{header}{' ' * (maxname - len(header))}{TAB}Scaling Factor\n"
)
for n, i in cdict.items():
# Pad name to length
stream.write(f"{n + ' ' * (maxname - len(n))}{TAB}{i}\n")
if ctype == Expression or ctype is None:
# Collect Expressions
edict = {}
for blkdata in blk.values():
for exprdata in blkdata.component_data_objects(
Expression, descend_into=descend_into
):
sf = get_scaling_factor(exprdata)
if sf is not None:
sfstr = "{:.3E}".format(sf)
else:
sfstr = "None"
edict[exprdata.name] = sfstr
# Write Expression section - skip if no Expressions
if len(edict) > 0:
# Get longest con name
header = "Expression"
maxname = len(max(edict.keys(), key=len))
if maxname < len(header):
maxname = len(header)
stream.write(
f"\n{header}{' ' * (maxname - len(header))}{TAB}Scaling Hint\n"
)
for n, i in edict.items():
# Pad name to length
stream.write(f"{n + ' ' * (maxname - len(n))}{TAB}{i}\n")
# Unscaled variables and constraints generators adopted from old scaling tools,
# originally by John Eslick
def unscaled_variables_generator(
blk: Block, descend_into: Boolean = True, include_fixed: Boolean = False
):
"""Generator for unscaled variables
Args:
block
Yields:
variables with no scale factor
"""
for v in blk.component_data_objects(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: 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: Block, descend_into=True):
"""Generator for unscaled constraints
Args:
block
Yields:
constraints with no scale factor
"""
for c in blk.component_data_objects(
Constraint, active=True, descend_into=descend_into
):
if get_scaling_factor(c) is None:
yield c
def list_unscaled_constraints(blk: 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 get_nominal_value(component):
"""
Get the signed nominal value for a VarData or ParamData component.
For Params, the current value of the component will be returned.
For Vars, the nominal value is determined using the assigned scaling factor
and the sign determined based on the bounds and domain of the variable (defaulting to
positive). If no scaling factor is set, then the current value will be used if set,
otherwise the absolute nominal value will be equal to 1.
Args:
component: component to determine nominal value for
Returns:
signed float with nominal value
Raises:
TypeError if component is not instance of VarData or ParamData
"""
# Determine if Var or Param
if isinstance(component, VarData):
# Get scaling factor for Var
sf = get_scaling_factor(component)
if sf is None:
# No scaling factor - see if Var has a value
if component.value is not None:
# If it has a value, use that as the nominal value
# As we are using the actual value, do not need to determine sign
return value(component)
else:
# Otherwise assign a nominal value of 1
sf = 1
# Try to determine expected sign of node
ub = component.ub
lb = component.lb
domain = component.domain
# To avoid NoneType errors, assign dummy values in place of None
if ub is None:
# No upper bound, take a positive value
ub = 1000
if lb is None:
# No lower bound, take a negative value
lb = -1000
if lb >= 0 or domain in [
NonNegativeReals,
PositiveReals,
PositiveIntegers,
NonNegativeIntegers,
Boolean,
Binary,
]:
# Strictly positive
sign = 1
elif ub <= 0 or domain in [
NegativeReals,
NonPositiveReals,
NegativeIntegers,
NonPositiveIntegers,
]:
# Strictly negative
sign = -1
else:
# Unbounded, see if there is a current value
# Assume positive until proven otherwise
sign = 1
if component.value is not None:
val = value(component)
if val < 0:
# Assigned negative value, assume value will remain negative
sign = -1
return sign / sf
elif isinstance(component, ParamData):
# Nominal value of a parameter is always its value
return value(component)
else:
# Not a Var or Param - invalid component type
raise TypeError(
f"get_nominal_value - {component.name} is not an instance of a Var or Param."
)
class NominalValueExtractionVisitor(EXPR.StreamBasedExpressionVisitor):
"""
Expression walker for collecting scaling factors in an expression and determining the
expected value of the expression using the scaling factors as nominal inputs.
By default, the get_nominal_value method is used to determine the nominal value for
all Vars and Params in the expression, however this can be changed by setting the
nominal_value_callback argument.
Returns a list of expected values for each additive term in the expression.
In order to properly assess the expected value of terms within functions, the sign
of each term is maintained throughout thus returned values may be negative. Functions
using this walker should handle these appropriately.
"""
def __init__(self, nominal_value_callback=get_nominal_value):
"""
Visitor class used to determine nominal values of all terms in an expression based on
scaling factors assigned to the associated variables. Do not use this class directly.
Args:
nominal_value_callback - method to use to get nominal value of root nodes.
Notes
-----
This class inherits from the :class:`StreamBasedExpressionVisitor` to implement
a walker that returns the nominal value corresponding to all additive terms in an
expression.
There are class attributes (dicts) that map the expression node type to the
particular method that should be called to return the nominal value of the node based
on the nominal value of its child arguments. This map is used in exitNode.
"""
super().__init__()
self._nominal_value_callback = nominal_value_callback
def _get_magnitude_base_type(self, node):
try:
return [self._nominal_value_callback(node)]
except TypeError:
# Not a Var or Param - something went wrong
raise BurntToast(
"NominalValueExtractionVisitor found root node that was not a Var or Param. "
"This should never happen - please contact the developers with this bug."
)
def _get_nominal_value_for_sum_subexpression(self, child_nominal_values):
return sum(i for i in child_nominal_values)
def _get_nominal_value_for_sum(self, node, child_nominal_values):
# For sums, collect all child values into a list
mag = []
for i in child_nominal_values:
for j in i:
mag.append(j)
return mag
def _get_nominal_value_for_product(self, node, child_nominal_values):
mag = []
for i in child_nominal_values[0]:
for j in child_nominal_values[1]:
mag.append(i * j)
return mag
def _get_nominal_value_for_division(self, node, child_nominal_values):