Skip to content

Parsing and utilities API

Used throughout the parsed-data showcase.

Column definitions are in the parser data dictionary.

get_vic3_directory

get_vic3_directory() -> Path

Search common Steam library paths and return the Victoria 3 game directory.

Returns:

  • Path –

    The absolute path to the Victoria 3/game directory.

Raises:

  • FileNotFoundError –

    If the Victoria 3 game directory cannot be found in any of the known Steam library locations.

Source code in src/vic3_analysis/utils.py
def get_vic3_directory() -> Path:
    """Search common Steam library paths and return the Victoria 3 game directory.

    Returns:
        The absolute path to the ``Victoria 3/game`` directory.

    Raises:
        FileNotFoundError: If the Victoria 3 game directory cannot be found
            in any of the known Steam library locations.
    """
    game_suffix = "Victoria 3/game"

    for prefix in prefixes:
        full = Path(prefix).expanduser() / game_suffix
        candidates = list(Path(full.root).glob(str(full.relative_to(full.root))))
        if candidates:
            return candidates[0]
    raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), game_suffix)

localization

localization(
    language: str, game_dir: str | Path | None = None
) -> DataFrame

Parse the effective Victoria 3 localization catalog.

Values are raw quoted contents: game references, markup, and escape sequences are not expanded. language is Victoria 3's internal name, such as "english" or "simp_chinese".

Source code in src/vic3_analysis/parse/localization.py
def localization(language: str, game_dir: str | Path | None = None) -> pd.DataFrame:
    """Parse the effective Victoria 3 localization catalog.

    Values are raw quoted contents: game references, markup, and escape
    sequences are not expanded.  ``language`` is Victoria 3's internal name,
    such as ``"english"`` or ``"simp_chinese"``.
    """
    values = _localization_values(language, game_dir)
    return pd.DataFrame(values.items(), columns=["key", "localization"], dtype="string")

parse_merge

parse_merge(
    path: str | Path, merge_levels: int = 0
) -> Tree

Parse all .txt files in path and merge them into a single Tree.

Parameters:

  • path

    (str | Path) –

    Directory containing the Paradox script (.txt) files to parse.

  • merge_levels

    (int, default: 0 ) –

    Number of levels deep to merge nested Trees. Passed directly to pyradox.Tree.merge. Defaults to 0.

Returns:

  • Tree –

    A pyradox.Tree representing the merged contents of all .txt

  • Tree –

    files found in path (sorted alphabetically).

Source code in src/vic3_analysis/utils.py
def parse_merge(path: str | Path, merge_levels: int = 0) -> pyradox.Tree:
    """Parse all ``.txt`` files in *path* and merge them into a single Tree.

    Args:
        path: Directory containing the Paradox script (``.txt``) files to parse.
        merge_levels: Number of levels deep to merge nested Trees. Passed
            directly to ``pyradox.Tree.merge``.  Defaults to ``0``.

    Returns:
        A ``pyradox.Tree`` representing the merged contents of all ``.txt``
        files found in *path* (sorted alphabetically).
    """

    result = pyradox.Tree()
    for entry in sorted(Path(path).iterdir(), key=lambda p: p.name):
        if entry.suffix != ".txt" or not entry.is_file():
            continue
        content = entry.read_text(encoding="utf-8-sig")
        # Replace all special strings with '=' to prevent pyradox from treating them as merge directives
        for str in replace_strings:
            content = content.replace(str, "=")
        tree = pyradox.parse(content)
        result.merge(tree, merge_levels)
    return result

buy_packages

buy_packages(
    game_dir: str | Path | None = None,
) -> DataFrame

Parse Victoria 3 buy-packages files into a pandas.DataFrame.

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.

Returns:

  • DataFrame –

    A DataFrame with one row per wealth level and columns for

  • DataFrame –

    "wealth", "political_strength", "total_popneeds", and one

  • DataFrame –

    column per popneed_* good. Missing consumption values are filled

  • DataFrame –

    with 0.

Source code in src/vic3_analysis/parse/buy_packages.py
def buy_packages(game_dir: str | Path | None = None) -> pd.DataFrame:
    """Parse Victoria 3 buy-packages files into a ``pandas.DataFrame``.

    Args:
        game_dir: Path to the Victoria 3 ``game`` directory.  If ``None`` the
            directory is located automatically via
            `get_vic3_directory`.

    Returns:
        A ``DataFrame`` with one row per wealth level and columns for
        ``"wealth"``, ``"political_strength"``, ``"total_popneeds"``, and one
        column per ``popneed_*`` good.  Missing consumption values are filled
        with ``0``.
    """
    if game_dir is None:
        game_dir = get_vic3_directory()

    parse_dir = Path(game_dir) / "common" / "buy_packages"
    tree = parse_merge(parse_dir)
    rows, popneed_columns = _parse_rows(tree)

    fieldnames = ["wealth", "political_strength", "total_popneeds", *popneed_columns]
    results: list[dict[str, Any]] = []
    for row in rows:
        normalized_row: dict[str, Any] = {
            "wealth": row["wealth"],
            "political_strength": row["political_strength"],
        }
        total_popneeds = 0
        for column in popneed_columns:
            value = row.get(column, 0)
            normalized_row[column] = value
            total_popneeds += value
        normalized_row["total_popneeds"] = total_popneeds
        results.append(normalized_row)

    return pd.DataFrame(results, columns=fieldnames)

BuildingGroupParser

BuildingGroupParser(game_dir: str | Path | None = None)

Bases: Tree

A pyradox.Tree populated with Victoria 3 building-group definitions.

On construction the parser reads all building-group .txt files from the game's common/building_groups directory. Raw entries can be iterated via items (inherited from Tree); a flat per-group table is built by to_dataframe; and a per-group attribute dict with land_usage and cash_reserves_max inheritance resolved is returned by resolved_attributes.

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.

Methods:

  • to_dataframe –

    Convert the building-groups tree to a flat pandas.DataFrame.

  • resolved_attributes –

    Return per-group attribute dicts with inherited attrs resolved.

Source code in src/vic3_analysis/parse/building_groups.py
def __init__(self, game_dir: str | Path | None = None):
    """Initialise and populate the building-groups tree.

    Args:
        game_dir: Path to the Victoria 3 ``game`` directory. If ``None``
            the directory is located automatically via
            `get_vic3_directory`.
    """
    super().__init__()
    self._python_cache: dict[str, dict[str, Any]] = {}
    if game_dir is None:
        game_dir = get_vic3_directory()
    self._game_dir = Path(game_dir)

    parse_dir = self._game_dir / "common" / "building_groups"
    parse_tree = parse_merge(parse_dir)
    self.update(parse_tree)

to_dataframe

to_dataframe(*, language: str | None = None) -> DataFrame

Convert the building-groups tree to a flat pandas.DataFrame.

