Skip to content

Economy modeling

For calculation dependencies and the owner of each state value, see the economy dependency graphs.

See the production-table dictionary for input coefficients and the economy-report dictionary for exported state columns, formulas, and units.

Economy data and alignment

An Economy model uses production, goods, pop-types, pop-needs, and buy-package tables, each represented by a parsed DataFrame in parsing.

You can either construct an Economy from the parsed tables, or let it parse them internally as below:

from vic3_analysis import Economy
economy = Economy()

Immutability

Economy is a frozen dataclass that deep-copies its input tables. Its public DataFrames remain mutable, but editing them is unsupported: construct a new Economy for changed model data, because cached coefficients are not invalidated. Indexes (building_index, goods_index, pop_index, and producible_goods) are cached tuples. Fixed coefficients such as base_prices, employment_matrix, employment_vector, construction_cost_vector, arable_land_vector, infrastructure_vector, and pop_wealth_init are cached, read-only arrays. The components of EconomyState own read-only copies of input arrays. Its realized building_goods_flow matrix has one row per configuration and one column per good, in model index order. Entries are weekly quantities after levels, throughput, and economy-of-scale bonuses: outputs are positive and inputs negative.

To display names while keeping the model's key-based alignment, request a language when parsing the internal tables:

from vic3_analysis import Economy

economy = Economy(language="english")

Localization is added only to internally parsed DataFrames. If df_production, df_goods, df_pop_types, df_pop_needs, or df_buy_packages is supplied explicitly, its contents are copied without adding localization or changing the supplied data.

import numpy as np

unscaled = economy.goods_input_matrix_unscaled
multipliers = np.full(unscaled.shape[0], 2.0)
scaled = economy.goods_input_matrix(throughput_multipliers=multipliers)

The unscaled input and output matrices are prepared together from the production table and cached as separate read-only arrays. Scaled matrices remain writable copies.

Snapshot helpers use the state argument name, including keyword calls such as economy.df_market(state=state) and economy.construction_cost(state=state).

Direct solving

Now that we have a model of the economy, if we know the levels of each building, we can solve for all kinds of metrics we care about, such as employment, goods flows, and GDP, all of which stored in the EconomyState dataclass.

import numpy as np
# Assume we have one level of everything
levels = np.ones(len(economy.building_index), dtype=np.float64)
state = economy.solve(levels)
print("Class of the state:", type(state))
print("Annual GDP: ", state.gdp(annual=True))
print("Total employed population: ", state.total_employment)
print("Workers and dependents: ", state.total_population)

Economy.solve() default to nominal mode, which ignores the market price mechanic and uses base prices for each good.

You can also use market mode, which derives national prices from buy and sell orders, just like the game does (still no local prices though).

Pass fixed weekly trade quantities with goods_import and goods_export. Both are non-negative finite 1-D arrays aligned to economy.goods_index, and default to zeros. Imports add sell orders; exports add buy orders. A good may have imports and exports simultaneously; both gross quantities affect prices.

imports = np.zeros(len(economy.goods_index), dtype=np.float64)
exports = np.zeros(len(economy.goods_index), dtype=np.float64)
imports[economy.goods_index.index("tools")] = 5.0
exports[economy.goods_index.index("tools")] = 2.0
state = economy.solve(
    levels, method="market", goods_import=imports, goods_export=exports
)

EconomyContext owns read-only goods_import and goods_export arrays. The state forwards these arrays and exposes imports and exports as aliases. goods_trade_flow is a read-only derived vector of net imports, goods_import - goods_export; market orders use the gross arrays. Optimization scenarios express each trade input as named (good, amount) pairs. Fixed demand assumptions in nominal/market studies can be supplied as export orders. Such orders appear as exports in reports and are separate from calculated household consumption. In equilibrium, exports are additional external demand.

Economy.solve() also supports throughput bonuses via economy of scale and parameterized throughput_multipliers. The two are applied additively to the raw throughput of each building, which is then used to compute goods flows.

Set economy_of_scale_level_cap=20.0 to apply up to +20% economy of scale throughput; leave it at 0.0 to disable the effect.

import numpy as np

levels = np.ones(len(economy.building_index), dtype=np.float64)
state = economy.solve(
    levels,
    method="market",
    throughput_multipliers=np.full(len(economy.building_index), 1.5),
    economy_of_scale_level_cap=20.0
)

To choose building levels from objectives and constraints, continue with the optimization guide. See the economy API for model parameters and state reports.

Snapshot components and preparation

