diff --git a/freeride/affine.py b/freeride/affine.py index 76578cc..a34e5e6 100644 --- a/freeride/affine.py +++ b/freeride/affine.py @@ -899,12 +899,22 @@ def equation(self, inverse=False): latex_str += r"\end{cases}" return f"${latex_str}$" - def price_elasticity(self, p, delta=.000001): + def price_elasticity(self, p, delta=.000001, atol=1e-12, rtol=1e-9): q = self.q(p) pt = np.array([p,q]) - if self.intersections and np.any(pt == self.intersections, axis=1).max(): - below = self.price_elasticity(p - delta) - above = self.price_elasticity(p + delta) + intersections = np.asarray(self.intersections) + is_kink = ( + intersections.size > 0 + and intersections.ndim == 2 + and intersections.shape[1] == 2 + and np.any( + np.isclose(pt[0], intersections[:, 0], atol=atol, rtol=rtol) + & np.isclose(pt[1], intersections[:, 1], atol=atol, rtol=rtol) + ) + ) + if is_kink: + below = self.price_elasticity(p - delta, delta=delta, atol=atol, rtol=rtol) + above = self.price_elasticity(p + delta, delta=delta, atol=atol, rtol=rtol) s = f"\nElasticity is {below:+.3f} below P={p} and {above:+.3f} above." raise ValueError("Point elasticity is not defined at a kink point."+s) else: diff --git a/tests/test_curves.py b/tests/test_curves.py index 5d0bc2d..517388f 100644 --- a/tests/test_curves.py +++ b/tests/test_curves.py @@ -126,6 +126,29 @@ def test_horizontal_sum(self): self.assertIsNone(active[2]) +class TestAffinePriceElasticityKinks(unittest.TestCase): + def setUp(self): + d1 = Demand(12, -1) + d2 = Demand(6, -0.5) + self.kinked_demand = d1 + d2 + self.kink_p = self.kinked_demand.intersections[0][0] + + def test_exact_kink_input_raises(self): + with self.assertRaisesRegex(ValueError, "kink point"): + self.kinked_demand.price_elasticity(self.kink_p) + + def test_near_kink_input_does_not_raise(self): + elasticity = self.kinked_demand.price_elasticity(self.kink_p + 1e-7) + self.assertTrue(np.isfinite(elasticity)) + + def test_floating_point_kink_representation_raises(self): + # 0.1 + 0.2 is not exactly representable; scaled here it lands + # extremely close to the true kink price of 6. + p_with_fp_noise = (0.1 + 0.2) * 20 + with self.assertRaisesRegex(ValueError, "kink point"): + self.kinked_demand.price_elasticity(p_with_fp_noise) + + class TestSurplusAndRevenue(unittest.TestCase): def setUp(self):