Each row is one building group; scalar attributes are preserved as columns, list values are concatenated into a +-joined string, and nested Tree/dict values (such as should_auto_expand trigger blocks) are omitted. No parent-chain inheritance is applied here — use resolved_attributes for inherited values.

Parameters:

  • language

    (str | None, default: None ) –

    Optional Victoria 3 internal language name. When provided, adds a nullable key_localization column.

Returns:

  • DataFrame –

    A DataFrame with one row per building group ("key" column)

  • DataFrame –

    and one column per scalar attribute declared on that group.

Source code in src/vic3_analysis/parse/building_groups.py
def to_dataframe(self, *, language: str | None = None) -> pd.DataFrame:
    """Convert the building-groups tree to a flat ``pandas.DataFrame``.

    Each row is one building group; scalar attributes are preserved as
    columns, ``list`` values are concatenated into a ``+``-joined string,
    and nested ``Tree``/``dict`` values (such as ``should_auto_expand``
    trigger blocks) are omitted.  No parent-chain inheritance is applied
    here — use `resolved_attributes` for inherited values.

    Args:
        language: Optional Victoria 3 internal language name. When
            provided, adds a nullable ``key_localization`` column.

    Returns:
        A ``DataFrame`` with one row per building group (``"key"`` column)
        and one column per scalar attribute declared on that group.
    """
    results: list[dict[str, Any]] = []
    for group_key, group_values in self.items():
        py: dict[str, Any] = self._group_to_python(group_key, group_values)
        row: dict[str, Any] = {"key": group_key}
        for attribute_key, attribute_value in py.items():
            if isinstance(attribute_value, list):
                row[attribute_key] = "+".join(str(v) for v in attribute_value)
            elif isinstance(attribute_value, (dict, Tree)):
                continue  # Skip nested containers
            else:
                row[attribute_key] = attribute_value
        results.append(row)
    frame = pd.DataFrame(results)
    if language is not None:
        _insert_localization_column(
            frame,
            "key",
            "key_localization",
            _localization_values(language, self._game_dir),
        )
    return frame

resolved_attributes

resolved_attributes() -> dict[str, dict[str, Any]]

Return per-group attribute dicts with inherited attrs resolved.

For each building group, a copy of its raw scalar attributes is returned with the following inherited attributes filled by walking the parent_group chain when they are unset on the group itself:

  • land_usage — inherited when missing.
  • cash_reserves_max — inherited when missing or explicitly 0 (per the vanilla file comment: "If unspecified or set to 0, it will use the value from the parent group").
  • economy_of_scale — inherited when missing because the vanilla rule applies to buildings in a flagged group or any child group.

Non-inherited attributes keep their raw, group-local values (including None/missing, which are simply absent from the dict). Cycle guards prevent infinite loops on malformed parent_group loops.

Returns:

  • dict[str, dict[str, Any]] –

    A dict mapping each building-group key to a dict of its resolved

  • dict[str, dict[str, Any]] –

    scalar attributes.

Source code in src/vic3_analysis/parse/building_groups.py
def resolved_attributes(self) -> dict[str, dict[str, Any]]:
    """Return per-group attribute dicts with inherited attrs resolved.

    For each building group, a copy of its raw scalar attributes is
    returned with the following inherited attributes filled by walking the
    ``parent_group`` chain when they are unset on the group itself:

    - ``land_usage`` — inherited when missing.
    - ``cash_reserves_max`` — inherited when missing *or* explicitly ``0``
      (per the vanilla file comment: "If unspecified or set to 0, it will
      use the value from the parent group").
    - ``economy_of_scale`` — inherited when missing because the vanilla
      rule applies to buildings in a flagged group or any child group.

    Non-inherited attributes keep their raw, group-local values (including
    ``None``/missing, which are simply absent from the dict).  Cycle guards
    prevent infinite loops on malformed ``parent_group`` loops.

    Returns:
        A dict mapping each building-group key to a dict of its resolved
        scalar attributes.
    """
    raw: dict[str, dict[str, Any]] = {}
    for group_key, group_values in self.items():
        py = self._group_to_python(group_key, group_values)
        scalar: dict[str, Any] = {}
        for attribute_key, attribute_value in py.items():
            if isinstance(attribute_value, (list, dict, Tree)):
                continue
            scalar[attribute_key] = attribute_value
        raw[group_key] = scalar

    resolved: dict[str, dict[str, Any]] = {}
    for group_key, attrs in raw.items():
        merged = dict(attrs)
        for attr, skip_zero in _inherited_attrs.items():
            current = merged.get(attr)
            if current is None or (skip_zero and current == 0):
                inherited = _inherit_attr(group_key, raw, attr, skip_zero=skip_zero)
                if inherited is not None:
                    merged[attr] = inherited
        resolved[group_key] = merged
    return resolved

BuildingsParser

BuildingsParser(game_dir: str | Path | None = None)

Bases: Tree

A pyradox.Tree populated with Victoria 3 building definitions.

On construction the parser reads all building .txt files from the game directory, resolves required_construction keys to their numeric point values using the game's script_values, and stores the resolved value under the required_construction_points key for each building entry.

Attributes:

  • cost_modifiers (dict[str, int]) –

    Mapping of construction-cost script-value names (e.g. "construction_cost_urban") to their integer values, extracted from common/script_values.

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.

Methods:

  • to_dataframe –

    Convert the buildings tree to a flat pandas.DataFrame.

  • production_method_groups –

    Return a mapping of building keys to their production-method-group lists.

  • building_groups –

    Return a mapping of building group keys to their member building keys.

Source code in src/vic3_analysis/parse/buildings.py
def __init__(self, game_dir: str | Path | None = None):
    """Initialise and populate the buildings tree.

    Args:
        game_dir: Path to the Victoria 3 ``game`` directory. If ``None``
            the directory is located automatically via
            `get_vic3_directory`.
    """
    super().__init__()
    self._python_cache: dict[str, dict[str, Any]] = {}
    self._group_attrs_cache: dict[str, dict[str, Any]] | None = None
    if game_dir is None:
        game_dir = get_vic3_directory()
    self._game_dir = Path(game_dir)

    parse_dir = Path(game_dir) / "common" / "buildings"
    parse_tree = parse_merge(parse_dir)
    self.update(parse_tree)

    self._resolve_construction_costs(game_dir)

to_dataframe

to_dataframe(*, language: str | None = None) -> DataFrame

Convert the buildings tree to a flat pandas.DataFrame.

Scalar attributes of each building are preserved as columns; nested Tree and dict values are omitted, and list values are concatenated into a +-joined string.

In addition, the resolved attributes of each building's building_group (parsed from common/building_groups) are joined onto the row. land_usage and cash_reserves_max are resolved along the parent_group chain; other group attributes are taken verbatim. Group-attribute columns share the group attribute's name unless that name collides with an existing building-level column, in which case the building_group_ prefix is applied.

