-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest.cpp
More file actions
1686 lines (1433 loc) · 64.7 KB
/
Copy pathtest.cpp
File metadata and controls
1686 lines (1433 loc) · 64.7 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
/*--------------------------------------------------------------------------*/
/*-------------------------- File test.cpp ---------------------------------*/
/*--------------------------------------------------------------------------*/
/** @file
* Main for testing LagrangianDualSolver with UCBlock.
*
* An UCBlock instance is loaded from netCDF file, all the Solver listed in the
* given BlockSolverConfig are registered to it, the UCBlock is solved by each
* of them and the results are cross-checked against each other (and against a
* reference objective value, where one is known). Each Solver enters the
* cross-check as its [ get_lb() , get_ub() ] interval, valid by the base
* Solver contract, and is measured against the best bounds all of them
* provide: correctness always, and the tolerance it declares when it says
* it delivered it. That tolerance is the dblRelAcc of its ComputeConfig
* unless -E overrides it. Nothing here is tied to a particular Solver, so
* bringing a new one into the comparison is a matter of listing it in the
* BlockSolverConfig.
*
* Although the tester does not even include BundleSolver, some
* BundleSolver-specific steps are done if a macro is set.
*
* The tester has some parts for the future extension when the UCBlock is
* repeatedly randomly modified and re-solved several times, but this is not
* done yet.
*
* Called as UCBlock_test --pollutant, with no other argument, the tester
* instead checks the pollutant budget constraints of UCBlock.
*
* Called as UCBlock_test --scale, with no other argument, it instead checks
* the scale factor of a unit, i.e., the number of copies of it the UCBlock
* holds. The Objective of a scaled unit is the scale factor times the cost
* of one copy, while its Variable stay those of one copy, which the rows of
* the UCBlock that use them multiply by the factor [see UnitBlock::scale()]:
* a unit scaled after the model is built must therefore give the model of
* one scaled before it, and the data of the unit must not move. What a
* dualizing Solver writes into the Objective, being scaled, is divided back
* into the cost of one copy, which is what the (physical) DP Solvers read,
* and they in turn answer for all the copies. This is checked on a thermal
* unit in each of the seven formulations, with and without the perspective
* cuts, and on a nuclear one, both of them carrying a cost of every kind
* the Objective can hold, the two reserves and the reactive power.
*
* The PyPSA instances of batch-pypsa check the pollutant budget
* constraints against the objective value of PyPSA, which however writes a
* limit with a single zone spanning all the nodes, and knows nothing of what
* is peculiar to UCBlock: several zones per pollutant, a node belonging to no
* zone, the scale of a unit, the Modification that change a budget, the
* Solution and the netCDF form of all this. These are checked here, on
* instances small enough that every optimum is known.
*
* All the instances share the same data: two time instants with a demand of
* 80 and 60 at node 2 of three nodes on a path, whose lines are never binding;
* three IntermittentUnitBlock U0, U1 and U2 at nodes 0, 1 and 2, with a
* capacity of 100 and a cost of 10, 30 and 60; a SlackUnitBlock at node 2
* with a cost of 1000; in some instances a BatteryUnitBlock at node 2, empty
* at the start, with a capacity of 100 in either direction and a maximum
* level of 100. They differ in the pollutants:
*
* - N: none, whose optimum is 1400;
*
* - A: CO2 with two zones, { 0 } with budget 100 and { 1 , 2 } with budget
* 1000, rates 1 and 0.5 for U0 and U1; NOx with one zone { 0 , 1 }, node 2
* belonging to none, budget 1000, rates 0.2, 0.1 and 5 for U0, U1 and U2
* (the rate of U2 must not count). Optimum 2200, dual of CO2 in zone 0
* equal to 20;
*
* - A2: A with a NOx budget of 20, optimum 3000 with dual 200; with U1
* scaled by 0.25, 3150 with dual 250;
*
* - B: CO2 alone, with neither NumberPollutantZones nor PollutantZones (one
* zone of all the nodes), budget 50 and rates depending on time (1 and 0.5
* at time 0, 1.5 and 0.5 at time 1), optimum 5400 with dual 60;
*
* - M1: CO2 with rates 1 and 2 for U0 and U1, no upper bound and a lower
* bound of 180 (PollutantMinBudget), optimum 2200 with dual 20;
*
* - M2: as M1 with both bounds equal to 150, optimum 1600 (1400 once the
* lower bound is removed);
*
* - S0 and S: the battery and CO2 with rates 1 and 0.5 and budget 100, the
* level of the battery at the end of the horizon counting -2 in S
* (PollutantStorageRho), which lowers the optimum from 3000 to 1800; with
* the battery scaled by 0.1 the optimum of S is 2700.
*
* The optima have been computed on a linear program written independently
* of UCBlock. On each instance it is checked that the value is the expected
* one, that the coefficients of every row are the scale of the unit times the
* factor of the data, that the dual has the expected absolute value (not on
* S0 and S, whose battery makes the problem a MILP), that UCBlock::is_feasible()
* holds at the optimum while a solution exceeding a budget violates the
* rows, and that the instance written back by UCBlock::serialize() has the
* same optimum. On A the duals also go through a UCBlockSolution and its
* netCDF form; on A2 and M2 the setters of the budget and of its lower bound,
* by range and by subset, change the rows of the attached Solver; on A2
* scaling a unit with the Solver attached gives the optimum of the scaled
* instance read from scratch, also when the unit is held by a LagBFunction
* as a LagrangianDualSolver does, and with a LagrangianDualSolver attached
* (whose ComputeConfig is LDCfg-tight.txt, the LDCfg.txt of the batches
* with a tighter threshold) the Lagrangian dual gives that same optimum, whether the unit is
* scaled before the Solver is attached or after; on S scaling the battery
* gives the expected optimum. Finally, an instance with inconsistent data
* must be refused by UCBlock::deserialize() in five ways: two zones and no
* PollutantZones, a PollutantRho of the wrong size, a
* TotalNumberPollutantZones that is not the sum of NumberPollutantZones, a
* NumberStorages that is not the number of storages of the units, and a
* PollutantStorageRho of the wrong size.
*
* The instances are written in a temporary directory, and solved by the first
* :MILPSolver in the Solver factory.
*
* \author Antonio Frangioni \n
* Dipartimento di Informatica \n
* Universita' di Pisa \n
*
* \author Donato Meoli \n
* Dipartimento di Informatica \n
* Universita' di Pisa \n
*
* \copyright © by Antonio Frangioni, Donato Meoli
*/
/*--------------------------------------------------------------------------*/
/*-------------------------------- MACROS ----------------------------------*/
/*--------------------------------------------------------------------------*/
#define LOG_LEVEL 2
// -1 = no log at all, not even pass/fail
// 0 = only pass/fail
// 1 = result of each test
// 2 = + solver log
// 3 = + save LP file
// 4 = + print data
#if( LOG_LEVEL >= 1 )
#define LOG1( x ) std::cout << x
#define CLOG1( y , x ) if( y ) std::cout << x
#if( LOG_LEVEL >= 2 )
#define LOG_ON_COUT 1
// if nonzero, the 2nd Solver (LagrangianDualSolver) log is sent on std::cout
// rather than on a file
#endif
#else
#define LOG1( x )
#define CLOG1( y , x )
#endif
/*--------------------------------------------------------------------------*/
// if nonzero, the 2nd Solver attached to the UCBlock is assumed to be a
// LagrangianDualSolver (or PrimalProximalHeur) using [Parallel]BundleSolver
// as the "inner" solver; parameters from the BlockSolverConfig are read and
// set so that, if "easy components" are used, all UnitBlock that are
// ThermalUnitBlock or HydroSystemUnitBlock are attached an appropriate
// Solver, whereas all other inner Block are treated as "easy components"
#define USE_BundleSolver 1
/*--------------------------------------------------------------------------*/
// if nonzero, the 1st Solver attached to the UCBlock is detached
// and re-attached to it at all iterations
#define DETACH_1ST 0
// if nonzero, the 2nd Solver attached to the UCBlock is detached and
// re-attached to it at all iterations
#define DETACH_2ND 0
/*--------------------------------------------------------------------------*/
// if nonzero, the two Block are not solved at every round of changes, but
// only every SKIP_BEAT + 1 rounds. this allows changes to accumulate, and
// therefore puts more pressure on the Modification handling of the Solver
// (in case this tries to do "smart" things rather than dumbly processing
// each one in turn)
//
// note that the number of rounds of changes is them multiplied by
// SKIP_BEAT + 1, so that the input parameter still dictates the number of
// Block solutions
#define SKIP_BEAT 0
/*--------------------------------------------------------------------------*/
/*------------------------------ INCLUDES ----------------------------------*/
/*--------------------------------------------------------------------------*/
#include <cmath>
#include <cstdlib>
#include <filesystem>
#include <random>
#include <unistd.h>
#include "common_utils.h"
#include "PolyhedralFunctionBlock.h"
#include "UCBlock.h"
#include "ThermalUnitBlock.h"
#include "HydroSystemUnitBlock.h"
#include "BatteryUnitBlock.h"
#include "BlockSolverConfig.h"
#include "CDASolver.h"
#include "LinearFunction.h"
#include "DQuadFunction.h"
#include "LagBFunction.h"
/*--------------------------------------------------------------------------*/
/*-------------------------------- USING -----------------------------------*/
/*--------------------------------------------------------------------------*/
using namespace SMSpp_di_unipi_it;
/*--------------------------------------------------------------------------*/
/*-------------------------------- TYPES -----------------------------------*/
/*--------------------------------------------------------------------------*/
using Subset = Block::Subset;
using FunctionValue = Function::FunctionValue;
/*--------------------------------------------------------------------------*/
/*------------------------------- CONSTANTS --------------------------------*/
/*--------------------------------------------------------------------------*/
const double scale = 10;
const char * const logF = "log.txt";
const FunctionValue INF = SMSpp_di_unipi_it::Inf< FunctionValue >();
/*--------------------------------------------------------------------------*/
/*------------------------------- GLOBALS ----------------------------------*/
/*--------------------------------------------------------------------------*/
Block * TestBlock; // the [UC]Block that is solved
std::mt19937 rg; // base random generator
std::uniform_real_distribution<> dis( 0.0 , 1.0 );
// if not-NaN, the objective value of the (only) Solver attached to the Block
// is compared against a reference value passed on the command line
// RefObjective is defined in common_utils.cpp (extern in common_utils.h)
/*--------------------------------------------------------------------------*/
/*------------------------------ FUNCTIONS ---------------------------------*/
/*--------------------------------------------------------------------------*/
static double rndfctr( void )
{
// return a random number between 0.5 and 2, with 50% probability of being
// < 1
double fctr = dis( rg ) - 0.5;
return( fctr < 0 ? - fctr : fctr * 4 );
}
/*--------------------------------------------------------------------------*/
// test-specific command-line knobs, set by process_specific_arg(); the
// standard parameters (instance positional, -B BlockConfig, -S
// BlockSolverConfig, -c/-p prefixes) are handled centrally by common_utils
// -r / --ref : reference objective value to compare against
// -V / --viol : how much the solution a relaxation reconstructs may
// violate the rows it has dualised
static double RelaxationViol = 1e-1;
static bool process_specific_arg( int opt )
{
switch( opt ) {
case( 'r' ): Str2Sthg( optarg , RefObjective ); return( true );
case( 'V' ): Str2Sthg( optarg , RelaxationViol ); return( true );
default: return( false );
}
}
/*--------------------------------------------------------------------------*/
namespace pollutant {
/*--------------------------------------------------------------------------*/
/*--------------- CONSTANTS OF THE POLLUTANT BUDGET CHECKS -----------------*/
/*--------------------------------------------------------------------------*/
/// the :MILPSolver to try, the first in the Solver factory being used
static const std::vector< std::string > SolverNames =
{ "CPXMILPSolver" , "GRBMILPSolver" , "HiGHSMILPSolver" , "SCIPMILPSolver" };
/// the relative tolerance of every comparison
static constexpr double Eps = 1e-6;
/*--------------------------------------------------------------------------*/
/*----------------- TYPES OF THE POLLUTANT BUDGET CHECKS -------------------*/
/*--------------------------------------------------------------------------*/
/// the data of one pollutant [see the file comment]
struct Pollutant {
Index nz; ///< number of zones
std::vector< Index > zones; ///< zone of each node
std::vector< double > ub; ///< PollutantBudget
std::vector< double > lb; ///< PollutantMinBudget, if any
std::vector< std::vector< double > > rho; ///< [ t or 0 ][ generator ]
std::vector< double > sigma; ///< [ t ] on the battery level
};
/// an instance [see the file comment]
struct Instance {
std::vector< Pollutant > pollutants;
bool battery = false;
bool zones = true; ///< write NumberPollutantZones, PollutantZones
// the defects of the instances that must be refused
bool drop_zones = false; ///< no PollutantZones although nz > 1
bool bad_rho = false; ///< PollutantRho over one generator less
Index bad_tnpz = 0; ///< added to TotalNumberPollutantZones
bool bad_storages = false; ///< NumberStorages one more than the units'
bool bad_sigma = false; ///< PollutantStorageRho over two storages
};
/*--------------------------------------------------------------------------*/
/*--------------- GLOBALS OF THE POLLUTANT BUDGET CHECKS -------------------*/
/*--------------------------------------------------------------------------*/
static bool all_passed = true; ///< false as soon as a check fails
static std::string solver_name;
static std::filesystem::path dir;
/*--------------------------------------------------------------------------*/
/*-------------- FUNCTIONS OF THE POLLUTANT BUDGET CHECKS ------------------*/
/*--------------------------------------------------------------------------*/
static void check( bool ok , const std::string & what )
{
std::cout << " " << what << ( ok ? " -> OK" : " -> Error" ) << std::endl;
if( ! ok )
all_passed = false;
}
/*--------------------------------------------------------------------------*/
static bool near( double a , double b )
{
return( std::abs( a - b ) <= Eps * std::max( 1.0 , std::abs( b ) ) );
}
/*--------------------------------------------------------------------------*/
/// writes the instance in the file with the given name, returning its path
static std::string write( const Instance & in , const std::string & name )
{
const Index T = 2;
const Index N = 3;
const Index G = in.battery ? 5 : 4;
const Index P = in.pollutants.size();
auto path = ( dir / ( name + ".nc4" ) ).string();
netCDF::NcFile f( path , netCDF::NcFile::replace );
f.putAtt( "SMS++_file_type" , netCDF::NcInt() , 1 );
auto g = f.addGroup( "Block_0" );
g.putAtt( "type" , "UCBlock" );
auto dT = g.addDim( "TimeHorizon" , T );
g.addDim( "NumberUnits" , G );
auto dG = g.addDim( "NumberElectricalGenerators" , G );
auto dN = g.addDim( "NumberNodes" , N );
auto dL = g.addDim( "NumberLines" , 2 );
const std::vector< double > demand = { 0 , 0 , 0 , 0 , 80 , 60 };
g.addVar( "ActivePowerDemand" , netCDF::NcDouble() ,
{ dN , dT } ).putVar( demand.data() );
std::vector< unsigned > gen_node = { 0 , 1 , 2 , 2 , 2 };
g.addVar( "GeneratorNode" , netCDF::NcUint() , dG ).putVar( gen_node.data() );
const std::vector< unsigned > start = { 0 , 1 } , end = { 1 , 2 };
g.addVar( "StartLine" , netCDF::NcUint() , dL ).putVar( start.data() );
g.addVar( "EndLine" , netCDF::NcUint() , dL ).putVar( end.data() );
const std::vector< double > maxf = { 1000 , 1000 } , minf = { -1000 , -1000 };
g.addVar( "MaxPowerFlow" , netCDF::NcDouble() , dL ).putVar( maxf.data() );
g.addVar( "MinPowerFlow" , netCDF::NcDouble() , dL ).putVar( minf.data() );
if( P ) {
auto dP = g.addDim( "NumberPollutants" , P );
Index tnpz = 0;
std::vector< unsigned > npz , pz;
std::vector< double > ub , lb;
bool any_lb = false;
for( const auto & p : in.pollutants ) {
tnpz += p.nz;
npz.push_back( p.nz );
pz.insert( pz.end() , p.zones.begin() , p.zones.end() );
ub.insert( ub.end() , p.ub.begin() , p.ub.end() );
if( p.lb.empty() )
lb.insert( lb.end() , p.nz , -INF );
else {
lb.insert( lb.end() , p.lb.begin() , p.lb.end() );
any_lb = true;
}
}
auto dZ = g.addDim( "TotalNumberPollutantZones" , tnpz + in.bad_tnpz );
if( in.zones ) {
g.addVar( "NumberPollutantZones" , netCDF::NcUint() ,
dP ).putVar( npz.data() );
if( ! in.drop_zones )
g.addVar( "PollutantZones" , netCDF::NcUint() ,
{ dP , dN } ).putVar( pz.data() );
}
ub.resize( tnpz + in.bad_tnpz , ub.back() );
lb.resize( tnpz + in.bad_tnpz , lb.back() );
g.addVar( "PollutantBudget" , netCDF::NcDouble() , dZ ).putVar( ub.data() );
if( any_lb )
g.addVar( "PollutantMinBudget" , netCDF::NcDouble() ,
dZ ).putVar( lb.data() );
// the rates, over one time instant if they all are constant
const Index RT = in.pollutants[ 0 ].rho.size();
const Index RG = in.bad_rho ? G - 1 : G;
std::vector< double > rho( RT * P * RG , 0 );
for( Index t = 0 ; t < RT ; ++t )
for( Index p = 0 ; p < P ; ++p )
for( Index h = 0 ; h < std::min( RG , Index( 4 ) ) ; ++h )
rho[ ( t * P + p ) * RG + h ] = in.pollutants[ p ].rho[ t ][ h ];
auto dRT = g.addDim( "PollutantRhoTime" , RT );
auto dRG = in.bad_rho ? g.addDim( "OneGeneratorLess" , RG ) : dG;
g.addVar( "PollutantRho" , netCDF::NcDouble() ,
{ dRT , dP , dRG } ).putVar( rho.data() );
// the factors of the storages, over the one battery: over two with a
// NumberStorages that says so if bad_storages, over two with no
// NumberStorages at all if bad_sigma
if( ! in.pollutants[ 0 ].sigma.empty() ) {
const Index S = ( in.bad_storages || in.bad_sigma ) ? 2 : 1;
auto dS = g.addDim( in.bad_sigma ? "TwoStorages" : "NumberStorages" , S );
std::vector< double > sigma( T * P * S , 0 );
for( Index t = 0 ; t < T ; ++t )
for( Index p = 0 ; p < P ; ++p )
if( ! in.pollutants[ p ].sigma.empty() )
sigma[ ( t * P + p ) * S ] = in.pollutants[ p ].sigma[ t ];
g.addVar( "PollutantStorageRho" , netCDF::NcDouble() ,
{ dT , dP , dS } ).putVar( sigma.data() );
}
}
auto unit = [ & ]( Index u , const char * type , double cost , double max ) {
auto ug = g.addGroup( "UnitBlock_" + std::to_string( u ) );
ug.putAtt( "type" , type );
ug.addVar( "MaxPower" , netCDF::NcDouble() ).putVar( & max );
ug.addVar( "ActivePowerCost" , netCDF::NcDouble() ).putVar( & cost );
if( std::string( type ) == "IntermittentUnitBlock" ) {
const double zero = 0;
ug.addVar( "MinPower" , netCDF::NcDouble() ).putVar( & zero );
}
};
unit( 0 , "IntermittentUnitBlock" , 10 , 100 );
unit( 1 , "IntermittentUnitBlock" , 30 , 100 );
unit( 2 , "IntermittentUnitBlock" , 60 , 100 );
unit( 3 , "SlackUnitBlock" , 1000 , 1000 );
if( in.battery ) {
auto bg = g.addGroup( "UnitBlock_4" );
bg.putAtt( "type" , "BatteryUnitBlock" );
auto scalar = [ & bg ]( const char * var , double value ) {
bg.addVar( var , netCDF::NcDouble() ).putVar( & value );
};
scalar( "MaxPower" , 100 );
scalar( "MinPower" , -100 );
scalar( "ExtractingBatteryRho" , 1 );
scalar( "StoringBatteryRho" , 1 );
scalar( "MinStorage" , 0 );
scalar( "MaxStorage" , 100 );
scalar( "InitialStorage" , 0 );
scalar( "Cost" , 0 );
}
return( path );
}
/*--------------------------------------------------------------------------*/
static UCBlock * load( const std::string & path )
{
auto uc = dynamic_cast< UCBlock * >( Block::deserialize( path ) );
if( ! uc )
throw( std::logic_error( path + " is not a UCBlock" ) );
return( uc );
}
/*--------------------------------------------------------------------------*/
/// attaches the :MILPSolver to the UCBlock (or detaches it if clear)
static void attach( UCBlock * uc , bool clear = false )
{
BlockSolverConfig bsc( 1 );
bsc.add_ComputeConfig( std::string( solver_name ) , nullptr );
if( clear )
bsc.clear();
bsc.apply( uc );
if( ! clear )
if( auto s = uc->get_registered_solvers().front() ) {
const auto par = s->int_par_str2idx( "intLogVerb" );
if( par < Inf< Solver::idx_type >() )
s->set_par( par , 0 );
}
}
/*--------------------------------------------------------------------------*/
/// solves the UCBlock, writing the solution in it, and returns the value
static double solve( UCBlock * uc )
{
auto solver = static_cast< CDASolver * >(
uc->get_registered_solvers().front() );
if( solver->compute( false ) != Solver::kOK )
return( std::numeric_limits< double >::quiet_NaN() );
solver->get_var_solution();
if( solver->has_dual_solution() )
solver->get_dual_solution();
return( solver->get_var_value() );
}
/*--------------------------------------------------------------------------*/
/// the index of the row of zone 0 of pollutant p
static Index first_zone( const UCBlock * uc , Index p )
{
Index k = 0;
for( Index q = 0 ; q < p ; ++q )
k += uc->get_number_pollutant_zones()[ q ];
return( k );
}
/*--------------------------------------------------------------------------*/
/// true if every row has exactly the terms of the data
/** Each row must have, for each generator of a unit at a node of its zone
* and each time with a nonzero rate, the active power with coefficient the
* scale of the unit times the rate, and for each storage the level with
* coefficient the scale times its factor; and nothing else. */
static bool rows_match_data( UCBlock * uc )
{
const Index T = uc->get_time_horizon();
for( Index p = 0 ; p < uc->get_number_pollutants() ; ++p )
for( Index z = 0 ; z < uc->get_number_pollutant_zones()[ p ] ; ++z ) {
const Index k = first_zone( uc , p ) + z;
const auto & row = uc->get_const_pollutant_constraints()[ p ][ z ];
if( ( row.get_rhs() != uc->get_pollutant_budget()[ k ] ) ||
( row.get_lhs() != uc->get_pollutant_min_budget()[ k ] ) )
return( false );
// the terms expected from the data
std::vector< std::pair< const ColVariable * , double > > expected;
Index eg = 0 , st = 0;
for( Index u = 0 ; u < uc->get_number_units() ; ++u ) {
auto ub = uc->get_unit_block( u );
const Index first_eg = eg;
for( Index h = 0 ; h < ub->get_number_generators() ; ++h , ++eg ) {
const Index node = uc->get_generator_node()[ eg ];
const Index zone = uc->get_pollutant_zone().empty() ? 0 :
uc->get_pollutant_zone()[ p ][ node ];
if( zone != z )
continue;
auto ap = ub->get_active_power( h );
for( Index t = 0 ; ap && ( t < T ) ; ++t )
if( uc->get_pollutant_rho( t , p , eg ) != 0 )
expected.emplace_back( & ap[ t ] ,
ub->get_scale() *
uc->get_pollutant_rho( t , p , eg ) );
}
if( ! uc->get_pollutant_storage_rho().empty() ) {
const Index node = ub->get_number_generators() ?
uc->get_generator_node()[ first_eg ] : 0;
const Index zone = uc->get_pollutant_zone().empty() ? 0 :
uc->get_pollutant_zone()[ p ][ node ];
for( Index s = 0 ; s < ub->get_number_storages() ; ++s ) {
auto level = ub->get_storage_level( s );
for( Index t = 0 ; level && ( zone == z ) && ( t < T ) ; ++t )
if( uc->get_pollutant_storage_rho( t , p , st + s ) != 0 )
expected.emplace_back( & level[ t ] , ub->get_scale() *
uc->get_pollutant_storage_rho( t , p ,
st + s ) );
}
st += ub->get_number_storages();
}
}
auto lf = static_cast< LinearFunction * >( row.get_function() );
if( lf->get_v_var().size() != expected.size() )
return( false );
for( const auto & [ var , coeff ] : expected ) {
const auto i = lf->is_active( var );
if( ( i >= lf->get_num_active_var() ) ||
( ! near( lf->get_coefficient( i ) , coeff ) ) )
return( false );
}
}
return( true );
}
/*--------------------------------------------------------------------------*/
/// checks value, rows, dual, is_feasible() and the netCDF round trip
static UCBlock * run( const std::string & name , const Instance & in ,
double value , double dual = -1 , Index dual_p = 0 ,
Index dual_z = 0 )
{
std::cout << name << std::endl;
auto uc = load( write( in , name ) );
attach( uc );
const double v = solve( uc );
check( near( v , value ) , "optimum " + std::to_string( v ) + " == " +
std::to_string( value ) );
check( rows_match_data( uc ) , "rows are scale times the data" );
if( dual >= 0 )
check( near( std::abs( uc->get_const_pollutant_constraints()[ dual_p ]
[ dual_z ].get_dual() ) , dual ) ,
"dual of pollutant " + std::to_string( dual_p ) + " zone " +
std::to_string( dual_z ) + " == " + std::to_string( dual ) );
SimpleConfiguration< double > tol( 1e-6 );
check( uc->is_feasible( false , & tol ) , "is_feasible() at the optimum" );
// the instance written by UCBlock has the same optimum
auto copy = ( dir / ( name + "-copy.nc4" ) ).string();
static_cast< Block * >( uc )->serialize( copy , eBlockFile );
auto rt = load( copy );
attach( rt );
check( near( solve( rt ) , value ) , "serialize() keeps the optimum" );
attach( rt , true );
delete rt;
return( uc );
}
/*--------------------------------------------------------------------------*/
static void expect_refused( const std::string & name , const Instance & in ,
const std::string & what )
{
bool refused = false;
try {
delete load( write( in , name ) );
}
catch( std::exception & e ) {
refused = true;
}
check( refused , what + " is refused" );
}
/*--------------------------------------------------------------------------*/
static void release( UCBlock * uc )
{
attach( uc , true );
delete uc;
}
/*--------------------------------------------------------------------------*/
/// the checks of the pollutant budget constraints [see the file comment]
static int test( void )
{
solver_name = first_Solver_of( SolverNames ,
"the checks of the pollutant budget" );
if( solver_name.empty() )
return( 0 );
std::cout << "solving with " << solver_name << std::endl;
dir = std::filesystem::temp_directory_path() /
( "UCBlock_pollutant_test_" + std::to_string( getpid() ) );
std::filesystem::create_directories( dir );
const std::vector< std::vector< double > > co2_rate = { { 1 , 0.5 , 0 , 0 } };
// N- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{
auto uc = run( "N" , Instance() , 1400 );
check( uc->get_const_pollutant_constraints().empty() , "no rows" );
release( uc );
}
// A- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Instance A;
A.pollutants = {
{ 2 , { 0 , 1 , 1 } , { 100 , 1000 } , {} , co2_rate , {} } ,
{ 1 , { 0 , 0 , 1 } , { 1000 } , {} , { { 0.2 , 0.1 , 5 , 0 } } , {} } };
{
auto uc = run( "A" , A , 2200 , 20 , 0 , 0 );
// the duals through a UCBlockSolution and its netCDF form
SimpleConfiguration< int > what( 128 );
auto sol = uc->get_Solution( & what , false );
auto sol_path = ( dir / "A-solution.nc4" ).string();
{
netCDF::NcFile f( sol_path , netCDF::NcFile::replace );
auto g = f.addGroup( "Solution_0" );
sol->serialize( g );
}
delete sol;
for( auto & zones : uc->get_pollutant_constraints() )
for( auto & row : zones )
row.set_dual( 0 );
{
netCDF::NcFile f( sol_path , netCDF::NcFile::read );
UCBlockSolution read;
read.deserialize( f.getGroup( "Solution_0" ) );
read.write( uc );
}
check( near( std::abs( uc->get_const_pollutant_constraints()[ 0 ][ 0 ]
.get_dual() ) , 20 ) ,
"the dual goes through the Solution" );
// a solution beyond the budget of zone 0 of CO2 violates the rows
auto u0 = uc->get_unit_block( 0 )->get_active_power( 0 );
u0[ 0 ].set_value( u0[ 0 ].get_value() + 50 );
check( ! RowConstraint::is_feasible( uc->get_pollutant_constraints() ,
1e-6 ) ,
"exceeding a budget violates the rows" );
release( uc );
}
// A2 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Instance A2 = A;
A2.pollutants[ 1 ].ub = { 20 };
{
auto uc = run( "A2" , A2 , 3000 , 200 , 1 , 0 );
std::vector< double > budget = { 1000 };
uc->set_pollutant_budget( budget.begin() , Block::Range( 2 , 3 ) ,
eModBlck , eModBlck );
check( near( solve( uc ) , 2200 ) , "set_pollutant_budget( range )" );
budget = { 20 };
uc->set_pollutant_budget( budget.begin() , Block::Subset( { 2 } ) , true ,
eModBlck , eModBlck );
check( near( solve( uc ) , 3000 ) , "set_pollutant_budget( subset )" );
uc->get_unit_block( 1 )->scale( 0.25 , eModBlck , eModBlck );
check( rows_match_data( uc ) , "rows follow the scale of a unit" );
check( near( solve( uc ) , 3150 ) , "scaled unit with the Solver attached" );
release( uc );
auto fresh = load( ( dir / "A2.nc4" ).string() );
fresh->get_unit_block( 1 )->scale( 0.25 , eNoMod , eNoMod );
attach( fresh );
check( near( solve( fresh ) , 3150 ) , "scaled unit read from scratch" );
check( near( std::abs( fresh->get_const_pollutant_constraints()[ 1 ][ 0 ]
.get_dual() ) , 250 ) , "dual of the scaled unit" );
release( fresh );
}
/* A2 with the unit scaled while a LagBFunction holds it, as it does while a
* LagrangianDualSolver is attached: the LagBFunction is then the father of
* the unit, and the unit is its only sub-Block, while the scale must still
* rewrite the rows of unit 1 and not those of unit 0. */
{
auto uc = load( ( dir / "A2.nc4" ).string() );
attach( uc );
auto unit = uc->get_unit_block( 1 );
auto lbf = new LagBFunction( unit );
lbf->set_f_Block( uc );
unit->scale( 0.25 , eModBlck , eModBlck );
check( rows_match_data( uc ) , "rows follow the scale of a unit under a "
"LagBFunction" );
lbf->set_inner_block( nullptr , false );
lbf->set_f_Block( nullptr );
unit->set_f_Block( uc );
delete lbf;
check( near( solve( uc ) , 3150 ) , "scaled under a LagBFunction" );
release( uc );
}
/* A2 with a LagrangianDualSolver attached, which by default gives each
* LagBFunction only the dual pairs of the relaxed constraints its sub-Block
* appears in [see intSparseLagPairs]: scaling a unit rewrites coefficients
* of relaxed rows, and each change has to reach the Lagrangian term of the
* right LagBFunction. The instance being continuous, the Lagrangian dual is
* its optimum, 3150 with the unit scaled, whether it is scaled before the
* Solver is attached or after. The ComputeConfig of the LagrangianDualSolver
* LDCfg-tight.txt, which is the LDCfg.txt of the batches, next to which the
* test is run [see CMakeLists.txt], with a tighter threshold on the
* residual: the one of the batches stops the Bundle some 6% away from the
* optimum, at a value that does not change with the scale, and the check
* would not see it. */
{
// the value of the Lagrangian dual of A2 with unit 1 scaled by 0.25,
// before the LagrangianDualSolver is attached or after
const auto lagrangian = [ & ]( bool after ) {
auto cc = dynamic_cast< ComputeConfig * >(
Configuration::deserialize( "LDCfg-tight.txt" ) );
if( ! cc )
return( std::numeric_limits< double >::quiet_NaN() );
auto uc = load( ( dir / "A2.nc4" ).string() );
if( ! after )
uc->get_unit_block( 1 )->scale( 0.25 , eNoMod , eNoMod );
BlockSolverConfig bsc( 1 );
bsc.add_ComputeConfig( "LagrangianDualSolver" , cc );
bsc.apply( uc );
if( after )
uc->get_unit_block( 1 )->scale( 0.25 , eModBlck , eModBlck );
auto solver = static_cast< CDASolver * >(
uc->get_registered_solvers().front() );
const auto status = solver->compute( false );
const auto v = ( ( status == Solver::kOK ) ||
( status == Solver::kLowPrecision ) ) ?
solver->get_var_value() :
std::numeric_limits< double >::quiet_NaN();
bsc.clear();
bsc.apply( uc );
delete uc;
return( v );
};
const auto before = lagrangian( false );
const auto after = lagrangian( true );
check( ( std::abs( before - 3150 ) <= 1e-5 * 3150 ) &&
( std::abs( after - 3150 ) <= 1e-5 * 3150 ) ,
"scaled unit with a LagrangianDualSolver attached: " +
std::to_string( after ) + " and, scaled before, " +
std::to_string( before ) + " == 3150" );
}
// B- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{
Instance B;
B.zones = false;
B.pollutants = { { 1 , { 0 , 0 , 0 } , { 50 } , {} ,
{ { 1 , 0.5 , 0 , 0 } , { 1.5 , 0.5 , 0 , 0 } } , {} } };
auto uc = run( "B" , B , 5400 , 60 , 0 , 0 );
check( ( uc->get_number_pollutant_zones().size() == 1 ) &&
( uc->get_number_pollutant_zones()[ 0 ] == 1 ) &&
uc->get_pollutant_zone().empty() , "one zone of all the nodes" );
release( uc );
}
// M1 and M2- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const std::vector< std::vector< double > > dirty = { { 1 , 2 , 0 , 0 } };
{
Instance M1;
M1.pollutants = { { 1 , { 0 , 0 , 0 } , { INF } , { 180 } , dirty , {} } };
release( run( "M1" , M1 , 2200 , 20 , 0 , 0 ) );
}
{
Instance M2;
M2.pollutants = { { 1 , { 0 , 0 , 0 } , { 150 } , { 150 } , dirty , {} } };
auto uc = run( "M2" , M2 , 1600 , 20 , 0 , 0 );
std::vector< double > floor = { -INF };
uc->set_pollutant_min_budget( floor.begin() , Block::Range( 0 , 1 ) ,
eModBlck , eModBlck );
check( near( solve( uc ) , 1400 ) , "set_pollutant_min_budget( range )" );
floor = { 150 };
uc->set_pollutant_min_budget( floor.begin() , Block::Subset( { 0 } ) ,
true , eModBlck , eModBlck );
check( near( solve( uc ) , 1600 ) , "set_pollutant_min_budget( subset )" );
release( uc );
}
// S0 and S - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Instance S0;
S0.battery = true;
S0.pollutants = { { 1 , { 0 , 0 , 0 } , { 100 } , {} , co2_rate , {} } };
release( run( "S0" , S0 , 3000 ) );
Instance S = S0;
S.pollutants[ 0 ].sigma = { 0 , -2 };
{
auto uc = run( "S" , S , 1800 );
check( uc->get_number_storages() == 1 , "one storage, the battery" );
uc->get_unit_block( 4 )->scale( 0.1 , eModBlck , eModBlck );
check( rows_match_data( uc ) , "rows follow the scale of the battery" );
check( near( solve( uc ) , 2700 ) , "scaled battery" );
release( uc );
}
// refused instances- - - - - - - - - - - - - - - - - - - - - - - - - - - -
std::cout << "refused instances" << std::endl;
{
Instance E = A;
E.drop_zones = true;
expect_refused( "E1" , E , "two zones and no PollutantZones" );
}
{
Instance E = A;
E.bad_rho = true;
expect_refused( "E2" , E , "a PollutantRho of the wrong size" );
}
{
Instance E = A;
E.bad_tnpz = 1;
expect_refused( "E3" , E , "a wrong TotalNumberPollutantZones" );
}
{
Instance E = S;
E.bad_storages = true;
expect_refused( "E4" , E , "a wrong NumberStorages" );
}
{
Instance E = S;
E.bad_sigma = true;
expect_refused( "E5" , E , "a PollutantStorageRho of the wrong size" );
}
std::filesystem::remove_all( dir );
if( all_passed )
std::cout << "All tests passed!!" << std::endl;
else
std::cout << "Shit happened!!" << std::endl;
return( all_passed ? 0 : 1 );
}
} // end( namespace pollutant )
/*--------------------------------------------------------------------------*/
namespace scaling {
/*--------------------------------------------------------------------------*/
/*------------------- CONSTANTS OF THE SCALING CHECKS ----------------------*/
/*--------------------------------------------------------------------------*/
/// the :MILPSolver to try, the first in the Solver factory being used
static const std::vector< std::string > SolverNames =
{ "CPXMILPSolver" , "GRBMILPSolver" , "HiGHSMILPSolver" , "SCIPMILPSolver" };
/// the relative tolerance of every comparison
static constexpr double Eps = 1e-6;
/// the scale factor the unit under investment is given
static constexpr double Kappa = 3;
/// the time horizon of the instances
static constexpr Index T = 8;
/*--------------------------------------------------------------------------*/
/*------------------- GLOBALS OF THE SCALING CHECKS ------------------------*/
/*--------------------------------------------------------------------------*/
static bool all_passed = true; ///< false as soon as a check fails
static std::string solver_name;
static std::filesystem::path dir;
/*--------------------------------------------------------------------------*/
/*------------------ FUNCTIONS OF THE SCALING CHECKS -----------------------*/
/*--------------------------------------------------------------------------*/
static void check( bool ok , const std::string & what )
{
std::cout << " " << what << ( ok ? " -> OK" : " -> Error" ) << std::endl;
if( ! ok )
all_passed = false;
}
/*--------------------------------------------------------------------------*/
static bool near( double a , double b )
{
return( std::abs( a - b ) <= Eps * std::max( 1.0 , std::abs( b ) ) );
}
/*--------------------------------------------------------------------------*/
/// writes the instance in the file with the given name, returning its path
/** The instance has one node, a unit that is scaled and a SlackUnitBlock
* that makes it feasible whatever the first one does. The scaled unit is a
* ThermalUnitBlock with a cost of every kind the Objective can carry, i.e.,
* start-up, shut-down, linear, quadratic and fixed, the two reserves and the
* reactive power, or the NuclearUnitBlock that adds to them the costs of the
* downward modulation steps and of the deep decreases. */
static std::string write( const std::string & name , bool nuclear ,
bool schedule = false )
{
auto path = ( dir / ( name + ".nc4" ) ).string();
netCDF::NcFile f( path , netCDF::NcFile::replace );
f.putAtt( "SMS++_file_type" , netCDF::NcInt() , 1 );
auto g = f.addGroup( "Block_0" );
g.putAtt( "type" , "UCBlock" );
auto dT = g.addDim( "TimeHorizon" , T );
g.addDim( "NumberUnits" , 2 );
auto dG = g.addDim( "NumberElectricalGenerators" , 2 );
auto dN = g.addDim( "NumberNodes" , 1 );
auto dP = g.addDim( "NumberPrimaryZones" , 1 );
auto dS = g.addDim( "NumberSecondaryZones" , 1 );