Skip to content

Latest commit

 

History

72 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

retrolca

retrolca is a toolkit for transforming retrosynthesis pathways into linked process chains using the openLCA IPC API.

The process generation with retrolca can be configured in many ways, including the maximum depth and number of variants of the generated process chains, the retrosynthesis backend, naming service, caching, and process features like balancing with waste flows or linking production processes.

The figure below shows an example of a process chain generated by retrolca. In this example, four intermediate processes are created and then automatically linked to ecoinvent background data.

Generated process chain

Requirements

The Python Environment and openLCA IPC

retrolca is a Python project which can be quickly set up with uv. When you download the project, you can then quickly install the required interpreter and dependencies via uv sync:

# open a terminal and navigate to the project folder
cd retrolca

# download the Python interpreter and dependencies
uv sync

# modify and run the scripts in this project
uv run examples/...py

As said above, retrolca communicates with openLCA via the openLCA IPC interface. In openLCA, activate the database where you want to generate the processes and start the IPC server via Tools > Developer tools > IPC Server. On the retrolca side, you then initialize the IpcContext which checks the connected database (see below) and creates an index of the required flow properties and chemical flows. It is then used to link and create datasets in openLCA:

import retrolca as r

ctx, err = r.IpcContext.of(ipc.Client())
if err:
  print("Failed to initialize the IPC context: ", err)
Configuring the provider selection

If the database contains several processes producing the same chemical (identified by a SMILES code), the IpcContext uses a provider selector to decide which one is linked as provider when a process for that chemical is required. By default this is a DefaultProviderSelector that scores the candidate processes by their location (preferring GLO) and by their name (processes named market for ... or ... production score higher). A custom selector can be passed to IpcContext.of; a selector receives the candidate providers of a chemical and returns the one to link:

class LocalProviders(r.ProviderSelector):
    def select(self, providers):
        for p in providers:
            if p.provider and p.provider.location == "DE":
                return p
        return providers[0] if providers else None

ctx, err = r.IpcContext.of(ipc.Client(), provider_selector=LocalProviders())

Returning None skips linking, so the retrosynthesis builder would instead generate a new production process for that chemical.

The Background Database

retrolca can be used with any openLCA database, but the flow properties Mass and Chemical amount must be present (as provided by the openLCA reference data). If no suitable background processes are available, retrolca creates the required product flows and processes for the synthesis routes.

However, its full potential is realized with a background database containing chemical production processes, such as ecoinvent. When the product flows of these processes have SMILES codes attached, retrolca can link them as production processes for reactants when generating synthesis routes.

Adding SMILES codes to product flows in openLCA

Currently, retrolca reads the SMILES codes from the additional properties of product flows, checking possible entries under SMILES, Absolute-SMILES, and Connectivity-SMILES (in this order):

Additional properties

In openLCA, you can try the PubChem tool to get the SMILES code of a chemical:

PubChem tool in openLCA

retrolca also contains tooling to enrich a database with SMILES codes and other chemical properties from PubChem. If it can find the corresponding data on PubChem, the pubchem decorator will also add Chemical amount as a flow property (using the molar mass to calculate the conversion factor).

For example, this script would try to decorate all flows with manufacture of basic chemicals in their category path with chemical properties from PubChem:

import olca_ipc as ipc
import retrolca as retro
import retrolca.pubchem as pub

client = ipc.Client()
ctx, _ = retro.IpcContext.of(client)
pub.IpcFlowDecorator(ctx).try_all(in_path="manufacture of basic chemicals")

Once a database is decorated, you can persist the collected PubChem decorations to JSON and later apply them to another database.

pub.dump_decorations(ctx, path)
pub.load_decorations(ctx, path)

A full example can be found in the pubchem_decorate_flows.py example

Using PubChem is only one possible way to add SMILES codes to chemical products in openLCA. You can of course use other data sources for this in the same ways. retrolca just needs to find flows with SMILES codes in order to link them in process chains.

The Retrosynthesis Tool

retrolca can use different retrosynthesis tools. Currently, it supports ASKCOS and AiZynthFinder, but another retrosynthesis backend could be easily added. It just needs to implement the RetroTool protocol, providing an expand method to generate possible reactions for a given SMILES code of a chemical:

class RetroTool(Protocol):
    id: str
    def expand(self, smiles: str) -> Res[list[Reaction]]: ...
Setting up AiZynthFinder

To setup AiZynthFinder as local retrosynthesis tool, install the project dependencies via uv sync as described above. You will then have a download_public_data models tool in your local Python environment, that can download the public AiZynthFinder models. Create a models folder and set this as the download target:

# create the models folder
mkdir models

# download the public AiZynthFinder models
./.venv/bin/download_public_data models

# or on Windows
.\.venv\Scripts\download_public_data.exe models

This will download the models and generate a models/config.yml file with which you can initialize the ZynthTool then, which wraps the AiZynthFinder in the RetroTool protocol. See the examples/zynthfinder_example.py for a full example.

import retrolca as r

# finds the models/config.yml file relative to the current script file
config = Path(__file__).parent.parent / "models/config.yml"

# initializes AiZynthFinder via the ZynthTool wrapper
tool = r.ZynthTool(config)
Connecting to ASKCOS

retrolca can connect to an ASKCOS server instance via its REST API. For this, you need to provide the login data of a valid user account as a JSON file with the following format:

{
  "endpoint": "https://your-askcos-instance/api",
  "user": "your-user",
  "password": "your-password"
}

You can also use the public ASKCOS instance and enter https://askcos.mit.edu/api as the API endpoint. You can put that file for example under auth/askcos_login.json, then load it and connect to ASKCOS like this

import retrolca as r

login = r.AskcosLogin.from_file("auth/askcos_login.json")
client = r.AskcosClient(login)

# close the client when you are done
client.close()

See the examples/zynthfinder_example.py for a full example. ASKCOS has quite some configuration options which retrolca can pass to the API. If you just want to change the model and keep the default options, just pass the model name to the client. You can use the constants retrolca defines for this, or just pass the name of the model to the client:

# ...
client = r.AskcosClient(login, model=r.AskcosModel.PISTACHIO)

You can also pass in a dictionary with all possible configuration options when constructing the client:

# ...
client = r.AskcosClient(login, options=
    {
        "retro_backend_options": [
            {
                "retro_backend": "template_relevance",
                "retro_model_name": "reaxys",
                "max_num_templates": 1000,
                "max_cum_prob": 0.995,
                "attribute_filter": [],
                "threshold": 0.3,
                "top_k": 10,
            }
        ],
        "use_fast_filter": True,
        "fast_filter_threshold": 0.75,
        "retro_rerank_backend": "relevance_heuristic",
        "atom_map_backend": "rxnmapper",
        # ...
    },
)

See the official ASKCOS API documentation for the full configuration options:

https://askcos.mit.edu/docs#/tree-search/askcos_run_retro_expansion_async

We call the /api/tree-search/expand-one/call-async endpoint with the provided options. The schema for the options of that method is defined under #components/schemas/ExpandOneInput in the API documentation.

Caching retrosynthesis results

A retrosynthesis tool can be wrapped in a CachingRetroTool. This will then store the returned results of the tool in a database and checks for stored results before redirecting to the wrapped tool. This can be very useful to avoid redundant computations. CachingRetroTool provides the standard tool protocol of retrolca and can be used everywhere where this protocol is expected.

caching_tool = r.CachingRetroTool("out/cached_reactions.db", tool)
caching_tool.expand("CCOP(=O)(OCC)OCC")
# ...

The Naming Service

A retrosynthesis tool typically only returns the SMILES codes of the reactants for a given SMILES code of a product. For creating product flows in openLCA for these SMILES code, we need a service that translates SMILES codes to names. By default retrolca uses CIRpy, a Python package that calls CIR, for resolving chemical names:

import retrolca as r

cir = r.CIR()
name = cir.get_name("CCCCN1CCCC1=O")

However, in the same way as for the retrosynthesis tool, there is a common protocol NamingService that should be easy to implement for using an alternative service. Also, like for the retroysnthesis it is possible and recommended to use the CachingNamingService to wrap the naming service to avoid unnecessary requests (APIs like CIR typically have request limits):

import retrolca as r