Parameters:

  • language

    (str | None, default: None ) –

    Optional Victoria 3 internal language name. When provided, adds nullable localization columns for the building and its building group.

Returns:

  • DataFrame –

    A DataFrame with one row per building and one column per scalar

  • DataFrame –

    attribute, augmented with the building's resolved group attributes.

Source code in src/vic3_analysis/parse/buildings.py
def to_dataframe(self, *, language: str | None = None) -> pd.DataFrame:
    """Convert the buildings tree to a flat ``pandas.DataFrame``.

    Scalar attributes of each building are preserved as columns; nested
    ``Tree`` and ``dict`` values are omitted, and ``list`` values are
    concatenated into a ``+``-joined string.

    In addition, the resolved attributes of each building's
    ``building_group`` (parsed from ``common/building_groups``) are joined
    onto the row.  ``land_usage`` and ``cash_reserves_max`` are resolved
    along the ``parent_group`` chain; other group attributes are taken
    verbatim.  Group-attribute columns share the group attribute's name
    unless that name collides with an existing building-level column, in
    which case the ``building_group_`` prefix is applied.

    Args:
        language: Optional Victoria 3 internal language name. When
            provided, adds nullable localization columns for the building
            and its building group.

    Returns:
        A ``DataFrame`` with one row per building and one column per scalar
        attribute, augmented with the building's resolved group attributes.
    """
    results: list[dict[str, Any]] = []
    for building_key, building_values in self.items():
        py: dict[str, Any] = self._building_to_python(building_key, building_values)
        row: dict[str, Any] = {"key": building_key}
        for attribute_key, attribute_value in py.items():
            if isinstance(attribute_value, list):
                row[attribute_key] = "+".join(str(v) for v in attribute_value)
            elif isinstance(attribute_value, (dict, Tree)):
                continue  # Skip nested containers
            else:
                row[attribute_key] = attribute_value
        results.append(row)
    _join_group_attrs(results, self._resolved_group_attrs())
    frame = pd.DataFrame(results)
    if language is not None:
        labels = _localization_values(language, self._game_dir)
        _insert_localization_column(frame, "key", "key_localization", labels)
        _insert_localization_column(
            frame,
            "building_group",
            "building_group_localization",
            labels,
        )
    return frame

production_method_groups

production_method_groups() -> dict[str, list[str]]

Return a mapping of building keys to their production-method-group lists.

Returns:

  • dict[str, list[str]] –

    A dict where each key is a building identifier and each value is a

  • dict[str, list[str]] –

    list of production-method-group keys associated with that building.

  • dict[str, list[str]] –

    Buildings without production_method_groups are omitted.

Source code in src/vic3_analysis/parse/buildings.py
def production_method_groups(self) -> dict[str, list[str]]:
    """Return a mapping of building keys to their production-method-group lists.

    Returns:
        A dict where each key is a building identifier and each value is a
        list of production-method-group keys associated with that building.
        Buildings without ``production_method_groups`` are omitted.
    """
    result: dict[str, list[str]] = {}
    for building_key, building_values in self.items():
        py: dict[str, Any] = self._building_to_python(building_key, building_values)
        pmg: list[str] | str | None = py.get("production_method_groups")
        if pmg is None:
            continue
        if isinstance(pmg, list):
            result[building_key] = pmg
        else:
            result[building_key] = [pmg]
    return result

building_groups

building_groups() -> dict[str, list[str]]

Return a mapping of building group keys to their member building keys.

Returns:

  • dict[str, list[str]] –

    A dict where each key is a building group identifier and each value

  • dict[str, list[str]] –

    is a list of building identifiers that belong to that group.

Source code in src/vic3_analysis/parse/buildings.py
def building_groups(self) -> dict[str, list[str]]:
    """Return a mapping of building group keys to their member building keys.

    Returns:
        A dict where each key is a building group identifier and each value
        is a list of building identifiers that belong to that group.
    """
    result: dict[str, list[str]] = {}
    for building_key, building_values in self.items():
        py: dict[str, Any] = self._building_to_python(building_key, building_values)
        group: str | None = py.get("building_group")
        if group is not None:
            if group not in result:
                result[group] = []
            result[group].append(building_key)
    return result

goods

goods(
    game_dir: str | Path | None = None,
    *,
    language: str | None = None,
) -> DataFrame

Parse Victoria 3 goods definitions and return them as a DataFrame.

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.

  • language

    (str | None, default: None ) –

    Optional Victoria 3 internal language name. When provided, adds a nullable key_localization column.

Returns:

  • DataFrame –

    A DataFrame with one row per tradeable good, where the "key"

  • DataFrame –

    column holds the good's identifier and remaining columns represent its

  • DataFrame –

    attributes (e.g. "cost").

Raises:

  • ValueError –

    If any entry in the goods tree is not a dict.

Source code in src/vic3_analysis/parse/goods.py
def goods(
    game_dir: str | Path | None = None, *, language: str | None = None
) -> pd.DataFrame:
    """Parse Victoria 3 goods definitions and return them as a DataFrame.

    Args:
        game_dir: Path to the Victoria 3 ``game`` directory.  If ``None`` the
            directory is located automatically via
            `get_vic3_directory`.
        language: Optional Victoria 3 internal language name. When provided,
            adds a nullable ``key_localization`` column.

    Returns:
        A ``DataFrame`` with one row per tradeable good, where the ``"key"``
        column holds the good's identifier and remaining columns represent its
        attributes (e.g. ``"cost"``).

    Raises:
        ValueError: If any entry in the goods tree is not a ``dict``.
    """
    if game_dir is None:
        game_dir = get_vic3_directory()

    parse_dir = Path(game_dir) / "common" / "goods"
    parse_tree = parse_merge(parse_dir)
    results: list[dict[str, Any]] = []
    for key, value in parse_tree.to_python().items():
        if not isinstance(value, dict):
            raise TypeError(f"Expected dict for {key}, got {type(value)}")
        results.append(
            {
                "key": key,
                **value,
            }
        )
    frame = pd.DataFrame(results)
    if language is not None:
        _insert_localization_column(
            frame,
            "key",
            "key_localization",
            _localization_values(language, game_dir),
        )
    return frame

PopNeedsParser

PopNeedsParser(game_dir: str | Path | None = None)

Bases: Tree

Parser for Victoria 3 pop need definitions.

Parameters:

  • game_dir

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

    Path to the Victoria 3 game directory. If None, locate the directory automatically.

Methods:

  • to_dataframe –

    Convert the pop-needs tree to a flat pandas.DataFrame.

Source code in src/vic3_analysis/parse/pop_needs.py
def __init__(self, game_dir: str | Path | None = None):
    """Initialise and populate the pop-needs tree.

    Args:
        game_dir: Path to the Victoria 3 ``game`` directory. If ``None``,
            locate the directory automatically.
    """
    super().__init__()

    if game_dir is None:
        game_dir = get_vic3_directory()
    self._game_dir = Path(game_dir)

    parse_dir = self._game_dir / "common" / "pop_needs"
    parse_tree = parse_merge(parse_dir)

    self.update(parse_tree)

