Connecting R to openLCA: the IPC Client

R has a large user base in LCA research. Universities, consultancies, and government agencies use it for statistical analysis, uncertainty modelling, and data visualisation. openLCA is a free, open-source LCA platform with a built-in IPC server that exposes its calculation engine over HTTP.

The olcar package connects R directly to openLCA’s IPC server. R scripts can query the database, run calculations, override parameters for scenario and sensitivity analysis, retrieve contribution breakdowns, and work with the results as native data frames. The data stays in one place, and the analysis pipeline runs end to end in a single R script.

The package covers the full openLCA IPC protocol: 60 JSON-RPC methods across data access, impact assessment, inventory analysis, cost analysis, upstream tree traversal, and Monte Carlo simulation. Every collection result comes back as a data frame.

The code is on GitHub. There are companion repositories for Python and Fortran.

What this package does

The package provides two R6 classes and a set of helper functions.

OlcaClient connects to the server and handles data access: listing entities, retrieving full objects, managing parameters, creating product systems, and launching calculations.

OlcaResult represents a calculation result held on the server. It provides methods for total impacts, normalised and weighted results, contribution analysis by tech flow or elementary flow, upstream tree traversal, cost analysis, and Sankey graph data. Every method that returns a collection produces a data frame.

Helper functions build the JSON structures the server expects: ref() creates entity references, calculation_setup() assembles calculation parameters, parameter_redef() builds parameter overrides, and linking_config() configures product system creation.

A typical workflow in R looks like this:

library(olcar)
client <- olca_client()

systems <- client$get_descriptors("ProductSystem")
methods <- client$get_descriptors("ImpactMethod")

setup <- calculation_setup(
  target        = ref("ProductSystem", id = systems$id[1]),
  impact_method = ref("ImpactMethod", id = methods$id[1])
)

result <- client$calculate(setup)
result$wait_until_ready()

impacts <- result$get_total_impacts()
print(impacts)

result$dispose()

The impacts object is a standard data frame with columns id, name, category, ref_unit, and amount. It works with dplyr, ggplot2, data.table, or base R.

How the IPC protocol works

openLCA’s IPC server speaks JSON-RPC 2.0 over HTTP. Every request is a POST to http://localhost:8080 with a JSON body specifying the method name and parameters. Every response contains either a result or an error. The openLCA IPC Protocol article documents the full protocol with curl commands for debugging.

The olcar package wraps this protocol using httr2 for HTTP and jsonlite for JSON parsing. The wrapper is thin: each R method maps directly to one JSON-RPC call. A call to result$get_total_impacts() sends {"jsonrpc":"2.0","id":8,"method":"result/total-impacts","params":{"@id":"result-uuid"}} and parses the response into a data frame.

openLCA’s JSON uses @id and @type as field names, following JSON-LD conventions. Some JSON libraries treat the @ character as a path operator, which caused issues in the Fortran client. R’s jsonlite handles these as ordinary field names, so no workaround is needed.

The method mapping comes from GreenDelta’s olca-ipc Python package (the official IPC client). The R package implements every JSON-RPC method that appears in the Python source. The method strings were compared line by line against ipc.py to confirm coverage. The parameter formats follow the same JSON structures as the Python client, though runtime behaviour should be verified against your own openLCA instance.

A calculation follows five steps:

  1. client$get_descriptors("ProductSystem") lists all product systems (their names and UUIDs).
  2. client$calculate(setup) starts an asynchronous calculation. The server returns a result handle.
  3. result$wait_until_ready() polls the server until the calculation finishes.
  4. result$get_total_impacts() (or any other result method) retrieves the numbers.
  5. result$dispose() frees the result on the server.

Steps 2 through 5 repeat for each calculation. Step 1 only runs once per session because UUIDs are stable within a database.

Installing the package

The package is installed from GitHub:

install.packages("remotes")
remotes::install_github("Below280/openLCA-IPC-tools-r")

This pulls three dependencies automatically: httr2 (HTTP), jsonlite (JSON), and R6 (classes). R >= 4.1 is required for the native pipe (|>) and lambda (\()) syntax.

If you plan to use olca_start_server() to launch the IPC server from R, also install processx:

install.packages("processx")

Starting the IPC server

The R package talks to openLCA through its IPC server. The package itself is independent of how the server starts. Three options exist.

GUI mode

Open openLCA, open your database, go to Tools > Developer Tools > IPC Server, and click the green play button. The server starts on port 8080.

This is the simplest path for interactive work. openLCA stays open, and you can switch between the GUI and R as needed.

Headless mode

openLCA ships startup scripts in its bin/ directory that run the IPC server without the GUI. From a terminal:

cd C:\openLCA\bin
.\ipc-server.cmd ecoinvent39

The server loads the named database from the default openLCA workspace (~/openLCA-data-1.4/databases/) and listens on port 8080. It runs until you press Ctrl+C.

The package can also launch the headless server from R:

server <- olca_start_server("C:/openLCA", "ecoinvent39")

If a server is already running on the port, this function detects it and returns without launching a second instance. Scripts that call olca_start_server() at the top are safe to run repeatedly.

Note: olca_start_server() is experimental. Launching the headless server from a terminal is the more tested path. The function calls openLCA’s bundled ipc-server script, which accepts a database name and starts on port 8080.

Set the openLCA path once in .Rprofile to avoid passing it every time:

options(olcar.openlca_dir = "C:/openLCA")

A note on Docker

GreenDelta’s gdt-server Docker images expose the openLCA REST API, which is a different protocol from the JSON-RPC that this package uses. olcar does not currently work with gdt-server containers. If you need a containerised setup, you would need to package the JSON-RPC IPC server (from the openLCA bin/ directory) into your own Docker image.

For remote servers or non-default ports where the JSON-RPC server is running:

client <- olca_client("http://lca-server.internal:3000")

The JSON-RPC protocol has no built-in authentication or encryption. Only expose the IPC server on trusted networks, not the public internet.

Connecting and querying data

Once the server is running:

library(olcar)
client <- olca_client()

The client provides several data access methods. All return data frames where the result is a collection.

# List entities by type
processes <- client$get_descriptors("Process")
systems   <- client$get_descriptors("ProductSystem")
methods   <- client$get_descriptors("ImpactMethod")
flows     <- client$get_descriptors("Flow")

# Get a single descriptor by name
steel <- client$get_descriptor("Process", name = "steel production")

# Get the full entity object (all exchanges, parameters, documentation)
process_detail <- client$get("Process", id = steel$id)

# Get parameters for a product system
params <- client$get_parameters("ProductSystem", systems$id[1])

The descriptor data frame has four columns: type, id, name, and category. The full entity object comes back as a nested list, the same structure as the JSON, accessible with standard R list indexing.

Running a calculation

A calculation needs a product system and an impact method:

setup <- calculation_setup(
  target        = ref("ProductSystem", id = systems$id[1]),
  impact_method = ref("ImpactMethod", id = methods$id[1])
)

result <- client$calculate(setup)
result$wait_until_ready()

impacts <- result$get_total_impacts()

The impacts data frame:

                  id                          name    ref_unit       amount
1 6cd38f2b-5e49-...           Global Warming     kg CO2eq   123.456
2 a1b2c3d4-5e6f-... Acidification potential      mol H+eq     0.789
...

Always call result$dispose() when finished. The server holds calculation results in memory until they are disposed or the server restarts. For scripts that run many calculations, use tryCatch with a finally block:

result <- client$calculate(setup)
tryCatch({
  result$wait_until_ready()
  impacts <- result$get_total_impacts()
}, finally = {
  result$dispose()
})

Scenario calculations

Scenario calculations override parameter values without modifying the database. The parameters argument of calculation_setup() takes a list of redefinitions:

# Check which parameters exist
params <- client$get_parameters("ProductSystem", systems$id[1])
print(params[, c("name", "value")])

# Run with overridden values
setup_a <- calculation_setup(
  target        = ref("ProductSystem", id = systems$id[1]),
  impact_method = ref("ImpactMethod", id = methods$id[1]),
  parameters    = list(
    parameter_redef("transport_distance", 200),
    parameter_redef("electricity_kwh", 150)
  )
)

result_a <- client$calculate(setup_a)
result_a$wait_until_ready()
impacts_a <- result_a$get_total_impacts()
impacts_a$scenario <- "Scenario A"
result_a$dispose()

Multiple scenarios run in a loop. Each iteration creates a setup, calculates, extracts results, and disposes:

scenarios <- list(
  baseline = list(transport_distance = 100, electricity_kwh = 200),
  low      = list(transport_distance = 50,  electricity_kwh = 100),
  high     = list(transport_distance = 500, electricity_kwh = 400)
)

all_results <- list()

for (name in names(scenarios)) {
  vals <- scenarios[[name]]
  redefs <- lapply(names(vals), \(p) parameter_redef(p, vals[[p]]))

  s <- calculation_setup(
    target        = ref("ProductSystem", id = systems$id[1]),
    impact_method = ref("ImpactMethod", id = methods$id[1]),
    parameters    = redefs
  )

  r <- client$calculate(s)
  r$wait_until_ready()
  df <- r$get_total_impacts()
  df$scenario <- name
  all_results[[name]] <- df
  r$dispose()
}

