-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsolver.py
More file actions
1448 lines (1263 loc) · 61.9 KB
/
solver.py
File metadata and controls
1448 lines (1263 loc) · 61.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import sys
from math import sqrt
import sqlite3
import numpy as np
from scipy.optimize import fsolve, root, minimize
import utils
MODEL_1, MODEL_2, MODEL_2_QUAD, MODEL_1_QUAD, MODEL_NB = 1, 2, 3, 4, 5
_CASE_ONE, _CASE_TWO = '1', '2'
_UNDEFINED_CASE = '0'
_CASE_ONE_A, _CASE_ONE_B, _CASE_ONE_C, _CASE_TWO_A, _CASE_TWO_B, _CASE_TWO_C = '1a','1b','1c','2a','2b','2c'
ALL_CASES = [_CASE_ONE_A, _CASE_ONE_B, _CASE_ONE_C, _CASE_TWO_A, _CASE_TWO_B, _CASE_TWO_C]
DECIMALS_ALLOW_NN = 10
def is_prof_pos(prof):
""" checks whether a given profit is positive - it allows also a -.1*10^14 as positive! """
return round(prof, 15) >= 0
def is_almost_equal(one, two):
""" compares two floats, allowing a deviation after 14 decimal places """
return round(one, DECIMALS_ALLOW_NN) == round(two, DECIMALS_ALLOW_NN) \
or (0 in (round(one, DECIMALS_ALLOW_NN), round(two, DECIMALS_ALLOW_NN)) and \
abs(round(one, DECIMALS_ALLOW_NN)) == abs(round(two, DECIMALS_ALLOW_NN)))
class ModelTwoFracQuad:
"""
This class offers methods to solve the Model 2 where rho is quadratic in the retailer function
"""
@staticmethod
def create_dec_vars(wn, pr, case, par):
try:
if case in (_CASE_ONE_A, _CASE_ONE_B, _CASE_ONE_C): #case 1: rho >= 1
pn = ModelTwoFracQuad._pn_case_one(wn, pr, par)
rho = ModelTwoFracQuad._rho_case_one(wn, pr, par)
else: #case 2: rho = 1
pn = ModelTwoFracQuad._pn_case_two(wn, pr, par)
rho = 1
qn = ModelTwoFracQuad._qn(pn, pr, par)
qr = ModelTwoFracQuad._qr(pn, pr, par)
except ZeroDivisionError as e:
raise e
dec = DecisionVariables(MODEL_2, pn=pn, pr=pr, wn=wn, rho=rho, qn=qn, qr=qr)
return dec
@staticmethod
def calc_profits(par, dec):
if dec == None:
return (None, None)
manu_profit = dec.qn * (dec.wn * (1- par.tau/dec.rho - par.cn)) + dec.qr * (dec.pr - par.cr) + ((par.tau/dec.rho)*dec.qn - dec.qr)*par.s
retailer_profit = dec.qn * (dec.pn - dec.wn) * (1 - par.tau/dec.rho) - (par.a*dec.rho - 1)**2
return manu_profit, retailer_profit
@staticmethod
def _qn(pn, pr, par):
return 1 - (pn - pr) / (1 - par.delta)
def _qr(pn, pr, par):
return (pn - pr) / (1 - par.delta) - pr /par.delta
@staticmethod
def _pn_case_one(wn, pr, par):
return .5 * (1-par.delta+pr+wn)
@staticmethod
def _rho_case_one(wn, pr, par):
return (par.tau**(1/3) * (-1+par.delta-pr+wn)**(2/3))/(2 * (par.a *(1-par.delta))**(1/3))
@staticmethod
def _pn_case_two(wn, pr, par):
return .5 * (1-par.delta+pr+wn)
@staticmethod
def is_valid(par, sol):
""" Tests whether a given solution is feasible regarding to all model subjects """
# check all variables positive
for var in (sol.dec.pn, sol.dec.pr, sol.dec.wn, sol.dec.qn, sol.dec.qr):
if var < -10**-DECIMALS_ALLOW_NN:
return False
# check profits
if not (sol.profit_man >= -10**-DECIMALS_ALLOW_NN and sol.profit_ret >= -10**-DECIMALS_ALLOW_NN):
return False
# check rho
if not (sol.dec.rho >= 1):
return False
# check qr
if not (-10**-DECIMALS_ALLOW_NN <= sol.dec.qr <= ((par.tau/sol.dec.rho) * sol.dec.qn)+10**-DECIMALS_ALLOW_NN):
return False
return True
@staticmethod
def is_valid_and_case_exact(par, sol):
""" Tests whether a given solution is feasible regarding to all model subjects and right case """
if not is_valid(par, sol):
return False
# check case constraints
if not (sol.dec.rho >= 1):
return False
if sol.case in (_CASE_ONE_A, _CASE_TWO_A):
if not is_almost_equal(sol.dec.qr, 0):
raise Exception()
return False
elif sol.case in (_CASE_ONE_B, _CASE_TWO_B):
if not (-10**-DECIMALS_ALLOW_NN <= sol.dec.qr <= ((par.tau/sol.dec.rho) * sol.dec.qn)+10**-DECIMALS_ALLOW_NN):
return False
elif sol.case in (_CASE_ONE_C, _CASE_TWO_C):
if not (is_almost_equal(sol.dec.qr, (par.tau/sol.dec.rho) * sol.dec.qn)):
return False
return True
class ModelTwoNumericalSolver:
"""
This class offers methods to solve Model 2 (With Online Store of the Manufacturer) numerically
"""
def __init__(self):
pass
def optimize(self, par):
"""
This is the core method of this class. It will return a Solution object having stored
the profit of the manufacturer, the retailer, the case that led to the solution and all decision vars
Args:
par (Parameter): A Parameter object of type MODEL_2
Returns:
Solution: An object of class Solution
or None if there is no solution possible
"""
if par.cn < par.cr: return None
cases = (
(_CASE_ONE_A, self._optimize_case_one_a),
(_CASE_ONE_B, self._optimize_case_one_b),
(_CASE_ONE_C, self._optimize_case_one_c),
(_CASE_TWO_A, self._optimize_case_two_a),
(_CASE_TWO_B, self._optimize_case_two_b),
(_CASE_TWO_C, self._optimize_case_two_c))
valid_solutions = []
for case_id, case_fct in cases:
try:
dec = case_fct(par)
profit_man, profit_ret = self.calc_profits(par, dec)
sol = Solution(dec, profit_man, profit_ret, case_id)
except ZeroDivisionError:
# there occured a zero division, so this solution will be skipped
sol = None
# check valid
if sol is not None and self._is_valid(par, sol):
valid_solutions.append(sol)
if len(valid_solutions) > 0:
#valid_solutions.sort(key=lambda sol: )
# take the best valid solution (manufacturer decides)
return max(valid_solutions, key=lambda sol: (sol.profit_man, sol.profit_ret))
else:
return None
def _is_valid(self, par, sol):
""" Tests whether a given solution is feasible regarding to all model subjects """
# check all variables positive
for var in (sol.dec.pn, sol.dec.pr, sol.dec.wn, sol.dec.qn, sol.dec.qr):
if var < -10**-DECIMALS_ALLOW_NN:
return False
# check case constraints
if not (sol.dec.rho >= 1):
return False
if sol.case in (_CASE_ONE_A, _CASE_TWO_A):
if not is_almost_equal(sol.dec.qr, 0):
raise Exception()
return False
elif sol.case in (_CASE_ONE_B, _CASE_TWO_B):
if not (-10**-DECIMALS_ALLOW_NN <= sol.dec.qr <= ((par.tau/sol.dec.rho) * sol.dec.qn)+10**-DECIMALS_ALLOW_NN):
return False
elif sol.case in (_CASE_ONE_C, _CASE_TWO_C):
if not (is_almost_equal(sol.dec.qr, ((par.tau/sol.dec.rho) * sol.dec.qn))):
return False
# check profits
if not (sol.profit_man >= -10**-DECIMALS_ALLOW_NN and sol.profit_ret >= -10**-DECIMALS_ALLOW_NN):
return False
return True
def calc_profits(self, par, dec):
"""
Returns the numeric result of the profit of the manufacturer and the retailer (a tuple containing first manufacturer, second retailer)
having set all decision variables
This method checks whether `dec` is not None. If its None - It will return a tuple of (None, None)
"""
if dec == None:
return (None, None)
manu_profit = dec.qn * (dec.wn * (1- par.tau/dec.rho) - par.cn) + dec.qr*(dec.pr-par.cr) + ((par.tau/dec.rho)*dec.qn-dec.qr)*par.s
retailer_profit = dec.qn * (dec.pn - dec.wn) * (1- par.tau/dec.rho) - par.a*(dec.rho-1)
return manu_profit, retailer_profit
def _optimize_case_one_a(self, par):
""" helper function that solves the case rho >= 1 and qr = 0 """
dec = DecisionVariables(MODEL_2,
wn = (par.cn+1)/2 - ((2-par.delta)/2) * sqrt((par.a*par.tau) / (1-par.delta)),
pr = ( par.delta*(-2*sqrt(par.tau*par.a*(1-par.delta))+ par.delta*(sqrt(par.tau*par.a*(1-par.delta))+2*par.delta-5) - par.cn*par.delta+par.cn+3 ) ) / (2*(par.delta-2)*(par.delta-1)),
lambda1 = 1 + par.cn + par.cr + par.s + (2 * (-1 + par.cn))/(-2 + par.delta) - par.delta,
lambda2 = 0
)
dec.rho = self.__rho_case_one(dec.wn, par.delta, dec.pr, par.tau, par.a)
dec.pn = self.__pn_case_one(dec.wn, par.delta, dec.pr)
dec.qn = self.__qn_case_one(dec.wn, par.delta, dec.pr)
#dec.qr = self.__qr_case_one(dec.wn, par.delta, dec.pr)
dec.qr = 0
return dec
def _optimize_case_one_b(self, par):
""" helper function that solves the case rho >= 1 and 0 <= qr <= (tau/rho)*qn """
dec = DecisionVariables(MODEL_2,
wn = (1+par.cn)/2 - ((2-par.delta)/2) * sqrt((par.a*par.tau)/(1-par.delta)),
pr = (par.cr+par.delta+par.s)/2 - (par.delta/2) * sqrt((par.a*par.tau)/(1-par.delta)),
lambda1=0, lambda2=0
)
dec.rho = self.__rho_case_one(dec.wn, par.delta, dec.pr, par.tau, par.a)
dec.pn = self.__pn_case_one(dec.wn, par.delta, dec.pr)
dec.qn = self.__qn_case_one(dec.wn, par.delta, dec.pr)
dec.qr = self.__qr_case_one(dec.wn, par.delta, dec.pr)
return dec
def _optimize_case_one_c(self, par):
""" helper function that solves the case rho >= 1 and qr = (tau/rho)*qn """
dec = DecisionVariables(MODEL_2,
wn = (par.cn+1)/2 - ((2-par.delta)/2)*sqrt((par.a*par.tau)/(1-par.delta)),
pr = (par.delta*(-6*sqrt(par.tau*par.a*(1-par.delta))+par.delta*(5*sqrt(par.tau*par.a*(1-par.delta))+2*par.delta-5)-par.cn*par.delta + par.cn + 3)) / ( 2*(par.delta-2)*(par.delta-1)),
lambda1 = 0,
lambda2 = (-par.cr * (-2 + par.delta) - par.s *(-2 + par.delta) + par.delta *(-par.cn - (-1 + par.delta)*(-1 + 4*par.a*sqrt(par.tau/(par.a - par.a *par.delta)))))/(-2 + par.delta)
)
dec.rho = self.__rho_case_one(dec.wn, par.delta, dec.pr, par.tau, par.a)
dec.pn = self.__pn_case_one(dec.wn, par.delta, dec.pr)
dec.qn = self.__qn_case_one(dec.wn, par.delta, dec.pr)
dec.qr = self.__qr_case_one(dec.wn, par.delta, dec.pr)
return dec
def _optimize_case_two_a(self, par):
""" helper function that solves the case rho = 1 and qr = 0 """
dec = DecisionVariables(MODEL_2,
wn = (1/(1-par.tau))*((1+par.cn)/2 - (par.tau*(1+par.s))/2),
pr = (par.delta * (par.cn+ 2*par.delta*(par.tau-1)-(par.s+3)*par.tau + 3) ) / (2*(par.delta-2)*(par.tau-1)),
qr = 0, rho = 1,
lambda1 = (2 * par.cr * (-2 + par.delta) * (-1 + par.tau) + par.delta * (-2 + 2 * par.delta + par.cn * (-2 + par.tau) + par.tau - 2 * par.delta * par.tau + par.tau**2) - par.s * (4 * (-1 + par.tau) + par.delta * (2 + (-4 + par.tau) * par.tau)))/(2 * (-2 + par.delta) * (-1 + par.tau)),
lambda2 = 0
)
dec.pn = self.__pn_case_two(dec.wn, par.delta, dec.pr)
dec.qn = self.__qn_case_two(dec.wn, par.delta, dec.pr)
return dec
def _optimize_case_two_b(self, par):
""" helper function that solves the case rho = 1 and 0 <= qr <= (tau/rho)*qn """
dec = DecisionVariables(MODEL_2,
wn = (par.tau*(-par.delta*(par.cn+5*par.s+5)-par.cr*(par.delta-2)+par.delta**2+6*par.s+4)+4*(par.cn+1)*(par.delta-1)+par.delta*par.s*par.tau**2) / (par.delta*((par.tau-8)*par.tau+8)+8*(par.tau-1)),
pr = (-par.tau*(par.delta*(par.cn+5*par.delta+3*par.s-5)+par.cr*(3*par.delta-4)-4*par.s)+4*(par.delta-1)*(par.cr+par.delta+par.s)+par.delta*par.tau**2*(par.delta+par.s-1)) / (par.delta*((par.tau-8)*par.tau+8)+8*(par.tau-1)),
lambda1 = 0, lambda2 = 0
)
dec.rho = self.__rho_case_two()
dec.pn = self.__pn_case_two(dec.wn, par.delta, dec.pr)
dec.qn = self.__qn_case_two(dec.wn, par.delta, dec.pr)
dec.qr = self.__qr_case_two(dec.wn, par.delta, dec.pr)
return dec
def _optimize_case_two_c(self, par):
""" helper function that solves the case rho = 1 and qr = (tau/rho) * qn """
dec = DecisionVariables(MODEL_2,
wn = (par.cn*(par.delta*(par.tau-1)+2)+par.delta*(par.tau*(par.cr*(par.tau-1)+par.delta*(-par.tau)+par.delta+par.tau+2)-1)+2*(par.cr-1)*par.tau+2) / (par.delta*(6*par.tau-2)-4*par.tau+4),
pr = (par.delta*(par.tau*(par.cn+par.cr+5*par.delta-4)+par.cn+par.tau**2*(par.cr-par.delta+1)-2*par.delta+3)) / (par.delta*(6*par.tau-2)-4*par.tau+4),
lambda1 = 0,
lambda2 = (-4 * (par.cr + par.s) + 2 * (1 + par.cn + par.cr + par.s) * par.delta - 2 * par.delta**2 + (4 * (par.cr + par.s) + (-3 + par.cn - 4 * par.cr - 6 * par.s) * par.delta + 4 * par.delta**2) * par.tau + (1 + par.cr - par.delta) * par.delta * par.tau**2)/(4 - 4 * par.tau + par.delta * (-2 + 6 * par.tau))
)
dec.rho = self.__rho_case_two()
dec.pn = self.__pn_case_two(dec.wn, par.delta, dec.pr)
dec.qn = self.__qn_case_two(dec.wn, par.delta, dec.pr)
dec.qr = self.__qr_case_two(dec.wn, par.delta, dec.pr)
return dec
def __qr(self, pn, pr, delta):
return (pn-pr)/(1-delta) - pr/delta
def __qn(self, pn, pr, delta):
return 1 - (pn-pr)/(1-delta)
def __qn_case_one(self, wn, delta, pr):
return self.__qn(self.__pn_case_one(wn, delta, pr), pr, delta)
def __qr_case_one(self, wn, delta, pr):
return self.__qr(self.__pn_case_one(wn, delta, pr), pr, delta)
def __rho_case_one(self, wn, delta, pr, tau, a):
return .5 * (1-delta+pr-wn)*sqrt(tau/(a*(1-delta)))
def __pn_case_one(self, wn, delta, pr):
return .5 * (1+wn-delta+pr)
def __rho_case_two(self):
return 1
def __pn_case_two(self, wn, delta, pr):
return .5 * (1+wn-delta+pr)
def __qn_case_two(self, wn, delta, pr):
return self.__qn(self.__pn_case_two(wn, delta, pr), pr, delta)
def __qr_case_two(self, wn, delta, pr):
return self.__qr(self.__pn_case_two(wn, delta, pr), pr, delta)
class ModelOneNumericalSolver:
"""
This class offers methods to solve Model 1 (Without Online Store of the Manufacturer) numerically
"""
def __init__(self):
pass
def calc_profits(self, par, dec_vars):
"""
Returns the numeric result of the profit of the manufacturer and the retailer (a tuple containing first manufacturer, second retailer)
having set all decision variables
This method checks whether `dec_vars` is not None. If its None - It will return a tuple of (None, None)
"""
if dec_vars == None:
return (None, None)
wn, pn, rho, qn = dec_vars.wn, dec_vars.pn, dec_vars.rho, dec_vars.qn
manu_profit = qn * (wn * (1- par.tau/rho) - par.cn + (par.tau/rho) * par.s)
retailer_profit = qn * (pn - wn) * (1 - par.tau/rho) - par.a * (rho-1)
return manu_profit, retailer_profit
def _is_valid(self, par, sol):
""" Tests whether a given solution is feasible regarding to all model subjects """
# check all variables positive
for var in (sol.dec.pn, sol.dec.qn):
if var < -10**-DECIMALS_ALLOW_NN:
return False
# check case constraints
if not (sol.dec.rho >= 1):
return False
# check profits
if not (sol.profit_man >= 10**-DECIMALS_ALLOW_NN and sol.profit_ret >= 10**-DECIMALS_ALLOW_NN):
return False
return True
def optimize(self, par):
"""
This is the core method of this class. It will return all four
decision variables to maximize the retailer's profit (with
respect to the profit maximization condition of the retailer)
Returns:
A Solution object or None if the solution is not possible
"""
## test two cases:
# case 1 - rho is >= 1
# case 2 - rho is == 1
if par.a == 0: return None
dec_vars_case_1 = self._optimize_case_one(par)
prof_man_case_1, prof_ret_case_1 = self.calc_profits(par, dec_vars_case_1)
dec_vars_case_2 = self._optimize_case_two(par)
prof_man_case_2, prof_ret_case_2 = self.calc_profits(par, dec_vars_case_2)
case = None
sol1 = Solution(dec_vars_case_1, prof_man_case_1, prof_ret_case_1, _CASE_ONE)
sol2 = Solution(dec_vars_case_2, prof_man_case_2, prof_ret_case_2, _CASE_TWO)
valid_solutions = []
for sol in (sol1, sol2):
if self._is_valid(par, sol):
valid_solutions.append(sol)
if len(valid_solutions) > 0:
# take the best valid solution (manufacturer decides)
return max(valid_solutions, key=lambda sol: (sol.profit_man, sol.profit_ret))
else:
return None
if dec_vars_case_1.rho < 1:
if is_prof_pos(prof_man_case_2) and is_prof_pos(prof_ret_case_2):
case = _CASE_TWO
else:
# case one and two not possible
return None
else:
# rho is greater than 1, we have to check both -
# the manufacturer decides:
if is_prof_pos(prof_man_case_1) and is_prof_pos(prof_ret_case_1) and is_prof_pos(prof_man_case_2) and is_prof_pos(prof_ret_case_2):
# both possible
case = _CASE_ONE if prof_man_case_1 >= prof_man_case_2 else _CASE_TWO
else:
if is_prof_pos(prof_man_case_1) and is_prof_pos(prof_ret_case_1):
case = _CASE_ONE
if is_prof_pos(prof_man_case_2) and is_prof_pos(prof_ret_case_2):
case = _CASE_TWO
if case == _CASE_ONE:
return Solution(dec_vars_case_1, prof_man_case_1, prof_ret_case_1, case)
elif case == _CASE_TWO:
return Solution(dec_vars_case_2, prof_man_case_2, prof_ret_case_2, case)
else:
return None
def _optimize_case_one(self, par):
"""
Returns a dec_vars dict having wn,pn,rho and qn stored
"""
# defining our helper function:
def __manufacturer_derivation_case_1(wn):
return (-1/2) - (par.cn/2) + wn + par.a * (par.tau/par.a)**(1/2)
# let scipy do the job:
opt = fsolve(__manufacturer_derivation_case_1, x0=0.5, full_output=True)
wn = opt[0][0]
pn = (1 + wn) / 2
rho = (1-wn) * (1/2) * sqrt(par.tau/par.a)
return DecisionVariables(MODEL_1, wn=wn, pn=pn, rho=rho, qn=1 - pn)
def _optimize_case_two(self, par):
"""
Returns a dec_vars dict having wn,pn,rho and qn stored
"""
#helper function
def __manufacturer_derivation_case_2(wn):
return (1/2) * (-1 -par.cn -2*wn * (-1 + par.tau) + par.tau + par.s*par.tau)
# hello scipy:
opt = fsolve(__manufacturer_derivation_case_2, x0=0.5, full_output=True)
wn = opt[0][0]
pn = (1 + wn) / 2
rho = 1
return DecisionVariables(MODEL_1, wn=wn, pn=pn, rho=rho, qn=1 - pn)
@staticmethod
def get_retailer_profit(par, wn, pn, rho):
""" returns the the retailer's profit having set wn, pn and rho (i.e. all decision variables are set) """
qn = 1 - pn
return qn * (pn - wn) * (1-par.tau/rho) - par.a * (rho - 1)
@staticmethod
def get_manufacturer_profit(par, wn, case=_UNDEFINED_CASE):
pn, rho = ModelOneNumericalSolver.get_retailer_decision(par, wn, case=case)
if pn is None or rho is None: return None
qn = 1 - pn
return qn * (wn*(1-par.tau/rho) - par.cn + (par.tau/rho) * par.s)
@staticmethod
def get_retailer_decision(par, wn, case=_UNDEFINED_CASE):
""" Returns a tuple of (pn, rho). Having wn set to a fixed value, retailer optimizes profits """
pn_1 = (1/2) * (1+wn)
rho_1 = (par.tau**(1/2) * (1-wn)) / (2*par.a**(1/2))
pn_2 = (1/2) * (1+wn)
rho_2 = 1
all = []
pn_rhos = ((pn_1, rho_1), (pn_2, rho_2))
if case != _UNDEFINED_CASE:
if case == _CASE_ONE: pn_rhos = ((pn_1, rho_1),)
elif case == _CASE_TWO: pn_rhos = ((pn_2, rho_2),)
for pn, rho in pn_rhos:
profit = ModelOneNumericalSolver.get_retailer_profit(par, wn, pn, rho)
if 0 <= pn <= 1 and rho >= 1 and profit > 0:
all.append([profit, (pn, rho)])
if len(all) == 0: return None, None
return max(all, key=lambda k: k[0])[1]
singleton = None
@staticmethod
def solve(par):
if ModelOneNumericalSolver.singleton is None: ModelOneNumericalSolver.singleton = ModelOneNumericalSolver()
return ModelOneNumericalSolver.singleton.optimize(par)
class Solution:
def __init__(self, dec, profit_man, profit_ret, case):
self.dec, self.profit_man, self.profit_ret, self.case = dec, profit_man, profit_ret, case
def __str__(self):
if self.dec.b is not None:
return 'Solution object (wn={}, b={})'.format(self.dec.wn, self.dec.b)
return 'Solution object (wn={}, pr={})'.format(self.dec.wn, self.dec.pr)
class Parameter:
"""
An object of this class is a struct like wrapper for all Model Input Parameter (constants)
"""
def __init__(self, model, tau=None, a=None, s=None, cr=None, cn=None, delta=None):
#if model == MODEL_1 or model == MODEL_1_QUAD:
# self.tau, self.a, self.s, self.cn = tau, a, s, cn
# self.cr, self.delta = None, None
#else:
# self.tau, self.a, self.s, self.cr, self.cn, self.delta = tau, a, s, cr, cn, delta
self.tau, self.a, self.s, self.cr, self.cn, self.delta = tau, a, s, cr, cn, delta
self.model = model
def __str__(self):
if self.model == MODEL_1 or self.model == MODEL_1_QUAD or self.model == MODEL_NB:
return 'tau={:.2f}, a={:.4f}, s={:.2f}, cn={:.4f}'.format(self.tau, self.a, self.s, self.cn)
else:
return 'tau={:.4f}, a={:.5f}, s={:.4f}, cr={:.4f}, cn={:.4f}, delta={:.4f}'.format(
self.tau, self.a, self.s, self.cr, self.cn, self.delta)
def __repr__(self):
return self.__str__()
class DecisionVariables:
"""
An object of this class is a struct like wrapper for all Model Decision variables
"""
def __init__(self, model, pn=None, pr=None, wn=None, rho=None, qn=None, qr=None, b=None, lambda1=None, lambda2=None):
self.pn, self.pr, self.wn, self.rho, self.qn, self.qr, self.b = pn, pr, wn, rho, qn, qr, b
self.lambda1, self.lambda2 = lambda1, lambda2
#if model == MODEL_1 or model == MODEL_:
# self.wn, self.pn, self.rho, self.qn = wn, pn, rho, qn
# self.pr, self.qr = None, None
#else:
# self.pn, self.pr, self.wn, self.rho, self.qn, self.qr = pn, pr, wn, rho, qn, qr
# self.lambda1, self.lambda2 = lambda1, lambda2
self.model = model
def __str__(self):
if self.model == MODEL_1:
return 'wn={:.5f}, pn={:.5f}, rho={:.5f}, qn={:.5f}'.format(self.wn, self.pn, self.rho, self.qn)
elif self.model == MODEL_NB:
return 'wn={:.5f}, b={:.3f}, pn={:.5f}, rho={:.5f}, qn={:.5f}'.format(self.wn, self.b, self.pn, self.rho, self.qn)
else:
return 'pn={:.5f}, pr={:.5f}, wn={:.5f}, rho={:.5f}, qn={:.5f}, qr={:.5f}'.format(self.pn, self.pr, self.wn, self.rho, self.qn, self.qr)
def __repr__(self):
return self.__str__()
def drange(start, end, step_size):
""" A floating point range from [start, end] with step size step_size"""
r = start
while r <= end:
yield r
r += step_size
def almost_equal(first, sec, tol=0.001):
return abs(first-sec) < tol
class ModelTwoGridTester:
def __init__(self, par):
self.par = par
self.points = []
def plot_profit_surface(self):
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
sol = self.search()
np_arr = np.zeros([len(self.points), 3])
# create numpy arrays
for i, (wn, pr, prof) in enumerate(self.points):
prof_val = prof if prof is not None else -100
np_arr[i, :] = [wn, pr, prof_val]
best_points = np.zeros((len(self.caller_info), 2))
for i, iter_inf in enumerate(self.caller_info):
best_points[i:] = (iter_inf['best_x'], iter_inf['best_y'])
from matplotlib import pyplot as plt
valids = np_arr[np_arr[:, 2] > -100]
non_valids =np_arr[np_arr[:, 2] <= -100]
#plt.plot(valids[:, 0], valids[:, 1], linestyle='', marker='o', color='green')
#ax.plot(valids[:, 0], valids[:, 1], valids[:, 2], linestyle='', marker='o', color='green')
ax.plot_trisurf(valids[:, 0], valids[:, 1], valids[:, 2])
#plt.plot(non_valids[:, 0], non_valids[:, 1], linestyle='', marker='x', color='red')
#plt.plot(best_points[:, 0], best_points[:, 1], linestyle='', marker='*', color='blue')
#ax.show3d()
ax.plot([0.5385829601790], [0.4856783722067], [0.1607711822739], linestyle='', marker='o', color='blue')
ax.plot([sol.dec.wn], [sol.dec.pr], [sol.profit_man], linestyle='', marker='o', color='red')
#ax.set_xlim(0.5, 0.6)
#ax.set_ylim(0.4, 0.6)
#ax.set_zlim(0, 0.3)
plt.show()
def plot(self):
sol = self.search()
np_arr = np.zeros([len(self.points), 3])
# create numpy arrays
for i, (wn, pr, prof) in enumerate(self.points):
prof_val = prof if prof is not None else -100
np_arr[i, :] = [wn, pr, prof_val]
best_points = np.zeros((len(self.caller_info), 2))
for i, iter_inf in enumerate(self.caller_info):
best_points[i:] = (iter_inf['best_x'], iter_inf['best_y'])
from matplotlib import pyplot as plt
valids = np_arr[np_arr[:, 2] > -100]
non_valids =np_arr[np_arr[:, 2] <= -100]
plt.plot(valids[:, 0], valids[:, 1], linestyle='', marker='o', color='green', label='looked at')
plt.plot(non_valids[:, 0], non_valids[:, 1], linestyle='', marker='x', color='red', label='no sol')
plt.plot(best_points[:, 0], best_points[:, 1], linestyle='', marker='*', color='blue', label='best fval in it')
plt.plot(0.5385829601790, 0.4856783722067, label='global opt', marker='8', color='magenta', linestyle='')
plt.legend()
plt.show()
def search(self):
if self.par.a == 0: return None
wn_range = [0, 1]
pr_range = [0, 1]
raster_size = 1
raster_start_size = 201
iter = 1
self.caller_info = []
sol_tuple = ImprovedGridSearch2D.maximize(self._grid_search_func,
self.par, wn_range, pr_range, raster_size, iter, raster_start_size=raster_start_size, caller_info=self.caller_info)
if sol_tuple is None: return None
dec = DecisionVariables(MODEL_2_QUAD, pn=sol_tuple[2], pr=sol_tuple[1],
wn=sol_tuple[0], rho=sol_tuple[3], qn=sol_tuple[4], qr=sol_tuple[5])
# get case:
qr_rel = dec.qr / (par.tau / dec.rho)*dec.qn
if dec.rho > 1:
case = _CASE_ONE
else:
case = _CASE_TWO
sol = Solution(dec, sol_tuple[6], sol_tuple[7], case)
return sol
def _grid_search_func(self, par, wn, pr):
ret_dec = ModelTwoGridSearch._retailer_decision(par, wn, pr)
if ret_dec is not None:
pn, rho, qn, qr, ret_prof = ret_dec
man_profit = qn*(wn*(1-self.par.tau/rho)-par.cn) + qr*(pr-self.par.cr)+((self.par.tau/rho)*qn - qr)*self.par.s
self.points.append([wn, pr, man_profit])
return man_profit, (wn, pr, pn, rho, qn, qr, man_profit, ret_prof)
else:
self.points.append([wn, pr, None])
return None, None
class ModelTwoQuadSolver:
""" This class solves Model Two Quad by first using the GridSearch - then a local search algorithm (nelder-mead) """
@staticmethod
def solve(par, resolution='high'):
if par.a == 0 or par.cn < par.cr: return None
case_solutions = []
if resolution == 'very-high':
raster_size = 2000
wn_range = [0.52, 0.55]
pr_range = [0.48, 0.49]
#search_cases = (_CASE_ONE, _CASE_TWO)
# only case one interesting
search_cases = (_CASE_ONE, )
do_local_search = True
elif resolution == 'low':
raster_size = 51
search_cases = (_UNDEFINED_CASE,) # all cases once
do_local_search = False
#wn_range = [0.52, 0.55]
#pr_range = [0.48, 0.52]
wn_range = [0, 1]
pr_range = [0, 1]
elif resolution == 'high' or (resolution == 'super high' and par.cn < 0.66):
raster_size = 51
search_cases = (_CASE_ONE, _CASE_TWO) # all cases sepearte
do_local_search = True
wn_range, pr_range = [0, 1], [0, 1]
elif resolution == 'super high' and par.cn >= 0.66:
raster_size = 51
search_cases = (_CASE_ONE, _CASE_TWO) # all cases sepearte
do_local_search = True
wn_range, pr_range = [0.94, 1], [0, 0.3]
# we have to check both cases:
for case in search_cases:
startP = ModelTwoQuadGridSearch.search(par, iter=1, raster_size=raster_size, case=case, wn_range=wn_range, pr_range=pr_range)
if startP is None:
#print('warning at point', par, ' in case', case, ',startP is None')
continue
start_vec = [startP.dec.wn, startP.dec.pr]
# improve wn, pr
result = minimize(ModelTwoQuadSolver._minimize_func, args={'par': par, 'case': case}, x0=start_vec,
method='Nelder-Mead', options={'xatol': 0.00000000000000001, 'fatol': 0.00000000000000001})
# build a new solution using wn, pr
sol = ModelTwoQuadSolver.getSolution(par, float(result.x[0]), float(result.x[1]))
# assert that wn/pr combination lies really in case `case`
if sol is not None:
case_solutions.append(sol)
if len(case_solutions) >= 1:
return max(case_solutions, key=lambda k: k.profit_man)
else: return None
@staticmethod
def _minimize_func(x, args):
wn, pr = float(x[0]), float(x[1])
par = args['par']
case = args['case']
man_profit, data = ModelTwoQuadSolver.profit(par, wn, pr, case)
if man_profit is None:
return sys.maxsize
return -man_profit
@staticmethod
def getSolution(par, wn, pr, case=_UNDEFINED_CASE):
man_profit, sol_tuple = ModelTwoQuadSolver.profit(par, wn, pr, case)
if sol_tuple is None or man_profit <= 0 or sol_tuple[7] <= 0: return None
dec = DecisionVariables(MODEL_2_QUAD, pn=sol_tuple[2], pr=sol_tuple[1],
wn=sol_tuple[0], rho=sol_tuple[3], qn=sol_tuple[4], qr=sol_tuple[5])
# get case:
qr_rel = dec.qr / ((par.tau / dec.rho)*dec.qn)
if dec.rho > 1:
if qr_rel < .01: case = _CASE_ONE_A
elif qr_rel > .99: case = _CASE_ONE_C
else: case = _CASE_ONE_B
else:
if qr_rel < .01: case = _CASE_TWO_A
elif qr_rel > .99: case = _CASE_TWO_C
else: case = _CASE_TWO_B
#case = _CASE_ONE if dec.rho > 1 else _CASE_TWO
sol = Solution(dec, sol_tuple[6], sol_tuple[7], case)
return sol
@staticmethod
def _retailer_decision(par, wn, pr, case=_UNDEFINED_CASE):
assert type(wn) == float and type(pr) == float
tau, a, s, cr, cn, delta = par.tau, par.a, par.s, par.cr, par.cn, par.delta
rho_1 = -(((-1)**(2/3) * par.tau**(1/3) *(-1 + par.delta - pr + wn)**(2/3))/( 2 *(par.a *(-1 + par.delta))**(1/3)))
pn_1 = .5 * (1+wn-delta+pr)
rho_2 = 1
pn_2 = .5 * (1+wn-delta+pr)
valids = []
if type(rho_1) == complex or type(rho_1) == np.complex128:
if abs(rho_1.imag) < .000001 and rho_1.real > 1:
rho_1 = rho_1.real
else:
rho_1 = -1
for pn, rho in ((pn_1, rho_1), (pn_2, rho_2)):
qn = 1 - (pn - pr)/(1-delta)
qr = (pn-pr)/(1-delta) - pr/delta
if rho == 0: continue
ret_profit = qn*(pn-wn)*(1-par.tau/rho)-par.a*(rho**2-1)
if (0 <= qr <= (par.tau/rho)*qn) and rho >= 1 and ret_profit >= 0:
valids.append([pn, rho, qn, qr, ret_profit])
if len(valids) > 0:
best_valid = max(valids, key=lambda k: k[4])
# only return solution if retailer decision is in case `case`
best_rho = best_valid[1]
if case == _CASE_ONE:
return best_valid if best_rho > 1 else None
if case == _CASE_TWO:
return best_valid if best_rho == 1 else None
if case == _UNDEFINED_CASE:
return best_valid
else:
return None
@staticmethod
def profit(par, wn, pr, case=_UNDEFINED_CASE):
ret_dec = ModelTwoQuadSolver._retailer_decision(par, wn, pr, case=case)
if ret_dec is None: return None, None
pn, rho, qn, qr, ret_prof = ret_dec
man_profit = qn*(wn*(1-par.tau/rho)-par.cn) + qr*(pr-par.cr)+((par.tau/rho)*qn - qr)*par.s
return man_profit, (wn, pr, pn, rho, qn, qr, man_profit, ret_prof)
class ModelTwoQuadGridSearch:
@staticmethod
def search(par, iter=1, raster_size=51, case=_UNDEFINED_CASE, wn_range=[0, 1], pr_range=[0, 1]):
if par.a == 0: return None
raster_start_size = raster_size
sol_tuple = ImprovedGridSearch2D.maximize(ModelTwoQuadGridSearch._grid_search_func,
{'par': par, 'case': case}, wn_range, pr_range, raster_size, iter, raster_start_size=raster_start_size)
if sol_tuple is None: return None
dec = DecisionVariables(MODEL_2_QUAD, pn=sol_tuple[2], pr=sol_tuple[1],
wn=sol_tuple[0], rho=sol_tuple[3], qn=sol_tuple[4], qr=sol_tuple[5])
case = _CASE_ONE if sol_tuple[3] > 1 else _CASE_TWO
sol = Solution(dec, sol_tuple[6], sol_tuple[7], case)
return sol
@staticmethod
def _grid_search_func(options, wn, pr):
par, case = options['par'], options['case']
return ModelTwoQuadSolver.profit(par, wn, pr, case)
class ModelOneQuadGridSearch:
@staticmethod
def _retailer_decision(par, wn):
tau, a, s, cn = par.tau, par.a, par.s, par.cn
#rho_1 = (par.tau**(1/3) * (-1 + wn)**(2/3))/(2 *par.a**(1/3))
rho_1 = -(((-1)**(1/3) * par.tau**(1/3) * (-1 + wn)**(2/3))/(2 *par.a**(1/3)))
#rho_1 = ((-1)**(2/3) * par.tau**(1/3) *(-1 + wn)**(2/3))/(2 * a**(1/3))
pn_1 = (1+wn)/2
rho_2 = 1
pn_2 = (1+wn)/2
valids = []
for pn, rho in ((pn_1, rho_1), (pn_2, rho_2)):
qn = 1 - pn
if type(rho) == complex and round(rho.imag, 10) == 0: rho = rho.real
if rho == 0: continue
ret_profit = qn*(pn-wn)*(1-tau/rho)-a*(rho**2-1)
if (rho >= 1 and ret_profit >= 0):
valids.append([pn, rho, qn, ret_profit])
if len(valids) > 0:
return max(valids, key=lambda k: k[3])
else:
return None
@staticmethod
def _grid_search_func(par, wn, _):
ret_dec = ModelOneQuadGridSearch._retailer_decision(par, wn)
if ret_dec is None: return None, None
pn, rho, qn, ret_prof = ret_dec
man_profit = qn*(wn*(1-par.tau/rho)- par.cn + (par.tau/rho) * par.s)
return man_profit, (wn, pn, rho, qn, man_profit, ret_prof)
def search(self, par, resolution='high'):
if par.a == 0: return None
wn_range = [0, 1]
y_range = [0, 0] #not there
raster_size = 30
iter = 20
sol_tuple = GridSearch2D.maximize(ModelOneQuadGridSearch._grid_search_func,
par, wn_range, y_range, raster_size, iter)
if sol_tuple is None or sol_tuple[4] <= 0: return None
dec = DecisionVariables(MODEL_1_QUAD, pn=sol_tuple[1], wn=sol_tuple[0],
rho=sol_tuple[2], qn=sol_tuple[3])
case = _CASE_ONE if sol_tuple[2] > 1 else _CASE_TWO
sol = Solution(dec, sol_tuple[4], sol_tuple[5], case)
if sol is not None and sol.profit_ret > 0.0001:
return sol
else: return None
def search_old(self, par):
""" Returns a solution object or None if no solution found """
max_man_profit = -1
# 0:wn 1:pn 2:rho 3:qn 4:ret_profit
max_param = None
for wn in np.arange(0, 1+.01, .01):
wn = float(wn)
ret_dec = self._retailer_decision(par, wn)
if ret_dec is None: continue
pn, rho, qn, ret_prof = ret_dec
man_profit = qn*(wn*(1-par.tau/rho))- par.cn + (par.tau/rho) * par.s
if man_profit > max_man_profit:
max_man_profit = man_profit
max_param = [wn, pn, rho, qn, ret_prof]
# if we found something, build a solution object and return
if max_man_profit >= 0:
dec = DecisionVariables(MODEL_1_QUAD, pn=max_param[1], wn=max_param[0],
rho=max_param[2], qn=max_param[3])
case = _CASE_ONE if max_param[2] > 1 else _CASE_TWO
sol = Solution(dec, max_man_profit, max_param[4], case)
return sol
else:
return None
class ModelNBZeroSolver:
@staticmethod
def solve(par, resolution='high'):
startP = ModelNBGridSearch.search(par, resolution, b_range=[0, 0])
#return startP
#try to improve
if startP is None: return None
start_vec = [startP.dec.wn]
result = minimize(ModelNBSolver._minimize_func_but_set_b_to_zero,
args={'par': par},
x0=start_vec, method='Nelder-Mead')
return ModelNBSolver.getSolution(par, float(result.x[0]), 0)
class ModelNBSolver:
""" This class solves Model NB Quad by first using the GridSearch - then a local search algorithm (nelder-mead) """
@staticmethod
def solveOld(par, resolution='high'):
raise RuntimeError()
solPositiveB = ModelNBSolver.solvePositiveB(par, resolution)
solZeroB = ModelNBZeroSolver.solve(par, resolution)
if solPositiveB is not None and solPositiveB.profit_man > solZeroB.profit_man:
diff = solPositiveB.profit_man - solZeroB.profit_man
if diff < 0.000001:
return solZeroB
else:
return solZeroB
@staticmethod
def solve(par, resolution='high'):
if par.a == 0: return None
if resolution == 'high':
wn_lower_lim = par.cn
wn_upper_lim = 1
wn_count = 100
b_count = 100
elif resolution == 'case study':
wn_lower_lim = 0.463
wn_upper_lim = 0.555
wn_count = 1000
b_count = 3
best_fval = -sys.maxsize
best_fobj = None
# case 1
for wn in np.linspace(wn_lower_lim, wn_upper_lim, wn_count):
for b in np.linspace(par.s, wn, b_count):
wn, b = float(wn), float(b)
# calc manufacturer profit
man_prof_tuple = ModelNBSolver._manufacturer_profit(par, wn, b)
if man_prof_tuple[0] is not None and man_prof_tuple[0] > best_fval:
# update
best_fval = man_prof_tuple[0]
best_fobj = man_prof_tuple[1]
# case 2
wn_2 = (-1 - par.cn + par.tau + par.s* par.tau)/(2 *(-1 + par.tau))
b_2 = wn_2
#b_2 = par.s
#wn_2 = (1+par.cn)/2 - (par.tau * (1+par.s) )/2 + b_2 * par.tau
pn_2 = (1 - par.tau - b_2 * par.tau + wn_2)/(2 * (1 - par.tau))
qn_2 = 1 - pn_2
profitr_case_2 = qn_2 * (pn_2*(1 - par.tau/1) - wn_2 + par.tau/ 1 * b_2)
man_profit_case_2 = qn_2*(wn_2 - par.cn - (par.tau/1)*b_2 + (par.tau/1)*par.s)
# use best solution (case one or two?)
if man_profit_case_2 > best_fval and profitr_case_2 >= 0:
best_fval = man_profit_case_2
best_fobj = (wn_2, b_2, pn_2, 1, qn_2, man_profit_case_2, profitr_case_2)
if best_fobj is None: return None
wn, b, pn, rho, qn, man_profit, ret_prof = best_fobj
dec = DecisionVariables(MODEL_NB, pn=pn, b=b, wn=wn, rho=rho, qn=qn)
case = _CASE_ONE if rho > 1 else _CASE_TWO
sol = Solution(dec, man_profit, ret_prof, case)
#return sol
# do not try localsearch
#return ModelNBSolver.local_search(par, sol)
@staticmethod
def local_search(par, sol):
start_vec = [sol.dec.wn, sol.dec.b]
result = minimize(ModelNBSolver.__local_search_minimize_func,
args={'par': par},
x0=start_vec, method='Nelder-Mead')
wn, b = result.x; wn, b = float(wn), float(b)
fval, fobj = ModelNBSolver._manufacturer_profit(par, wn, b)
wn, b, pn, rho, qn, man_profit, ret_prof = fobj
dec = DecisionVariables(MODEL_NB, pn=pn, b=b, wn=wn, rho=rho, qn=qn)
case = _CASE_ONE if rho > 1 else _CASE_TWO
sol = Solution(dec, man_profit, ret_prof, case)
return sol
@staticmethod
def __local_search_minimize_func(x, args):
wn, b, par = float(x[0]), float(x[1]), args['par']
fval, f_obj = ModelNBSolver._manufacturer_profit(par, wn, b)
if fval is None:
return sys.maxsize
return -fval
def _manufacturer_profit(par, wn, b):
if b < par.s: return None, None
ret_prof_tuple = ModelNBSolver._retailer_profit(par, wn, b)
if ret_prof_tuple is None: return None, None
ret_prof, pn, rho = ret_prof_tuple
qn = 1 - pn
man_profit = qn*(wn - par.cn - (par.tau/rho)*b + (par.tau/rho)*par.s)
return man_profit, (wn, b, pn, rho, qn, man_profit, ret_prof)
@staticmethod
def _retailer_profit(par, wn, b):
valids = []
for case in (1,):
if case == 1:
pn, rho = utils.model_nb_pn_rho_case_one(par, wn, b)
qn = 1 - pn
if not(0 <= qn <= 1 and rho >= 1):
continue
profitr = qn * (pn*(1 - par.tau/rho) - wn + par.tau/ rho * b) - par.a*(rho - 1)
if profitr >= 0:
valids.append( (profitr, pn, rho) )
if len(valids) > 0:
return max(valids, key=lambda k: k[0])
return None
@staticmethod
def _pn_rho_case_one(par, wn, b):
rho = (1 - b)* (1/((wn - b)**2 + 4*par.a/par.tau))**(1/2)
pn = (rho - par.tau - b * par.tau + rho * wn)/(2 * (rho - par.tau))