to_dataframe

to_dataframe(*, language: str | None = None) -> DataFrame

Convert the pop-needs tree to a flat pandas.DataFrame.

Parameters:

  • language

    (str | None, default: None ) –

    Optional Victoria 3 internal language name. When provided, adds a nullable key_localization column.

Returns:

  • DataFrame –

    A DataFrame with one row per pop need entry. Columns include

  • DataFrame –

    "key" (the pop need type), "goods", "weight",

  • DataFrame –

    "max_supply_share", "min_supply_share", and "is_default".

Source code in src/vic3_analysis/parse/pop_needs.py
def to_dataframe(self, *, language: str | None = None) -> pd.DataFrame:
    """Convert the pop-needs tree to a flat ``pandas.DataFrame``.

    Args:
        language: Optional Victoria 3 internal language name. When
            provided, adds a nullable ``key_localization`` column.

    Returns:
        A ``DataFrame`` with one row per pop need entry.  Columns include
        ``"key"`` (the pop need type), ``"goods"``, ``"weight"``,
        ``"max_supply_share"``, ``"min_supply_share"``, and ``"is_default"``.
    """

    results: list[dict[str, Any]] = []
    for pop_need_type, pop_need_values in self.items():
        if isinstance(pop_need_values, Tree):
            py = pop_need_values.to_python()
        elif isinstance(pop_need_values, dict):
            py = pop_need_values
        else:
            continue

        default_good = py.get("default")

        entries = py.get("entry")
        if entries is None:
            # A pop need type must have at least one entry
            raise ValueError(f"Pop need type '{pop_need_type}' has no entries.")

        # normalize single entry vs multiple entries
        if not isinstance(entries, list):
            entries = [entries]

        for entry in entries:
            if not isinstance(entry, dict):
                continue

            goods = entry.get("goods")

            results.append(
                {
                    "key": pop_need_type,
                    "goods": goods,
                    "weight": entry.get("weight", 1),
                    "max_supply_share": entry.get("max_supply_share", 1),
                    "min_supply_share": entry.get("min_supply_share", 0),
                    "is_default": goods == default_good,
                }
            )

    frame = pd.DataFrame(
        results,
        columns=[
            "key",
            "goods",
            "weight",
            "max_supply_share",
            "min_supply_share",
            "is_default",
        ],
    )
    if language is not None:
        _insert_localization_column(
            frame,
            "key",
            "key_localization",
            _localization_values(language, self._game_dir),
        )
    return frame

PopTypesParser

PopTypesParser(game_dir: str | Path | None = None)

Bases: Tree

A pyradox.Tree populated with Victoria 3 pop-type definitions.

On construction the parser reads all pop-type .txt files from the game's common/pop_types directory and stores the parsed entries.

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.

Methods:

  • to_dataframe –

    Convert the pop-types tree to a flat pandas.DataFrame.

  • flags –

    Return a mapping of boolean-flag names to the pop types that set them.

Source code in src/vic3_analysis/parse/pop_types.py
def __init__(self, game_dir: str | Path | None = None):
    """Initialise and populate the pop-types tree.

    Args:
        game_dir: Path to the Victoria 3 ``game`` directory. If ``None``
            the directory is located automatically via
            `get_vic3_directory`.
    """
    super().__init__()
    self._python_cache: dict[str, dict[str, Any]] = {}
    if game_dir is None:
        game_dir = get_vic3_directory()
    self._game_dir = Path(game_dir)

    parse_dir = self._game_dir / "common" / "pop_types"
    parse_tree = parse_merge(parse_dir)
    self.update(parse_tree)

to_dataframe

to_dataframe(*, language: str | None = None) -> DataFrame

Convert the pop-types tree to a flat pandas.DataFrame.

Scalar attributes of each pop type are preserved as columns; nested Tree and dict values are omitted, and list values are concatenated into a +-joined string.

Parameters:

  • language

    (str | None, default: None ) –

    Optional Victoria 3 internal language name. When provided, adds a nullable key_localization column.

Returns:

  • DataFrame –

    A DataFrame with one row per pop type and one column per scalar

  • DataFrame –

    attribute.

Source code in src/vic3_analysis/parse/pop_types.py
def to_dataframe(self, *, language: str | None = None) -> pd.DataFrame:
    """Convert the pop-types tree to a flat ``pandas.DataFrame``.

    Scalar attributes of each pop type are preserved as columns; nested
    ``Tree`` and ``dict`` values are omitted, and ``list`` values are
    concatenated into a ``+``-joined string.

    Args:
        language: Optional Victoria 3 internal language name. When
            provided, adds a nullable ``key_localization`` column.

    Returns:
        A ``DataFrame`` with one row per pop type and one column per scalar
        attribute.
    """
    results: list[dict[str, Any]] = []
    for pop_type_key, pop_type_values in self.items():
        py: dict[str, Any] = self._pop_type_to_python(pop_type_key, pop_type_values)
        row: dict[str, Any] = {"key": pop_type_key}
        for attribute_key, attribute_value in py.items():
            if isinstance(attribute_value, list):
                row[attribute_key] = "+".join(str(v) for v in attribute_value)
            elif isinstance(attribute_value, (dict, Tree)):
                continue  # Skip nested containers
            else:
                row[attribute_key] = attribute_value
        results.append(row)
    frame = pd.DataFrame(results)
    if language is not None:
        _insert_localization_column(
            frame,
            "key",
            "key_localization",
            _localization_values(language, self._game_dir),
        )
    return frame

flags

flags() -> dict[str, list[str]]

Return a mapping of boolean-flag names to the pop types that set them.

Scans each pop type for boolean attributes whose value is True and groups pop-type keys by flag name (e.g. "is_slave", "military", "unemployment").

Returns:

  • dict[str, list[str]] –

    A dict where each key is a boolean attribute name and each value is

  • dict[str, list[str]] –

    a list of pop-type identifiers for which that flag is set to

  • dict[str, list[str]] –

    True.

Source code in src/vic3_analysis/parse/pop_types.py
def flags(self) -> dict[str, list[str]]:
    """Return a mapping of boolean-flag names to the pop types that set them.

    Scans each pop type for boolean attributes whose value is ``True`` and
    groups pop-type keys by flag name (e.g. ``"is_slave"``,
    ``"military"``, ``"unemployment"``).

    Returns:
        A dict where each key is a boolean attribute name and each value is
        a list of pop-type identifiers for which that flag is set to
        ``True``.
    """
    result: dict[str, list[str]] = {}
    for pop_type_key, pop_type_values in self.items():
        py: dict[str, Any] = self._pop_type_to_python(pop_type_key, pop_type_values)
        for attribute_key, attribute_value in py.items():
            if attribute_value is True:
                result.setdefault(attribute_key, []).append(pop_type_key)
    return result