All four state classes are available from vic3_analysis:

Component Stored inputs Derived values
EconomyContext (state.context) Building levels, realized goods flows, trade, workers, workforce ratio, optional baskets and scales Goods input/output totals, sell orders, non-household buy orders, population totals
MarketState (state.market) Shared context reference and prices Building surplus, deficit, and GDP
PopState (state.households) Wealth, household demand, optional income and expenses Balance, when both finances are supplied
EconomyState References to these three components Total buy orders, wealth statistics, per-capita reports

Existing flat reads such as state.pops and state.market_prices forward to their owning components. Direct construction now uses EconomyState(context=context, market=market, households=households). The market must reference the exact same context object.

Nominal and market solves leave goods_buy_packages, consumption_scales, pop_income, and pop_expenses as None. They continue to accept externally supplied population demand without evaluating household finances. pop_balance is None until both finances exist; df_pop() reports unavailable balances as NaN.

Prepare the fixed consumption inputs explicitly, then share them across snapshots:

from dataclasses import replace
from vic3_analysis import MarketState

context = economy.prepare_context(state.context)
state = replace(
    state,
    context=context,
    market=MarketState(context=context, market_prices=state.market_prices),
)
needs = economy.pop_buy_orders(state)
expenses = economy.pop_expenses(state)

The context stores goods_buy_packages as an immutable GoodsBuyPackages object containing read-only arrays: wealth holds ascending positive integer row labels, and quantities holds the wealth-by-goods matrix. Goods columns follow the owning economy's goods_index; worker columns follow pop_index. goods_buy_packages(context) returns GoodsBuyPackages, reusing the prepared object when present. df_goods_buy_packages(source) exports an independent DataFrame from a state, context, or basket object, while pop_consumption_scales(context) returns an independent writable array. Both calculators also accept complete states.

To supply custom prepared baskets, construct one object with both arrays:

import numpy as np

from vic3_analysis import GoodsBuyPackages

packages = GoodsBuyPackages(
    wealth=np.array([1, 5]),
    quantities=np.zeros((2, len(economy.goods_index))),
)
context = replace(state.context, goods_buy_packages=packages)

GoodsBuyPackages owns copies of its inputs, validates their alignment, and sorts labels and quantities together. Contexts share the immutable object. Use context.goods_buy_packages.wealth and context.goods_buy_packages.quantities after checking that baskets are prepared. This replaces the former separate buy_packages array and buy_package_wealth attributes.

prepare_context() computes only missing fields, returning the original context when fully prepared. Needs and expenses resolve baskets and scales independently: explicit arguments take precedence, then prepared context values, then calculation. No household-by-goods tensor is retained.

Refresh baskets when production, trade, or substitution assumptions change. Refresh scales when workers, workforce ratio, or profession multipliers change. For example, changing the workforce ratio requires clearing the old scales:

context = economy.prepare_context(
    replace(state.context, workforce_ratio=0.25, consumption_scales=None)
)
state = replace(state, context=context, market=replace(state.market, context=context))

Set goods_buy_packages=None when refreshing baskets. Wealth and prices may change while sharing the same context. Context preparation does not evaluate wealth or implement iteration.

Workers and dependents

EconomyState.pops always stores employed workers. Set the economy-wide workforce_ratio to the worker share of total population; it defaults to 1.0 to preserve worker-only accounting. The ratio must be finite and in (0, 1].

state = economy.solve(levels, method="market", workforce_ratio=0.25)
print(state.dependents_per_worker)  # 3.0, a cached property
print(state.total_employment)      # workers
print(state.total_population)      # four times the workers

Dependents are derived without another population matrix. They share each worker group's profession and wealth, consume half as much per person, and earn the profession's annual dependent_wage divided by 52 per week. For 100 workers at a ratio of 0.25, there are 300 dependents, 400 people, and 250 standard-worker equivalents of consumption before profession consumption multipliers.

GDP per capita divides production GDP by workers plus dependents. Total wealth weights wealth tiers by all people; averages, wealth distributions, Gini, and profession shares are unchanged by a uniform ratio. df_pop() reports separate employment, dependents, and population counts. Unemployed workers and demographic transitions are not inferred.

Building flows and cooperative income

state.building_goods_input and state.building_goods_output are cached, read-only totals derived from state.building_goods_flow. Signs are split before summing configurations, so a good produced by one configuration and consumed by another contributes to both sell and buy orders. The matrix retains the production table's signed net coefficients within each configuration; it does not recover gross inputs offset by outputs of the same good within a row.