combined <- do.call(rbind, all_results)

The combined data frame has one row per impact category per scenario, ready for plotting or statistical comparison.

Sensitivity analysis

Sensitivity analysis varies each parameter individually by a fixed percentage above and below its baseline. The structure is similar to scenario calculations, but automated:

params <- client$get_parameters("ProductSystem", systems$id[1])
variation <- 0.10  # +/- 10%

all_results <- list()

# Baseline
s <- calculation_setup(
  target        = ref("ProductSystem", id = systems$id[1]),
  impact_method = ref("ImpactMethod", id = methods$id[1])
)
r <- client$calculate(s)
r$wait_until_ready()
df <- r$get_total_impacts()
df$scenario <- "baseline"
all_results[["baseline"]] <- df
r$dispose()

# Each parameter, +/- variation
for (i in seq_len(nrow(params))) {
  pname <- params$name[i]
  pval  <- params$value[i]
  if (pval == 0) next

  for (direction in c("minus", "plus")) {
    new_val <- if (direction == "minus") pval * (1 - variation) else pval * (1 + variation)

    s <- calculation_setup(
      target        = ref("ProductSystem", id = systems$id[1]),
      impact_method = ref("ImpactMethod", id = methods$id[1]),
      parameters    = list(parameter_redef(pname, new_val))
    )

    r <- client$calculate(s)
    r$wait_until_ready()
    df <- r$get_total_impacts()
    df$scenario  <- paste0(pname, " ", direction)
    df$parameter <- pname
    df$direction <- direction
    all_results[[paste0(pname, "_", direction)]] <- df
    r$dispose()
  }
}

sensitivity <- do.call(rbind, all_results)

Contribution analysis

The result object provides several methods for understanding which processes and flows drive the impact results.

get_impact_contributions_of() returns the direct contribution of each tech flow to a specific impact category:

result <- client$calculate(setup)
result$wait_until_ready()

# List available impact categories
categories <- result$get_impact_categories()

# Get contributions to GWP (or whichever category)
gwp_ref <- ref("ImpactCategory", id = categories$id[1])
contributions <- result$get_impact_contributions_of(gwp_ref)

# Top 10 contributors
contributions <- contributions[order(-abs(contributions$amount)), ]
head(contributions, 10)

The contributions data frame has columns provider_id, provider_name, flow_id, flow_name, and amount.

For upstream (total, including supply chain) impacts of a specific tech flow:

tech_flows <- result$get_tech_flows()
upstream <- result$get_total_impacts_of(tech_flows[1, ])

For elementary flow contributions to an impact category:

flow_contributions <- result$get_flow_impacts_of(gwp_ref)

Always dispose:

result$dispose()

Monte Carlo simulation

The package supports openLCA’s Monte Carlo simulation through client$simulate() and result$simulate_next():

result <- client$simulate(setup)
result$wait_until_ready()

iterations <- list()
for (i in 1:1000) {
  state <- result$simulate_next()
  impacts <- result$get_total_impacts()
  impacts$iteration <- i
  iterations[[i]] <- impacts
}

mc_results <- do.call(rbind, iterations)
result$dispose()

The mc_results data frame has one row per impact category per iteration. Standard R tools handle the statistical analysis:

# Summary statistics per category
aggregate(amount ~ name, data = mc_results, FUN = \(x) {
  c(mean = mean(x), sd = sd(x), q05 = quantile(x, 0.05), q95 = quantile(x, 0.95))
})

# Distribution plot
library(ggplot2)
gwp <- mc_results[mc_results$name == "Global Warming", ]
ggplot(gwp, aes(x = amount)) +
  geom_histogram(bins = 50) +
  labs(x = "kg CO2eq", y = "Count", title = "GWP distribution (1000 iterations)")

Working with data frames

Every collection method returns a data frame. This means the full R ecosystem is available immediately.

dplyr for filtering and transformation:

library(dplyr)

impacts |>
  filter(amount != 0) |>
  arrange(desc(abs(amount))) |>
  mutate(pct = amount / sum(amount) * 100)

ggplot2 for visualisation:

library(ggplot2)

ggplot(impacts, aes(x = reorder(name, amount), y = amount)) +
  geom_col() +
  coord_flip() +
  labs(x = NULL, y = "Impact amount")

Comparing scenarios side by side:

library(tidyr)

combined |>
  select(name, amount, scenario) |>
  pivot_wider(names_from = scenario, values_from = amount)

Writing results to CSV or Excel:

write.csv(impacts, "impacts.csv", row.names = FALSE)

Package architecture

The package has seven source files totalling around 1,400 lines of R code.

rpc.R contains a single function, rpc_call(), that sends a JSON-RPC request and parses the response. It uses httr2 for HTTP. This is the only file that touches the network.

helpers.R provides the builder functions: ref(), calculation_setup(), parameter_redef(), and linking_config(). These construct the named lists that the server expects as JSON parameters.

parsers.R converts JSON response lists into data frames. Each response type (impact values, tech flows, envi flows, upstream nodes, cost values, parameters) has its own parser. Three vectorised extraction helpers (vchr, vnum, vlgl) handle nested nulls safely.

client.R defines the OlcaClient R6 class. It holds the server URL and a request counter, and provides methods for every data/* and result/calculate JSON-RPC call.

result.R defines the OlcaResult R6 class. It holds the result UUID and a reference to the client, and provides methods for every result/* JSON-RPC call. Each method delegates to rpc_call() and pipes the response through the appropriate parser.

server.R provides olca_start_server() and the OlcaServer R6 class for managing headless server instances from R.

olcar-package.R contains the package-level documentation.

The design mirrors the Python olca-ipc package in method coverage but follows R conventions in interface: R6 classes instead of Python classes, data frames instead of typed objects, snake_case instead of camelCase. The JSON-RPC method strings match those in the Python source. The package includes man pages for all exported functions and a test suite covering the helpers, parsers, and live server integration (server-dependent tests are skipped automatically when no server is available).

Method reference

Client methods

get_descriptors(type) lists all entities of a type as a data frame (type, id, name, category).

get_descriptor(type, id, name) returns a single descriptor as a one-row data frame.

get(type, id, name) returns the full entity as a nested list.

get_all(type) returns all full entities as a list of nested lists.

get_parameters(type, id) returns parameters as a data frame. For product systems, columns include context_id and is_protected. For processes, columns include formula, is_input, parameter_scope, and description.

get_providers(flow) lists technology flow providers.

put(model) inserts or updates an entity. Returns a descriptor.

put_source_file(source, file_path) uploads a file attachment to a Source entity.

delete(model) deletes an entity. Returns a descriptor.

create_product_system(process, config) creates a linked product system from a process. Returns a descriptor.

calculate(setup) starts a calculation. Returns an OlcaResult.

simulate(setup) starts a Monte Carlo simulation. Returns an OlcaResult.

Result methods

The result object provides methods across several groups. All collection methods return data frames.

Lifecycle: get_state(), wait_until_ready(), simulate_next(), dispose()

Structure: get_demand(), get_tech_flows(), get_envi_flows(), get_impact_categories()

Tech flows: get_total_requirements(), get_total_requirements_of(tf), get_scaling_factors(), get_scaled_tech_flows_of(tf), get_unscaled_tech_flows_of(tf)

Inventory: get_total_flows(), get_total_flow_value_of(ef), get_flow_contributions_of(ef), get_direct_interventions_of(tf), get_direct_intervention_of(ef, tf), get_flow_intensities_of(tf), get_flow_intensity_of(ef, tf), get_total_interventions_of(tf), get_total_intervention_of(ef, tf), get_upstream_interventions_of(ef, path), get_grouped_flow_results_of(ef)

Impacts: get_total_impacts(), get_total_impact_value_of(ic), get_normalized_impacts(), get_weighted_impacts(), get_impact_contributions_of(ic), get_direct_impacts_of(tf), get_direct_impact_of(ic, tf), get_impact_intensities_of(tf), get_impact_intensity_of(ic, tf), get_total_impacts_of(tf), get_total_impact_of(ic, tf), get_impact_factors_of(ic), get_impact_factor_of(ic, ef), get_flow_impacts_of(ic), get_flow_impact_of(ic, ef), get_upstream_impacts_of(ic, path), get_grouped_impact_results_of(ic)

Costs: get_total_costs(), get_cost_contributions(), get_direct_costs_of(tf), get_cost_intensities_of(tf), get_total_costs_of(tf), get_upstream_costs_of(path), get_grouped_cost_results()

Visualisation: get_sankey_graph(config)

Further reading

The openLCA IPC Protocol article documents the JSON-RPC methods, request/response formats, and curl commands for debugging from any language.

The Python examples in the openLCA scripting knowledge base cover the same calculation workflows using the olca-ipc package.

The Fortran batch calculator article covers the same IPC protocol from a compiled-language perspective.

The source code is on GitHub.

Was this article helpful?

Join our mailing list to find out about the things we do, the articles we publish, and anything we find particularly interesting!