production_method_groups

production_method_groups(
    game_dir: str | Path | None = None,
) -> dict[str, list[str]]

Parse Victoria 3 production-method-group data into a dict.

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.

Returns:

  • dict[str, list[str]] –

    A dict mapping each production-method-group key to its ordered list of

  • dict[str, list[str]] –

    production-method keys.

Source code in src/vic3_analysis/parse/production_method_groups.py
def production_method_groups(
    game_dir: str | Path | None = None,
) -> dict[str, list[str]]:
    """Parse Victoria 3 production-method-group data into a dict.

    Args:
        game_dir: Path to the Victoria 3 ``game`` directory.  If ``None`` the
            directory is located automatically via
            `get_vic3_directory`.

    Returns:
        A dict mapping each production-method-group key to its ordered list of
        production-method keys.
    """
    if game_dir is None:
        game_dir = get_vic3_directory()

    parse_dir = Path(game_dir) / "common" / "production_method_groups"
    parse_tree = parse_merge(parse_dir)
    results: dict[str, list[str]] = {}
    for key, subtree in parse_tree.items():
        if not isinstance(subtree, Tree):
            continue  # Skip non-tree entries
        production_methods = subtree.to_python().get("production_methods")
        if production_methods is None:
            continue
        if isinstance(production_methods, list):
            results[key] = production_methods
        else:
            results[key] = [production_methods]

    return results

ProductionMethodParser

ProductionMethodParser(game_dir: str | Path | None = None)

Bases: Tree

A pyradox.Tree populated with Victoria 3 production-method definitions.

On construction the parser reads all production-method .txt files from the game's common/production_methods directory. Raw entries can be iterated via items (inherited from Tree); per-method employment (total and broken down by profession) is available via employment; per-method state modifiers are available via state_modifiers; and a flat per-configuration table of production-method attributes plus appended employment, goods-flow and state-modifier columns is built by to_dataframe.

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.

Methods:

  • employment –

    Return per-method employment broken down by profession.

  • state_modifiers –

    Return per-method state modifiers flattened across scaling blocks.

  • to_dataframe –

    Build a flat DataFrame of per-configuration production-method stats.

Source code in src/vic3_analysis/parse/production_methods.py
def __init__(self, game_dir: str | Path | None = None):
    """Initialise and populate the production-methods tree.

    Args:
        game_dir: Path to the Victoria 3 ``game`` directory. If ``None``
            the directory is located automatically via
            `get_vic3_directory`.
    """
    super().__init__()
    self._python_cache: dict[str, dict[str, Any]] = {}
    if game_dir is None:
        game_dir = get_vic3_directory()
    self._game_dir = Path(game_dir)

    parse_dir = Path(game_dir) / "common" / "production_methods"
    parse_tree = parse_merge(parse_dir)
    self.update(parse_tree)

employment

employment() -> dict[str, dict[str, Any]]

Return per-method employment broken down by profession.

Iterates every production method in the tree and, for each one that defines building_modifiers, reads its level_scaled modifiers to compute the total employment and one employment_<profession> value per profession (e.g. employment_laborers). Methods without level_scaled employment modifiers still appear with a zero total.

Returns:

  • dict[str, dict[str, Any]] –

    A dict mapping each production-method key to a dict containing an

  • dict[str, dict[str, Any]] –

    "employment" value (total across all professions) and one

  • dict[str, dict[str, Any]] –

    "employment_<profession>" value per profession that the method

  • dict[str, dict[str, Any]] –

    employs.

Source code in src/vic3_analysis/parse/production_methods.py
def employment(self) -> dict[str, dict[str, Any]]:
    """Return per-method employment broken down by profession.

    Iterates every production method in the tree and, for each one that
    defines ``building_modifiers``, reads its ``level_scaled`` modifiers to
    compute the total employment and one ``employment_<profession>``
    value per profession (e.g. ``employment_laborers``).  Methods without
    ``level_scaled`` employment modifiers still appear with a zero total.

    Returns:
        A dict mapping each production-method key to a dict containing an
        ``"employment"`` value (total across all professions) and one
        ``"employment_<profession>"`` value per profession that the method
        employs.
    """
    result: dict[str, dict[str, Any]] = {}
    for key, building_modifiers in self._iter_building_modifiers():
        pm_entry: dict[str, Any] = {"employment": 0}
        level_scaled = building_modifiers.get("level_scaled")
        if isinstance(level_scaled, dict):
            for level_str, value in level_scaled.items():
                if level_str.startswith("building_employment_"):
                    pm_entry["employment"] += value
                    match = re.match(r"building_employment_(.+)_add$", level_str)
                    if match:
                        pm_entry[f"employment_{match.group(1)}"] = value
        result[key] = pm_entry
    return result

state_modifiers

state_modifiers() -> dict[str, dict[str, Any]]

Return per-method state modifiers flattened across scaling blocks.

Iterates every production method in the tree and, for each one that defines state_modifiers, flattens its unscaled, level_scaled and workforce_scaled blocks into a single dict mapping modifier keys (e.g. state_infrastructure_add) to their values. The scaling distinction is dropped; vanilla never defines the same modifier key under two scaling blocks of one method.

Returns:

  • dict[str, dict[str, Any]] –

    A dict mapping each production-method key that defines

  • dict[str, dict[str, Any]] –

    state_modifiers to a dict of its modifier keys and values.

Raises:

  • ValueError –

    If the same modifier key is defined under more than one scaling block of a single production method.

Source code in src/vic3_analysis/parse/production_methods.py
def state_modifiers(self) -> dict[str, dict[str, Any]]:
    """Return per-method state modifiers flattened across scaling blocks.

    Iterates every production method in the tree and, for each one that
    defines ``state_modifiers``, flattens its ``unscaled``,
    ``level_scaled`` and ``workforce_scaled`` blocks into a single dict
    mapping modifier keys (e.g. ``state_infrastructure_add``) to their
    values.  The scaling distinction is dropped; vanilla never defines
    the same modifier key under two scaling blocks of one method.

    Returns:
        A dict mapping each production-method key that defines
        ``state_modifiers`` to a dict of its modifier keys and values.

    Raises:
        ValueError: If the same modifier key is defined under more than
            one scaling block of a single production method.
    """
    result: dict[str, dict[str, Any]] = {}
    for key, subtree in self.items():
        if not isinstance(subtree, Tree):
            continue
        state_modifiers = self._pm_to_python(key, subtree).get("state_modifiers")
        if not isinstance(state_modifiers, dict):
            continue
        pm_entry: dict[str, Any] = {}
        scaling_seen: dict[str, str] = {}
        for scaling_key, scaling_block in state_modifiers.items():
            if not isinstance(scaling_block, dict):
                continue
            for modifier_key, value in scaling_block.items():
                previous = scaling_seen.get(modifier_key)
                if previous is not None:
                    raise ValueError(
                        f"State modifier {modifier_key} of production "
                        f"method {key} is defined under both {previous} "
                        f"and {scaling_key}"
                    )
                scaling_seen[modifier_key] = scaling_key
                pm_entry[modifier_key] = value
        result[key] = pm_entry
    return result