Realized flows belong to EconomyContext. When changing an existing state, replace its context and rebind the market to the new context. Clear prepared baskets because the changed flows affect substitution:

from dataclasses import replace

context = replace(
    state.context,
    building_goods_flow=state.building_goods_flow * 2,
    goods_buy_packages=None,
)
changed_state = replace(state, context=context, market=replace(state.market, context=context))

state.building_surplus is each configuration's weekly goods value added at the state's prices, before wages. state.building_deficit is the positive magnitude of a negative surplus, representing input costs not covered by output revenue. Both are cached, read-only arrays aligned to building_index. Replacing flows or prices creates a new state with fresh caches.

pop_income(state) implements a simplified worker-cooperative policy: each configuration distributes all positive surplus to its employees in proportion to employee counts times profession wage_weight. Every employed profession participates regardless of paid_private_wage; zero weights receive zero cooperative income. Configurations of the same building type distribute independently.

state = economy.solve(levels, method="market")
income = economy.pop_income(state)
income_by_profession = income.sum(axis=0)
deficits_by_configuration = state.building_deficit

The returned writable float64 matrix matches state.pops.shape and records household totals in pounds per week, rather than per-worker income. It uses stored flows and prices directly, so no throughput arguments are needed. Loss-making configurations pay zero wages and retain an unfunded deficit. A positive surplus without any positive recipient weight raises ValueError identifying the configuration. Missing, negative, or non-finite wage weights are also errors.

Dependent earnings are added as pops * dependents_per_worker * dependent_wage / 52; missing wages mean zero. Only coefficients used by represented dependents are validated. These exogenous earnings do not reduce building surplus or increase production GDP, and remain available when a workplace has a loss.

The calculation leaves the state and pop_balance unchanged. Nominal and market solves do not automatically calculate income. Equilibrium funds operating losses through the income tax described below by default. Setting subsidize_operating_losses=False disables that tax. The model excludes other transfers, reserves, and subsistence income.

Population needs

pop_buy_orders(state) returns weekly goods quantities aligned to goods_index. It uses the state's employed-worker counts and integer wealth, the model's df_buy_packages, and the goods substitution definitions in df_pop_needs. Both tables can be passed as keyword arguments when constructing Economy; omitted tables are parsed from the game files.

from dataclasses import replace
from vic3_analysis import EconomyState

state = economy.solve(levels)
needs = economy.pop_buy_orders(state)
state_with_needs = replace(state, households=replace(state.households, pop_buy_orders=needs))
# Evaluate demand, prices, and household finances at the state's fixed wealth:
market, households = economy.evaluate_wealth(state.context, state.pop_wealth)
market_state = EconomyState(market.context, market, households)

solve() in nominal/market mode leaves household demand at zero. evaluate_wealth() calculates demand and prices at fixed wealth; equilibrium solving also iterates wealth. The calculation leaves the supplied state unchanged. pop_wealth and pop_wealth_init are read-only int64 arrays; integer-valued floating inputs are accepted, while fractional, negative, non-finite, or overflowing wealth is rejected. Occupied wealth levels must have a buy package. Empty cells can use zero or other undefined integer wealth levels.

For each need, effective supply is sell orders minus half of building and export demand, clamped to zero. Goods' shares of that supply are clipped to their configured minimum and maximum, multiplied by their weights, and then normalized. These limits apply before normalization, so a final purchase share can exceed its configured maximum supply share. Minimum shares can generate demand for absent goods; if all purchase weights are zero, the default good receives the whole category's demand.

Wealth packages specify category values at base prices per 10,000 workers. Values are scaled by employment * (1 + 0.5 * dependents_per_worker) and profession consumption_mult (default 1, with peasants normally consuming 0.05), allocated by purchase weights, and divided by each good's base price. Existing population demand and market prices do not affect the calculation. Cultural modifiers, prestige goods, local-state effects, weekly adjustment, and wealth feedback are not modeled here.

Standard packages by wealth

goods_buy_packages(state) returns immutable GoodsBuyPackages of weekly goods quantities per 10,000 standard workers. Its read-only wealth array contains ascending positive int64 labels, and quantities is a read-only float64 matrix with columns in goods_index order and zeros for unconsumed goods. Prepared context baskets are authoritative and reused; otherwise every level defined in the raw df_buy_packages table is calculated.

