Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 88 additions & 17 deletions freeride/equilibrium.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,6 @@ def __init__(
world_price=None,
tariff=0
):
# Check for perfectly elastic or inelastic curves
if demand.has_perfect_segment:
raise ValueError(
"Equilibrium does not currently support perfectly elastic or inelastic demand curves "
"due to implementation limitations (not economic theory). "
"These curves have indeterminate quantities at the equilibrium price."
)
if supply.has_perfect_segment:
raise ValueError(
"Equilibrium does not currently support perfectly elastic or inelastic supply curves "
"due to implementation limitations (not economic theory). "
"These curves have indeterminate quantities at the equilibrium price."
)

self.demand = demand
self.supply = supply

Expand Down Expand Up @@ -188,10 +174,20 @@ def _compute_small_open(self):
self._net_imports = Qd - Qs

def _find_intersection(self, dcurve, scurve):
if dcurve.has_perfect_segment or scurve.has_perfect_segment:
perfect_solution = self._solve_with_perfect_segments(
dcurve, scurve
)
if perfect_solution is not None:
return perfect_solution

for dpiece in dcurve.pieces:
for spiece in scurve.pieces:
if dpiece and spiece:
p_temp, q_temp = intersection(dpiece, spiece)
try:
p_temp, q_temp = intersection(dpiece, spiece)
except np.linalg.LinAlgError:
continue
if self._valid_in_domain(dpiece, spiece, q_temp):
return p_temp, q_temp

Expand All @@ -203,14 +199,89 @@ def _find_intersection(self, dcurve, scurve):
p = 0.5*(p_max+p_min)
return p, 0

def _solve_with_perfect_segments(self, dcurve, scurve):
candidates = []
has_unsupported = False
has_fixed_price_indeterminacy = False

for dpiece in dcurve.pieces:
for spiece in scurve.pieces:
if not (dpiece and spiece):
continue

d_horizontal = dpiece.slope == 0
s_horizontal = spiece.slope == 0
d_vertical = np.isinf(dpiece.slope)
s_vertical = np.isinf(spiece.slope)
d_regular = not (d_horizontal or d_vertical)
s_regular = not (s_horizontal or s_vertical)

# Horizontal demand/supply against regular opposite curve.
if (d_horizontal and s_regular) or (d_regular and s_horizontal):
p_temp, q_temp = intersection(dpiece, spiece)
if self._valid_in_domain(dpiece, spiece, q_temp):
candidates.append((p_temp, q_temp))
continue

# Vertical demand/supply against regular opposite curve.
if (d_vertical and s_regular) or (d_regular and s_vertical):
p_temp, q_temp = intersection(dpiece, spiece)
if self._valid_in_domain(dpiece, spiece, q_temp):
candidates.append((p_temp, q_temp))
continue

# Horizontal vs vertical gives a single point if domains allow.
if (d_horizontal and s_vertical) or (d_vertical and s_horizontal):
p_temp, q_temp = intersection(dpiece, spiece)
if self._valid_in_domain(dpiece, spiece, q_temp):
candidates.append((p_temp, q_temp))
continue

# Horizontal vs horizontal can pin a price but not quantity.
if d_horizontal and s_horizontal:
if np.isclose(dpiece.intercept, spiece.intercept):
has_fixed_price_indeterminacy = True
else:
has_unsupported = True
continue

# Vertical vs vertical is not handled by this equilibrium model.
if d_vertical and s_vertical:
has_unsupported = True

if len(candidates) == 1:
p_star, q_star = candidates[0]
return float(p_star), float(q_star)

if len(candidates) > 1:
unique_candidates = {
(round(float(p), 12), round(float(q), 12))
for p, q in candidates
}
if len(unique_candidates) == 1:
return tuple(unique_candidates.pop())
raise ValueError(
"unsupported combination: multiple distinct perfect-segment intersections"
)

if has_fixed_price_indeterminacy:
raise ValueError(
"economically indeterminate quantity at fixed price"
)
if has_unsupported:
raise ValueError(
"unsupported combination: perfect-segment pairing not implemented"
)
return None

def _valid_in_domain(self, dpiece, spiece, q_val):
d_dom = dpiece._domain
s_dom = spiece._domain
if d_dom:
if not (d_dom[1] <= q_val <= d_dom[0]):
if not (min(d_dom) <= q_val <= max(d_dom)):
return False
if s_dom:
if not (s_dom[0] <= q_val <= s_dom[1]):
if not (min(s_dom) <= q_val <= max(s_dom)):
return False
return True

Expand Down
35 changes: 23 additions & 12 deletions tests/test_horizontal_curves.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,28 +71,39 @@ def test_horizontal_demand_q_method(self):
# Below P=10, should return inf
self.assertTrue(np.isinf(d.q(9)))

def test_equilibrium_blocks_horizontal_supply(self):
"""Test that Equilibrium blocks horizontal supply curves."""
def test_equilibrium_allows_horizontal_supply_with_regular_demand(self):
"""Horizontal supply with regular demand should produce a unique point."""
with warnings.catch_warnings():
warnings.simplefilter("ignore")
s = Supply(5, 0) # Horizontal supply
d = Demand(10, -1) # Normal demand

with self.assertRaises(ValueError) as cm:
eq = Equilibrium(d, s)

self.assertIn("perfectly elastic", str(cm.exception))
self.assertIn("supply", str(cm.exception).lower())
eq = Equilibrium(d, s)
self.assertEqual(eq.p, 5)
self.assertEqual(eq.q, 5)

def test_equilibrium_blocks_horizontal_demand(self):
"""Test that Equilibrium blocks horizontal demand curves."""
def test_equilibrium_allows_horizontal_demand_with_regular_supply(self):
"""Horizontal demand with regular supply should produce a unique point."""
with warnings.catch_warnings():
warnings.simplefilter("ignore")
d = Demand(8, 0) # Horizontal demand
s = Supply(2, 1) # Normal supply

eq = Equilibrium(d, s)
self.assertEqual(eq.p, 8)
self.assertEqual(eq.q, 6)

def test_equilibrium_horizontal_horizontal_is_indeterminate(self):
"""Equal fixed-price demand and supply should raise an indeterminate error."""
with warnings.catch_warnings():
warnings.simplefilter("ignore")
d = Demand(8, 0)
s = Supply(8, 0)

with self.assertRaises(ValueError) as cm:
eq = Equilibrium(d, s)
Equilibrium(d, s)

self.assertIn("perfectly elastic", str(cm.exception))
self.assertIn("demand", str(cm.exception).lower())
self.assertIn(
"economically indeterminate quantity at fixed price",
str(cm.exception),
)
Loading