to_dataframe

to_dataframe(
    df_goods: DataFrame | None = None,
    df_buildings: DataFrame | None = None,
    pmg_dict: dict[str, list[str]] | None = None,
    *,
    language: str | None = None,
) -> DataFrame

Build a flat DataFrame of per-configuration production-method stats.

Combines the parsed production methods with building and production-method-group data into one row per (building, production-method-group, production-method) combination. The production method's scalar attributes are preserved as columns, list values are concatenated into +-joined strings, and nested dict/Tree values (such as building_modifiers) are omitted. Total and per-profession employment and per-good net flows (positive = output, negative = input) are appended as columns at the end. Goods columns follow the goods table's row order and are prefixed with goods_ to distinguish them from attribute columns. State modifiers (flattened across the state_modifiers scaling blocks, e.g. state_infrastructure_add) are appended after the goods columns.

Parameters:

  • df_goods

    (DataFrame | None, default: None ) –

    Optional pre-parsed goods table.

  • df_buildings

    (DataFrame | None, default: None ) –

    Optional pre-parsed buildings table.

  • pmg_dict

    (dict[str, list[str]] | None, default: None ) –

    Optional production-method-group mapping.

  • language

    (str | None, default: None ) –

    Optional Victoria 3 internal language name. When provided, adds nullable localization columns for the building, production-method group, and production method.

Returns:

  • DataFrame –

    A DataFrame with "building",

  • DataFrame –

    "production_method_group" and "production_method" key

  • DataFrame –

    columns, the production method's scalar/list attributes, and

  • DataFrame –

    appended "employment" (total), "employment_<profession>",

  • DataFrame –

    "goods_<good>" and state-modifier (e.g.

  • DataFrame –

    "state_infrastructure_add") columns. Employment, goods and

  • DataFrame –

    state-modifier columns are zero-filled; scalar-attribute columns

  • DataFrame –

    are left missing (NaN) when a method does not define them,

  • DataFrame –

    matching BuildingsParser.to_dataframe.

Raises:

  • ValueError –

    If a goods modifier string cannot be classified as either an input or an output, if the associated good key cannot be identified, if a state modifier is defined under multiple scaling blocks of one production method, or if a production-method-group referenced by a building is not found.

Source code in src/vic3_analysis/parse/production_methods.py
def to_dataframe(
    self,
    df_goods: pd.DataFrame | None = None,
    df_buildings: pd.DataFrame | None = None,
    pmg_dict: dict[str, list[str]] | None = None,
    *,
    language: str | None = None,
) -> pd.DataFrame:
    """Build a flat DataFrame of per-configuration production-method stats.

    Combines the parsed production methods with building and
    production-method-group data into one row per
    (building, production-method-group, production-method) combination.
    The production method's scalar attributes are preserved as columns,
    ``list`` values are concatenated into ``+``-joined strings, and nested
    ``dict``/``Tree`` values (such as ``building_modifiers``) are omitted.
    Total and per-profession employment and per-good net flows (positive =
    output, negative = input) are appended as columns at the end.  Goods
    columns follow the goods table's row order and are prefixed with
    ``goods_`` to distinguish them from attribute columns.  State modifiers
    (flattened across the ``state_modifiers`` scaling blocks, e.g.
    ``state_infrastructure_add``) are appended after the goods columns.

    Args:
        df_goods: Optional pre-parsed goods table.
        df_buildings: Optional pre-parsed buildings table.
        pmg_dict: Optional production-method-group mapping.
        language: Optional Victoria 3 internal language name. When
            provided, adds nullable localization columns for the building,
            production-method group, and production method.

    Returns:
        A ``DataFrame`` with ``"building"``,
        ``"production_method_group"`` and ``"production_method"`` key
        columns, the production method's scalar/list attributes, and
        appended ``"employment"`` (total), ``"employment_<profession>"``,
        ``"goods_<good>"`` and state-modifier (e.g.
        ``"state_infrastructure_add"``) columns.  Employment, goods and
        state-modifier columns are zero-filled; scalar-attribute columns
        are left missing (``NaN``) when a method does not define them,
        matching `BuildingsParser.to_dataframe`.

    Raises:
        ValueError: If a goods modifier string cannot be classified as
            either an input or an output, if the associated good key
            cannot be identified, if a state modifier is defined under
            multiple scaling blocks of one production method, or if a
            production-method-group referenced by a building is not found.
    """
    game_dir = self._game_dir
    if df_goods is None:
        df_goods = goods(game_dir)
    goods_key_order: list[str] = df_goods["key"].tolist()
    goods_keys = set(goods_key_order)

    if df_buildings is None:
        buildings_tree = BuildingsParser(game_dir)
        buildings_pmg_dict = buildings_tree.production_method_groups()
    else:
        buildings_pmg_dict = dict(
            zip(
                df_buildings["key"].tolist(),
                df_buildings["production_method_groups"].tolist(),
            )
        )

    if pmg_dict is None:
        pmg_dict = production_method_groups(game_dir)

    employment_dict = self.employment()
    goods_flows = self._goods_io(goods_keys)
    state_modifiers_dict = self.state_modifiers()

    # Collect every per-profession employment key (e.g. "employment_laborers")
    employment_profession_keys: list[str] = sorted(
        {
            k
            for pm_data in employment_dict.values()
            for k in pm_data
            if k.startswith("employment_") and k != "employment"
        }
    )

    state_modifier_keys: list[str] = sorted(
        {
            modifier_key
            for pm_data in state_modifiers_dict.values()
            for modifier_key in pm_data
        }
    )

    # Precompute per-method scalar/list attributes (skip dict/Tree), cached
    pm_attrs: dict[str, dict[str, Any]] = {}
    for pm_key, pm_values in self.items():
        py = self._pm_to_python(pm_key, pm_values)
        attrs: dict[str, Any] = {}
        for attribute_key, attribute_value in py.items():
            if isinstance(attribute_value, list):
                attrs[attribute_key] = "+".join(str(v) for v in attribute_value)
            elif isinstance(attribute_value, (dict, Tree)):
                continue  # Skip nested containers
            else:
                attrs[attribute_key] = attribute_value
        pm_attrs[pm_key] = attrs

    data: list[dict[str, Any]] = []
    for building, pmg_list in buildings_pmg_dict.items():
        for pmg in pmg_list:
            if pmg not in pmg_dict:
                raise ValueError(
                    f"Production method group {pmg} not found for building {building}"
                )
            for pm in pmg_dict[pmg]:
                emp = employment_dict.get(pm, {})
                flows = goods_flows.get(pm, {})
                state_mods = state_modifiers_dict.get(pm, {})
                row: dict[str, Any] = {
                    "building": building,
                    "production_method_group": pmg,
                    "production_method": pm,
                    **pm_attrs.get(pm, {}),
                    "employment": emp.get("employment", 0),
                    **{k: emp.get(k, 0) for k in employment_profession_keys},
                    **{f"goods_{gk}": flows.get(gk, 0) for gk in goods_key_order},
                    **{k: state_mods.get(k, 0) for k in state_modifier_keys},
                }
                data.append(row)
    frame = pd.DataFrame(data)
    if language is not None and not frame.empty:
        labels = _localization_values(language, game_dir)
        for source, target in (
            ("building", "building_localization"),
            ("production_method_group", "production_method_group_localization"),
            ("production_method", "production_method_localization"),
        ):
            _insert_localization_column(frame, source, target, labels)
    return frame