packages = economy.goods_buy_packages(state)
wealth = int(packages.wealth[0])
goods_at_wealth = packages.quantities[0]
# Convert only when a table or export is needed:
report = economy.df_goods_buy_packages(packages)

The packages use the state's building inputs/outputs, imports, and exports for goods substitution. They do not depend on population counts, occupied wealth levels, profession consumption multipliers, market prices, or existing population demand. Even an empty population has a complete set of calculated baskets. Each row's value at base prices equals its wealth tier's total category budget. The returned object owns read-only arrays. df_goods_buy_packages(state), df_goods_buy_packages(context), or df_goods_buy_packages(packages) returns an independently mutable report with int64 index named wealth and goods columns in model order. Editing that report changes no baskets or model tables.

Pass the basket object to pop_buy_orders to reuse its goods quantities:

packages = economy.goods_buy_packages(state)
needs = economy.pop_buy_orders(state, goods_buy_packages=packages)
next_state = economy.solve(
    levels, method="market", throughput_multipliers=np.full(len(levels), 1.1)
)
next_needs = economy.pop_buy_orders(next_state, goods_buy_packages=packages)

Supplied baskets stay fixed when market supply changes. Demand is calculated directly from each occupied wealth tier's goods quantities, scaled by worker- equivalent counts including half-rate dependent consumption, and profession consumption multipliers per 10,000 workers. Substitution, base-price conversion, and validation of unused state goods flows and raw need/package tables are skipped.

GoodsBuyPackages construction validates unique positive integer wealth labels and finite nonnegative quantities, and sorts rows with their labels. Quantity columns must already follow the model's goods_index order; consumption calculations check the column count and coverage of occupied tiers. Only occupied wealth tiers need rows; empty population cells need no matching package. Explicit baskets, prepared context baskets, and freshly calculated baskets use the same selection path. DataFrames are not accepted as basket arguments. The basket object and state remain unchanged.

Reusable household consumption scales

pop_consumption_scales(state) returns a fresh writable float64 matrix aligned to configurations × professions. Each cell is a dimensionless multiple of a weekly basket for 10,000 standard workers:

scale = workers / 10_000 * consumption_mult * (1 + 0.5 * dependents_per_worker)

Missing profession multipliers default to 1. Scales are independent of wealth, market prices, and goods flows. Reuse scales while worker counts, workforce ratio, and profession multipliers remain unchanged. Reuse the basket table while production, trade, and substitution assumptions remain unchanged; both can be reused after changing household wealth or market prices.

Pass either or both precomputed inputs to needs and expense calculations:

packages = economy.goods_buy_packages(state)
scales = economy.pop_consumption_scales(state)
needs = economy.pop_buy_orders(
    state, goods_buy_packages=packages, consumption_scales=scales
)
expenses = economy.pop_expenses(
    state, goods_buy_packages=packages, consumption_scales=scales
)
income = economy.pop_income(state)
balance = income - expenses

Supplied scales are authoritative: they must match state.pops.shape, contain finite nonnegative values, and be zero wherever worker counts are zero. They already include all population factors, which are not reapplied or checked against the current profession multipliers or workforce ratio. The caller is responsible for refreshing stale scales. Omitted scales reuse prepared context scales when present, otherwise calling pop_consumption_scales(state). Every occupied wealth tier needs a basket even if its scale is zero; empty cells need no defined wealth tier.

Household expenses

pop_expenses(state) returns a fresh writable float64 matrix shaped like state.pops, containing household consumption expenses in pounds per week. It values each occupied wealth tier's goods basket at state.market_prices and multiplies by its consumption scale, including profession and dependent effects. Prices must be finite, nonnegative, and goods-aligned; zero prices are valid. Changing prices changes expenses without changing basket quantities.

The goods_buy_packages argument follows the same contract as for needs. Supplied baskets bypass unused production flows, base prices, and raw needs/package tables. Empty households have zero expenses; an empty population may use an empty supplied basket table. All calculated costs and expenses must be finite.

With matching baskets and scales, total expenses equal needs @ state.market_prices. The methods aggregate quantities by wealth tier and do not allocate a configuration-by-profession-by-goods tensor. They leave the state, supplied arrays/tables, and model tables unchanged. In particular, balance above is calculated explicitly; state.pop_balance is unchanged. Store finances explicitly to obtain a derived balance on a new snapshot:

state = replace(
    state,
    households=replace(state.households, pop_income=income, pop_expenses=expenses),
)
balance = state.pop_balance  # Read-only income minus expenses.

