Skip to content

Commit 6aaeecf

Browse files
Harden kink detection in Affine.price_elasticity
1 parent 94a6f01 commit 6aaeecf

2 files changed

Lines changed: 37 additions & 4 deletions

File tree

freeride/affine.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -899,12 +899,22 @@ def equation(self, inverse=False):
899899
latex_str += r"\end{cases}"
900900
return f"${latex_str}$"
901901

902-
def price_elasticity(self, p, delta=.000001):
902+
def price_elasticity(self, p, delta=.000001, atol=1e-12, rtol=1e-9):
903903
q = self.q(p)
904904
pt = np.array([p,q])
905-
if self.intersections and np.any(pt == self.intersections, axis=1).max():
906-
below = self.price_elasticity(p - delta)
907-
above = self.price_elasticity(p + delta)
905+
intersections = np.asarray(self.intersections)
906+
is_kink = (
907+
intersections.size > 0
908+
and intersections.ndim == 2
909+
and intersections.shape[1] == 2
910+
and np.any(
911+
np.isclose(pt[0], intersections[:, 0], atol=atol, rtol=rtol)
912+
& np.isclose(pt[1], intersections[:, 1], atol=atol, rtol=rtol)
913+
)
914+
)
915+
if is_kink:
916+
below = self.price_elasticity(p - delta, delta=delta, atol=atol, rtol=rtol)
917+
above = self.price_elasticity(p + delta, delta=delta, atol=atol, rtol=rtol)
908918
s = f"\nElasticity is {below:+.3f} below P={p} and {above:+.3f} above."
909919
raise ValueError("Point elasticity is not defined at a kink point."+s)
910920
else:

tests/test_curves.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,29 @@ def test_horizontal_sum(self):
126126
self.assertIsNone(active[2])
127127

128128

129+
class TestAffinePriceElasticityKinks(unittest.TestCase):
130+
def setUp(self):
131+
d1 = Demand(12, -1)
132+
d2 = Demand(6, -0.5)
133+
self.kinked_demand = d1 + d2
134+
self.kink_p = self.kinked_demand.intersections[0][0]
135+
136+
def test_exact_kink_input_raises(self):
137+
with self.assertRaisesRegex(ValueError, "kink point"):
138+
self.kinked_demand.price_elasticity(self.kink_p)
139+
140+
def test_near_kink_input_does_not_raise(self):
141+
elasticity = self.kinked_demand.price_elasticity(self.kink_p + 1e-7)
142+
self.assertTrue(np.isfinite(elasticity))
143+
144+
def test_floating_point_kink_representation_raises(self):
145+
# 0.1 + 0.2 is not exactly representable; scaled here it lands
146+
# extremely close to the true kink price of 6.
147+
p_with_fp_noise = (0.1 + 0.2) * 20
148+
with self.assertRaisesRegex(ValueError, "kink point"):
149+
self.kinked_demand.price_elasticity(p_with_fp_noise)
150+
151+
129152
class TestSurplusAndRevenue(unittest.TestCase):
130153

131154
def setUp(self):

0 commit comments

Comments
 (0)