🇬🇧 English · 🇪🇸 Español
Condensed one-factor Leontief tariff model
puremacro.trade.condensed solves a static multi-country, multi-sector tariff
model with fixed input coefficients, one composite factor per country and
lump-sum rebated import duties, eliminates prices and outputs exactly, certifies
every reported equilibrium from the raw table flows, and reports only
numeraire-free measures. It is a different model from
solve_trade_equilibrium (consistent accounting); the comparison table below
says where they differ.
Runnable example on a synthetic table
import numpy as np
from puremacro.trade import calibrate_trade_model
from puremacro.trade.data import synthetic_codes, synthetic_icio_table
from puremacro.trade.condensed import (
BalancedIOTable, build_tariff_wedges, calibrate_condensed, compute_measures, solve_condensed,
)
# A seeded synthetic table: five countries, four sectors (the first two are merchandise).
countries, sectors = synthetic_codes(5, 4)
trade_calib = calibrate_trade_model(synthetic_icio_table(5, 4, seed=0),
country_codes=countries, sector_codes=sectors)
table = BalancedIOTable.from_trade_calibration(trade_calib, merchandise_mask=[True, True, False, False])
calib = calibrate_condensed(table)
# C01 raises a 15% duty on merchandise from C02 and C03, and 5% on everyone else.
wedges = build_tariff_wedges(calib, {"C01": {"*": 0.05, "C02": 0.15, "C03": 0.15}})
print(wedges.to_dataframe().head())
result = solve_condensed(calib, wedges) # gate, Newton, certificate
print(result.summary())
measures = compute_measures(calib, wedges, result.state, country="C01", partners=("C02", "C03"))
print(measures.to_markdown())
The synthetic sector codes S01..S04 belong to no known registry, so the
bridge takes an explicit merchandise_mask; the duties then fall on the two
merchandise sectors only. The run takes six Newton iterations and returns a
certificate maximum block near 3e-12 (below the 1e-10 gate). For C01
the effective rate on dutiable imports lies between the 5% and 15% rates
(about 11.7%), tariff revenue is about 1.8% of GDP, and the equivalent
variation splits exactly into real factor income, real net revenue and the
deficit-and-inventory transfer. The numbers come from generic random flows,
not from any economy. Three-region and other synthetic examples are in
tests/test_trade_condensed.py.
What it computes
Indices: countries k, cells i = country * S + sector (M = N S), final
categories f in {C, G, X} with Cobb-Douglas shares and exogenous inventories V.
The benchmark has all prices, wages and multipliers equal to one.
Calibration from a balanced table (table_from_arrays, then calibrate_condensed):
a_ij = Z_ij / y0_j b_j = VA_j / y0_j t_j = TLS_j / y0_j
omega^f_ik = F^f_ik / B^f_k A0^f_k = B^f_k + TFD^f_k t^f_k = TFD^f_k / A0^f_k
theta^f_k = A0^f_k / sum_f A0^f_k qV_ik = F^V_ik TV_k = TFD^V_k
Fbar_k = sum_{j in k} VA_j Y0_k = Fbar_k + sum_{j in k} TLS_j + sum_f TFD^f_k + TV_k
XN0_k = fob exports - imports s_k = XN0_k / sum_l Fbar_l
Equilibrium in levels (2M + 3N unknowns, one equation redundant by Walras' law):
(E1) (1 - t_j) p_j = b_j w_c(j) + sum_i a_ij tau_ij p_i zero profit
(E2) y_i = sum_j a_ij y_j + sum_k (sum_f omega^f_ik q^f_k + qV_ik) goods clearing
(E3) sum_{j in k} b_j y_j = Fbar_k factor market
(E4) Y_k = w_k Fbar_k + PT_k + TR_k income
(E5) XN_k = s_k sum_l w_l Fbar_l closure
(E6) sum_l w_l Fbar_l = sum_l Fbar_l numeraire
P^f_k = sum_i omega^f_ik tau^f_ik p_i E^f_k = theta^f_k (Y_k - XN_k - E^V_k)
q^f_k = (1 - t^f_k) E^f_k / P^f_k E^V_k = sum_i tau^V_ik p_i qV_ik + TV_k P^G_k
PT_k collects product taxes on output and on final purchases; TR_k collects
every duty ((tau - 1) x producer value) on intermediate, final and inventory
purchases and is rebated through (E4). The trade-balance closure (E5) keeps
each country's balance a fixed share of world factor income (Dekle, Eaton and
Kortum, 2008).
Condensation: given z = (log w, Y / Y0), (E1) is solved exactly by one LU of
I - B_tau^T with B_tau,ij = a_ij tau_ij / (1 - t_j), and (E2) by one LU of
I - a. The residuals are (E3) and (E4) scaled by base quantities, with the
reference country's income equation replaced by the numeraire; that dropped
equation is the Walras check reported with every state. The Jacobian uses
forward differences (h = 1e-7) with prices cached across the income columns.
Solvers (solve_condensed): damped Newton with Armijo backtracking, a
0.3 clamp on log-wage displacements and the stopping rule
max |r| <= 1e-12, |Walras| <= 1e-10; natural-parameter continuation in the
tariff scale (halve on failure down to 1/256, secant predictor); a nested
fallback that solves the affine income system given wages; optional
pseudo-arclength continuation with fold monitors (dlambda/ds, sign det J,
sigma_min / sigma_max) and a landing check at 1e-10; optional near-start
multistart raising MultipleEquilibria when a converged start lands farther
than 1e-9. A Collatz-Wielandt existence gate rho(B_tau) < 1 - 1e-3
(puremacro.trade.regularize.compute_spectral_radius) runs before every
solve and fails closed: bounds that certify rho < 1 but not the margin raise
ProductivityUncertified ("margin not met", with the bounds attached), bounds
that certify nothing raise it as "inconclusive", and neither claims
nonexistence; wedges built for another table are refused with
CalibrationError. When continuation stalls, the nested fallback's diagnostic
tag and its stage record (continuation_lam_reached) name the stall.
Certificate (certify_raw_flows): ten blocks recomputed from Z, F, VA, TLS,
TFD and the wedges, never from a, b, omega, theta or the factorizations:
zero profit on active cells, goods clearing, factor markets, household budget,
national income, tariff-revenue identity, closure, current-account identity
(NX from bilateral producer values), world adding-up and the numeraire. Every
block must be at most 1e-10; solve_condensed raises CertificationFailure
otherwise and never returns converged=True after a failure. The certificate
uses the state's basket prices and quantities, so it is independent of the
calibrated coefficients but not of the demand system that produced them. The
two per-cell blocks (zero profit, goods clearing) are scaled by
max(p_j y_j, cell_floor) and max(y_i, cell_floor) with cell_floor = 1 in
the table's own units, so cells below one unit are judged at absolute
1e-10; pass a smaller cell_floor to certify_raw_flows for tables in
large units. Phantom cells (empty cells the build fills with 1e-6 flows) are
left out of the zero-profit block only; every other block uses their true
output ratio y_j / y0_j, because the model lets phantom output move and
phantom value added is part of Fbar. solve_condensed(..., certify=False)
skips the certificate for diagnostics only: such a result is converged but
neither passed nor certified.
Measures (compute_measures, all numeraire-free, P^U_k = prod_f (P^f_k /
P0^f_k)^theta^f_k):
| Measure | Definition |
|---|---|
| consumer prices relative to wages | P^C_k / (P0^C_k w_k) - 1, the inverse real consumption wage (not a CPI) |
| equivalent variation | Atil^1_k / P^U_k - Atil^0_k, percent of base GDP, with the exact split into real factor income, real net revenue (tariff and other taxes) and the deficit-and-inventory transfer |
| real national income | (Y_k / P^U_k) / Y0_k - 1 |
| Fisher terms of trade | fob, ex-duty, sqrt(L^X P^X / (L^M P^M)) - 1 at base quantities |
| tariff revenue | TR_k / Y_k and the effective rate on dutiable imports (inventories count as dutiable only when the state was solved with inventories="tariffed") |
| constant-price real GDP | sum (VA_j + TLS_j) yhat_j + sum_f TFD^f qhat^f + TFD^V, a composition effect only |
| PE unit-cost bound | p^PE = (I - B_tau^T)^{-1} b / (1 - t), ln(1 + Pi^C) = ln(1 + Pi^{C,PE}) + Delta^rel, and the first-order direct/cascade split |
| sector incidence | sector_incidence(..., groups=...) with output-weighted within-group dispersion |
separate_baseline_tariffs(calib, wedges) re-expresses observed benchmark
duties as explicit gross wedges with residual product taxes, so counterfactual
increments stack on nonzero initial duties without counting benchmark customs
revenue twice (gates at 1e-11). It is an accounting separation, not evidence
that an unobserved tariff is zero.
Data contract
table_from_arrays (or BalancedIOTable.from_arrays) accepts the layout
Z (M, M), F (M, N, K), VA (M,), TLS (M,), TFD (N, K),
country_codes, sector_codes and fd_codes in any of the six OECD codes,
("C", "G", "X", "V"), ("C", "G", "X", "V", "VAL") (VAL merged into V and
recorded), ("C", "G", "X"), the processed-table ("C", "I", "Cx"), or the
final-demand vocabularies of the puremacro.trade.data loaders: FIGARO
(P3_S14, P3_S15, P3_S13 summed into C, P51G into G, P5M into V),
EXIOBASE (HFCE, NPISH, GGFC, GFCF, INVNT, VALUABLES merged into V,
and an EXPORT column that must be identically zero in a closed table), WIOD
(CONS_h, CONS_np, CONS_g, GFCF, INVT) and Eora (HFCE, NPISH,
GGFC, GFCF, INVNT, ACQ_VAL merged into V). Codes that map to one
category are summed; C and G are required, X and V are zero when absent.
Layouts without a purchases-abroad column (all but the OECD one) give an empty
X basket, so calibrate_condensed needs allow_empty_purchases_abroad=True
for them (X copies the C basket as an auxiliary price; its expenditure is
identically zero). Three-category layouts and any table with negative G cells
need negative_investment="raise" (default) or "to_inventory".
BalancedIOTable.from_io_table takes any object with those attributes (sparse
Z included), from_raw takes a puremacro.trade.data.RawIOData from any of
the five loaders, and from_trade_calibration bridges a
TradeCalibrationResult (labour and capital are summed into the single
factor; G contains inventories; Cx becomes X for a table without a recorded
final-use mapping and for the OECD DPABR mapping, while the FIGARO, WIOD and
Eora loaders' Cx becomes the exogenous V, so those tables calibrate with
allow_empty_purchases_abroad=True, and EXIOBASE's Cx needs an explicit
cx_category). The build adds 1e-6 phantoms to empty cells, floors
non-positive value added at 1e-3 of output, recomputes TLS as the column
residual and gates the change (1e-4 of world |TLS|, 1e-2 of a cell's
output), and assigns world round-off in trade balances to the reference
country (the last code by default). The merchandise mask (ISIC Rev.4 sections
A-C; utilities, construction and services are not merchandise) is inferred
for the OECD 45-industry registry, the ISIC Rev.4 section letters and the
frozen three-sector fixture; other
registries (the FIGARO, EXIOBASE, WIOD and Eora sector codes, synthetic codes)
pass merchandise_mask, for example from puremacro.trade.mrio.goods_mask.
Tariffs are ad-valorem rates r >= 0; domestic transactions are never
tariffed and residents' purchases abroad (X) are exempt. Multipliers below one
(import subsidies) are refused by TariffWedges.from_arrays, which keeps the
cost matrix nondecreasing in the tariff scale so that one existence
certificate at the full duty covers every continuation stage.
build_tariff_wedges takes a scalar, an (N, S) origin-by-sector table for
one importer, an (N, N, S) array, a mapping {importer: {origin | "*": rate}}
(retaliation; a named origin always overrides "*", whatever the key order) or
a callable such as TariffScenario.get_rate. wedges.to_dataframe() lists
the nonzero rates by origin, importer and sector. Coverage is merchandise by
default (coverage="all" includes services); fd_tariffed selects which of
C, G, V carry the duty. Under coverage="goods" a sector-explicit
specification (an array with a sector axis, an (S,) array in a mapping, or a
callable) that places duties on service sectors is trimmed to merchandise with
a RuntimeWarning, and the trimmed sectors are recorded in
wedges.metadata["dropped_service_rates"]; scalar rates mean "every covered
sector" and are silent. Concordances and aggregated tables belong to
puremacro.trade.mrio (its tables enter through from_io_table).
Two models, not one
solve_trade_equilibrium (consistent accounting) |
puremacro.trade.condensed |
|
|---|---|---|
| Factors | labour and capital, Cobb-Douglas value added | one composite factor per country |
| Final demand | C, I, Cx shares of income; fixed foreign saving deducted from I | C, G, X Cobb-Douglas over absorption net of exogenous inventories V |
| Trade balance | fixed baseline foreign saving (numeraire units or a share of world factor income) | shares of world factor income (also world GDP or own GDP) |
| Numeraire | first producer price = 1 | world factor income (or any country's wage) |
| Unknowns | full system (Newton, sparse LU, Krylov, Keller PAC) | exact 2N condensation (log w, Y / Y0) |
| Tolerance and acceptance | residual and account ledger at the solver tolerance | 1e-12 residual, 1e-10 Walras, ten raw-flow blocks at 1e-10 |
| Welfare | Hicksian EV/CV with endpoint Shapley attribution | EV with an exact additive split, real national income, Fisher ToT |
| Baseline duties | observed flows are basic-price flows | separate_baseline_tariffs |
Results are not interchangeable across the two columns. Use the condensed model when a certified one-factor Leontief benchmark with numeraire-free measures is wanted; use consistent accounting when the two-factor structure or the Hicksian policy tools are needed.
What is and is not validated
Verified in this repository (tests/test_trade_condensed.py): benchmark
reproduction at 1e-14 for all closures and numeraires; certificates at or
below 1e-10 on a synthetic 3x4 table, the frozen OECD 3x3 fixture, a
synthetic 20x6 table and a synthetic 5x4 table; the certificate reacts to
1e-5 perturbations of w, p, y, Y, XN, TR in the named block and to a
1e-9 wage perturbation; certificates of natural-unit tables with one to six
empty (phantom) cells; EV-split additivity at 1e-12; numeraire invariance of
eighteen measures and split components at 1e-9; a duty on fixed inventory
quantities is neutral under the factor closure and not under the world-GDP
closure; homogeneity under scaling the table by 1000; an exact 10% effective
rate for a uniform duty, also when inventories are untariffed; forward and
central Jacobians agree to 1e-6 relative; the PE bound lies below the GE
consumer price; continuation stages, stall detection, the nested fallback,
arclength monitors and landing, near-start multistart and the far-start
recovery on the 20x6 table; a fail-closed existence boundary on the synthetic
3x4 economy (a 500% goods duty by its first country passes the existence
gate, but the equilibrium with a positive wage in that country ceases to exist
near a 259% duty, lambda* = 0.51773 of 500%: along the branch lambda
rises monotonically while the wage tends to zero; natural-parameter
continuation stalls at lambda = 0.515625, pseudo-arclength raises
FoldDetected where the Jacobian is numerically singular, and
solve_condensed ends in EquilibriumNotFound naming the stall); and the
five loader layouts of puremacro.trade.data on synthetic 3x3 tables.
Not validated: no global uniqueness, universal fold threshold or global Newton
guarantee is asserted. Numerical residual checks do not establish global
existence, uniqueness or optimality of an equilibrium, and the certificate
thresholds are floating-point verification criteria, not interval root
enclosures. Arclength and multistart are evidence, not proofs. No turning point
has been exhibited on an economic example: the fold monitor is exercised by a
synthetic determinant sign change and by the existence boundary above, so
FoldDetected means that the path cannot be continued regularly (a turning
point or an existence boundary), and the boundary is not a universal fold
threshold. Static EV includes investment in the absorption composite. Full
45-sector runs on the OECD tables use dense LU factorizations of
M x M matrices with M in the thousands and are not a browser target.
References
- Dekle, R., Eaton, J., & Kortum, S. (2008). "Global Rebalancing with Gravity: Measuring the Burden of Adjustment." IMF Staff Papers, 55(3), 511–540.
Related: consistent trade accounting, Hicksian consumption welfare, Hicksian tariff policy, MRIO tables, structural validation status.