Replacing just the market or household component shares state.context and its production arrays. The caller must refresh dependent results when changing wealth or prices; constructing a snapshot does not recalculate demand or finances. Expenses price target consumption without affordability limits, rationing, taxes, or wealth transitions. Nominal and market modes do not automatically calculate expenses or balances.

Evaluating fixed wealth

evaluate_wealth(context, pop_wealth) evaluates one wealth assignment and returns matching market and household components. It calls prepare_context() to fill missing baskets and consumption scales, preserving any supplied prepared fields. It then calculates household goods demand, derives market prices from industrial, export, and household buy orders against production and import sell orders, and evaluates cooperative income and household expenses at those same prices.

initial = economy.solve(building_levels, workforce_ratio=0.25)
market, households = economy.evaluate_wealth(initial.context, initial.pop_wealth)
state = EconomyState(market.context, market, households)
balance = state.pop_balance  # Evaluated, read-only weekly income minus expenses.

Use market.context when composing the result: it is the supplied context when already fully prepared, or a newly prepared context otherwise. Inputs and model tables remain unchanged. Returned arrays are read-only, and supplied wealth values are preserved as int64. Wealth must match configurations × professions; every occupied tier needs a basket, including households with zero consumption scales. Empty cells may use undefined nonnegative integer tiers.

Total household expenses equal households.pop_buy_orders @ market.market_prices within floating-point precision. Existing preparation, input validation, and income-allocation errors propagate. This evaluates target consumption without affordability limits or wealth transitions; nominal and market solve modes continue to leave household finances unevaluated.

Iterating wealth

Initialize matching components, then take one simultaneous step:

market, households = economy.evaluate_wealth(initial.context, initial.pop_wealth)
context = market.context
market, households = economy.iterate(context, market, households)
state = EconomyState(context, market, households)

iterate() trusts the supplied components' economic consistency. It requires evaluated income and expenses, a market referencing the exact same context, and prepared baskets/scales. Reevaluate after externally changing wealth, production, or trade. Prepared wealth labels must be consecutive; occupied wealth must be covered. Empty cells retain their original wealth labels.

Using previous prices and income, each occupied household decreases one tier when income is below 98% of its current-tier cost. Otherwise, it increases one tier when income reaches 102% of its next-tier cost. Other households retain their tier. Decreases take precedence even if basket costs are nonmonotonic. These constants follow WEALTH_PROGRESS_ACCEPTABLE_DEFICIT and WEALTH_PROGRESS_WANTED_BUFFER in the local game's common/defines/00_defines.txt. The steps are numerical; weekly wealth-progress timing is not simulated.

For a threshold B, comparisons use t = atol + rtol * max(abs(income), abs(B)): decrease when income < B - t, advance when income >= B - t. Defaults are rtol=1e-9, atol=1e-8; zero tolerances recover exact comparisons. Costs and income are weekly totals for each configuration/profession household group. At the minimum tier, downward movement is suppressed; at the maximum tier, advancement is omitted. If nothing changes, iterate() returns the supplied objects. Otherwise, it evaluates the new assignment once and returns matching read-only components sharing the fixed context.

Solving equilibrium

from vic3_analysis import EquilibriumError

try:
    state = economy.solve(
        building_levels,
        method="equilibrium",
        workforce_ratio=0.25,
        max_iterations=1000,
        # initial_wealth=previous_state.pop_wealth,  # Optional full-matrix warm start.
    )
except EquilibriumError as error:
    last_state = error.state
    print(error.diagnostics.status, error.diagnostics.iterations)
    print(error.diagnostics.increasing_cells, error.diagnostics.decreasing_cells)
else:
    diagnostics = state.equilibrium_diagnostics
    print(diagnostics)

Equilibrium prepares fixed production, trade, baskets, consumption scales, cooperative allocation shares, and dependent earnings once. The solver then iterates wealth, demand, prices, income, and expenses. initial_wealth defaults to profession starting tiers, or accepts a nonnegative integer matrix matching configurations × professions. Unsupported occupied tiers are rejected rather than clipped. Household demand is calculated endogenously, alongside any fixed import and export orders.

Success requires no permitted wealth changes and a fresh final verification through the accounting calculators. An initially stable assignment reports zero iterations. max_iterations is a positive integer counting completed transition steps; initialization and final verification do not count. Convergence and cycles are checked at the final allowed step before exhaustion is reported.