cir = r.CIR()
caching_cir = r.CachingNamingService("out/cached_names.db", cir)
name = caching_cir.get_name("CCCCN1CCCC1=O")

The Process Builder

The ProcessBuilder is the central component of retrolca which combines the described tools above to recursively build a process chain that models the production of a given chemical based on possible chemical reactions. Here is a minimal example how it can be used:

import olca_ipc as ipc
import retrolca as r


ctx, _ = r.IpcContext.of(ipc.Client())
tool = r.ZynthTool("models/config.yml")
naming = r.CachingNamingService("out/cir_names.db", r.Cir())

builder = r.ProcessBuilder(
    ctx,
    tool,
    max_levels=3,
    max_variants=2,
    gen_process="83083965-4104-4c87-88af-bc200b6a520c",
    bal_process="4ad86534-aba4-3106-ac12-81e322834704",
    naming=naming,
)

builder.build(
    "CCCCN1CCCC1=O",
    name="1-butylpyrrolidin-2-one",
    category="Retrosynthesis/Inbox",
)

This example should then generate the following processes:

Process tree

For each generated process, retrolca also creates a reaction image showing the reactants together with the product, making it easy to review the synthesis route later in openLCA.

Reaction image

The parameters of the `ProcessBuilder`

When you create a ProcessBuilder it takes the following parameters:

  • ctx (required) - The openLCA IPC context, as described above.
  • tool (required) - The retrosynthesis tool, as described above.
  • max_levels (optional, default is 3) - The maximum number of levels or the maximum depth of the process chain the process builder can build. When the process builder transforms a chemical reaction into a process, it first checks if the respective reactants already exist as product flows in the databases, and if yes, it links the respective processes that produce these products as providers of the inputs. If not, it recursively generates processes for these inputs and links them.
  • max_variants (optional, default is 1) - For a chemical product, the retrosynthesis tool can return multiple possible chemical reactions with a score for probability and feasibility. It then selects the reaction with the best scores and generates a process for this. For a product input, this process is then linked as a provider. If max_variants > 1, it will also generate processes for the next best possible reactions. Note that in this case, it can quickly result in a large number of processes, since processes are again generated for the inputs of these alternatives according to the same rules of the builder.
  • gen_process (optional, default is None) - The ID of a generic production process can be provided that is linked to every process the builder generates. This process needs to describe the generic production of chemicals per mass of product output (this is also known as the Gendorf Approach). It is then linked with 1 kg as input for every generated process, as the builder always generates processes related to 1 kg of product output.
  • bal_process (optional, default is None) - In the generated process, the mass of the reactant inputs is in most cases smaller than the mass of the product output, so < 1 kg. A balancing process can be provided, that is linked with that difference to an output. In case the provided balancing process is a waste treatment process, the balancing flow will be a waste output linked to that waste treatment process. If a product process is provided, the balancing flow will be added as an avoided product output linked to that process.
  • naming (optional, default is a CIR instance) - The naming service as described above.

As shown in the example above, the build method is then used to generate the process chains. The same builder instance can be used to build process chains for different chemicals. The build method takes the following arguments:

  • smiles_code (required) - The SMILES code of the chemical for which the chain (tree) of production processes should be created.
  • name (optional, default is None) - The name of the root chemical. If not provided, the naming service of the builder will be used to determine the name but this is often not necessary as the name of this chemical is typically known.
  • category (optional, default is None) - An optional category path under which the generated processes and products will be stored. Additionally, the processes will be stored under sub-categories for the levels of these processes.

Expanding existing processes

As described in the details above, the max_levels controls the depth of the generated process chain. For the continuation of process chain creation, the expand_process method can be used, which takes the ID of the process of which the supply chain should be completed as argument:

builder.expand_process(
    "c74ccb59-b79a-4d51-8df0-33f7320f4b53",
    category="Retrosynthesis/Inbox",
)

Deleting generated processes

When you use the multiple-variants feature of the process builder (which often makes sense as the reaction of the highest score is not necessarily the most realistic option), many processes could be created. The intended workflow is for you to review and edit these generated processes, link them in product systems or move them from some inbox category to another category of the database. After this, you can run the cleanup.jy script directly in openLCA to delete the other generated processes.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages