Skip to content

Economy and production API

Used by the economy guide and all optimization showcases.

For returned columns, see the production dictionary and economy-report dictionary.

EconomyState(context, market, households) composes EconomyContext, MarketState, and PopState. All four are exported from vic3_analysis. Flat attribute reads are preserved; constructors and replacements use the component fields. market.context must be the exact same object as context. Components own read-only copies of their arrays; constructing a complete state does not copy its context or evaluate dependent results.

EconomyContext stores realized signed building_goods_flow quantities shaped as configurations × goods, trade, workers, and workforce ratio. It derives goods input/output totals and fixed orders. MarketState stores prices and derives building surplus/deficit and GDP from those prices and the shared context. PopState stores wealth, demand, and optional pop_income and pop_expenses. Its derived pop_balance is None unless both finances exist. Nominal/market solves leave these finances unevaluated; df_pop() reports balances as NaN.

Economy.prepare_context(context) returns a context with prepared baskets and scales, computing only missing fields and returning a fully prepared context unchanged. goods_buy_packages is an optional immutable GoodsBuyPackages object with read-only quantities (wealth × goods) and ascending integer wealth labels; it defaults to None. Construct it with GoodsBuyPackages(wealth=..., quantities=...) or clear it with goods_buy_packages=None. consumption_scales is an optional read-only configuration-by-profession array. Goods and profession axes follow the owning economy's indexes. Existing solves leave these fields unprepared.

Economy.evaluate_wealth(context, pop_wealth) returns (market, households) for one fixed wealth assignment. It automatically prepares missing context baskets and scales, calculates demand and market prices, then evaluates income and expenses at those prices. Balance is derived from the returned finances. The market references the prepared context: fully prepared inputs retain their identity; incomplete inputs produce a new context without modifying the original. Compose the results with EconomyState(market.context, market, households). See evaluating fixed wealth.

Economy.iterate(context, market, households, *, rtol=1e-9, atol=1e-8) returns matching (MarketState, PopState) after one simultaneous one-tier wealth transition. Initialize through evaluate_wealth() first: iteration trusts economic consistency, requires evaluated finances and prepared consecutive tiers, and shares the supplied context. An unchanged assignment returns the same components. See iterating wealth for affordability thresholds and numerical comparisons.

Economy.solve(..., method="equilibrium", initial_wealth=None, max_iterations=1000, rtol=1e-9, atol=1e-8) calculates endogenous household demand and returns a complete, verified state with equilibrium_diagnostics. Fixed goods_import and goods_export orders are added independently to supply and demand. The optional initial wealth is a full configuration/profession integer matrix. Nominal/market modes ignore these controls and leave diagnostics as None.

EquilibriumDiagnostics and EquilibriumError are exported from vic3_analysis. The immutable diagnostics distinguish stationary wealth from financial sustainability: unaffordable floor households and unfunded building losses are flags on successful results. Errors retain .state and .diagnostics for cycles, exhausted iterations, evaluation failures, and final-verification failures. Setup/initial-evaluation errors remain ValueError. See solving equilibrium for exception handling and diagnostic fields.

Economy.pop_income(state) returns a fresh writable float64 matrix of weekly cooperative income totals aligned to configurations × professions. It allocates each configuration's positive surplus using employee counts times profession wage weights, including professions with paid_private_wage=False. It adds dependent earnings from profession dependent_wage values, converted from annual to weekly amounts. See the cooperative income guide for accounting assumptions and failure behavior.

EconomyContext and Economy.solve() accept keyword-only workforce_ratio=1.0. This uniform worker share must be in (0, 1]; dependents_per_worker is cached as (1 - workforce_ratio) / workforce_ratio. pops and total_employment count workers; total_population includes dependents. Scenario.workforce_ratio passes the same assumption to optimizer-created states.

Economy.goods_buy_packages(state_or_context) returns immutable GoodsBuyPackages, reusing prepared baskets when available. Its wealth labels and quantities matrix are read-only; goods columns follow economy.goods_index order. Economy.df_goods_buy_packages(source) exports a fresh mutable DataFrame from an EconomyState, EconomyContext, or GoodsBuyPackages, copying both quantities and wealth labels. The index is named wealth and columns follow goods_index. The raw Economy.df_buy_packages input table remains separate and unchanged. Economy.pop_consumption_scales(state_or_context) returns a fresh writable float64 matrix of dimensionless household scales: pops / 10_000 * consumption_mult * (1 + 0.5 * dependents_per_worker). pop_buy_orders() and pop_expenses() accept keyword-only goods_buy_packages: GoodsBuyPackages | None = None and consumption_scales=None to reuse baskets and scales independently. Each omitted argument uses its prepared context value before falling back to calculation. Explicit and prepared baskets share the same selection and alignment checks; DataFrame basket arguments are rejected. Supplied scales are authoritative, finite, nonnegative, population-shaped, and zero for empty cells; profession multipliers are not recomputed. Refresh them when the population assumptions change.

Economy.pop_expenses(state) returns a fresh writable float64 matrix of weekly household consumption expenses aligned to configurations × professions. It prices each wealth basket at the state's market prices and applies the household scale. It does not change wealth, demand, or pop_balance. See household expenses for validation, reuse conditions, and an explicit income - expenses example.

production_table

production_table(
    df_buildings: DataFrame,
    df_goods: DataFrame,
    df_pm: DataFrame,
    df_tech: DataFrame | None = None,
    df_pop_types: DataFrame | None = None,
) -> DataFrame

Build the production table of all building configurations.

