11import unittest
2+ import numpy as np
23from freeride .curves import Demand
34from freeride .costs import Cost
45from freeride .monopoly import Monopoly
@@ -22,6 +23,140 @@ def test_piecewise_demand_zero_cost(self):
2223 self .assertAlmostEqual (m .p , 3.75 )
2324 self .assertAlmostEqual (m .profit , 28.125 )
2425
26+ def test_kinked_demand_monopoly (self ):
27+ """Test monopoly with kinked demand curve (discontinuous MR)."""
28+ # Create a kinked demand: steep segment + flat segment
29+ d1 = Demand (20 , - 1 ) # P = 20 - Q, steep segment
30+ d2 = Demand (10 , - 0.5 ) # P = 10 - 0.5*Q, flat segment
31+ kinked_demand = d1 + d2
32+
33+ # Use constant marginal cost
34+ cost = Cost (0 , 3 ) # MC = 3
35+ m = Monopoly (kinked_demand , cost )
36+
37+ # Verify this is truly profit-maximizing by checking grid
38+ q_grid = np .linspace (0.1 , 25 , 1000 )
39+ profits = []
40+ for q in q_grid :
41+ p = kinked_demand .p (q )
42+ if p > 0 : # Only consider positive prices
43+ profit = p * q - cost .cost (q )
44+ profits .append (profit )
45+ else :
46+ profits .append (- np .inf )
47+
48+ max_profit_grid = max (profits )
49+
50+ # Our solution should be within 1% of the grid maximum
51+ self .assertGreater (m .profit , 0.99 * max_profit_grid )
52+
53+ def test_kinked_demand_with_quadratic_cost (self ):
54+ """Test kinked demand with quadratic cost function."""
55+ # Create kinked demand
56+ d1 = Demand (15 , - 0.8 ) # P = 15 - 0.8*Q
57+ d2 = Demand (8 , - 0.3 ) # P = 8 - 0.3*Q
58+ kinked_demand = d1 + d2
59+
60+ # Quadratic cost: TC = 2 + Q + 0.1*Q^2, so MC = 1 + 0.2*Q
61+ cost = Cost ([2 , 1 , 0.1 ])
62+ m = Monopoly (kinked_demand , cost )
63+
64+ # Verify this is profit-maximizing using grid search
65+ q_grid = np .linspace (0.1 , 30 , 2000 )
66+ profits = []
67+ for q in q_grid :
68+ p = kinked_demand .p (q )
69+ if p > 0 :
70+ profit = p * q - cost .cost (q )
71+ profits .append (profit )
72+ else :
73+ profits .append (- np .inf )
74+
75+ max_profit_grid = max (profits )
76+ best_q_idx = np .argmax (profits )
77+ best_q_grid = q_grid [best_q_idx ]
78+
79+ # Our solution should be very close to grid optimum
80+ self .assertGreater (m .profit , 0.99 * max_profit_grid )
81+ self .assertAlmostEqual (m .q , best_q_grid , delta = 0.1 )
82+
83+ def test_kinked_demand_single_segment_only (self ):
84+ """Test kinked demand where monopolist serves only one segment."""
85+ # Use your exact suggestion: P = 10 - Q and P = 1 - 10*Q
86+ d1 = Demand (10 , - 1 ) # P = 10 - Q
87+ d2 = Demand (1 , - 10 ) # P = 1 - 10*Q (very steep, low willingness to pay)
88+ kinked_demand = d1 + d2
89+
90+ # With MC = 0, monopolist will choose Q where MR = 0 on the profitable segment
91+ cost = Cost (0 , 0 )
92+ m = Monopoly (kinked_demand , cost )
93+
94+ # For first segment P = 10 - Q, MR = 10 - 2Q
95+ # Setting MR = 0: Q = 5, P = 5
96+ # At Q = 5, first segment gives profit = 5 * 5 = 25
97+ # Second segment at any reasonable Q gives much lower prices/profits
98+ # So monopolist should choose Q = 5, P = 5 (still pricing above second segment)
99+
100+ self .assertAlmostEqual (m .q , 5.0 , places = 1 )
101+ self .assertAlmostEqual (m .p , 5.0 , places = 1 )
102+ # Verify this price is indeed above what second segment would offer
103+ self .assertGreater (m .p , 1.0 ) # Much higher than second segment's max price
104+
105+ # Verify optimality with grid search
106+ q_grid = np .linspace (0.1 , 15 , 1000 )
107+ profits = []
108+ for q in q_grid :
109+ p = kinked_demand .p (q )
110+ if p > 0 :
111+ profit = p * q - cost .cost (q )
112+ profits .append (profit )
113+ else :
114+ profits .append (- np .inf )
115+
116+ max_profit_grid = max (profits )
117+ self .assertGreater (m .profit , 0.99 * max_profit_grid )
118+
119+ def test_kinked_demand_both_segments (self ):
120+ """Test kinked demand where monopolist serves both segments."""
121+ # Create kinked demand where both segments are attractive
122+ d1 = Demand (15 , - 0.5 ) # P = 15 - 0.5*Q, gentle slope, high willingness to pay
123+ d2 = Demand (12 , - 1 ) # P = 12 - Q, steeper but still reasonable
124+ kinked_demand = d1 + d2
125+
126+ # Use marginal cost that makes serving both segments optimal
127+ cost = Cost (0 , 2 ) # MC = 2
128+ m = Monopoly (kinked_demand , cost )
129+
130+ # Find the kink point (where the segments meet)
131+ # d1: P = 15 - 0.5*Q, d2: P = 12 - Q
132+ # They intersect when 15 - 0.5*Q = 12 - Q
133+ # 3 = -0.5*Q, so Q = 6, P = 12
134+ kink_q = 6.0
135+ kink_p = 12.0
136+
137+ # Monopolist should operate beyond the kink (serve both segments)
138+ self .assertGreater (m .q , kink_q ) # Should serve both segments
139+ self .assertLess (m .p , kink_p ) # Price should be below kink point
140+
141+ # Verify optimality with grid search
142+ q_grid = np .linspace (0.1 , 20 , 1500 )
143+ profits = []
144+ for q in q_grid :
145+ p = kinked_demand .p (q )
146+ if p > 0 :
147+ profit = p * q - cost .cost (q )
148+ profits .append (profit )
149+ else :
150+ profits .append (- np .inf )
151+
152+ max_profit_grid = max (profits )
153+ best_q_idx = np .argmax (profits )
154+ best_q_grid = q_grid [best_q_idx ]
155+
156+ # Verify we found the optimum
157+ self .assertGreater (m .profit , 0.99 * max_profit_grid )
158+ self .assertAlmostEqual (m .q , best_q_grid , delta = 0.1 )
159+
25160
26161if __name__ == "__main__" :
27162 unittest .main ()
0 commit comments