forked from SECQUOIA/dsda-gdp
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdsda_functions.py
1525 lines (1355 loc) · 63.1 KB
/
dsda_functions.py
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
import copy
import csv
import itertools as it
import os
import time
from math import isnan
import matplotlib.pyplot as plt
import numpy as np
import pyomo.environ as pe
from gdp.dsda.model_serializer import StoreSpec, from_json, to_json
from pyomo.common.errors import InfeasibleConstraintException
from pyomo.contrib.fbbt.fbbt import fbbt
from pyomo.contrib.gdpopt.data_class import MasterProblemResult
from pyomo.core.base.misc import display
from pyomo.core.plugins.transform.logical_to_linear import \
update_boolean_vars_from_binary
from pyomo.gdp import Disjunct, Disjunction
from pyomo.opt import SolutionStatus, SolverResults
from pyomo.opt import TerminationCondition as tc
from pyomo.opt.base.solvers import SolverFactory
def get_external_information(
m: pe.ConcreteModel(),
ext_ref,
tee: bool = False,
):
"""
Function that obtains information from the model to perform the reformulation with external variables.
The model must be a GDP problem with exactly one "Exactly(k_j, [Y_j1,Y_j2,Y_j3,...])" constraint for each list of variables
[Y_j1,Y_j2,Y_j3,...] that is going to be reformulated over set j.
Args:
m: GDP model that is going to be reformulated
ext_ref: Dictionary with Boolean variables to be reformulated (keys) and their corresponding ordered sets (values). Both keys and values are pyomo objects.
tee: Display reformulation
Returns:
reformulation_dict: A dictionary of dictionaries that looks as follows:
{1:{'exactly_number':Number of external variables for this type,
'Boolean_vars_names':list with names of the ordered Boolean variables to be reformulated,
'Boolean_vars_ordered_index': Indexes where the external reformulation is applied,
'Ext_var_lower_bound': Lower bound for this type of external variable,
'Ext_var_upper_bound': Upper bound for this type of external variable },
2:{...},...}
The first key (positive integer) represent a type of external variable identified in the model. For this type of external variable
a dictionary is created.
number_of_external_variables: Number of external variables
lower_bounds: Dictionary with positive integer keys identifying the external variable, and its lower bound as value
upper_bounds: Dictionary with positive integer keys identifying the external variable, and its upper bound as value
"""
# If Boolean variables that are going to be reformulated are defined over multiple sets try:
try:
# index of the set where reformultion can be applied for a given boolean variable
ref_index = {}
# index of the sets where the reformulation cannot be applied for a given boolean variable
no_ref_index = {}
for i in ext_ref:
ref_index[i] = []
no_ref_index[i] = []
for index_set in range(len(i.index_set()._sets)):
if i.index_set()._sets[index_set].name == ext_ref[i].name:
ref_index[i].append(index_set)
else:
no_ref_index[i].append(index_set)
# If boolean variables that are going to be reformulated are defined over a single set except:
except:
# index of the set where reformultion can be applied for a given boolean variable
ref_index = {}
# index of the sets where the reformulation cannot be applied for a given boolean variable
no_ref_index = {}
for i in ext_ref:
ref_index[i] = []
no_ref_index[i] = []
if i.index_set().name == ext_ref[i].name:
ref_index[i].append(0)
else:
no_ref_index[i].append(0)
# Identify the variables that can be reformulated by performing a loop over logical constraints
count = 1
# dict of dicts: it contains information from the exactly variables that can be reformulated into external variables.
reformulation_dict = {}
for c in m.component_data_objects(pe.LogicalConstraint, descend_into=True):
if c.body.getname() == 'exactly':
exactly_number = c.body.args[0]
for possible_Boolean in ext_ref:
# expected boolean variable where the reformulation is going to be applied
expected_Boolean = possible_Boolean.name
Boolean_name_list = []
Boolean_name_list = Boolean_name_list + \
[c.body.args[1:][k]._component()._name for k in range(
len(c.body.args[1:]))]
if all(x == expected_Boolean for x in Boolean_name_list):
# expected ordered set index where the reformulation is going to be applied
expected_ordered_set_index = ref_index[possible_Boolean]
# index of sets where the reformulation is not applied
index_of_other_sets = no_ref_index[possible_Boolean]
if len(index_of_other_sets) >= 1: # If there are other indexes
Other_Sets_listOFlists = []
verification_Other_Sets_listOFlists = []
for j in index_of_other_sets:
Other_Sets_listOFlists.append(
[c.body.args[1:][k].index()[j] for k in range(len(c.body.args[1:]))])
if all(c.body.args[1:][x].index()[j] == c.body.args[1:][0].index()[j] for x in range(len(c.body.args[1:]))):
verification_Other_Sets_listOFlists.append(
True)
else:
verification_Other_Sets_listOFlists.append(
False)
# If we get to this point and it is true, it means that we can apply the reformulation for this combination of Boolean var and Exactly-type constraint
if all(verification_Other_Sets_listOFlists):
reformulation_dict[count] = {}
reformulation_dict[count]['exactly_number'] = exactly_number
# rearange boolean vars in constraint
sorted_args = sorted(c.body.args[1:], key=lambda x: x.index()[
expected_ordered_set_index[0]])
# Now work with the ordered version sorted_args instead of c.body.args[1:]
reformulation_dict[count]['Boolean_vars_names'] = [
sorted_args[k].name for k in range(len(sorted_args))]
reformulation_dict[count]['Boolean_vars_ordered_index'] = [sorted_args[k].index(
)[expected_ordered_set_index[0]] for k in range(len(sorted_args))]
reformulation_dict[count]['Ext_var_lower_bound'] = 1
reformulation_dict[count]['Ext_var_upper_bound'] = len(
sorted_args)
count = count+1
# If there is only one index, then we can apply the reformulation at this point
else:
reformulation_dict[count] = {}
reformulation_dict[count]['exactly_number'] = exactly_number
# rearange boolean vars in constraint
sorted_args = sorted(
c.body.args[1:], key=lambda x: x.index())
# Now work with the ordered version sorted_args instead of c.body.args[1:]
reformulation_dict[count]['Boolean_vars_names'] = [
sorted_args[k].name for k in range(len(sorted_args))]
reformulation_dict[count]['Boolean_vars_ordered_index'] = [
sorted_args[k].index() for k in range(len(sorted_args))]
reformulation_dict[count]['Ext_var_lower_bound'] = 1
reformulation_dict[count]['Ext_var_upper_bound'] = len(
sorted_args)
count = count+1
number_of_external_variables = sum(
reformulation_dict[j]['exactly_number'] for j in reformulation_dict)
lower_bounds = {}
upper_bounds = {}
exvar_num = 1
for i in reformulation_dict:
for j in range(reformulation_dict[i]['exactly_number']):
lower_bounds[exvar_num] = reformulation_dict[i]['Ext_var_lower_bound']
upper_bounds[exvar_num] = reformulation_dict[i]['Ext_var_upper_bound']
exvar_num = exvar_num+1
if tee:
print('\nReformulation Summary\n--------------------------------------------------------------------------')
exvar_num = 0
for i in reformulation_dict:
for j in range(reformulation_dict[i]['exactly_number']):
print('External variable x['+str(exvar_num)+'] '+' is associated to '+str(reformulation_dict[i]['Boolean_vars_names']) +
' and it must be within '+str(reformulation_dict[i]['Ext_var_lower_bound'])+' and '+str(reformulation_dict[i]['Ext_var_upper_bound'])+'.')
exvar_num = exvar_num+1
print('\nThere are '+str(number_of_external_variables) +
' external variables in total')
return reformulation_dict, number_of_external_variables, lower_bounds, upper_bounds
def external_ref(
m: pe.ConcreteModel(),
x,
extra_logic_function,
dict_extvar: dict = {},
mip_ref: bool = False,
transformation: str = 'bigm',
tee: bool = False
):
"""
Function that
Args:
m: GDP model that is going to be reformulated
x: List with current value of the external variables
extra_logic_function: Function that returns a list of lists of the form [a,b], where a is an expressions of the reformulated Boolean variables and b is an equivalent Boolean or indicator variable (b<->a)
dict_extvar: A dictionary of dictionaries that looks as follows:
{1:{'exactly_number':Number of external variables for this type,
'Boolean_vars_names':list with names of the ordered Boolean variables to be reformulated,
'Boolean_vars_ordered_index': Indexes where the external reformulation is applied,
'Binary_vars_names':list with names of the ordered Binary variables to be reformulated, [Potentially]
'Binary_vars_ordered_index': Indexes where the external reformulation is applied, [Potentially]
'Ext_var_lower_bound': Lower bound for this type of external variable,
'Ext_var_upper_bound': Upper bound for this type of external variable },
2:{...},...}
The first key (positive integer) represent a type of external variable identified in the model. For this type of external variable
a dictionary is created.
mip_ref: whether the reformulation will consider binary variables besides Booleans coming from a GDP->MIP reformulation
tee: Display reformulation
Returns:
m: A model where the independent Boolean variables that were reformulated are fixed and Boolean/indicator variables that are calculated in
terms of the independent Boolean variables are fixed too (depending on the extra_logic_function provided by the user)
"""
# This part of code is required due to the deep copy issue: we have to compare Boolean variables by name
for i in dict_extvar:
dict_extvar[i]['Boolean_vars'] = []
for j in dict_extvar[i]['Boolean_vars_names']:
for boolean in m.component_data_objects(pe.BooleanVar, descend_into=True):
if(boolean.name == j):
dict_extvar[i]['Boolean_vars'] = dict_extvar[i]['Boolean_vars']+[boolean]
if mip_ref:
# This part of code is required due to the deep copy issue: we have to compare binary variables by name
# By uncommenting in previous function extvars_gdp_to_mip we would pass directly dict_extvar[i]['Binary_vars']
dict_extvar[i]['Binary_vars'] = []
for j in dict_extvar[i]['Binary_vars_names']:
for binary in m.component_data_objects(pe.Var, descend_into=True):
if(binary.name == j):
dict_extvar[i]['Binary_vars'] = dict_extvar[i]['Binary_vars']+[binary]
# The function would start here if there were no problems with deep copy.
ext_var_position = 0
for i in dict_extvar:
for j in range(dict_extvar[i]['exactly_number']):
for k in range(1, len(dict_extvar[i]['Boolean_vars'])+1):
if x[ext_var_position] == k:
if not mip_ref:
# fix True variables: depending on the current value of the external variables, some Independent Boolean variables can be fixed
dict_extvar[i]['Boolean_vars'][k-1].fix(True)
else:
# fix 0 variables: depending on the current value of the external variables, some Independent Binary variables can be fixed
dict_extvar[i]['Binary_vars'][k-1].fix(1)
dict_extvar[i]['Boolean_vars'][k-1].set_value(True)
ext_var_position = ext_var_position+1
# Double loop required from fact that exactly_number >= 1. TODO Is there a better way to do this?
for j in range(dict_extvar[i]['exactly_number']):
for k in range(1, len(dict_extvar[i]['Boolean_vars'])+1):
if not mip_ref:
# fix False variables: If the independent Boolean variable is not fixed at "True", then it is fixed at "False".
if not dict_extvar[i]['Boolean_vars'][k-1].is_fixed():
dict_extvar[i]['Boolean_vars'][k-1].fix(False)
else:
# fix 0 variables: If the independent Boolean variable is not fixed at "1", then it is fixed at "0".
if not dict_extvar[i]['Binary_vars'][k-1].is_fixed():
dict_extvar[i]['Binary_vars'][k-1].fix(0)
dict_extvar[i]['Boolean_vars'][k-1].set_value(False)
# Other Boolean and Indicator variables are fixed depending on the information provided by the user
logic_expr = extra_logic_function(m)
for i in logic_expr:
if not mip_ref:
i[1].fix(pe.value(i[0]))
else:
i[1].set_value(pe.value(i[0]))
pe.TransformationFactory('core.logical_to_linear').apply_to(m)
if mip_ref: # Transform problem to MINLP
transformation_string = 'gdp.' + transformation
pe.TransformationFactory(transformation_string).apply_to(m)
else: # Deactivate disjunction's constraints in the case of pure GDP
pe.TransformationFactory('gdp.fix_disjuncts').apply_to(m)
pe.TransformationFactory('contrib.deactivate_trivial_constraints').apply_to(
m, tmp=False, ignore_infeasible=True)
if tee:
print('\nFixed variables at current iteration:\n')
print('\n Independent Boolean variables\n')
for i in dict_extvar:
for k in range(1, len(dict_extvar[i]['Boolean_vars'])+1):
print(dict_extvar[i]['Boolean_vars_names'][k-1] +
'='+str(dict_extvar[i]['Boolean_vars'][k-1].value))
print('\n Dependent Boolean variables and disjunctions\n')
for i in logic_expr:
print(i[1].name+'='+str(i[1].value))
if mip_ref:
print('\n Independent binary variables\n')
for i in dict_extvar:
for k in range(1, len(dict_extvar[i]['Binary_vars'])+1):
print(dict_extvar[i]['Binary_vars_names'][k-1] +
'='+str(dict_extvar[i]['Binary_vars'][k-1].value))
return m
def extvars_gdp_to_mip(
m: pe.ConcreteModel(),
gdp_dict_extvar: dict = {},
transformation: str = 'bigm',
):
"""
Function that
Args:
m: GDP model that is going to be reformulated
gdp_dict_extvar: A dictionary of dictionaries that looks as follows:
{1:{'exactly_number':Number of external variables for this type,
'Boolean_vars_names':list with names of the ordered Boolean variables to be reformulated,
'Boolean_vars_ordered_index': Indexes where the external reformulation is applied,
'Ext_var_lower_bound': Lower bound for this type of external variable,
'Ext_var_upper_bound': Upper bound for this type of external variable },
2:{...},...}
transformation: GDP to MINLP transformation to be used
The first key (positive integer) represent a type of external variable identified in the model. For this type of external variable
a dictionary is created.
tee: Display reformulation
Returns:
m: A MIP model transformed from the original GDP m model via the 'transformation' argument
mip_dict_extvar: A dictionary of dictionaries that looks as follows:
{1:{'exactly_number':Number of external variables for this type,
'Boolean_vars_names':list with names of the ordered Boolean variables to be reformulated,
'Boolean_vars_ordered_index': Indexes where the external reformulation is applied,
'Binary_vars_names':list with names of the ordered Binary variables to be reformulated,
'Binary_vars_ordered_index': Indexes where the external reformulation is applied,
'Ext_var_lower_bound': Lower bound for this type of external variable,
'Ext_var_upper_bound': Upper bound for this type of external variable },
2:{...},...}
"""
# Transformation step
pe.TransformationFactory('core.logical_to_linear').apply_to(m)
transformation_string = 'gdp.' + transformation
pe.TransformationFactory(transformation_string).apply_to(m)
mip_dict_extvar = copy.deepcopy(gdp_dict_extvar)
# This part of code is required due to the deep copy issue: we have to compare Boolean variables by name
for i in mip_dict_extvar.keys():
mip_dict_extvar[i]['Boolean_vars'] = []
for j in mip_dict_extvar[i]['Boolean_vars_names']:
for boolean in m.component_data_objects(pe.BooleanVar, descend_into=True):
if(boolean.name == j):
mip_dict_extvar[i]['Boolean_vars'] = mip_dict_extvar[i]['Boolean_vars']+[boolean]
# Add extra terms to the dictionary to be relevant for binary variables
mip_dict_extvar[i]['Binary_vars_names'] = [
boolean.get_associated_binary().name for boolean in mip_dict_extvar[i]['Boolean_vars']]
# Uncomment the next line in case that deepcopy works
# mip_dict_extvar[key]['Binary_vars'] = [
# boolean.get_associated_binary() for boolean in gdp_dict_extvar[key]['Boolean_vars']]
mip_dict_extvar[i]['Binary_vars_ordered_index'] = mip_dict_extvar[i]['Boolean_vars_ordered_index']
return m, mip_dict_extvar
def preprocess_problem(m, simple: bool = True):
"""
Function that applies certain tranformations to the mdoel to first verify that it is not trivially
infeasible (via FBBT) and second, remove extra constraints to help NLP solvers
Args:
m: MI(N)LP model that is going to be preprocessed
simple: Boolean variable to carry on a simple preprocessing (only FBBT) or a more complete one, prone to fail
Returns:
"""
if not simple:
pe.TransformationFactory('contrib.detect_fixed_vars').apply_to(m)
pe.TransformationFactory('contrib.propagate_fixed_vars').apply_to(m)
pe.TransformationFactory('contrib.remove_zero_terms').apply_to(m)
pe.TransformationFactory('contrib.propagate_zero_sum').apply_to(m)
pe.TransformationFactory(
'contrib.constraints_to_var_bounds').apply_to(m)
pe.TransformationFactory('contrib.detect_fixed_vars').apply_to(m)
pe.TransformationFactory('contrib.propagate_zero_sum').apply_to(m)
pe.TransformationFactory('contrib.deactivate_trivial_constraints').apply_to(
m, tmp=False, ignore_infeasible=True)
fbbt(m)
def solve_subproblem(
m: pe.ConcreteModel(),
subproblem_solver: str = 'knitro',
subproblem_solver_options: dict = {},
timelimit: float = 10,
gams_output: bool = False,
tee: bool = False,
rel_tol: float = 1e-3,
) -> pe.ConcreteModel():
"""
Function that checks feasibility and subproblem model.
Note integer variables have to be previously fixed in the external reformulation
Args:
m: Fixed subproblem model that is to be solved
subproblem_solver: MINLP or NLP solver algorithm
timelimit: time limit in seconds for the solve statement
gams_output: Determine keeping or not GAMS files
tee: Display iteration output
rel_tol: Relative optimality tolerance
Returns:
m: Solved subproblem model
"""
# Initialize D-SDA status
m.dsda_status = 'Initialized'
m.dsda_usertime = 0
try:
# Feasibility and preprocessing checks
preprocess_problem(m, simple=True)
except InfeasibleConstraintException:
m.dsda_status = 'FBBT_Infeasible'
return m
output_options = {}
# Output report
if gams_output:
dir_path = os.path.dirname(os.path.abspath(__file__))
gams_path = os.path.join(dir_path, "gamsfiles/")
if not(os.path.exists(gams_path)):
print('Directory for automatically generated files ' +
gams_path + ' does not exist. We will create it')
os.makedirs(gams_path)
output_options = {'keepfiles': True,
'tmpdir': gams_path,
'symbolic_solver_labels': True}
subproblem_solver_options['add_options'] = subproblem_solver_options.get(
'add_options', [])
subproblem_solver_options['add_options'].append(
'option reslim=%s;' % timelimit)
subproblem_solver_options['add_options'].append(
'option optcr=%s;' % rel_tol)
# Solve
solvername = 'gams'
opt = SolverFactory(solvername, solver=subproblem_solver)
m.results = opt.solve(m, tee=tee,
**output_options,
**subproblem_solver_options,
skip_trivial_constraints=True,
)
m.dsda_usertime = m.results.solver.user_time
# Assign D-SDA status
if m.results.solver.termination_condition == 'infeasible':
m.dsda_status = 'Evaluated_Infeasible'
else: # Considering locallyOptimal, optimal, globallyOptimal, and maxtime TODO Fix this
m.dsda_status = 'Optimal'
# if m.results.solver.termination_condition == 'locallyOptimal' or m.results.solver.termination_condition == 'optimal' or m.results.solver.termination_condition == 'globallyOptimal':
# m.dsda_status = 'Optimal'
return m
def solve_with_minlp(
m: pe.ConcreteModel(),
transformation: str = 'bigm',
minlp: str = 'baron',
minlp_options: dict = {},
timelimit: float = 10,
gams_output: bool = False,
tee: bool = False,
rel_tol: float = 0.001,
) -> pe.ConcreteModel():
"""
Function that transforms a GDP model and solves it as a mixed-integer nonlinear
programming (MINLP) model.
Args:
m: Pyomo GDP model that is to be solved using MINLP
transformation: GDP to MINLP transformation to be used
minlp: MINLP solver algorithm
minlp_options: MINLP solver algorithm options
timelimit: time limit in seconds for the solve statement
gams_output: Determine keeping or not GAMS files
tee: Dsiplay iterations
rel_tol: Relative optimality tolerance
Returns:
m: Solved MINLP model
"""
# Transformation step
pe.TransformationFactory('core.logical_to_linear').apply_to(m)
transformation_string = 'gdp.' + transformation
pe.TransformationFactory(transformation_string).apply_to(m)
# Output report
output_options = {}
if gams_output:
dir_path = os.path.dirname(os.path.abspath(__file__))
gams_path = os.path.join(dir_path, "gamsfiles/")
if not(os.path.exists(gams_path)):
print('Directory for automatically generated files ' +
gams_path + ' does not exist. We will create it')
os.makedirs(gams_path)
output_options = {'keepfiles': True,
'tmpdir': gams_path,
'symbolic_solver_labels': True}
minlp_options['add_options'] = minlp_options.get('add_options', [])
minlp_options['add_options'].append('option reslim=%s;' % timelimit)
minlp_options['add_options'].append('option optcr=%s;' % rel_tol)
# Solve
solvername = 'gams'
opt = SolverFactory(solvername, solver=minlp)
m.results = opt.solve(m, tee=tee,
**output_options,
**minlp_options,
)
# update_boolean_vars_from_binary(m)
return m
def solve_with_gdpopt(
m: pe.ConcreteModel(),
mip: str = 'cplex',
mip_options: dict = {},
nlp: str = 'knitro',
nlp_options: dict = {},
minlp: str = 'baron',
minlp_options: dict = {},
timelimit: float = 10,
strategy: str = 'LOA',
mip_output: bool = False,
nlp_output: bool = False,
minlp_output: bool = False,
tee: bool = False,
rel_tol: float = 1e-3,
) -> pe.ConcreteModel():
"""
Function that solves GDP model using GDPopt
Args:
m: GDP model that is to be solved
mip: MIP solver algorithm
nlp: NLP solver algorithm
timelimit: time limit in seconds for the solve statement
strategy: GDPopt strategy
mip_output: Determine keeping or not GAMS files of the MIP model
nlp_output: Determine keeping or not GAMS files of the NLP model
tee: Display iterations
rel_tol: Relative optimality tolerance for subproblems and GDPOpt itself
Returns:
m: Solved GDP model
"""
# Transformation step
pe.TransformationFactory('core.logical_to_linear').apply_to(m)
# Output report
mip_options['add_options'] = mip_options.get('add_options', [])
mip_options['add_options'].append('option optcr=0.0;')
nlp_options['add_options'] = nlp_options.get('add_options', [])
nlp_options['add_options'].append('option optcr=%s;' % rel_tol)
minlp_options['add_options'] = minlp_options.get('add_options', [])
minlp_options['add_options'].append('option optcr=%s;' % rel_tol)
if mip_output:
dir_path = os.path.dirname(os.path.abspath(__file__))
gams_path = os.path.join(dir_path, "gamsfiles/")
if not(os.path.exists(gams_path)):
print('Directory for automatically generated files ' +
gams_path + ' does not exist. We will create it')
os.makedirs(gams_path)
mip_options['keepfiles'] = True
mip_options['tmpdir'] = gams_path
mip_options['symbolic_solver_labels'] = True
if nlp_output:
dir_path = os.path.dirname(os.path.abspath(__file__))
gams_path = os.path.join(dir_path, "gamsfiles/")
if not(os.path.exists(gams_path)):
print('Directory for automatically generated files ' +
gams_path + ' does not exist. We will create it')
os.makedirs(gams_path)
nlp_options['keepfiles'] = True
nlp_options['tmpdir'] = gams_path
nlp_options['symbolic_solver_labels'] = True
if minlp_output:
dir_path = os.path.dirname(os.path.abspath(__file__))
gams_path = os.path.join(dir_path, "gamsfiles/")
if not(os.path.exists(gams_path)):
print('Directory for automatically generated files ' +
gams_path + ' does not exist. We will create it')
os.makedirs(gams_path)
minlp_options['keepfiles'] = True
minlp_options['tmpdir'] = gams_path
minlp_options['symbolic_solver_labels'] = True
# Solve
solvername = 'gdpopt'
opt = SolverFactory(solvername)
m.results = opt.solve(m, tee=tee,
strategy=strategy,
time_limit=timelimit,
mip_solver='gams',
mip_solver_args=dict(
solver=mip, warmstart=True, **mip_options),
nlp_solver='gams',
nlp_solver_args=dict(
solver=nlp, warmstart=True, tee=tee, **nlp_options),
minlp_solver='gams',
minlp_solver_args=dict(
solver=minlp, warmstart=True, tee=tee, **minlp_options),
# mip_presolve=True,
init_strategy='fix_disjuncts',
# set_cover_iterlim=0,
iterlim=1000,
force_subproblem_nlp=True,
subproblem_presolve=False
# bound_tolerance=rel_tol
# calc_disjunctive_bounds=True
)
# update_boolean_vars_from_binary(m)
return m
def neighborhood_k_eq_2(dimension: int = 2) -> dict:
"""
Function creates a k=2 neighborhood of the given dimension
Args:
dimension: Dimension of the neighborhood
Returns:
directions: Dictionary contaning in each item a list with a direction within the neighborhood
"""
num_neigh = 2*dimension
neighbors = np.concatenate(
(np.eye(dimension, dtype=int), -np.eye(dimension, dtype=int)), axis=1)
directions = {}
for i in range(num_neigh):
direct = []
directions[i+1] = direct
for j in range(dimension):
direct.append(neighbors[j, i])
return directions
def neighborhood_k_eq_inf(dimension: int = 2) -> dict:
"""
Function creates a k=Infinity neighborhood of the given dimension
Args:
dimension: Dimension of the neighborhood
Returns:
temp: Dictionary contaning in each item a list with a direction within the neighborhood
TODO change temp name here to something more useful
"""
neighbors = list(it.product([-1, 0, 1], repeat=dimension))
directions = {}
for i in range(len(neighbors)):
directions[i+1] = list(neighbors[i])
temp = directions.copy()
for i in directions.keys():
if temp[i] == [0]*dimension:
temp.pop(i, None)
return temp
def initialize_model(
m: pe.ConcreteModel(),
json_path=None,
from_feasible: bool = False,
feasible_model: str = '',
) -> pe.ConcreteModel():
"""
Function that return an initialized model from an existing json file
Args:
m: Pyomo model that is to be initialized
from_feasible: If initialization is made from an external file
feasible_model: Feasible initialization path or example
Returns:
m: Initialized Pyomo model
"""
wts = StoreSpec.value()
if json_path is None:
os.path.join(os.path.curdir)
dir_path = os.path.dirname(os.path.abspath(__file__))
if from_feasible:
json_path = os.path.join(
dir_path, feasible_model+'_initialization.json')
else:
json_path = os.path.join(
dir_path, 'dsda_initialization.json')
from_json(m, fname=json_path, wts=wts)
return m
def generate_initialization(
m: pe.ConcreteModel(),
starting_initialization: bool = False,
model_name: str = '',
human_read: bool = True,
wts=StoreSpec.value(),
):
"""
Function that creates a json file for initialization based on a model m
Args:
m: Base Pyomo model for initializtion
starting_intialization: Use to create "dsda_starting_initialization.json" file with a known feasible initialized model m
model_name: Name of the model for the initialization
human_read: Make the json file readable by a human
wts: What to save, initially the values, but we might want something different. Check model_serializer tests for examples
Returns:
json_path: Path where json file is stored
"""
dir_path = os.path.dirname(os.path.abspath(__file__))
if starting_initialization:
json_path = os.path.join(
dir_path, model_name + '_initialization.json')
else:
if model_name != '':
json_path = os.path.join(
dir_path, model_name + '_initialization.json')
else:
json_path = os.path.join(
dir_path, 'dsda_initialization.json')
to_json(m, fname=json_path, human_read=human_read, wts=wts)
return json_path
def find_actual_neighbors(
start: list,
neighborhood: dict,
min_allowed: dict = {},
max_allowed: dict = {}
) -> dict:
"""
Function that creates all neighbors of a given point. Neighbor 0 is the starting point
Args:
start: Point of which neighbors want to be created
neighborhood: Neighborhood (output of a k-Neighborhood function)
min_allowed: In keys contains external variables and in items their respective lower bounds
max_allowed: In keys contains external variables and in items their respective upper bounds
Returns:
new_neighbors: Contains neighbors of the actual point
"""
neighbors = {0: start}
for i in neighborhood.keys(): # Calculate neighbors
neighbors[i] = list(map(sum, zip(start, list(neighborhood[i]))))
new_neighbors = {}
num_vars = len(neighbors[0])
for i in neighbors.keys():
checked = 0
for j in range(num_vars): # Check if within bounds
if neighbors[i][j] >= min_allowed[j+1] and neighbors[i][j] <= max_allowed[j+1]:
checked += 1
if checked == num_vars: # Add neighbor if all variables are within bounds
new_neighbors[i] = neighbors[i]
return new_neighbors
def evaluate_neighbors(
ext_vars: dict,
fmin: float,
model_function,
model_args: dict,
ext_dict: dict,
ext_logic,
mip_transformation: bool = False,
transformation: str = 'bigm',
subproblem_solver: str = 'knitro',
subproblem_solver_options: dict = {},
iter_timelimit: float = 10,
current_time: float = 0,
timelimit: float = 3600,
gams_output: bool = False,
tee: bool = False,
global_tee: bool = True,
rel_tol: float = 1e-3,
global_evaluated: list = [],
init_path=None,
):
"""
Function that evaluates a group of given points and returns the best
Args:
ext_vars: dict with neighbors where neighbor 0 is actual point
fmin: Objective at actual point
model_function: function that returns GDP model to be solved
model_args: Contains the argument values needed for model_function
ext_dict: Dictionary with Boolean variables to be reformulated (keys) and their corresponding ordered sets (values)
ext_logic: Function that returns a list of lists of the form [a,b], where a is an expressions of the reformulated Boolean variables and b is an equivalent Boolean or indicator variable (b<->a)
mip_transformation: Whether to solve the enumeration using the external variables applied to the MIP problem insed of the GDP
transformation: Which transformation to apply to the GDP
subproblem_solver: MINLP or NLP solver algorithm
subproblem_solver_options: MINLP or NLP solver algorithm options
iter_timelimit: time limit in seconds for the solve statement for each iteration
current_time: Current time in global algorithm
timelimit: time limit in seconds for the algorithm
gams_output: Determine keeping or not GAMS files
tee: Display iteration output
global_tee: display D-SDA iteration output
rel_tol: Relative optimality tolerance
global_evaluated: list with points already evaluated
init_path: path to initialization file
Returns:
fmin: Type int and gives the best neighbor's objective
best_var: Type list and gives the best neighbor
best_dir: Type int and is the steepest direction (key in neighborhood)
improve: Type bool and shows if an improvement was made while looking for neighbors
evaluation_time: Total solver-statement time only
ns_evaluated: evaluations in neighbor search
best_path: path to json with best solution found
"""
# Global Tolerance parameters
epsilon = 1e-10
abs_tol = 1e-5
min_improve = 1e-5
min_improve_rel = 1e-3
# Initialize
ns_evaluated = []
evaluation_time = 0
improve = False
best_var = ext_vars[0]
here = ext_vars[0]
best_dir = 0 # Position in dictionary
best_dist = 0
best_path = init_path
temp = ext_vars # TODO change name to something more saying. Points? Combinations?
temp.pop(0, None)
if global_tee:
print()
print('Neighbor search around:', best_var)
for i in temp.keys(): # Solve all models
if temp[i] not in global_evaluated:
m = model_function(**model_args)
m_init = initialize_model(m, json_path=init_path)
if mip_transformation: # If you want a MIP reformulation, go ahead and use it'
m_init, ext_dict = extvars_gdp_to_mip(
m=m,
gdp_dict_extvar=ext_dict,
transformation=transformation,
)
m_fixed = external_ref(
m=m_init,
x=temp[i],
extra_logic_function=ext_logic,
dict_extvar=ext_dict,
mip_ref=mip_transformation,
tee=False,
)
t_remaining = min(iter_timelimit, timelimit -
(time.perf_counter() - current_time))
if t_remaining < 0: # No time reamining for optimization
break
m_solved = solve_subproblem(
m=m_fixed,
subproblem_solver=subproblem_solver,
subproblem_solver_options=subproblem_solver_options,
timelimit=t_remaining,
gams_output=gams_output,
tee=tee)
evaluation_time += m_solved.dsda_usertime
ns_evaluated.append(temp[i])
t_end = time.perf_counter()
if m_solved.dsda_status == 'Optimal': # Check if D-SDA status is optimal
if global_tee:
print('Evaluated:', temp[i], ' | Objective:', round(pe.value(
m_solved.obj), 5), ' | Global Time:', round(t_end - current_time, 2))
dist = sum((x-y)**2 for x, y in zip(temp[i], here))
act_obj = pe.value(m_solved.obj)
# Assuming minimization problem
# Implements heuristic of largest move
if not improve:
# We want a minimum improvement in the first found solution
if (fmin - act_obj) > min_improve or (fmin - act_obj)/(abs(fmin)+epsilon) > min_improve_rel:
fmin = act_obj
best_var = temp[i]
best_dir = i
best_dist = dist
improve = True
best_path = generate_initialization(
m_solved, starting_initialization=False, model_name='best')
else:
# We want slightly worse solutions if the distance is larger
if (((act_obj - fmin) < abs_tol) or ((act_obj - fmin)/(abs(fmin)+epsilon) < rel_tol)) and dist >= best_dist:
fmin = act_obj
best_var = temp[i]
best_dir = i
best_dist = dist
improve = True
best_path = generate_initialization(
m_solved, starting_initialization=False, model_name='best')
if time.perf_counter() - current_time > timelimit: # current
break
if global_tee:
print()
print('New best neighbor:', best_var)
return fmin, best_var, best_dir, improve, evaluation_time, ns_evaluated, best_path
def do_line_search(
start: list,
fmin: float,
direction: list,
model_function,
model_args: dict,
ext_dict: dict,
ext_logic,
mip_transformation: bool = False,
transformation: str = 'bigm',
subproblem_solver: str = 'knitro',
subproblem_solver_options: dict = {},
min_allowed: dict = {},
max_allowed: dict = {},
iter_timelimit: float = 10,
timelimit: float = 3600,
current_time: float = 0,
gams_output: bool = False,
tee: bool = False,
global_tee: bool = False,
rel_tol: float = 1e-3,
global_evaluated: list = [],
init_path=None
):
"""
Function that moves in a given "best direction" and evaluates the new moved point
Args:
start: Point of that is to be moved
fmin: Objective at actual point
direction: moving direction
model_function: function that returns GDP model to be solved
model_args: Contains the argument values needed for model_function
ext_dict: Dictionary with Boolean variables to be reformulated (keys) and their corresponding ordered sets (values)
ext_logic: Function that returns a list of lists of the form [a,b], where a is an expressions of the reformulated Boolean variables and b is an equivalent Boolean or indicator variable (b<->a)
mip_transformation: Whether to solve the enumeration using the external variables applied to the MIP problem insed of the GDP
transformation: Which transformation to apply to the GDP
subproblem_solver: MINLP or NLP solver algorithm
subproblem_solver_options: MINLP or NLP solver algorithm options
min_allowed: In keys contains external variables and in items their respective lower bounds
max_allowed: In keys contains external variables and in items their respective upper bounds
iter_timelimit: time limit in seconds for the solve statement for each iteration
current_time: Current time in global algorithm
gams_output: Determine keeping or not GAMS files
tee: Display iteration output
global_tee: display D-SDA iteration output
rel_tol: Relative optimality tolerance
global_evaluated: list with points already evaluated
init_path: path to initialization file
Returns:
fmin: Type int and gives the moved point objective
best_var: Type list and gives the moved point
moved: Type bool and shows if an improvement was made while line searching
ls_time: Total solver-statement time only
ls_evaluated: evaluations in line search
new_path: path of best json file
"""
# Global Tolerance parameters
epsilon = 1e-10
min_improve = 1e-5
min_improve_rel = 1e-3
# Initialize
ls_evaluated = []
ls_time = 0
best_var = start
moved = False
new_path = init_path
# Line search in given direction
moved_point = list(map(sum, zip(list(start), list(direction))))
checked = 0
for j in range(len(moved_point)): # Check if within bounds
if moved_point[j] >= min_allowed[j+1] and moved_point[j] <= max_allowed[j+1]:
checked += 1
if checked == len(moved_point): # Solve model
if moved_point not in global_evaluated:
m = model_function(**model_args)
m_init = initialize_model(m, json_path=init_path)
if mip_transformation: # If you want a MIP reformulation, go ahead and use it'
m_init, ext_dict = extvars_gdp_to_mip(
m=m,
gdp_dict_extvar=ext_dict,
transformation=transformation,
)