state.equilibrium_diagnostics.status == "converged" describes stationary wealth. It can coexist with unaffordable_floor_cells or loss_buildings. The former identifies households below their lower affordability threshold at the minimum tier. The latter identifies unfunded losses larger than atol + rtol * max(output_value, input_value), using the existing signed-flow accounting. Unfunded loss flags can accompany a successful solve when subsidize_operating_losses=False. Dependent earnings remain exogenous, and trade remains fixed orders.

Tax-funded operating losses

Equilibrium covers every configuration's operating loss through a proportional tax on all represented household income by default:

state = economy.solve(levels, method="equilibrium")
gross_income = state.pop_income_before_tax
tax = state.pop_income_tax
disposable_income = state.pop_income

At each price evaluation, total subsidy cost is D = state.building_deficit.sum(). Gross income Y includes both cooperative surplus distributions and dependent earnings. Every household pays the same rate D / Y.sum(); zero-income households pay zero. No losses means zero tax, including for an empty economy. Subsidies pay building costs directly; they add no payroll or household transfer. Wealth transitions and balances use disposable income, and taxes are recomputed when prices change. GDP still measures production value added; it is not reduced again by the subsidy transfer.

building_surplus and building_deficit continue to describe production before subsidies. Funded losses do not appear in diagnostic loss_buildings. pop_income_tax and derived pop_income_before_tax are read-only matrices aligned to pops. Evaluated untaxed states have zero tax arrays; unevaluated finances remain None. economy.pop_income(state) and economy.pop_income_tax(state) recompute the policy from the stored context and prices.

Exactly 100% tax is allowed. Losses exceeding all gross income raise EquilibriumError with status insufficient_income, including at initialization. Its snapshot contains the failing wealth and prices, gross income in pop_income, and zero tax collected; it is not a successful funded state. Direct income calculation and evaluate_wealth() instead raise ValueError. A failure during iteration retains the failing evaluation rather than the preceding funded one.

Disable the policy with subsidize_operating_losses=False. Direct EconomyContext construction and equilibrium optimizers also default to enabled. The context retains the selected policy for evaluate_wealth() and iterate(). Economy.solve() resolves an omitted or None policy to enabled for equilibrium and disabled for nominal/market modes. Explicitly enabling it for nominal or market modes is rejected.

EquilibriumError distinguishes cycle, max_iterations, evaluation_failed, verification_failed, and insufficient_income. Its immutable .state retains the last complete evaluation and references the same .diagnostics. Diagnostics identify cells requiring changes and financial flags; cycles additionally report cycle_start and cycle_length. Household coordinates are (configuration_index, profession_index) in economy index order; building flags are configuration indices. Calculation failures chain their original exception as __cause__. Invalid setup or initial evaluations other than insufficient income raise ValueError directly. A detected cycle or exhausted budget does not prove that no equilibrium exists.

Nominal and market modes retain their existing behavior and leave equilibrium diagnostics as None; equilibrium controls are ignored in those modes.

For concise live diagnostics, enable the standard logger vic3_analysis.analysis.economy.equilibrium at DEBUG. It reports solver start, completed wealth transitions, elapsed seconds, success/failure status, cycle information on failure, and financial-flag counts on success. An iteration summary appears after initialization (inner_iterations=0) and after each completed wealth transition. It counts cells eligible to increase or decrease wealth, unaffordable floor cells, and loss-making building configurations. The increase/decrease counts describe changes permitted from the current assignment, rather than changes already applied. Verification-started and verification-completed events bracket successful final verification; failure is reported by the existing solver failure summary. Library code does not configure handlers; see logging configuration and optimizer progress.

Exporting standard packages

To export packages for an economy with no buildings, run examples/buy_packages.py from the repository root:

uv run python examples/buy_packages.py
# Equivalent module invocation:
uv run python -m examples.buy_packages

The example parses the local game installation directly, without requiring previously exported parser tables. Initialize the artifact checkout first with git submodule update --init --recursive if necessary. It constructs an EconomyState with zero building levels, production, imports, exports, and represented employment. Standard packages remain defined for all wealth tiers: with zero supply, minimum shares apply, and categories with zero purchase weights fall back to their default goods.

The example calls economy.df_goods_buy_packages(state) for the DataFrame export. The UTF-8 output is artifacts/analyses/buy_packages/default.csv, with wealth as the first column followed by goods in model order. Re-running the example replaces this CSV, and the script prints its output path.

The method's docstring links the game definitions, the Needs formulas, and the official developer diary. The half-non-pop-demand deduction comes from community reverse engineering documented by the wiki; zero-clamping effective supply is a model convention.