For every building, enumerates every combination of production methods (one per production-method group) and aggregates the per-method employment (total and per profession) and net goods flows (positive = output, negative = input). When pop types are supplied, wage-normalized employment is the sum of each profession's employment multiplied by its wage weight, restricted to pop types whose paid_private_wage flag is true (defaulting to true when undefined). The infrastructure_usage_per_level column holds the net per-level footprint: the building group's usage minus the state_infrastructure_add generation summed over the chosen production methods, so provider configurations (ports, railways, urban centers) carry negative values under the full-employment nominal model. Buildings without a construction cost are kept with a construction cost of 0. Each row also records the technologies required to unlock the configuration (the building's own unlocking technologies followed by those of every chosen production method, de-duplicated in order of first appearance). When technology data is supplied, each row also records the earliest era at which the configuration becomes available (the maximum era of those technologies, 0 when none). Nominal values are evaluated at the goods table's base prices; the profit ratio columns follow plain division semantics (x / 0 is inf and 0 / 0 is NaN). Each input_ratio_goods_<good> column is the good's physical net-input quantity divided by the total physical net-input quantity across all goods. Outputs and zero net flows contribute 0, and configurations without net inputs receive 0 in every input-ratio column. These ratios are quantity-based analytical measures; Victoria 3's prestige-goods throughput bonus instead uses the proportion of gross input base value.

Parameters:

  • df_buildings

    (DataFrame) –

    Buildings table (BuildingsParser.to_dataframe() or tables/buildings.csv) with key, building_group, urbanization, infrastructure_usage_per_level, required_construction_points, production_method_groups, and unlocking_technologies columns. Optional economy_of_scale and is_subsistence columns determine economy-of-scale eligibility; missing columns default to false.

  • df_goods

    (DataFrame) –

    Goods table (goods or tables/goods.csv) with key and cost columns. Its row order fixes the goods_<good> column order of the result.

  • df_pm

    (DataFrame) –

    Production-methods table (ProductionMethodParser.to_dataframe() or tables/production_methods.csv) with building, production_method_group, production_method, unlocking_technologies, employment, employment_<profession>, state_infrastructure_add, and goods_<good> columns (the state-infrastructure column is zero-filled with a warning when absent).

  • df_tech

    (DataFrame | None, default: None ) –

    Optional technology table (technology or tables/technology.csv) with key and era columns. When omitted, era and unlocking_tech_localization are not produced.

  • df_pop_types

    (DataFrame | None, default: None ) –

    Optional pop-types table (PopTypesParser.to_dataframe() or tables/pop_types.csv) with key and wage_weight columns, plus an optional paid_private_wage column. Missing private-wage flags default to true, including when the column is absent in older game data. When the table is omitted, the wage-normalized employment columns are not produced.

Returns:

  • DataFrame –

    A DataFrame with one row per building configuration. The

  • DataFrame –

    "production_method" column holds the chosen production methods

  • DataFrame –

    concatenated with +. The remaining columns are "building",

  • DataFrame –

    "building_group", "parent_group", "land_usage",

  • DataFrame –

    "is_subsistence", "discoverable_resource",

  • DataFrame –

    "depletable_resource", "economy_of_scale", "urbanization",

  • DataFrame –

    "infrastructure_usage_per_level" (net of the configuration's

  • DataFrame –

    state_infrastructure_add generation), optional "era",

  • DataFrame –

    "unlocking_tech", "employment", optional

  • DataFrame –

    "wage_normalized_employment", "construction_cost",

  • DataFrame –

    "value_goods_inputs_nominal", "value_goods_outputs_nominal",

  • DataFrame –

    "profit_nominal", "profit_margin_nominal",

  • DataFrame –

    "profit_per_capita_nominal",

  • DataFrame –

    optional "profit_per_wage_normalized_employment_nominal", and

  • DataFrame –

    "profit_per_construction_cost_nominal", followed by one

  • DataFrame –

    goods_<good> column per good, each immediately followed by its

  • DataFrame –

    input_ratio_goods_<good> column, and one

  • DataFrame –

    employment_<profession> column per profession.

Raises:

  • ValueError –

    If a supplied technology table is missing an unlocking technology, or if a supplied pop-types table cannot uniquely provide a valid private-wage weight for each employment profession.

Source code in src/vic3_analysis/analysis/production.py
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
def production_table(
    df_buildings: pd.DataFrame,
    df_goods: pd.DataFrame,
    df_pm: pd.DataFrame,
    df_tech: pd.DataFrame | None = None,
    df_pop_types: pd.DataFrame | None = None,
) -> pd.DataFrame:
    """Build the production table of all building configurations.

    For every building, enumerates every combination of production methods
    (one per production-method group) and aggregates the per-method
    employment (total and per profession) and net goods flows (positive =
    output, negative = input). When pop types are supplied, wage-normalized
    employment is the sum of each profession's employment multiplied by its
    wage weight, restricted to pop types whose ``paid_private_wage`` flag is
    true (defaulting to true when undefined). The
    ``infrastructure_usage_per_level``
    column holds the net per-level footprint: the building group's usage
    minus the ``state_infrastructure_add`` generation summed over the chosen
    production methods, so provider configurations (ports, railways, urban
    centers) carry negative values under the full-employment nominal model.
    Buildings without a construction cost are
    kept with a construction cost of ``0``.  Each row also records the
    technologies required to unlock the configuration (the building's own
    unlocking technologies followed by those of every chosen production
    method, de-duplicated in order of first appearance). When technology data
    is supplied, each row also records the earliest era at which the
    configuration becomes available (the maximum era of those technologies,
    ``0`` when none). Nominal values are evaluated at the
    goods table's base prices; the profit ratio columns follow plain division
    semantics (``x / 0`` is ``inf`` and ``0 / 0`` is ``NaN``).
    Each ``input_ratio_goods_<good>`` column is the good's physical net-input
    quantity divided by the total physical net-input quantity across all
    goods. Outputs and zero net flows contribute ``0``, and configurations
    without net inputs receive ``0`` in every input-ratio column. These ratios
    are quantity-based analytical measures; Victoria 3's prestige-goods
    throughput bonus instead uses the proportion of gross input base value.

    Args:
        df_buildings: Buildings table (``BuildingsParser.to_dataframe()`` or
            ``tables/buildings.csv``) with ``key``, ``building_group``,
            ``urbanization``, ``infrastructure_usage_per_level``,
            ``required_construction_points``, ``production_method_groups``,
            and ``unlocking_technologies`` columns. Optional
            ``economy_of_scale`` and ``is_subsistence`` columns determine
            economy-of-scale eligibility; missing columns default to false.
        df_goods: Goods table (`goods` or
            ``tables/goods.csv``) with ``key`` and ``cost`` columns.  Its row
            order fixes the ``goods_<good>`` column order of the result.
        df_pm: Production-methods table
            (``ProductionMethodParser.to_dataframe()`` or
            ``tables/production_methods.csv``) with ``building``,
            ``production_method_group``, ``production_method``,
            ``unlocking_technologies``, ``employment``,
            ``employment_<profession>``, ``state_infrastructure_add``, and
            ``goods_<good>`` columns (the state-infrastructure column is
            zero-filled with a warning when absent).
        df_tech: Optional technology table (`technology` or
            ``tables/technology.csv``) with ``key`` and ``era`` columns. When
            omitted, ``era`` and ``unlocking_tech_localization`` are not
            produced.
        df_pop_types: Optional pop-types table
            (`PopTypesParser.to_dataframe()` or ``tables/pop_types.csv``) with
            ``key`` and ``wage_weight`` columns, plus an optional
            ``paid_private_wage`` column. Missing private-wage flags default
            to true, including when the column is absent in older game data.
            When the table is omitted,
            the wage-normalized employment columns are not produced.

    Returns:
        A ``DataFrame`` with one row per building configuration.  The
        ``"production_method"`` column holds the chosen production methods
        concatenated with ``+``.  The remaining columns are ``"building"``,
        ``"building_group"``, ``"parent_group"``, ``"land_usage"``,
        ``"is_subsistence"``, ``"discoverable_resource"``,
        ``"depletable_resource"``, ``"economy_of_scale"``, ``"urbanization"``,
        ``"infrastructure_usage_per_level"`` (net of the configuration's
        ``state_infrastructure_add`` generation), optional ``"era"``,
        ``"unlocking_tech"``, ``"employment"``, optional
        ``"wage_normalized_employment"``, ``"construction_cost"``,
        ``"value_goods_inputs_nominal"``, ``"value_goods_outputs_nominal"``,
        ``"profit_nominal"``, ``"profit_margin_nominal"``,
        ``"profit_per_capita_nominal"``,
        optional ``"profit_per_wage_normalized_employment_nominal"``, and
        ``"profit_per_construction_cost_nominal"``, followed by one
        ``goods_<good>`` column per good, each immediately followed by its
        ``input_ratio_goods_<good>`` column, and one
        ``employment_<profession>`` column per profession.

    Raises:
        ValueError: If a supplied technology table is missing an unlocking
            technology, or if a supplied pop-types table cannot uniquely
            provide a valid private-wage weight for each employment
            profession.
    """
    goods_cols = [f"goods_{key}" for key in df_goods["key"]]
    input_ratio_cols = [f"input_ratio_{column}" for column in goods_cols]
    goods_and_ratio_cols = [
        column
        for goods_and_ratio in zip(goods_cols, input_ratio_cols)
        for column in goods_and_ratio
    ]
    profession_cols = [col for col in df_pm.columns if col.startswith("employment_")]
    sum_cols = ["employment", *profession_cols, "state_infrastructure_add", *goods_cols]

    wage_weights: np.ndarray | None = None
    if df_pop_types is not None:
        required_pop_columns = {"key", "wage_weight"}
        missing_pop_columns = required_pop_columns - set(df_pop_types.columns)
        if missing_pop_columns:
            raise ValueError(
                "Pop-types table missing required columns: "
                f"{sorted(missing_pop_columns)}"
            )

        duplicate_pop_keys = df_pop_types.loc[
            df_pop_types["key"].duplicated(keep=False), "key"
        ]
        if not duplicate_pop_keys.empty:
            raise ValueError(
                "Duplicate pop-type keys: "
                f"{sorted(str(key) for key in duplicate_pop_keys.unique())}"
            )

        profession_keys = [
            column.removeprefix("employment_") for column in profession_cols
        ]
        pop_types_by_key = df_pop_types.set_index("key")
        missing_professions = sorted(set(profession_keys) - set(pop_types_by_key.index))
        if missing_professions:
            raise ValueError(
                f"Pop-types table missing employment professions: {missing_professions}"
            )

        profession_pop_types = pop_types_by_key.reindex(profession_keys)
        if "paid_private_wage" in profession_pop_types.columns:
            private_wage_flags = profession_pop_types["paid_private_wage"].fillna(True)
        else:
            private_wage_flags = pd.Series(True, index=profession_pop_types.index)
        invalid_private_wage_flags = ~private_wage_flags.map(
            lambda value: isinstance(value, (bool, np.bool_))
        )
        if invalid_private_wage_flags.any():
            invalid_professions = [
                profession
                for profession, invalid in zip(
                    profession_keys, invalid_private_wage_flags
                )
                if invalid
            ]
            raise ValueError(
                "Pop-types table has non-boolean paid_private_wage values for: "
                f"{invalid_professions}"
            )

        private_wage_mask = private_wage_flags.to_numpy(dtype=bool)
        numeric_wage_weights = pd.to_numeric(
            profession_pop_types["wage_weight"], errors="coerce"
        ).to_numpy(dtype=np.float64)
        invalid_wage_weight_mask = private_wage_mask & (
            ~np.isfinite(numeric_wage_weights) | (numeric_wage_weights < 0)
        )
        if invalid_wage_weight_mask.any():
            invalid_professions = [
                profession
                for profession, invalid in zip(
                    profession_keys, invalid_wage_weight_mask
                )
                if invalid
            ]
            raise ValueError(
                "Pop-types table has invalid private wage weights for: "
                f"{invalid_professions}"
            )
        wage_weights = np.where(private_wage_mask, numeric_wage_weights, 0.0)

    missing_goods_cols = [col for col in goods_cols if col not in df_pm.columns]
    if missing_goods_cols:
        warnings.warn(
            f"Goods columns missing from the production-method table, "
            f"zero-filled: {sorted(missing_goods_cols)}",
            stacklevel=2,
        )
        df_pm = df_pm.reindex(
            columns=[*df_pm.columns, *missing_goods_cols], fill_value=0
        )
    if "state_infrastructure_add" not in df_pm.columns:
        warnings.warn(
            "State-infrastructure column missing from the production-method "
            "table, zero-filled: state_infrastructure_add",
            stacklevel=2,
        )
        df_pm = df_pm.reindex(
            columns=[*df_pm.columns, "state_infrastructure_add"], fill_value=0
        )
    unpriced_goods_cols = [
        col
        for col in df_pm.columns
        if col.startswith("goods_") and col not in goods_cols
    ]
    if unpriced_goods_cols:
        warnings.warn(
            f"Goods columns without a base price, ignored: "
            f"{sorted(unpriced_goods_cols)}",
            stacklevel=2,
        )

    era_by_tech: dict[str, int] = {}
    if df_tech is not None:
        era_by_tech = {
            key: int(era) for key, era in zip(df_tech["key"], df_tech["era"])
        }

    def localization_map(frame: pd.DataFrame, key: str, value: str) -> dict[str, str]:
        if key not in frame or value not in frame:
            return {}
        return {
            str(item_key): item_value
            for item_key, item_value in zip(frame[key], frame[value])
            if isinstance(item_value, str)
        }

    building_localizations = localization_map(df_buildings, "key", "key_localization")
    has_building_localization = "key_localization" in df_buildings.columns
    building_group_localizations = localization_map(
        df_buildings, "building_group", "building_group_localization"
    )
    has_building_group_localization = (
        "building_group_localization" in df_buildings.columns
    )
    pm_localizations = localization_map(
        df_pm, "production_method", "production_method_localization"
    )
    has_pm_localization = "production_method_localization" in df_pm.columns
    tech_localizations = (
        localization_map(df_tech, "key", "key_localization")
        if df_tech is not None
        else {}
    )
    has_tech_localization = (
        df_tech is not None and "key_localization" in df_tech.columns
    )

    def localize_compound(value: str, values: dict[str, str]) -> object:
        keys = [key for key in value.split("+") if key]
        if not keys:
            return pd.NA
        localized = [values.get(key) for key in keys]
        if any(item is None for item in localized):
            return pd.NA
        return "+".join(item for item in localized if item is not None)

    pm_keys = df_pm["production_method"].tolist()
    pm_techs = [_tech_keys(value) for value in df_pm["unlocking_technologies"]]
    pm_eras = (
        [_era_of_techs(techs, era_by_tech) for techs in pm_techs]
        if df_tech is not None
        else []
    )

    group_positions: dict[tuple[str, str], list[int]] = {}
    for position, (building, group) in enumerate(
        zip(df_pm["building"], df_pm["production_method_group"])
    ):
        group_positions.setdefault((building, group), []).append(position)

    building_columns = [
        "key",
        "building_group",
        "urbanization",
        "infrastructure_usage_per_level",
        "required_construction_points",
        "production_method_groups",
        "unlocking_technologies",
    ]
    economy_of_scale = (
        df_buildings["economy_of_scale"].fillna(False).eq(True)
        if "economy_of_scale" in df_buildings.columns
        else pd.Series(False, index=df_buildings.index)
    )
    is_subsistence = (
        df_buildings["is_subsistence"].fillna(False).eq(True)
        if "is_subsistence" in df_buildings.columns
        else pd.Series(False, index=df_buildings.index)
    )
    economy_of_scale_eligible = economy_of_scale & ~is_subsistence
    combo_rows: list[dict[str, object]] = []
    membership: list[tuple[int, int]] = []

    for (
        building,
        building_group,
        urbanization,
        infrastructure,
        construction_cost,
        pmg_value,
        unlock_value,
        has_economy_of_scale,
    ) in zip(
        *[df_buildings[column] for column in building_columns],
        economy_of_scale_eligible,
    ):
        if not isinstance(pmg_value, str) or not pmg_value:
            warnings.warn(
                f"Building {building} has no production method groups, skipped",
                stacklevel=2,
            )
            continue

        groups: list[list[int]] = []
        for pmg in pmg_value.split("+"):
            positions = group_positions.get((building, pmg))
            if not positions:
                warnings.warn(
                    f"Building {building} has no production methods in group "
                    f"{pmg}, skipped",
                    stacklevel=2,
                )
                groups = []
                break
            groups.append(positions)
        if not groups:
            continue

        building_techs = _tech_keys(unlock_value)
        building_era = (
            _era_of_techs(building_techs, era_by_tech) if df_tech is not None else 0
        )
        if pd.isna(construction_cost):
            construction_cost = 0
        if pd.isna(urbanization):
            urbanization = 0.0
        if pd.isna(infrastructure):
            infrastructure = 0.0

        for positions in product(*groups):
            combo_id = len(combo_rows)
            era = building_era
            techs = list(building_techs)
            for position in positions:
                if df_tech is not None:
                    era = max(era, pm_eras[position])
                techs.extend(pm_techs[position])
            combo_rows.append(
                {
                    "building": building,
                    "production_method": "+".join(
                        pm_keys[position] for position in positions
                    ),
                    "building_group": building_group,
                    "economy_of_scale": bool(has_economy_of_scale),
                    "urbanization": urbanization,
                    "infrastructure_usage_per_level": infrastructure,
                    "unlocking_tech": "+".join(dict.fromkeys(techs)),
                    "construction_cost": int(construction_cost),
                }
            )
            if df_tech is not None:
                combo_rows[-1]["era"] = era
            if has_building_localization:
                combo_rows[-1]["building_localization"] = building_localizations.get(
                    str(building), pd.NA
                )
            if has_pm_localization:
                pm_value = "+".join(pm_keys[position] for position in positions)
                combo_rows[-1]["production_method_localization"] = localize_compound(
                    pm_value, pm_localizations
                )
            if has_building_group_localization:
                combo_rows[-1]["building_group_localization"] = (
                    building_group_localizations.get(str(building_group), pd.NA)
                )
            if has_tech_localization:
                combo_rows[-1]["unlocking_tech_localization"] = localize_compound(
                    "+".join(dict.fromkeys(techs)), tech_localizations
                )
            membership.extend((combo_id, position) for position in positions)

    building_metadata_defaults = {
        "parent_group": pd.NA,
        "land_usage": pd.NA,
        "is_subsistence": False,
        "discoverable_resource": False,
        "depletable_resource": False,
    }
    column_order = [
        "building",
        *(["building_localization"] if has_building_localization else []),
        "production_method",
        *(["production_method_localization"] if has_pm_localization else []),
        "building_group",
        *(["building_group_localization"] if has_building_group_localization else []),
        *building_metadata_defaults,
        "economy_of_scale",
        "urbanization",
        "infrastructure_usage_per_level",
        *(["era"] if df_tech is not None else []),
        "unlocking_tech",
        *(["unlocking_tech_localization"] if has_tech_localization else []),
        "employment",
        *(["wage_normalized_employment"] if wage_weights is not None else []),
        "construction_cost",
        "value_goods_inputs_nominal",
        "value_goods_outputs_nominal",
        "profit_nominal",
        "profit_margin_nominal",
        "profit_per_capita_nominal",
        *(
            ["profit_per_wage_normalized_employment_nominal"]
            if wage_weights is not None
            else []
        ),
        "profit_per_construction_cost_nominal",
        *goods_and_ratio_cols,
        *profession_cols,
    ]
    if not combo_rows:
        empty = pd.DataFrame(columns=column_order)
        for column in column_order:
            if column.endswith("_localization"):
                empty[column] = empty[column].astype("string")
            elif column in input_ratio_cols:
                empty[column] = empty[column].astype(np.float64)
        return empty

    member_positions = [position for _, position in membership]
    combo_ids = [combo_id for combo_id, _ in membership]
    members = df_pm.iloc[member_positions]
    sums = (
        members.groupby(np.asarray(combo_ids), sort=True)[sum_cols]
        .sum()
        .reset_index(drop=True)
    )

    result = pd.concat([pd.DataFrame(combo_rows), sums], axis=1)

    for column, default in building_metadata_defaults.items():
        if column in df_buildings.columns:
            values: dict[str, object] = {
                str(key): value
                for key, value in zip(df_buildings["key"], df_buildings[column])
            }
            result[column] = result["building"].map(
                lambda building, values=values, default=default: values.get(
                    str(building), default
                )
            )
            if isinstance(default, bool):
                result[column] = result[column].fillna(default).eq(True)
        else:
            result[column] = default

    result["infrastructure_usage_per_level"] = (
        result["infrastructure_usage_per_level"] - result["state_infrastructure_add"]
    )
    if wage_weights is not None:
        result["wage_normalized_employment"] = (
            result[profession_cols].to_numpy(dtype=np.float64) @ wage_weights
        )

    prices = df_goods["cost"].to_numpy(dtype=np.float64)
    flows = result[goods_cols].to_numpy(dtype=np.float64)
    input_quantities = np.maximum(-flows, 0.0)
    input_quantity_totals = input_quantities.sum(axis=1, keepdims=True)
    input_ratios = np.divide(
        input_quantities,
        input_quantity_totals,
        out=np.zeros_like(input_quantities),
        where=input_quantity_totals != 0,
    )
    result = pd.concat(
        [
            result,
            pd.DataFrame(
                input_ratios,
                columns=input_ratio_cols,
                index=result.index,
            ),
        ],
        axis=1,
    )
    result["value_goods_inputs_nominal"] = input_quantities @ prices
    result["value_goods_outputs_nominal"] = np.maximum(flows, 0.0) @ prices
    result["profit_nominal"] = (
        result["value_goods_outputs_nominal"] - result["value_goods_inputs_nominal"]
    )
    result["profit_margin_nominal"] = (
        result["profit_nominal"] / result["value_goods_outputs_nominal"]
    )
    result["profit_per_capita_nominal"] = (
        result["profit_nominal"] / result["employment"]
    )
    if wage_weights is not None:
        result["profit_per_wage_normalized_employment_nominal"] = (
            result["profit_nominal"] / result["wage_normalized_employment"]
        )
    result["profit_per_construction_cost_nominal"] = (
        result["profit_nominal"] / result["construction_cost"]
    )

    result = result.reindex(columns=column_order)
    for column in column_order:
        if column.endswith("_localization"):
            result[column] = result[column].astype("string")
    return result

Economy dataclass

Economy(
    game_dir: str | Path | None = None,
    df_production: DataFrame | None = None,
    df_goods: DataFrame | None = None,
    df_pop_types: DataFrame | None = None,
    *,
    df_pop_needs: DataFrame | None = None,
    df_buy_packages: DataFrame | None = None,
    language: str | None = None,
)

Builds EconomyState instances from building-level vectors.

Wraps production, goods, pop-types, pop-needs, and buy-package tables parsed from the Victoria 3 game files and lazily caches the coefficients needed to derive prices, supply, demand, employment, and wealth for a given building_levels vector.

Input tables are defensively copied. Public DataFrames remain mutable, but editing them is unsupported because cached coefficients are not invalidated. Construct a new Economy when model data changes. Cached indexes are tuples and cached arrays are owned and read-only. Starting wealth uses int64; other numeric coefficients use float64.

Attributes:

  • df_production (DataFrame) –

    The production table with missing metadata default-filled.

  • df_goods (DataFrame) –

    The goods table DataFrame.

  • df_pop_types (DataFrame) –

    The pop-types table DataFrame.

  • df_pop_needs (DataFrame) –

    Per-category goods, substitution weights, shares, and defaults.

  • df_buy_packages (DataFrame) –

    Need values at base prices per 10,000 workers by integer wealth.

Parameters:

  • game_dir

    (str | Path | None, default: None ) –

    Path to the Victoria 3 game directory. If None the directory is located automatically via get_vic3_directory. Ignored for any table whose DataFrame is passed directly.

  • df_production

    (DataFrame | None, default: None ) –

    Pre-built production table DataFrame. When provided, overrides the table that would otherwise be generated by production_table. Useful for reusing a filtered or cached table.

  • df_goods

    (DataFrame | None, default: None ) –

    Pre-built goods table DataFrame. When provided, overrides the table that would otherwise be generated by goods.

  • df_pop_types

    (DataFrame | None, default: None ) –

    Pre-built pop-types table DataFrame. When provided, overrides the table that would otherwise be generated by PopTypesParser.

  • df_pop_needs

    (DataFrame | None, default: None ) –

    Pre-built substitution definitions. When omitted, parsed by PopNeedsParser.

  • df_buy_packages

    (DataFrame | None, default: None ) –

    Pre-built wealth packages. When omitted, parsed by buy_packages.

  • language

    (str | None, default: None ) –

    Optional Victoria 3 localization language used only when parsing tables internally. Explicit DataFrames are authoritative and are not modified.

Methods:

  • economy_of_scale_bonuses –

    Return per-configuration economy-of-scale throughput bonuses.

  • goods_input_matrix –

    Return writable gross inputs using the cached unscaled matrix.

  • goods_output_matrix –

    Return writable gross outputs using the cached unscaled matrix.

  • prepare_context –

    Return a context with reusable baskets and consumption scales.

  • evaluate_wealth –

    Evaluate demand, market prices, and finances at fixed household wealth.

  • iterate –

    Apply one simultaneous wealth transition and evaluate matching results.

  • goods_buy_packages –

    Calculate weekly goods packages per 10,000 standard workers by wealth.

  • pop_consumption_scales –

    Return household consumption as multiples of a standard buy package.

  • pop_buy_orders –

    Calculate weekly population buy orders for the represented workers.

  • pop_expenses –

    Return weekly household expenses for target goods consumption.

  • pop_income –

    Return disposable cooperative and dependent household income.

  • pop_income_tax –

    Recompute weekly household taxes funding operating losses.

  • market_prices_variance –

    Calculate the market prices variance from base prices.

  • market_prices –

    Calculate market prices from buy and sell orders.

  • construction_cost –

    Calculate the total construction cost for an EconomyState.

  • arable_land_consumption –

    Return the total arable land consumed by an economy state.

  • levels_per_building –

    Return total levels per building across all PM configurations.

  • solve –

    Solve for an EconomyState from a building-level vector.

  • df_goods_buy_packages –

    Export standard goods baskets as an independent, mutable DataFrame.

  • df_buildings –

    Export an EconomyState's building registry to a DataFrame.

  • df_market –

    Export an EconomyState's market stats to a DataFrame.

  • df_pop –

    Export an EconomyState's pop employment and wealth to a DataFrame.

Source code in src/vic3_analysis/analysis/economy/model.py
def __init__(
    self,
    game_dir: str | Path | None = None,
    df_production: pd.DataFrame | None = None,
    df_goods: pd.DataFrame | None = None,
    df_pop_types: pd.DataFrame | None = None,
    *,
    df_pop_needs: pd.DataFrame | None = None,
    df_buy_packages: pd.DataFrame | None = None,
    language: str | None = None,
):
    """Initialise the model by parsing the required game tables.

    Args:
        game_dir: Path to the Victoria 3 ``game`` directory. If ``None``
            the directory is located automatically via
            `get_vic3_directory`. Ignored for
            any table whose DataFrame is passed directly.
        df_production: Pre-built production table DataFrame. When
            provided, overrides the table that would otherwise be generated
            by `production_table`. Useful for reusing a
            filtered or cached table.
        df_goods: Pre-built goods table DataFrame. When provided,
            overrides the table that would otherwise be generated by
            `goods`.
        df_pop_types: Pre-built pop-types table DataFrame. When provided,
            overrides the table that would otherwise be generated by
            `PopTypesParser`.
        df_pop_needs: Pre-built substitution definitions. When omitted,
            parsed by `PopNeedsParser`.
        df_buy_packages: Pre-built wealth packages. When omitted, parsed
            by `buy_packages`.
        language: Optional Victoria 3 localization language used only when
            parsing tables internally. Explicit DataFrames are authoritative
            and are not modified.
    """
    if df_goods is None:
        df_goods = goods(game_dir, language=language)
    if df_pop_types is None:
        df_pop_types = PopTypesParser(game_dir).to_dataframe(language=language)
    if df_pop_needs is None:
        df_pop_needs = PopNeedsParser(game_dir).to_dataframe(language=language)
    if df_buy_packages is None:
        df_buy_packages = buy_packages(game_dir)
    if df_production is None:
        df_production = production_table(
            BuildingsParser(game_dir).to_dataframe(language=language),
            df_goods,
            ProductionMethodParser(game_dir).to_dataframe(language=language),
            technology(game_dir, language=language),
            df_pop_types,
        )
    object.__setattr__(self, "df_production", df_production.copy(deep=True))
    object.__setattr__(self, "df_goods", df_goods.copy(deep=True))
    object.__setattr__(self, "df_pop_types", df_pop_types.copy(deep=True))
    object.__setattr__(self, "df_pop_needs", df_pop_needs.copy(deep=True))
    object.__setattr__(self, "df_buy_packages", df_buy_packages.copy(deep=True))
    _production._fill_production_metadata_defaults(self)
    goods_columns = [f"goods_{good}" for good in self.goods_index]

    missing = set(goods_columns) - set(self.df_production.columns)
    if missing:
        warnings.warn(f"Missing goods input columns: {sorted(missing)}")

building_index cached property

building_index: tuple[str, ...]

Return production-configuration keys in production-table row order.

Each key combines the row's building and production-method strings with "+". The resulting order defines the alignment of building-level vectors and the rows of building matrices.

goods_index cached property

goods_index: tuple[str, ...]

Return good keys in goods-table row order.

This order defines the alignment of goods vectors and the columns of goods matrices.

producible_goods cached property

producible_goods: tuple[str, ...]

Return goods that have at least one producer configuration.

Returns:

  • str –

    A tuple of good keys that can be produced by at least one building

  • ... –

    configuration in the production table.

pop_index cached property

pop_index: tuple[str, ...]

Return profession keys in pop-types-table row order.

This order defines the alignment of profession vectors and employment matrix columns.

base_prices cached property

base_prices: ndarray

Return Victoria 3 base prices aligned to goods_index.

goods_input_matrix_unscaled cached property

goods_input_matrix_unscaled: ndarray

Cached read-only gross inputs per level, without throughput scaling.

goods_output_matrix_unscaled cached property

goods_output_matrix_unscaled: ndarray

Cached read-only gross outputs per level, without throughput scaling.

employment_matrix cached property

employment_matrix: ndarray

Return employment by production configuration and profession.

Rows align to building_index and columns align to pop_index. Values are the number of employees per building level; missing profession columns are filled with zero.

pop_wealth_init cached property

pop_wealth_init: ndarray

Return read-only int64 starting wealth aligned to pop_index.

pop_wage_weights cached property

pop_wage_weights: ndarray

Return all professions' cooperative wage weights in pop_index order.

employment_vector cached property

employment_vector: ndarray

Total employment per building level, aligned to production-table rows.

Returns:

  • ndarray –

    A 1-D array of shape (n_buildings,) giving the total employment

  • ndarray –

    of each building configuration per level ("employment" column,

  • ndarray –

    missing values zero-filled).

construction_cost_vector cached property

construction_cost_vector: ndarray

Return per-level construction cost aligned to building_index.

infrastructure_vector cached property

infrastructure_vector: ndarray

Cached infrastructure usage per level; missing values default to zero.

arable_land_vector cached property

arable_land_vector: ndarray

Return arable-land consumption per production configuration.

Returns:

  • ndarray –

    A float vector aligned to the production table, with 1.0 for

  • ndarray –

    agricultural, plantation, ranching, and subsistence land-use

  • ndarray –

    configurations and 0.0 otherwise.

economy_of_scale_bonuses

economy_of_scale_bonuses(
    building_levels: ndarray[tuple[int], dtype[float64]],
    level_cap: float = 20.0,
) -> ndarray

Return per-configuration economy-of-scale throughput bonuses.

Levels are summed across all production-method configurations of each building. Eligible configurations receive one percentage point of throughput per total building level, capped by level_cap. Returned values are additive bonuses (for example, 0.2 for +20%), not full multipliers.

Parameters:

  • building_levels

    (ndarray[tuple[int], dtype[float64]]) –

    Production-row-aligned building levels.

  • level_cap

    (float, default: 20.0 ) –

    Maximum number of levels contributing to the bonus.

Returns:

  • ndarray –

    A 1-D bonus vector aligned to the production table.

Raises:

  • ValueError –

    If building_levels is not aligned to the production table or level_cap is negative or non-finite.

Source code in src/vic3_analysis/analysis/economy/model.py
def economy_of_scale_bonuses(
    self,
    building_levels: np.ndarray[tuple[int], np.dtype[np.float64]],
    level_cap: float = 20.0,
) -> np.ndarray:
    """Return per-configuration economy-of-scale throughput bonuses.

    Levels are summed across all production-method configurations of each
    building. Eligible configurations receive one percentage point of
    throughput per total building level, capped by *level_cap*. Returned
    values are additive bonuses (for example, ``0.2`` for +20%), not full
    multipliers.

    Args:
        building_levels: Production-row-aligned building levels.
        level_cap: Maximum number of levels contributing to the bonus.

    Returns:
        A 1-D bonus vector aligned to the production table.

    Raises:
        ValueError: If *building_levels* is not aligned to the production
            table or *level_cap* is negative or non-finite.
    """
    return _production.economy_of_scale_bonuses(self, building_levels, level_cap)

goods_input_matrix

goods_input_matrix(
    throughput_multipliers: ndarray[
        tuple[int], dtype[float64]
    ]
    | None = None,
) -> ndarray

Return writable gross inputs using the cached unscaled matrix.

Source code in src/vic3_analysis/analysis/economy/model.py
def goods_input_matrix(
    self,
    throughput_multipliers: (
        np.ndarray[tuple[int], np.dtype[np.float64]] | None
    ) = None,
) -> np.ndarray:
    """Return writable gross inputs using the cached unscaled matrix."""
    return _production.goods_input_matrix(self, throughput_multipliers)

goods_output_matrix

goods_output_matrix(
    throughput_multipliers: ndarray[
        tuple[int], dtype[float64]
    ]
    | None = None,
) -> ndarray

Return writable gross outputs using the cached unscaled matrix.

Source code in src/vic3_analysis/analysis/economy/model.py
def goods_output_matrix(
    self,
    throughput_multipliers: (
        np.ndarray[tuple[int], np.dtype[np.float64]] | None
    ) = None,
) -> np.ndarray:
    """Return writable gross outputs using the cached unscaled matrix."""
    return _production.goods_output_matrix(self, throughput_multipliers)

prepare_context

prepare_context(context: EconomyContext) -> EconomyContext

Return a context with reusable baskets and consumption scales.

Compute only missing prepared fields. Existing fields are authoritative; a fully prepared context is returned unchanged. No household wealth, demand, prices, or finances are evaluated. Clear affected fields before refreshing changed production, trade, or population assumptions.

Source code in src/vic3_analysis/analysis/economy/model.py
def prepare_context(self, context: EconomyContext) -> EconomyContext:
    """Return a context with reusable baskets and consumption scales.

    Compute only missing prepared fields. Existing fields are authoritative;
    a fully prepared context is returned unchanged. No household wealth,
    demand, prices, or finances are evaluated. Clear affected fields before
    refreshing changed production, trade, or population assumptions.
    """
    return _consumption.prepare_context(self, context)

evaluate_wealth

evaluate_wealth(
    context: EconomyContext, pop_wealth: ndarray
) -> tuple[MarketState, PopState]

Evaluate demand, market prices, and finances at fixed household wealth.

Prepare missing context baskets and consumption scales, preserving any supplied prepared fields. Calculate demand first, then market prices from total buy orders and fixed sell orders, and finally cooperative income and household expenses at those same prices.

Parameters:

  • context

    (EconomyContext) –

    Fixed production, trade, and worker counts aligned to this economy. Prepared baskets and scales are optional.

  • pop_wealth

    (ndarray) –

    Non-negative integer wealth matrix aligned to configurations and professions. Occupied tiers need baskets; empty cells may use undefined tiers.

Returns:

  • MarketState –

    Matching market and household components, in that order, with

  • PopState –

    read-only arrays and fully evaluated household finances. Wealth

  • tuple[MarketState, PopState] –

    values are preserved. The market shares the supplied context when

  • tuple[MarketState, PopState] –

    fully prepared; otherwise it references a newly prepared context.

  • tuple[MarketState, PopState] –

    Compose a complete snapshot with

  • tuple[MarketState, PopState] –

    EconomyState(market.context, market, households).

Raises:

  • ValueError –

    If context inputs or wealth are invalid or misaligned, preparation fails, occupied tiers lack baskets, income cannot be allocated, or calculated results are non-finite.

Notes

Inputs and model tables are unchanged. This evaluates one wealth assignment without affordability limits or wealth transitions.

Source code in src/vic3_analysis/analysis/economy/model.py
def evaluate_wealth(
    self, context: EconomyContext, pop_wealth: np.ndarray
) -> tuple[MarketState, PopState]:
    """Evaluate demand, market prices, and finances at fixed household wealth.

    Prepare missing context baskets and consumption scales, preserving any
    supplied prepared fields. Calculate demand first, then market prices
    from total buy orders and fixed sell orders, and finally cooperative
    income and household expenses at those same prices.

    Args:
        context: Fixed production, trade, and worker counts aligned to this
            economy. Prepared baskets and scales are optional.
        pop_wealth: Non-negative integer wealth matrix aligned to
            configurations and professions. Occupied tiers need baskets;
            empty cells may use undefined tiers.

    Returns:
        Matching market and household components, in that order, with
        read-only arrays and fully evaluated household finances. Wealth
        values are preserved. The market shares the supplied context when
        fully prepared; otherwise it references a newly prepared context.
        Compose a complete snapshot with
        ``EconomyState(market.context, market, households)``.

    Raises:
        ValueError: If context inputs or wealth are invalid or misaligned,
            preparation fails, occupied tiers lack baskets, income cannot
            be allocated, or calculated results are non-finite.

    Notes:
        Inputs and model tables are unchanged. This evaluates one wealth
        assignment without affordability limits or wealth transitions.
    """
    return _equilibrium.evaluate_wealth(self, context, pop_wealth)

iterate

iterate(
    context: EconomyContext,
    market: MarketState,
    households: PopState,
    *,
    rtol: float = 1e-09,
    atol: float = 1e-08,
) -> tuple[MarketState, PopState]

Apply one simultaneous wealth transition and evaluate matching results.

Initialize with evaluate_wealth() and pass its prepared market context. Inputs must be consistent: this method validates alignment and finances, but trusts their economic consistency without reevaluating the old wealth. Decrease below 98% of current cost; otherwise increase at or above 102% of next-tier cost, using previous prices and household scales. Each threshold accepts equality within atol + rtol * max(income, threshold). Decreases take precedence. Empty cells and bounded tiers are preserved.

Returns:

  • MarketState –

    Matching read-only market and population components sharing context.

  • PopState –

    If wealth is unchanged, the exact supplied components are returned.

  • tuple[MarketState, PopState] –

    A step is numerical rather than a simulated week and makes no

  • tuple[MarketState, PopState] –

    convergence or financial-sustainability claim.

Raises:

  • ValueError –

    If tolerances, context identity, prepared tiers, component axes, prices, or finances are invalid, or calculations fail.

Source code in src/vic3_analysis/analysis/economy/model.py
def iterate(
    self,
    context: EconomyContext,
    market: MarketState,
    households: PopState,
    *,
    rtol: float = 1e-9,
    atol: float = 1e-8,
) -> tuple[MarketState, PopState]:
    """Apply one simultaneous wealth transition and evaluate matching results.

    Initialize with ``evaluate_wealth()`` and pass its prepared market context.
    Inputs must be consistent: this method validates alignment and finances,
    but trusts their economic consistency without reevaluating the old wealth.
    Decrease below 98% of current cost; otherwise increase at or above 102%
    of next-tier cost, using previous prices and household scales. Each
    threshold accepts equality within ``atol + rtol * max(income, threshold)``.
    Decreases take precedence. Empty cells and bounded tiers are preserved.

    Returns:
        Matching read-only market and population components sharing ``context``.
        If wealth is unchanged, the exact supplied components are returned.
        A step is numerical rather than a simulated week and makes no
        convergence or financial-sustainability claim.

    Raises:
        ValueError: If tolerances, context identity, prepared tiers, component
            axes, prices, or finances are invalid, or calculations fail.
    """
    return _equilibrium.iterate(
        self, context, market, households, rtol=rtol, atol=atol
    )

goods_buy_packages

goods_buy_packages(
    state: EconomyState | EconomyContext,
) -> GoodsBuyPackages

Calculate weekly goods packages per 10,000 standard workers by wealth.

Convert the model's wealth-by-category values to goods quantities using the supplied state's substitution shares and the model's base prices: wealth_category_values @ category_purchase_shares / base_prices. Substitution uses effective supply, configured weights, minimum/maximum supply shares, and default goods as described by pop_buy_orders.

Parameters:

  • state

    (EconomyState | EconomyContext) –

    Snapshot or context aligned to this economy. Prepared context baskets are reused when present; otherwise only building inputs/outputs, imports, and exports are used.

Returns:

  • GoodsBuyPackages –

    Immutable GoodsBuyPackages with ascending int64 wealth labels and

  • GoodsBuyPackages –

    read-only float64 quantities in goods_index column order.

  • GoodsBuyPackages –

    Prepared context baskets are returned unchanged after validating

  • GoodsBuyPackages –

    goods alignment; otherwise all tiers in df_buy_packages are

  • GoodsBuyPackages –

    calculated. Quantities are goods units per week per 10,000 standard

  • GoodsBuyPackages –

    workers, with zero for unconsumed goods. Inputs are unchanged.

  • GoodsBuyPackages –

    Use Economy.df_goods_buy_packages for a mutable DataFrame export.

Raises:

  • ValueError –

    If goods flows are misaligned, negative, or non-finite, base prices are not positive, or package/substitution definitions are invalid or reference missing categories/goods.

Notes

Packages are independent of represented population, workforce ratio, occupied wealth, profession consumption multipliers, market prices, and existing population demand. Culture, prestige goods, local-state effects, and weekly adjustment are not modeled. Subtracting half of non-pop demand from supply is community-derived, documented by the Needs wiki rather than game-file comments. Clamping negative effective supply to zero is a model convention.

References
  • common/buy_packages/00_buy_packages.txt: category values by integer wealth.
  • common/pop_needs/00_pop_needs.txt: substitution weights, supply-share limits, and default goods.
  • Needs wiki, Formulas: scaling per 10,000 workers and substitution, including the community-derived half-non-pop-demand deduction.
  • Official developer diary #13: wealth-dependent needs and valuation at goods' base prices.
Source code in src/vic3_analysis/analysis/economy/model.py
def goods_buy_packages(
    self, state: EconomyState | EconomyContext
) -> GoodsBuyPackages:
    """Calculate weekly goods packages per 10,000 standard workers by wealth.

    Convert the model's wealth-by-category values to goods quantities using
    the supplied state's substitution shares and the model's base prices:
    ``wealth_category_values @ category_purchase_shares / base_prices``.
    Substitution uses effective supply, configured weights, minimum/maximum
    supply shares, and default goods as described by `pop_buy_orders`.

    Args:
        state: Snapshot or context aligned to this economy. Prepared
            context baskets are reused when present; otherwise only
            building inputs/outputs, imports, and exports are used.

    Returns:
        Immutable `GoodsBuyPackages` with ascending int64 ``wealth`` labels and
        read-only float64 ``quantities`` in `goods_index` column order.
        Prepared context baskets are returned unchanged after validating
        goods alignment; otherwise all tiers in ``df_buy_packages`` are
        calculated. Quantities are goods units per week per 10,000 standard
        workers, with zero for unconsumed goods. Inputs are unchanged.
        Use `Economy.df_goods_buy_packages` for a mutable DataFrame export.

    Raises:
        ValueError: If goods flows are misaligned, negative, or non-finite,
            base prices are not positive, or package/substitution
            definitions are invalid or reference missing categories/goods.

    Notes:
        Packages are independent of represented population, workforce ratio,
        occupied wealth, profession consumption multipliers, market prices,
        and existing population demand. Culture, prestige goods, local-state
        effects, and weekly adjustment are not modeled.
        Subtracting half of non-pop demand from supply is community-derived,
        documented by the Needs wiki rather than game-file comments.
        Clamping negative effective supply to zero is a model convention.

    References:
        - ``common/buy_packages/00_buy_packages.txt``: category values by
          integer wealth.
        - ``common/pop_needs/00_pop_needs.txt``: substitution weights,
          supply-share limits, and default goods.
        - [Needs wiki, Formulas](https://vic3.paradoxwikis.com/Needs#Formulas):
          scaling per 10,000 workers and substitution, including the
          community-derived half-non-pop-demand deduction.
        - [Official developer diary #13](https://steamcommunity.com/games/529340/announcements/detail/2965045886379003737):
          wealth-dependent needs and valuation at goods' base prices.
    """
    return _consumption.goods_buy_packages(self, state)

pop_consumption_scales

pop_consumption_scales(
    state: EconomyState | EconomyContext,
) -> ndarray

Return household consumption as multiples of a standard buy package.

Calculate pops / 10_000 * consumption_mult * (1 + 0.5 * dependents_per_worker). Missing profession multipliers default to one. Workers and dependents share a wealth tier; dependents consume half as much per person. Wealth values, prices, and goods flows are not used.

Parameters:

  • state

    (EconomyState | EconomyContext) –

    Snapshot or context with aligned worker counts. Prepared context scales are copied when present. Snapshot wealth must also have the population shape.

Returns:

  • ndarray –

    A fresh, owned, writable float64 matrix aligned to configurations

  • ndarray –

    and professions. Values are dimensionless multiples of weekly

  • ndarray –

    goods packages per 10,000 standard workers. Reuse them while worker

  • ndarray –

    counts, workforce ratio, and profession multipliers remain fixed.

  • ndarray –

    The state and model tables are unchanged.

Raises:

  • ValueError –

    If household shapes, worker counts, profession keys, or consumption multipliers are invalid, or scales overflow.

Source code in src/vic3_analysis/analysis/economy/model.py
def pop_consumption_scales(
    self, state: EconomyState | EconomyContext
) -> np.ndarray:
    """Return household consumption as multiples of a standard buy package.

    Calculate ``pops / 10_000 * consumption_mult * (1 + 0.5 *
    dependents_per_worker)``. Missing profession multipliers default to one.
    Workers and dependents share a wealth tier; dependents consume half as
    much per person. Wealth values, prices, and goods flows are not used.

    Args:
        state: Snapshot or context with aligned worker counts. Prepared
            context scales are copied when present. Snapshot wealth must
            also have the population shape.

    Returns:
        A fresh, owned, writable float64 matrix aligned to configurations
        and professions. Values are dimensionless multiples of weekly
        goods packages per 10,000 standard workers. Reuse them while worker
        counts, workforce ratio, and profession multipliers remain fixed.
        The state and model tables are unchanged.

    Raises:
        ValueError: If household shapes, worker counts, profession keys,
            or consumption multipliers are invalid, or scales overflow.
    """
    return _consumption.pop_consumption_scales(self, state)

pop_buy_orders

pop_buy_orders(
    state: EconomyState,
    *,
    goods_buy_packages: GoodsBuyPackages | None = None,
    consumption_scales: ndarray | None = None,
) -> ndarray

Calculate weekly population buy orders for the represented workers.

Prepared context baskets and scales are reused when their explicit arguments are omitted. Without prepared baskets, each need category's effective supply is max(sell_orders - 0.5 * (building_goods_input + exports), 0). Divide each eligible good's effective supply by the category total, clip that share to its configured minimum and maximum, multiply by its configured weight, and normalize these purchase weights. Share limits apply before normalization; they do not cap final purchases. With zero effective supply, start from zero shares, retaining any configured minimum shares. If all purchase weights are zero, use the category's default good exclusively.

Each occupied cell's integer wealth selects a buy package, whose category values are measured in pounds at base prices per 10,000 workers. Multiply worker counts by 1 + 0.5 * dependents_per_worker, scale by consumption_mult / 10_000, distribute by normalized purchase weights, and divide by goods' base prices to obtain units per week. Household scales are aggregated by wealth before allocating goods baskets, without a building-by-profession-by-goods tensor.

Parameters:

  • state

    (EconomyState) –

    Snapshot aligned to this economy's building, profession, and goods indexes. Workers and their dependents consume at the same wealth tier; dependents consume half as much per person. Empty cells need no wealth package.

  • goods_buy_packages

    (GoodsBuyPackages | None, default: None ) –

    Optional immutable GoodsBuyPackages, as returned by goods_buy_packages. Wealth labels must cover every occupied tier. Quantity columns must follow goods_index order; column count is checked against the economy. Construction validates and sorts wealth labels and quantities together. Supplied baskets are scaled directly by worker- equivalent population and profession consumption multipliers, without substitution or base-price conversion, and remain fixed when supply changes. Unused state goods flows and raw need/package tables are not validated in this mode. The baskets are unchanged.

  • consumption_scales

    (ndarray | None, default: None ) –

    Optional precomputed matrix returned by pop_consumption_scales. Supplied values are authoritative, finite, non-negative, and shaped like pops; empty cells must have zero scale. They already include all population factors, so profession multipliers are not read or reapplied. The caller ensures scales remain valid for the population assumptions. The matrix is unchanged.

Returns:

  • ndarray –

    A fresh writable float64 vector of shape (n_goods,) aligned

  • ndarray –

    to goods_index, suitable for PopState.pop_buy_orders.

  • ndarray –

    The state is unchanged.

Raises:

  • TypeError –

    If supplied baskets are not a GoodsBuyPackages instance.

  • ValueError –

    If relevant state arrays are misaligned, quantities are negative or non-finite, base prices are not positive, occupied wealth tiers are undefined, or model tables have invalid definitions or missing category/goods references. Also raised for invalid supplied package labels, quantities, or consumption scales, or non-finite calculated demand.

Notes

The half-non-pop-demand deduction is community-derived and documented by the Needs wiki; it is not confirmed by the comments in the game files. Clamping negative effective supply to zero is this model's convention. This is target consumption, without weekly adjustment, culture, prestige goods, local-state effects, demographic changes, or wealth feedback. Market prices and existing state.pop_buy_orders do not influence the result.

References
Source code in src/vic3_analysis/analysis/economy/model.py
def pop_buy_orders(
    self,
    state: EconomyState,
    *,
    goods_buy_packages: GoodsBuyPackages | None = None,
    consumption_scales: np.ndarray | None = None,
) -> np.ndarray:
    """Calculate weekly population buy orders for the represented workers.

    Prepared context baskets and scales are reused when their explicit
    arguments are omitted. Without prepared baskets, each need category's
    effective supply is
    ``max(sell_orders - 0.5 * (building_goods_input + exports), 0)``.
    Divide each eligible good's effective supply by the category total,
    clip that share to its configured minimum and maximum, multiply by
    its configured weight, and normalize these purchase weights. Share
    limits apply before normalization; they do not cap final purchases.
    With zero effective supply, start from zero shares, retaining any
    configured minimum shares. If all purchase weights are zero, use the
    category's default good exclusively.

    Each occupied cell's integer wealth selects a buy package, whose
    category values are measured in pounds at base prices per 10,000
    workers. Multiply worker counts by ``1 + 0.5 * dependents_per_worker``,
    scale by ``consumption_mult / 10_000``, distribute by
    normalized purchase weights, and divide by goods' base prices to
    obtain units per week. Household scales are aggregated by wealth before
    allocating goods baskets, without a building-by-profession-by-goods tensor.

    Args:
        state: Snapshot aligned to this economy's building, profession,
            and goods indexes. Workers and their dependents consume at the
            same wealth tier; dependents consume half as much per person.
            Empty cells need no wealth package.
        goods_buy_packages: Optional immutable `GoodsBuyPackages`, as returned by
            `goods_buy_packages`. Wealth labels must cover every occupied
            tier. Quantity columns must follow `goods_index` order; column
            count is checked against the economy. Construction validates and
            sorts wealth labels and quantities together. Supplied baskets
            are scaled directly by worker-
            equivalent population and profession consumption multipliers,
            without substitution or base-price conversion, and remain fixed
            when supply changes.
            Unused state goods flows and raw need/package tables are not
            validated in this mode. The baskets are unchanged.
        consumption_scales: Optional precomputed matrix returned by
            `pop_consumption_scales`. Supplied values are
            authoritative, finite, non-negative, and shaped like ``pops``;
            empty cells must have zero scale. They already include all
            population factors, so profession multipliers are not read or
            reapplied. The caller ensures scales remain valid for the
            population assumptions. The matrix is unchanged.

    Returns:
        A fresh writable float64 vector of shape ``(n_goods,)`` aligned
        to `goods_index`, suitable for ``PopState.pop_buy_orders``.
        The state is unchanged.

    Raises:
        TypeError: If supplied baskets are not a `GoodsBuyPackages` instance.
        ValueError: If relevant state arrays are misaligned, quantities
            are negative or non-finite, base prices are not positive,
            occupied wealth tiers are undefined, or model tables have
            invalid definitions or missing category/goods references. Also
            raised for invalid supplied package labels, quantities, or
            consumption scales, or non-finite calculated demand.

    Notes:
        The half-non-pop-demand deduction is community-derived and
        documented by the Needs wiki; it is not confirmed by the comments
        in the game files. Clamping negative effective supply to zero is
        this model's convention. This is target consumption, without
        weekly adjustment, culture, prestige goods, local-state effects,
        demographic changes, or wealth feedback. Market prices and
        existing ``state.pop_buy_orders`` do not influence the result.

    References:
        - ``common/pop_needs/00_pop_needs.txt``: substitution weights,
          minimum/maximum supply shares, comments on their meaning, and
          default goods.
        - ``common/buy_packages/00_buy_packages.txt``: category values
          indexed by integer wealth.
        - ``common/pop_types/``: profession ``consumption_mult`` values.
        - [Needs wiki, Formulas](https://vic3.paradoxwikis.com/Needs#Formulas):
          population scaling and detailed substitution formulas. The
          non-pop-demand deduction is attributed there to community
          reverse engineering:
          https://www.reddit.com/r/victoria3/comments/yudc80/pop_needs_consumer_good_substitution_and_why_oil/
        - [Official developer diary #13](https://steamcommunity.com/games/529340/announcements/detail/2965045886379003737):
          wealth-dependent needs and valuation using goods' base prices.
    """
    return _consumption.pop_buy_orders(
        self,
        state,
        goods_buy_packages=goods_buy_packages,
        consumption_scales=consumption_scales,
    )

pop_expenses

pop_expenses(
    state: EconomyState,
    *,
    goods_buy_packages: GoodsBuyPackages | None = None,
    consumption_scales: ndarray | None = None,
) -> ndarray

Return weekly household expenses for target goods consumption.

Value each occupied wealth tier's standard goods basket at the state's market prices, then multiply by its household consumption scale. This uses the same baskets and scales as pop_buy_orders, without a configuration-by-profession-by-goods tensor.

Parameters:

  • state

    (EconomyState) –

    Snapshot with aligned household matrices, finite non-negative worker counts, and finite non-negative goods-aligned market prices. Zero prices are valid. Empty cells need no wealth basket.

  • goods_buy_packages

    (GoodsBuyPackages | None, default: None ) –

    Optional goods baskets with the same contract as in pop_buy_orders. When omitted, reuse prepared context baskets or call goods_buy_packages(state). Supplied baskets bypass unused goods flows, base prices, and raw needs/package definitions.

  • consumption_scales

    (ndarray | None, default: None ) –

    Optional authoritative precomputed household scales, with the same contract as in pop_buy_orders. When omitted, reuse context scales or calculate them.

Returns:

  • ndarray –

    A fresh, owned, writable float64 matrix shaped like state.pops

  • ndarray –

    containing household totals in pounds per week. Empty cells have

  • ndarray –

    zero expenses. Inputs, model tables, and pop_balance are unchanged.

Raises:

  • TypeError –

    If supplied baskets are not a GoodsBuyPackages instance.

  • ValueError –

    If relevant state inputs, baskets, or scales are invalid, occupied tiers are undefined, or calculated costs are non-finite.

Notes

Expenses price target consumption without affordability restrictions, rationing, taxes, or wealth transitions. Prices affect expenses but do not affect the basket quantities or consumption scales.

Source code in src/vic3_analysis/analysis/economy/model.py
def pop_expenses(
    self,
    state: EconomyState,
    *,
    goods_buy_packages: GoodsBuyPackages | None = None,
    consumption_scales: np.ndarray | None = None,
) -> np.ndarray:
    """Return weekly household expenses for target goods consumption.

    Value each occupied wealth tier's standard goods basket at the state's
    market prices, then multiply by its household consumption scale. This
    uses the same baskets and scales as `pop_buy_orders`, without a
    configuration-by-profession-by-goods tensor.

    Args:
        state: Snapshot with aligned household matrices, finite non-negative
            worker counts, and finite non-negative goods-aligned market
            prices. Zero prices are valid. Empty cells need no wealth basket.
        goods_buy_packages: Optional goods baskets with the same contract as in
            `pop_buy_orders`. When omitted, reuse prepared context
            baskets or call `goods_buy_packages(state)`. Supplied baskets
            bypass unused goods flows, base prices, and raw needs/package
            definitions.
        consumption_scales: Optional authoritative precomputed household
            scales, with the same contract as in `pop_buy_orders`.
            When omitted, reuse context scales or calculate them.

    Returns:
        A fresh, owned, writable float64 matrix shaped like ``state.pops``
        containing household totals in pounds per week. Empty cells have
        zero expenses. Inputs, model tables, and ``pop_balance`` are unchanged.

    Raises:
        TypeError: If supplied baskets are not a `GoodsBuyPackages` instance.
        ValueError: If relevant state inputs, baskets, or scales are invalid,
            occupied tiers are undefined, or calculated costs are non-finite.

    Notes:
        Expenses price target consumption without affordability restrictions,
        rationing, taxes, or wealth transitions. Prices affect expenses but
        do not affect the basket quantities or consumption scales.
    """
    return _consumption.pop_expenses(
        self,
        state,
        goods_buy_packages=goods_buy_packages,
        consumption_scales=consumption_scales,
    )

pop_income

pop_income(state: EconomyState) -> ndarray

Return disposable cooperative and dependent household income.

Each configuration distributes all of its positive building_surplus in proportion to state.pops * wage_weight. Every profession is included regardless of paid_private_wage; zero weights receive no cooperative income. Configurations are independent, including those of the same building type. Losses pay zero cooperative wages and remain visible through state.building_deficit, before any optional operating-loss subsidy. Add pops * dependents_per_worker * dependent_wage / 52 for household dependent earnings; missing annual dependent wages default to zero.

Parameters:

  • state

    (EconomyState) –

    Snapshot aligned to this economy's configuration, goods, and profession indexes. Stored signed flows and market prices are authoritative; production and throughput are not reconstructed.

Returns:

  • ndarray –

    A fresh writable float64 matrix shaped like state.pops containing

  • ndarray –

    total household income in £ per week for each configuration-profession

  • ndarray –

    group, rather than per-worker rates. The state and model tables are unchanged.

Raises:

  • ValueError –

    If relevant arrays are misaligned or non-finite, prices, populations, or wage weights are negative, profession keys are invalid, wage weights are missing, used dependent wages are invalid, calculated income is non-finite, or a configuration has positive surplus but no positive recipient weight.

Notes

This is a simplified worker-cooperative accounting policy. It applies proportional income tax when context.subsidize_operating_losses is enabled. Losses exceeding total gross income raise ValueError. It excludes transfers, subsistence income, reserves, and household expenses, and does not change wealth or pop_balance. Dependent earnings do not reduce building surplus or increase production GDP.

Source code in src/vic3_analysis/analysis/economy/model.py
def pop_income(self, state: EconomyState) -> np.ndarray:
    """Return disposable cooperative and dependent household income.

    Each configuration distributes all of its positive ``building_surplus``
    in proportion to ``state.pops * wage_weight``. Every profession is
    included regardless of ``paid_private_wage``; zero weights receive no
    cooperative income. Configurations are independent, including those of the same
    building type. Losses pay zero cooperative wages and remain visible
    through ``state.building_deficit``, before any optional operating-loss subsidy.
    Add ``pops * dependents_per_worker * dependent_wage / 52`` for household
    dependent earnings; missing annual dependent wages default to zero.

    Args:
        state: Snapshot aligned to this economy's configuration, goods, and
            profession indexes. Stored signed flows and market prices are
            authoritative; production and throughput are not reconstructed.

    Returns:
        A fresh writable float64 matrix shaped like ``state.pops`` containing
        total household income in £ per week for each configuration-profession
        group, rather than per-worker rates. The state and model tables are unchanged.

    Raises:
        ValueError: If relevant arrays are misaligned or non-finite, prices,
            populations, or wage weights are negative, profession keys are
            invalid, wage weights are missing, used dependent wages are invalid,
            calculated income is non-finite, or a configuration has positive
            surplus but no positive recipient weight.

    Notes:
        This is a simplified worker-cooperative accounting policy. It applies
        proportional income tax when context.subsidize_operating_losses is enabled.
        Losses exceeding total gross income raise ValueError. It excludes
        transfers, subsistence income, reserves, and household expenses, and
        does not change wealth or ``pop_balance``. Dependent earnings do not
        reduce building surplus or increase production GDP.
    """
    return _income.pop_income(self, state)

pop_income_tax

pop_income_tax(state: EconomyState) -> ndarray

Recompute weekly household taxes funding operating losses.

Returns a fresh matrix aligned to state.pops. The context policy taxes cooperative and dependent earnings proportionally, or returns zeros when disabled. Raises ValueError if losses cannot be financed.

Source code in src/vic3_analysis/analysis/economy/model.py
def pop_income_tax(self, state: EconomyState) -> np.ndarray:
    """Recompute weekly household taxes funding operating losses.

    Returns a fresh matrix aligned to ``state.pops``. The context policy
    taxes cooperative and dependent earnings proportionally, or returns
    zeros when disabled. Raises ValueError if losses cannot be financed.
    """
    return _income.pop_income_tax(self, state)

market_prices_variance

market_prices_variance(state: EconomyState) -> ndarray

Calculate the market prices variance from base prices.

Parameters:

  • state

    (EconomyState) –

    The EconomyState to evaluate (as produced by solve).

Returns:

  • ndarray –

    A 1-D array of shape (n_goods,) containing the market prices variance.

Source code in src/vic3_analysis/analysis/economy/model.py
def market_prices_variance(self, state: EconomyState) -> np.ndarray:
    """Calculate the market prices variance from base prices.

    Args:
        state: The `EconomyState` to evaluate (as produced by
            `solve`).

    Returns:
        A 1-D array of shape ``(n_goods,)`` containing the market prices variance.
    """
    return _market.market_prices_variance(self, state)

market_prices

market_prices(
    buy_orders: ndarray[tuple[int], dtype[float64]],
    sell_orders: ndarray[tuple[int], dtype[float64]],
) -> ndarray

Calculate market prices from buy and sell orders.

Parameters:

  • buy_orders

    (ndarray[tuple[int], dtype[float64]]) –

    1-D array of non-negative buy orders aligned to the goods table.

  • sell_orders

    (ndarray[tuple[int], dtype[float64]]) –

    1-D array of non-negative sell orders aligned to the goods table.

Returns:

  • ndarray –

    A float64 array of prices aligned to goods_index.

Raises:

  • ValueError –

    If either input is not a 1-D goods-aligned array, does not contain finite values, or contains negative values.

Source code in src/vic3_analysis/analysis/economy/model.py
def market_prices(
    self,
    buy_orders: np.ndarray[tuple[int], np.dtype[np.float64]],
    sell_orders: np.ndarray[tuple[int], np.dtype[np.float64]],
) -> np.ndarray:
    """Calculate market prices from buy and sell orders.

    Args:
        buy_orders: 1-D array of non-negative buy orders aligned to the
            goods table.
        sell_orders: 1-D array of non-negative sell orders aligned to the
            goods table.

    Returns:
        A float64 array of prices aligned to `goods_index`.

    Raises:
        ValueError: If either input is not a 1-D goods-aligned array, does
            not contain finite values, or contains negative values.
    """
    return _market.market_prices(self, buy_orders, sell_orders)

construction_cost

construction_cost(state: EconomyState) -> float

Calculate the total construction cost for an EconomyState.

Parameters:

  • state

    (EconomyState) –

    The EconomyState to evaluate (as produced by solve).

Returns:

  • float –

    The dot product of building_levels with the per-configuration

  • float –

    construction cost vector.

Source code in src/vic3_analysis/analysis/economy/model.py
def construction_cost(self, state: EconomyState) -> float:
    """Calculate the total construction cost for an `EconomyState`.

    Args:
        state: The `EconomyState` to evaluate (as produced by
            `solve`).

    Returns:
        The dot product of ``building_levels`` with the per-configuration
        construction cost vector.
    """
    return _production.construction_cost(self, state)

arable_land_consumption

arable_land_consumption(state: EconomyState) -> float

Return the total arable land consumed by an economy state.

Each building level consumes one unit of arable land when its building group is agricultural, plantation, ranching, or subsistence land use.

Parameters:

  • state

    (EconomyState) –

    The EconomyState whose land consumption is summed.

Returns:

  • float –

    The sum of building-configuration levels in arable-land-consuming

  • float –

    building groups.

Raises:

  • ValueError –

    If the state's building-level vector is not aligned to the production table.

Source code in src/vic3_analysis/analysis/economy/model.py
def arable_land_consumption(self, state: EconomyState) -> float:
    """Return the total arable land consumed by an economy state.

    Each building level consumes one unit of arable land when its building
    group is agricultural, plantation, ranching, or subsistence land use.

    Args:
        state: The `EconomyState` whose land consumption is summed.

    Returns:
        The sum of building-configuration levels in arable-land-consuming
        building groups.

    Raises:
        ValueError: If the state's building-level vector is not aligned to
            the production table.
    """
    return _production.arable_land_consumption(self, state)

levels_per_building

levels_per_building(
    state: EconomyState,
) -> dict[str, float]

Return total levels per building across all PM configurations.

Building levels in EconomyState are aligned to production-table rows, where one building can appear in many production-method configurations. This method aggregates those configuration levels by building key while preserving the buildings' first-appearance order in df_production.

Parameters:

  • state

    (EconomyState) –

    The EconomyState whose levels are aggregated.

Returns:

  • dict[str, float] –

    A mapping from every building key in the production table to its

  • dict[str, float] –

    total level. Unused buildings are included with value 0.0.

Raises:

  • ValueError –

    If the state's building-level vector is not aligned to the production table.

Source code in src/vic3_analysis/analysis/economy/model.py
def levels_per_building(self, state: EconomyState) -> dict[str, float]:
    """Return total levels per building across all PM configurations.

    Building levels in `EconomyState` are aligned to production-table
    rows, where one building can appear in many production-method
    configurations.  This method aggregates those configuration levels by
    building key while preserving the buildings' first-appearance order in
    `df_production`.

    Args:
        state: The `EconomyState` whose levels are aggregated.

    Returns:
        A mapping from every building key in the production table to its
        total level.  Unused buildings are included with value ``0.0``.

    Raises:
        ValueError: If the state's building-level vector is not aligned to
            the production table.
    """
    return _production.levels_per_building(self, state)

solve

solve(
    building_levels: ndarray[tuple[int], dtype[float64]],
    method: str = "nominal",
    goods_import: ndarray[tuple[int], dtype[float64]]
    | None = None,
    goods_export: ndarray[tuple[int], dtype[float64]]
    | None = None,
    throughput_multipliers: ndarray[
        tuple[int], dtype[float64]
    ]
    | None = None,
    economy_of_scale_level_cap: float = 0.0,
    *,
    workforce_ratio: float = 1.0,
    subsidize_operating_losses: bool | None = None,
    initial_wealth: ndarray | None = None,
    max_iterations: int = 1000,
    rtol: float = 1e-09,
    atol: float = 1e-08,
) -> EconomyState

Solve for an EconomyState from a building-level vector.

Parameters:

  • building_levels

    (ndarray[tuple[int], dtype[float64]]) –

    1-D array of shape (n_buildings,) specifying the level of each building configuration in the production table.

  • method

    (str, default: 'nominal' ) –

    The solving method to use. "nominal" uses base prices; "market" derives prices from supplied orders; "equilibrium" iterates household wealth and endogenous consumption demand. Nominal and market modes leave household demand at zero.

  • goods_import

    (ndarray[tuple[int], dtype[float64]] | None, default: None ) –

    Non-negative finite 1-D array of shape (n_goods,) adding sell orders. Defaults to zeros.

  • goods_export

    (ndarray[tuple[int], dtype[float64]] | None, default: None ) –

    Non-negative finite 1-D array of shape (n_goods,) adding buy orders. Defaults to zeros. A good may have both import and export orders.

  • throughput_multipliers

    (ndarray[tuple[int], dtype[float64]] | None, default: None ) –

    Optional 1-D array of shape (n_buildings,) scaling each configuration's gross goods inputs and outputs. Defaults to no scaling.

  • economy_of_scale_level_cap

    (float, default: 0.0 ) –

    Maximum building level contributing to economy of scale. The default 0.0 disables the effect; use 20.0 to cap the throughput bonus at +20%.

  • workforce_ratio

    (float, default: 1.0 ) –

    Economy-wide workers / total population in (0, 1]. Defaults to 1.0 for worker-only accounting.

  • subsidize_operating_losses

    (bool | None, default: None ) –

    Equilibrium-only proportional tax on all household income to fund operating losses. None (the default) enables it for equilibrium and disables it for other modes. Pass False to disable it explicitly. No payroll is guaranteed; unaffordable losses fail the solve.

  • initial_wealth

    (ndarray | None, default: None ) –

    Equilibrium-only full configuration/profession matrix. Defaults to profession starting tiers. Occupied tiers must have baskets.

  • max_iterations

    (int, default: 1000 ) –

    Positive integer limit on equilibrium transition steps, excluding initialization and verification. Defaults to 1000.

  • rtol

    (float, default: 1e-09 ) –

    Non-negative relative tolerance for equilibrium thresholds and final verification. Defaults to 1e-9.

  • atol

    (float, default: 1e-08 ) –

    Non-negative absolute tolerance, in weekly household pounds for affordability and building pounds for losses. Defaults to 1e-8. Equilibrium controls are ignored by nominal and market modes.

Returns:

  • EconomyState –

    An EconomyState describing the resulting state. Equilibrium results

  • EconomyState –

    include diagnostics; floor unaffordability and unsubsidized losses are flags

  • EconomyState –

    and do not prevent numerical convergence. With subsidies enabled,

  • EconomyState –

    operating losses exceeding household income raise EquilibriumError

  • EconomyState –

    with status insufficient_income and the failing pre-tax snapshot.

Raises:

  • ValueError –

    If method is unsupported, if building_levels is not 1-D or its length does not match the number of rows in the production table, if goods_import or goods_export is not a non-negative finite goods-aligned vector, or if throughput_multipliers is not aligned to the production table, or if the economy-of-scale cap is negative or non-finite, or if workforce_ratio is outside (0, 1] or non-finite. Equilibrium rejects invalid controls, initial wealth, prepared tiers, or initial evaluation.

  • EquilibriumError –

    On cycles, exhausted iterations, subsequent calculation failures, or failed final verification; retains the last evaluated state.

Source code in src/vic3_analysis/analysis/economy/model.py
def solve(
    self,
    building_levels: np.ndarray[tuple[int], np.dtype[np.float64]],
    method: str = "nominal",
    goods_import: np.ndarray[tuple[int], np.dtype[np.float64]] | None = None,
    goods_export: np.ndarray[tuple[int], np.dtype[np.float64]] | None = None,
    throughput_multipliers: (
        np.ndarray[tuple[int], np.dtype[np.float64]] | None
    ) = None,
    economy_of_scale_level_cap: float = 0.0,
    *,
    workforce_ratio: float = 1.0,
    subsidize_operating_losses: bool | None = None,
    initial_wealth: np.ndarray | None = None,
    max_iterations: int = 1000,
    rtol: float = 1e-9,
    atol: float = 1e-8,
) -> EconomyState:
    """Solve for an `EconomyState` from a building-level vector.

    Args:
        building_levels: 1-D array of shape ``(n_buildings,)`` specifying
            the level of each building configuration in the production
            table.
        method: The solving method to use. ``"nominal"`` uses base prices;
            ``"market"`` derives prices from supplied orders; ``"equilibrium"``
            iterates household wealth and endogenous consumption demand.
            Nominal and market modes leave household demand at zero.
        goods_import: Non-negative finite 1-D array of shape ``(n_goods,)``
            adding sell orders. Defaults to zeros.
        goods_export: Non-negative finite 1-D array of shape ``(n_goods,)``
            adding buy orders. Defaults to zeros. A good may have both
            import and export orders.
        throughput_multipliers: Optional 1-D array of shape
            ``(n_buildings,)`` scaling each configuration's gross goods
            inputs and outputs. Defaults to no scaling.
        economy_of_scale_level_cap: Maximum building level contributing to
            economy of scale. The default ``0.0`` disables the effect; use
            ``20.0`` to cap the throughput bonus at +20%.
        workforce_ratio: Economy-wide workers / total population in ``(0, 1]``.
            Defaults to ``1.0`` for worker-only accounting.
        subsidize_operating_losses: Equilibrium-only proportional tax on all
            household income to fund operating losses. None (the default)
            enables it for equilibrium and disables it for other modes.
            Pass False to disable it explicitly.
            No payroll is guaranteed; unaffordable losses fail the solve.
        initial_wealth: Equilibrium-only full configuration/profession matrix.
            Defaults to profession starting tiers. Occupied tiers must have baskets.
        max_iterations: Positive integer limit on equilibrium transition steps,
            excluding initialization and verification. Defaults to 1000.
        rtol: Non-negative relative tolerance for equilibrium thresholds and
            final verification. Defaults to ``1e-9``.
        atol: Non-negative absolute tolerance, in weekly household pounds for
            affordability and building pounds for losses. Defaults to ``1e-8``.
            Equilibrium controls are ignored by nominal and market modes.

    Returns:
        An `EconomyState` describing the resulting state. Equilibrium results
        include diagnostics; floor unaffordability and unsubsidized losses are flags
        and do not prevent numerical convergence. With subsidies enabled,
        operating losses exceeding household income raise EquilibriumError
        with status insufficient_income and the failing pre-tax snapshot.

    Raises:
        ValueError: If *method* is unsupported, if
            *building_levels* is not 1-D or its length does not match the
            number of rows in the production table, if *goods_import* or
            *goods_export* is not a non-negative finite goods-aligned vector, or if
            *throughput_multipliers* is not aligned to the production
            table, or if the economy-of-scale cap is
            negative or non-finite, or if *workforce_ratio* is outside ``(0, 1]``
            or non-finite. Equilibrium rejects invalid controls, initial wealth,
            prepared tiers, or initial evaluation.
        EquilibriumError: On cycles, exhausted iterations, subsequent calculation
            failures, or failed final verification; retains the last evaluated state.
    """

    if subsidize_operating_losses is None:
        subsidize_operating_losses = method == "equilibrium"
    if not isinstance(subsidize_operating_losses, (bool, np.bool_)):
        raise TypeError("subsidize_operating_losses must be a boolean.")
    if subsidize_operating_losses and method != "equilibrium":
        raise ValueError(
            "subsidize_operating_losses requires method='equilibrium'."
        )
    if method == "equilibrium":
        _equilibrium._validate_equilibrium_tolerances(rtol, atol)
        if (
            isinstance(max_iterations, (bool, np.bool_))
            or not isinstance(max_iterations, (int, np.integer))
            or max_iterations <= 0
        ):
            raise ValueError("max_iterations must be a positive integer.")
    if building_levels.ndim != 1:
        raise ValueError("building_levels must be a 1-D array.")
    if building_levels.shape[0] != len(self.building_index):
        raise ValueError(
            "building_levels length must match the number of rows in the "
            "production table."
        )
    if goods_import is None:
        goods_import = np.zeros(len(self.goods_index), dtype=np.float64)
    else:
        goods_import = _validation._validate_goods_vector(
            self, goods_import, "goods_import"
        )
    if goods_export is None:
        goods_export = np.zeros(len(self.goods_index), dtype=np.float64)
    else:
        goods_export = _validation._validate_goods_vector(
            self, goods_export, "goods_export"
        )
    bonuses = self.economy_of_scale_bonuses(
        building_levels, economy_of_scale_level_cap
    )
    if throughput_multipliers is None:
        throughput_multipliers = np.ones(len(self.df_production), dtype=np.float64)
    else:
        _production._validate_throughput_multipliers(self, throughput_multipliers)
    throughput_multipliers = throughput_multipliers + bonuses
    if method == "nominal":
        return _market._solve_nominal(
            self,
            building_levels,
            goods_import,
            goods_export,
            throughput_multipliers,
            workforce_ratio=workforce_ratio,
        )
    if method == "market":
        return _market._solve_market(
            self,
            building_levels,
            goods_import,
            goods_export,
            throughput_multipliers,
            workforce_ratio=workforce_ratio,
        )
    if method == "equilibrium":
        initial = _market._solve_nominal(
            self,
            building_levels,
            goods_import,
            goods_export,
            throughput_multipliers,
            workforce_ratio=workforce_ratio,
        )
        return _equilibrium._solve_equilibrium(
            self,
            replace(
                initial.context,
                subsidize_operating_losses=subsidize_operating_losses,
            ),
            initial.pop_wealth if initial_wealth is None else initial_wealth,
            max_iterations=max_iterations,
            rtol=rtol,
            atol=atol,
        )
    raise ValueError(f"Invalid method: {method!r}")

df_goods_buy_packages

df_goods_buy_packages(
    source: EconomyState
    | EconomyContext
    | GoodsBuyPackages,
) -> DataFrame

Export standard goods baskets as an independent, mutable DataFrame.

Parameters:

Returns:

  • DataFrame –

    A fresh DataFrame with ascending int64 index named wealth and

  • DataFrame –

    float64 goods quantities in goods_index column order. Quantities

  • DataFrame –

    are weekly units per 10,000 standard workers. Both data and labels

  • DataFrame –

    are copied; editing the report does not modify its source.

Raises:

  • TypeError –

    If source is not a state, context, or GoodsBuyPackages.

  • ValueError –

    If goods alignment or basket calculation is invalid.

Source code in src/vic3_analysis/analysis/economy/model.py
def df_goods_buy_packages(
    self, source: EconomyState | EconomyContext | GoodsBuyPackages
) -> pd.DataFrame:
    """Export standard goods baskets as an independent, mutable DataFrame.

    Args:
        source: A snapshot or context from which to calculate or reuse baskets,
            or immutable `GoodsBuyPackages` with columns in `goods_index` order.

    Returns:
        A fresh DataFrame with ascending int64 index named ``wealth`` and
        float64 goods quantities in `goods_index` column order. Quantities
        are weekly units per 10,000 standard workers. Both data and labels
        are copied; editing the report does not modify its source.

    Raises:
        TypeError: If source is not a state, context, or `GoodsBuyPackages`.
        ValueError: If goods alignment or basket calculation is invalid.
    """
    return _reports.df_goods_buy_packages(self, source)

df_buildings

df_buildings(state: EconomyState) -> DataFrame

Export an EconomyState's building registry to a DataFrame.

Parameters:

  • state

    (EconomyState) –

    The EconomyState to export (as produced by solve).

Returns:

  • DataFrame –

    An independent, mutable DataFrame with columns "building",

  • DataFrame –

    "production_method", "level", "construction_cost",

  • DataFrame –

    "building_surplus", "profit_per_capita" and "employment",

  • DataFrame –

    followed by signed goods_<good> flows in goods-index order and

  • DataFrame –

    employment_<profession> counts in pop-index order. Flows and surplus

  • DataFrame –

    are weekly realized totals; surplus is value added before wages at state

  • DataFrame –

    prices. Profit per capita divides surplus by employed workers, excluding

  • DataFrame –

    dependents, and is NaN for zero employment. Rows have strictly positive

  • DataFrame –

    levels and are sorted by "level" descending.

Source code in src/vic3_analysis/analysis/economy/model.py
def df_buildings(self, state: EconomyState) -> pd.DataFrame:
    """Export an `EconomyState`'s building registry to a DataFrame.

    Args:
        state: The `EconomyState` to export (as produced by
            `solve`).

    Returns:
        An independent, mutable ``DataFrame`` with columns ``"building"``,
        ``"production_method"``, ``"level"``, ``"construction_cost"``,
        ``"building_surplus"``, ``"profit_per_capita"`` and ``"employment"``,
        followed by signed ``goods_<good>`` flows in goods-index order and
        ``employment_<profession>`` counts in pop-index order. Flows and surplus
        are weekly realized totals; surplus is value added before wages at state
        prices. Profit per capita divides surplus by employed workers, excluding
        dependents, and is NaN for zero employment. Rows have strictly positive
        levels and are sorted by ``"level"`` descending.
    """
    return _reports.df_buildings(self, state)

df_market

df_market(state: EconomyState) -> DataFrame

Export an EconomyState's market stats to a DataFrame.

Parameters:

  • state

    (EconomyState) –

    The EconomyState to export (as produced by solve).

Returns:

  • DataFrame –

    A DataFrame with columns "goods", "market_prices",

  • DataFrame –

    "market_prices_variance", "sell_orders", "buy_orders",

  • DataFrame –

    "building_goods_input", "building_goods_output",

  • DataFrame –

    "imports", "exports" and "pop_needs", filtered to goods

  • DataFrame –

    with non-zero sell_orders or buy_orders and sorted by

  • DataFrame –

    "sell_orders" descending.

Source code in src/vic3_analysis/analysis/economy/model.py
def df_market(self, state: EconomyState) -> pd.DataFrame:
    """Export an `EconomyState`'s market stats to a DataFrame.

    Args:
        state: The `EconomyState` to export (as produced by
            `solve`).

    Returns:
        A ``DataFrame`` with columns ``"goods"``, ``"market_prices"``,
        ``"market_prices_variance"``, ``"sell_orders"``, ``"buy_orders"``,
        ``"building_goods_input"``, ``"building_goods_output"``,
        ``"imports"``, ``"exports"`` and ``"pop_needs"``, filtered to goods
        with non-zero ``sell_orders`` or ``buy_orders`` and sorted by
        ``"sell_orders"`` descending.
    """
    return _reports.df_market(self, state)

df_pop

df_pop(state: EconomyState) -> DataFrame

Export an EconomyState's pop employment and wealth to a DataFrame.

Each row describes one profession, aggregating the per-building arrays in state.

Parameters:

  • state

    (EconomyState) –

    The EconomyState to export (as produced by solve).

Returns:

  • DataFrame –

    A DataFrame with columns "profession", "employment",

  • DataFrame –

    "employment_share", "dependents", "population",

  • DataFrame –

    "avg_wealth", "total_wealth" and

  • DataFrame –

    "pop_balance", filtered to professions with non-zero employment

  • DataFrame –

    and sorted by "employment" descending.

Source code in src/vic3_analysis/analysis/economy/model.py
def df_pop(self, state: EconomyState) -> pd.DataFrame:
    """Export an `EconomyState`'s pop employment and wealth to a DataFrame.

    Each row describes one profession, aggregating the per-building arrays
    in *state*.

    Args:
        state: The `EconomyState` to export (as produced by
            `solve`).

    Returns:
        A ``DataFrame`` with columns ``"profession"``, ``"employment"``,
        ``"employment_share"``, ``"dependents"``, ``"population"``,
        ``"avg_wealth"``, ``"total_wealth"`` and
        ``"pop_balance"``, filtered to professions with non-zero employment
        and sorted by ``"employment"`` descending.
    """
    return _reports.df_pop(self, state)

EconomyContext dataclass

EconomyContext(
    building_levels: ndarray,
    building_goods_flow: ndarray,
    goods_import: ndarray,
    goods_export: ndarray,
    pops: ndarray,
    *,
    workforce_ratio: float = 1.0,
    subsidize_operating_losses: bool = True,
    goods_buy_packages: GoodsBuyPackages | None = None,
    consumption_scales: ndarray | None = None,
)

Fixed production, trade, and population inputs shared by snapshots.

Direct array inputs are owned, read-only copies; immutable GoodsBuyPackages objects are shared. Goods columns follow the owning economy's goods_index; population columns follow its pop_index. Prepared consumption data is optional and remains fixed as wealth and prices change. Clear affected prepared fields when changing their inputs, then call Economy.prepare_context to refresh them.

Attributes:

  • building_levels (ndarray) –

    Levels per production configuration.

  • building_goods_flow (ndarray) –

    Realized weekly signed quantities by configuration and good, including throughput and economy-of-scale bonuses.

  • goods_import (ndarray) –

    Non-negative weekly import quantities by good.

  • goods_export (ndarray) –

    Non-negative weekly export quantities by good. The same good may have both import and export orders.

  • goods_trade_flow (ndarray) –

    Derived net imports (imports minus exports).

  • imports (ndarray) –

    Read-only alias for goods_import.

  • exports (ndarray) –

    Read-only alias for goods_export.

  • pops (ndarray) –

    Employed-worker counts by configuration and profession.

  • workforce_ratio (float) –

    Workers / total population, in (0, 1].

  • subsidize_operating_losses (bool) –

    Fund operating losses by taxing all household income proportionally; no payroll guarantee. Defaults to True.

  • goods_buy_packages (GoodsBuyPackages | None) –

    Optional immutable goods baskets with their wealth labels, shared across contexts and snapshots.

  • consumption_scales (ndarray | None) –

    Optional dimensionless basket multiples, shaped like pops and zero in empty cells.

goods_trade_flow cached property

goods_trade_flow: ndarray

Return read-only net imports, preserving gross orders separately.

imports property

imports: ndarray

Return read-only weekly import quantities.

exports property

exports: ndarray

Return read-only weekly export quantities.

building_goods_input cached property

building_goods_input: ndarray

Return total weekly input quantities, splitting signs before summing.

building_goods_output cached property

building_goods_output: ndarray

Return total weekly output quantities, splitting signs before summing.

sell_orders cached property

sell_orders: ndarray

Calculate the sell orders for each good.

Returns:

  • ndarray –

    The sell orders vector, computed as the sum of building_goods_output and imports

dependents_per_worker cached property

dependents_per_worker: float

Return the number of dependents per employed worker.

total_employment cached property

total_employment: float

Return the employed-worker headcount, excluding dependents.

total_population cached property

total_population: float

Return workers plus their dependents, using the uniform workforce ratio.

employment_by_profession cached property

employment_by_profession: ndarray

Calculate total employment per profession.

Returns:

  • ndarray –

    A 1-D array of shape (n_professions,) giving the column sums of

  • ndarray –

    pops.

profession_shares cached property

profession_shares: ndarray

Calculate each profession's share of total employment.

Returns:

  • ndarray –

    A 1-D array of shape (n_professions,) summing to 1.0, or

  • ndarray –

    all zeros when total employment is zero.

non_pop_buy_orders cached property

non_pop_buy_orders: ndarray

Return weekly industrial input and export orders by good.

GoodsBuyPackages dataclass

GoodsBuyPackages(wealth: ndarray, quantities: ndarray)

Owned, read-only goods baskets and their ascending wealth labels.

Attributes:

  • wealth (ndarray) –

    Unique positive int64 wealth tiers, sorted on construction.

  • quantities (ndarray) –

    Weekly goods quantities per 10,000 standard workers, shaped as wealth tiers × goods in the owning economy's goods_index order. Rows are sorted with wealth.

Empty tiers are allowed with a quantities matrix of shape (0, n_goods).

MarketState dataclass

MarketState(
    context: EconomyContext, market_prices: ndarray
)

Prices and building finances for a shared fixed economy context.

Attributes:

  • context (EconomyContext) –

    Fixed inputs shared with the enclosing EconomyState.

  • market_prices (ndarray) –

    Goods-aligned prices in pounds per unit.

Methods:

  • gdp –

    Calculate the weekly or annual GDP from net goods and prices.

building_surplus cached property

building_surplus: ndarray

Return weekly goods value added per configuration before wages (£).

building_deficit cached property

building_deficit: ndarray

Return weekly operating losses before subsidies per configuration (£).

gdp_weekly cached property

gdp_weekly: float

Calculate the weekly GDP from net goods and prices.

Returns:

  • float –

    The sum of ``(building_goods_output - building_goods_input)

  • float –
    • market_prices`` (weekly).

gdp

gdp(annual: bool = False) -> float

Calculate the weekly or annual GDP from net goods and prices.

Parameters:

  • annual

    (bool, default: False ) –

    If True, multiply the weekly GDP by 52 to annualise it.

Returns:

  • float –

    The sum of ``(building_goods_output - building_goods_input)

  • float –
    • market_prices`` (weekly), or 52 times that value when annual
  • float –

    is set.

Source code in src/vic3_analysis/analysis/economy/states.py
def gdp(self, annual: bool = False) -> float:
    """Calculate the weekly or annual GDP from net goods and prices.

    Args:
        annual: If ``True``, multiply the weekly GDP by 52 to annualise it.

    Returns:
        The sum of ``(building_goods_output - building_goods_input)
        * market_prices`` (weekly), or 52 times that value when *annual*
        is set.
    """
    if annual:
        return self.gdp_weekly * 52
    return self.gdp_weekly

PopState dataclass

PopState(
    pop_wealth: ndarray,
    pop_buy_orders: ndarray,
    pop_income: ndarray | None = None,
    pop_expenses: ndarray | None = None,
    *,
    pop_income_tax: ndarray | None = None,
)

Changing household wealth, demand, and optional weekly finances.

Wealth, income, and expenses use configuration-by-profession matrices; demand is goods-aligned. None finances mean unevaluated, including for nominal and market solves. Balance is derived only when both are supplied.

Attributes:

  • pop_wealth (ndarray) –

    Non-negative integer wealth tiers.

  • pop_buy_orders (ndarray) –

    Weekly household goods demand.

  • pop_income (ndarray | None) –

    Optional disposable household income in pounds per week.

  • pop_income_tax (ndarray | None) –

    Optional household tax funding operating losses, in pounds per week. Evaluated untaxed states contain zero taxes.

  • pop_expenses (ndarray | None) –

    Optional total consumption expenses in pounds per week.

pop_income_before_tax cached property

pop_income_before_tax: ndarray | None

Return gross weekly household income, or None when unevaluated.

pop_balance cached property

pop_balance: ndarray | None

Return weekly income minus expenses, or None when unevaluated.

EconomyState dataclass

EconomyState(
    context: EconomyContext,
    market: MarketState,
    households: PopState,
    *,
    equilibrium_diagnostics: EquilibriumDiagnostics
    | None = None,
)

Complete snapshot composed from fixed context, market, and population.

Components share immutable data without copying production arrays. Familiar flat attribute reads forward to their owners; constructors and replacements use the component fields. Composition does not evaluate household finances or establish consistency between supplied wealth, demand, and prices.

Attributes:

  • context (EconomyContext) –

    Fixed production, trade, population, and prepared consumption.

  • market (MarketState) –

    Prices and building finances referencing this exact context.

  • households (PopState) –

    Household wealth, demand, and optional finances.

  • equilibrium_diagnostics (EquilibriumDiagnostics | None) –

    Optional solver outcome and financial flags.

Methods:

  • gdp –

    Return weekly GDP, or annual GDP when requested.

  • gdp_per_capita –

    Calculate the weekly or annual GDP per capita.

building_levels property

building_levels: ndarray

Return context.building_levels.

building_goods_flow property

building_goods_flow: ndarray

Return context.building_goods_flow.

goods_import property

goods_import: ndarray

Return context.goods_import.

goods_export property

goods_export: ndarray

Return context.goods_export.

goods_trade_flow property

goods_trade_flow: ndarray

Return net imports from context.goods_trade_flow.

imports property

imports: ndarray

Return context.imports.

exports property

exports: ndarray

Return context.exports.

pops property

pops: ndarray

Return context.pops.

workforce_ratio property

workforce_ratio: float

Return context.workforce_ratio.

goods_buy_packages property

goods_buy_packages: GoodsBuyPackages | None

Return context.goods_buy_packages.

consumption_scales property

consumption_scales: ndarray | None

Return context.consumption_scales.

building_goods_input property

building_goods_input: ndarray

Return context.building_goods_input.

building_goods_output property

building_goods_output: ndarray

Return context.building_goods_output.

sell_orders property

sell_orders: ndarray

Return context.sell_orders.

dependents_per_worker property

dependents_per_worker: float

Return context.dependents_per_worker.

total_employment property

total_employment: float

Return context.total_employment.

total_population property

total_population: float

Return context.total_population.

employment_by_profession property

employment_by_profession: ndarray

Return context.employment_by_profession.

profession_shares property

profession_shares: ndarray

Return context.profession_shares.

non_pop_buy_orders property

non_pop_buy_orders: ndarray

Return context.non_pop_buy_orders.

market_prices property

market_prices: ndarray

Return market.market_prices.

building_surplus property

building_surplus: ndarray

Return market.building_surplus.

building_deficit property

building_deficit: ndarray

Return market.building_deficit.

gdp_weekly property

gdp_weekly: float

Return market.gdp_weekly.

pop_wealth property

pop_wealth: ndarray

Return households.pop_wealth.

pop_buy_orders property

pop_buy_orders: ndarray

Return households.pop_buy_orders.

pop_income property

pop_income: ndarray | None

Return households.pop_income.

pop_income_tax property

pop_income_tax: ndarray | None

Return weekly household taxes funding operating losses.

pop_income_before_tax property

pop_income_before_tax: ndarray | None

Return gross weekly household income.

pop_expenses property

pop_expenses: ndarray | None

Return households.pop_expenses.

pop_balance property

pop_balance: ndarray | None

Return households.pop_balance.

buy_orders cached property

buy_orders: ndarray

Calculate the buy orders for each good.

Returns:

  • ndarray –

    The buy orders vector, computed as the sum of building_goods_input, exports and population buy orders.

total_wealth_by_profession cached property

total_wealth_by_profession: ndarray

Return read-only employment-weighted wealth sums by profession.

Values sum pop_wealth * pops over configurations, excluding dependents. Divide by workforce_ratio for population-weighted totals.

average_wealth cached property

average_wealth: float

Calculate the employment-weighted average wealth.

With a uniform workforce ratio and dependents sharing household wealth, this also equals total_wealth / total_population.

Returns:

  • float –

    The weighted average of wealth by employment, or 0.0

  • float –

    when total employment is zero.

total_wealth cached property

total_wealth: float

Calculate total population-weighted wealth, including dependents.

Returns:

  • float –

    The sum of pop_wealth * pops / workforce_ratio. This is a

  • float –

    population-weighted sum of wealth tiers, not a monetary stock.

average_wealth_by_profession cached property

average_wealth_by_profession: ndarray

Calculate the employment-weighted average wealth per profession.

Returns:

  • ndarray –

    A 1-D array of shape (n_professions,); entries are 0.0

  • ndarray –

    where a profession has no employment.

wealth_distribution cached property

wealth_distribution: dict[str, float]

Calculate weighted wealth-distribution statistics across pops.

Returns:

  • dict[str, float] –

    A dict with keys "min", "mean", "median", "std" and

  • dict[str, float] –

    "max" of wealth, weighted by employment. All values are 0.0

  • dict[str, float] –

    when there is no employment.

gini_wealth cached property

gini_wealth: float

Calculate the Gini coefficient of the wealth distribution.

Returns:

  • float –

    A float in [0, 1] (0 = perfectly equal, 1 = maximally

  • float –

    unequal), weighted by employment. Returns 0.0 when there is no

  • float –

    employment or no total wealth.

gdp

gdp(annual: bool = False) -> float

Return weekly GDP, or annual GDP when requested.

Source code in src/vic3_analysis/analysis/economy/states.py
def gdp(self, annual: bool = False) -> float:
    """Return weekly GDP, or annual GDP when requested."""
    return self.market.gdp(annual)

gdp_per_capita

gdp_per_capita(annual: bool = False) -> float

Calculate the weekly or annual GDP per capita.

Parameters:

  • annual

    (bool, default: False ) –

    If True, multiply the weekly GDP by 52 to annualise it.

Returns:

  • float –

    The GDP per capita, computed as gdp(annual) / total_population,

  • float –

    or 0.0 when total population is zero.

Source code in src/vic3_analysis/analysis/economy/states.py
def gdp_per_capita(self, annual: bool = False) -> float:
    """Calculate the weekly or annual GDP per capita.

    Args:
        annual: If ``True``, multiply the weekly GDP by 52 to annualise it.

    Returns:
        The GDP per capita, computed as ``gdp(annual) / total_population``,
        or ``0.0`` when total population is zero.
    """
    if self.total_population == 0:
        return 0.0
    return self.gdp(annual) / self.total_population

EquilibriumDiagnostics dataclass

EquilibriumDiagnostics(
    status: Literal[
        "converged",
        "cycle",
        "max_iterations",
        "evaluation_failed",
        "verification_failed",
        "insufficient_income",
    ],
    iterations: int,
    increasing_cells: tuple[tuple[int, int], ...] = (),
    decreasing_cells: tuple[tuple[int, int], ...] = (),
    unaffordable_floor_cells: tuple[
        tuple[int, int], ...
    ] = (),
    loss_buildings: tuple[int, ...] = (),
    cycle_start: int | None = None,
    cycle_length: int | None = None,
)

Numerical outcome and financial flags for an equilibrium snapshot.

Attributes:

  • status (Literal['converged', 'cycle', 'max_iterations', 'evaluation_failed', 'verification_failed', 'insufficient_income']) –

    converged, cycle, max_iterations, evaluation_failed, verification_failed, or insufficient_income. Convergence means wealth is stationary, including at its boundaries; it does not promise financial sustainability.

  • iterations (int) –

    Completed transition steps, excluding initialization and verification.

  • increasing_cells (tuple[tuple[int, int], ...]) –

    Configuration/profession coordinates requiring advancement.

  • decreasing_cells (tuple[tuple[int, int], ...]) –

    Configuration/profession coordinates requiring a lower tier.

  • unaffordable_floor_cells (tuple[tuple[int, int], ...]) –

    Occupied minimum-tier cells below their affordability threshold. These cannot move down and do not prevent convergence.

  • loss_buildings (tuple[int, ...]) –

    Configuration indices with material unfunded weekly losses.

  • cycle_start (int | None) –

    First iteration of a repeated assignment, or None.

  • cycle_length (int | None) –

    Number of transitions in the detected cycle, or None.

Coordinates follow Economy.building_index and Economy.pop_index. Tuple fields contain immutable Python integer indices in row-major order.

EquilibriumError

EquilibriumError(message: str, state: EconomyState)

Bases: RuntimeError

Failure after initialization, retaining the last evaluated snapshot.

Attributes:

  • state –

    Immutable last evaluated state with solver diagnostics attached.

  • diagnostics –

    The same diagnostics object as on state.

Cycles and iteration exhaustion do not prove that no equilibrium exists. Calculation failures retain the original exception through __cause__.

Source code in src/vic3_analysis/analysis/economy/states.py
def __init__(self, message: str, state: EconomyState) -> None:
    """Attach a diagnostic-bearing snapshot to a solver failure message."""
    super().__init__(message)
    if state.equilibrium_diagnostics is None:
        raise ValueError("EquilibriumError requires state diagnostics.")
    self.state = state
    self.diagnostics = state.equilibrium_diagnostics