StateRegionsParser

StateRegionsParser(game_dir: str | Path | None = None)

Bases: Tree

A pyradox.Tree subclass for parsing Victoria 3 state region definitions.

Reads all .txt files from the game's map_data/state_regions directory and stores the parsed data in a tree structure that mirrors the original file hierarchy. Provides helper methods for converting to a flat pandas.DataFrame and for looking up state region attributes & resources.

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.

Methods:

  • to_dataframe –

    Convert the state regions tree to a flat pandas.DataFrame.

Source code in src/vic3_analysis/parse/state_regions.py
def __init__(self, game_dir: str | Path | None = None):
    """Initialise and populate the state regions tree.

    Args:
        game_dir: Path to the Victoria 3 ``game`` directory. If ``None``
            the directory is located automatically via
            `get_vic3_directory`.
    """
    super().__init__()
    if game_dir is None:
        game_dir = get_vic3_directory()
    self._game_dir = Path(game_dir)

    parse_dir = self._game_dir / "map_data" / "state_regions"
    parse_tree = parse_merge(parse_dir)
    self.update(parse_tree)

to_dataframe

to_dataframe(*, language: str | None = None) -> DataFrame

Convert the state regions tree to a flat pandas.DataFrame.

Scalar attributes of each state region are preserved as columns; nested Tree, list, and dict values are omitted.

Parameters:

  • language

    (str | None, default: None ) –

    Optional Victoria 3 internal language name. When provided, adds a nullable key_localization column.

Returns:

  • DataFrame –

    A DataFrame with one row per state region and one column per scalar

  • DataFrame –

    attribute.

Source code in src/vic3_analysis/parse/state_regions.py
def to_dataframe(self, *, language: str | None = None) -> pd.DataFrame:
    """Convert the state regions tree to a flat ``pandas.DataFrame``.

    Scalar attributes of each state region are preserved as columns; nested
    ``Tree``, ``list``, and ``dict`` values are omitted.

    Args:
        language: Optional Victoria 3 internal language name. When
            provided, adds a nullable ``key_localization`` column.

    Returns:
        A ``DataFrame`` with one row per state region and one column per scalar
        attribute.
    """
    results: list[dict[str, Any]] = []
    for state_region_key, state_region_values in self.items():
        state_region: dict[str, Any] = {"key": state_region_key}
        state_region["province_count"] = len(
            list(state_region_values.find_all("provinces"))
        )
        for attribute_key, attribute_value in state_region_values.items():
            if attribute_key == "capped_resources":
                for resource_key, resource_value in attribute_value.items():
                    state_region[f"resource_{resource_key}"] = resource_value
                continue
            if attribute_key == "resource":
                if not isinstance(attribute_value, Tree):
                    raise TypeError(
                        f"Expected 'resource' attribute to be a Tree, got {type(attribute_value).__name__}"
                    )
                resource_key = attribute_value["type"]
                undiscovered_raw = attribute_value.find("undiscovered_amount", 0)
                if not isinstance(undiscovered_raw, (int, float)):
                    raise TypeError(
                        f"Expected numeric undiscovered_amount, got {type(undiscovered_raw).__name__}"
                    )
                undiscovered_amount = int(undiscovered_raw)
                discovered_raw = attribute_value.find("discovered_amount", 0)
                if not isinstance(discovered_raw, (int, float)):
                    raise TypeError(
                        f"Expected numeric discovered_amount, got {type(discovered_raw).__name__}"
                    )
                discovered_amount = int(discovered_raw)
                state_region[f"resource_{resource_key}"] = (
                    undiscovered_amount + discovered_amount
                )
                state_region[f"undiscovered_amount_resource_{resource_key}"] = (
                    undiscovered_amount
                )
                state_region[f"discovered_amount_resource_{resource_key}"] = (
                    discovered_amount
                )
                continue
            if attribute_key in _skip_keys or isinstance(
                attribute_value, (list, dict, Tree)
            ):
                continue
            state_region[attribute_key] = attribute_value
        results.append(state_region)
    df = pd.DataFrame(results)
    if language is not None:
        _insert_localization_column(
            df,
            "key",
            "key_localization",
            _localization_values(language, self._game_dir),
        )
    # For every column whose name starts with "resource_" or "undiscovered_amount_resource_" or "discovered_amount_resource_",
    # convert the column to numeric, coercing errors to NaN, and then fill NaN values with 0
    for column in df.columns:
        if column.startswith(
            (
                "resource_",
                "undiscovered_amount_resource_",
                "discovered_amount_resource_",
            )
        ):
            df[column] = (
                pd.to_numeric(df[column], errors="coerce").fillna(0).astype(int)  # pyright: ignore[reportAttributeAccessIssue]
            )
    return df

state_region_arable_land_limit

state_region_arable_land_limit(
    state_regions_df: DataFrame,
    state_region_keys: Iterable[str],
) -> float

Return total arable land across selected state regions.

Parameters:

  • state_regions_df

    (DataFrame) –

    Parsed state-region DataFrame.

  • state_region_keys

    (Iterable[str]) –

    State-region keys to aggregate. Repeated keys are counted once. Unknown keys are ignored with a warning.

Returns:

  • float –

    The sum of the selected regions' arable_land values.

Raises:

  • ValueError –

    If required columns are missing.

Warns:

  • UserWarning –

    If a requested state-region key is unknown.

Source code in src/vic3_analysis/parse/state_regions.py
def state_region_arable_land_limit(
    state_regions_df: pd.DataFrame, state_region_keys: Iterable[str]
) -> float:
    """Return total arable land across selected state regions.

    Args:
        state_regions_df: Parsed state-region DataFrame.
        state_region_keys: State-region keys to aggregate. Repeated keys are
            counted once. Unknown keys are ignored with a warning.

    Returns:
        The sum of the selected regions' ``arable_land`` values.

    Raises:
        ValueError: If required columns are missing.

    Warns:
        UserWarning: If a requested state-region key is unknown.
    """
    selected = _select_state_regions(state_regions_df, state_region_keys)
    if "arable_land" not in selected.columns:
        raise ValueError("state_regions_df must contain an 'arable_land' column.")
    values = selected["arable_land"]
    if not isinstance(values, pd.Series):
        raise TypeError("Expected unique column 'arable_land'.")
    numeric = (
        pd.to_numeric(values, errors="coerce")
        .fillna(0)  # pyright: ignore[reportAttributeAccessIssue]
        .to_numpy(dtype=np.float64)
    )
    return float(np.sum(numeric))

