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-2569c6773c340c21b103077f94054df7a789214f8f6d564f260111904c79dfe321f
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
.spoldentries - 141.574-second export
- 0 approximation warnings
Output identities
ILCD ZIPb9aaddfe469e874631af99038db551a9de31ded42f6769fd3f3d3c5f8dc221c2
EcoSpold 1 XML4b14a92f695f3e0e5cb9b095981739274cf4750bd77456f3adeb5623d93afe24
EcoSpold 2 ZIP5e13e4ad8da4478cdd982ec1c4afbb30611c6415ea9971bef0e62fa3c855f3fa
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.
# /// 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.