Reproducible export check · VoLCA 0.9.1

Agribalyse from SimaPro CSV to ILCD and EcoSpold

One isolated import, three real exports, and a machine-readable report recording source identity, output checksums, durations, warnings, and dataset-count invariants.

The checked path

Agribalyse 3.2 SimaPro CSV → VoLCA 0.9.1 → ILCD ZIP / EcoSpold 1 XML / EcoSpold 2 ZIP

The 93,820,509-byte source archive was copied to an isolated run directory before loading. The server log confirmed that no pre-existing matrix cache was found.

Source SHA-256
9c6773c340c21b103077f94054df7a789214f8f6d564f260111904c79dfe321f

Observed results

Fresh import

21,510 products/processes loaded

  • 17,361 unique activity UUIDs
  • 21,510 unique product UUIDs
  • 0 duplicate process IDs
  • 78.942 seconds including startup

ILCD

21,510 process datasets

  • 268,892,571-byte ZIP
  • 52,585 archive entries
  • 137.636-second export
  • 1,626 explicit multi-output grouping warnings

EcoSpold 1

21,510 datasets

  • 1,319,829,916-byte XML
  • Well-formed XML rechecked after export
  • 112.117-second export
  • 0 approximation warnings

EcoSpold 2

21,510 activity datasets

  • 339,140,196-byte ZIP
  • 21,510 .spold entries
  • 141.574-second export
  • 0 approximation warnings

Output identities

ILCD ZIP
b9aaddfe469e874631af99038db551a9de31ded42f6769fd3f3d3c5f8dc221c2

EcoSpold 1 XML
4b14a92f695f3e0e5cb9b095981739274cf4750bd77456f3adeb5623d93afe24

EcoSpold 2 ZIP
5e13e4ad8da4478cdd982ec1c4afbb30611c6415ea9971bef0e62fa3c855f3fa

Checks performed

  • Input and output SHA-256 recomputation
  • Fresh-import and source-format confirmation
  • Input process-ID uniqueness
  • Dataset-count preservation across all three exports
  • ZIP CRC integrity for ILCD and EcoSpold 2
  • XML validation of 52,585 ILCD members, the complete EcoSpold 1 document, and 21,510 EcoSpold 2 datasets
  • Complete export-warning capture

Boundary

This is a structural interoperability check, not an LCIA-result equivalence claim. The generated exports were inspected but were not re-imported and re-scored in this first run.

ILCD retained all 21,510 products as process datasets, but its profile cannot retain the original grouping of 1,626 multi-output activities. VoLCA reported that approximation explicitly rather than silently hiding it.

Reusable example

Import a database, then export the requested formats

This pyvolca 0.8.0 example downloads VoLCA when needed, makes a temporary copy of the source database, starts a local server that loads that private copy, and exports every requested format before removing the complete run directory.

uv run convert_database.py \
  --source database.zip \
  --format ilcd ecospold2 \
  --out exports

Accepted export values are simapro, ilcd, ecospold1, ecospold2, and brightway. The script's inline dependency metadata lets uv run install pyvolca 0.8.0 automatically. Use --engine-version to select another VoLCA release and --force to replace existing outputs.

convert_database.py Python 3 · pyvolca 0.8.0
# /// script
# requires-python = ">=3.10"
# dependencies = ["pyvolca==0.8.1"]
# ///
"""Download VoLCA, import one database, and export one or more formats.

Usage:
    uv run import_export/convert_database.py --source <database file> -f ilcd brightway

Docs: https://www.volca.run/docs/python/
"""

import argparse
import json
import os
import re
import secrets
import shutil
import tempfile
from pathlib import Path

from volca import Client, Server, download

EXT = {
    "simapro": ".simapro.csv",
    "ilcd": ".ilcd.zip",
    "ecospold1": ".ecospold1.xml",
    "ecospold2": ".ecospold2.zip",
    "brightway": ".brightway.xlsx",
}


def safe_slug(slug):
    if not isinstance(slug, str) or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}", slug):
        raise ValueError("server returned an unsafe database slug")
    return slug


def publish(data, output, force):
    with tempfile.NamedTemporaryFile(dir=output.parent, delete=False) as stream:
        temporary = Path(stream.name)
        stream.write(data)
    try:
        if force:
            os.replace(temporary, output)
            temporary = None
        else:
            os.link(temporary, output)
    finally:
        if temporary:
            temporary.unlink(missing_ok=True)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--source", required=True, type=Path)
    parser.add_argument("-f", "--format", nargs="+", required=True, choices=EXT)
    parser.add_argument("-o", "--out", type=Path, default=Path.cwd())
    parser.add_argument("--engine-version", default="v0.9.2")
    parser.add_argument("--force", action="store_true")
    args = parser.parse_args()

    source, out = args.source.expanduser().resolve(), args.out.expanduser().resolve()
    if not source.is_file():
        raise FileNotFoundError(source)
    out.mkdir(parents=True, exist_ok=True)
    installed = download(version=args.engine_version)

    with tempfile.TemporaryDirectory(prefix="volca-convert-") as raw:
        root, password = Path(raw), secrets.token_urlsafe(32)
        db_name = safe_slug(source.stem)
        local_source = root / source.name
        shutil.copy2(source, local_source)
        config = root / "volca.toml"
        config.write_text(
            f'[server]\nhost="127.0.0.1"\npassword="{password}"\n\n'
            f'[[databases]]\nname={json.dumps(db_name)}\n'
            f'path={json.dumps(str(local_source))}\nload=false\n'
        )
        with Server(config=str(config), port="auto", binary=str(installed.binary)) as server:
            client = Client(server.base_url, db=db_name, password=server.password)
            client.load_database(db_name)
            outputs = {
                fmt: out / (db_name + EXT[fmt])
                for fmt in dict.fromkeys(args.format)
            }
            if not args.force:
                existing = [path for path in outputs.values() if os.path.lexists(path)]
                if existing:
                    raise FileExistsError(existing[0])
            for fmt, output in outputs.items():
                publish(client.export_database(fmt), output, args.force)
                print(f"{fmt}: {output}")


if __name__ == "__main__":
    main()

Want to check another database or export path?

The source archive and the 1.93 GB of generated exports are not redistributed. Contact us to discuss the same kind of bounded, checksum-backed interoperability check for another database or target format.