state_region_resource_limits

state_region_resource_limits(
    state_regions_df: DataFrame,
    state_region_keys: Iterable[str],
) -> dict[str, float]

Aggregate state-region resource capacity into building-level limits.

The input is the flattened output of StateRegionsParser.to_dataframe. Only total-potential resource_* columns are used; discovered and undiscovered component columns and arable-land data are intentionally excluded. Every resource building represented by the DataFrame is returned, including a zero limit when it is absent from the selected regions.

Gold fields are a discoverable precursor to gold mines. For long-run nominal optimisation their potential is added to building_gold_mine and building_gold_field is assigned a zero limit.

Parameters:

  • state_regions_df

    (DataFrame) –

    Parsed state-region DataFrame.

  • state_region_keys

    (Iterable[str]) –

    State-region keys to aggregate. Repeated keys are counted once. Unknown keys are ignored with a warning.

Returns:

  • dict[str, float] –

    An insertion-ordered mapping suitable for

  • dict[str, float] –

    Scenario(building_level_caps=tuple(limits.items())).

Raises:

  • ValueError –

    If the DataFrame has no key column.

Warns:

  • UserWarning –

    If a requested state-region key is unknown.

Source code in src/vic3_analysis/parse/state_regions.py
def state_region_resource_limits(
    state_regions_df: pd.DataFrame, state_region_keys: Iterable[str]
) -> dict[str, float]:
    """Aggregate state-region resource capacity into building-level limits.

    The input is the flattened output of `StateRegionsParser.to_dataframe`.
    Only total-potential ``resource_*`` columns are used; discovered and
    undiscovered component columns and arable-land data are intentionally
    excluded.  Every resource building represented by the DataFrame is returned,
    including a zero limit when it is absent from the selected regions.

    Gold fields are a discoverable precursor to gold mines.  For long-run
    nominal optimisation their potential is added to ``building_gold_mine`` and
    ``building_gold_field`` is assigned a zero limit.

    Args:
        state_regions_df: Parsed state-region DataFrame.
        state_region_keys: State-region keys to aggregate. Repeated keys are
            counted once. Unknown keys are ignored with a warning.

    Returns:
        An insertion-ordered mapping suitable for
        ``Scenario(building_level_caps=tuple(limits.items()))``.

    Raises:
        ValueError: If the DataFrame has no ``key`` column.

    Warns:
        UserWarning: If a requested state-region key is unknown.
    """
    selected = _select_state_regions(state_regions_df, state_region_keys)
    resource_columns = [
        column
        for column in state_regions_df.columns
        if isinstance(column, str) and column.startswith("resource_")
    ]

    limits: dict[str, float] = {}
    for column in resource_columns:
        building = column.removeprefix("resource_")
        column_values = selected[column]
        if not isinstance(column_values, pd.Series):
            raise TypeError(f"Expected unique column {column!r}.")
        values = (
            pd.to_numeric(column_values, errors="coerce")
            .fillna(0)  # pyright: ignore[reportAttributeAccessIssue]
            .to_numpy(dtype=np.float64)
        )
        capacity = float(np.sum(values))
        if building == "building_gold_field":
            limits.setdefault("building_gold_field", 0.0)
            limits["building_gold_mine"] = (
                limits.get("building_gold_mine", 0.0) + capacity
            )
        elif building == "building_gold_mine":
            limits[building] = limits.get(building, 0.0) + capacity
        else:
            limits[building] = capacity
    return limits

technology

technology(
    game_dir: str | Path | None = None,
    *,
    language: str | None = None,
) -> DataFrame

Parse Victoria 3 technology definitions into a DataFrame.

Reads all .txt files from common/technology/technologies, skipping keys that are not useful for analysis (modifier, ai_weight, unlocking_technologies, on_researched), and converts era_N strings to their integer era numbers.

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.

  • language

    (str | None, default: None ) –

    Optional Victoria 3 internal language name. When provided, adds a nullable key_localization column.

Returns:

  • DataFrame –

    A DataFrame with one row per technology. Always contains a

  • DataFrame –

    "key" column and an "era" column (integer), plus any

  • DataFrame –

    additional scalar attributes defined in the game files.

Raises:

  • ValueError –

    If a technology entry contains a nested Tree value for an unexpected key, or if the "era" value cannot be parsed.

Source code in src/vic3_analysis/parse/technology.py
def technology(
    game_dir: str | Path | None = None, *, language: str | None = None
) -> pd.DataFrame:
    """Parse Victoria 3 technology definitions into a DataFrame.

    Reads all ``.txt`` files from ``common/technology/technologies``, skipping
    keys that are not useful for analysis (``modifier``, ``ai_weight``,
    ``unlocking_technologies``, ``on_researched``), and converts ``era_N``
    strings to their integer era numbers.

    Args:
        game_dir: Path to the Victoria 3 ``game`` directory.  If ``None`` the
            directory is located automatically via
            `get_vic3_directory`.
        language: Optional Victoria 3 internal language name. When provided,
            adds a nullable ``key_localization`` column.

    Returns:
        A ``DataFrame`` with one row per technology.  Always contains a
        ``"key"`` column and an ``"era"`` column (integer), plus any
        additional scalar attributes defined in the game files.

    Raises:
        ValueError: If a technology entry contains a nested ``Tree`` value for
            an unexpected key, or if the ``"era"`` value cannot be parsed.
    """
    if game_dir is None:
        game_dir = get_vic3_directory()

    parse_dir = Path(game_dir) / "common" / "technology" / "technologies"
    parse_tree = parse_merge(parse_dir)
    results: list[dict[str, Any]] = []
    for tech_key, subtree in parse_tree.items():
        tech_item: dict[str, Any] = {"key": tech_key}
        for key, value in subtree.items():
            if isinstance(value, list):
                value = "+".join(str(v) for v in value)
            elif isinstance(value, (dict, Tree)):
                continue
            elif key == "era":
                # Extract era number from string like "era_1"
                match = re.match(r"era_(\d+)", value)
                if match:
                    tech_item[key] = int(match.group(1))
                else:
                    raise ValueError(
                        f"Could not extract era number from string: {value}"
                    )
            else:
                tech_item[key] = value
        results.append(tech_item)

    frame = pd.DataFrame(results)
    if language is not None:
        _insert_localization_column(
            frame,
            "key",
            "key_localization",
            _localization_values(language, game_dir),
        )
    return frame