Example using the SCOUT content solver

This example shows how different process behaviours influence the flow of different content types through the model. We will start with a simple material flow model, and then build up the solution for content flows until it reflects what we’d like to see.

Load example model

A PACTOT materia flow model is defined in example_model.py. This was chosen to be fairly simple, but complicated enough to demonstrate the main features of the content solver. It describes the steelmaking, construction and waste processing.

Load and solve the model, solution vector X and flows table:

import numpy as np
import pandas as pd

import example_model

m, X, flows = example_model.get_model()

Show the basic Sankey diagram (using the layout defined in sdd.py):

from sdd import sdd
from floweaver import weave, QuantitativeScale

weave(sdd, flows).to_widget(debugging=True, width=1000, height=300)

Set up for solving content

We’ll consider 3 content types: “Wood”, “Metal” and “Gas”.

content_types = ["Wood", "Metal", "Gas"]

If we do nothing, there will be zero content of all these types in all the flows. We’ll build up the details of the model gradually.

Let’s start by defining some functions to help with plotting the Sankey diagrams, using the colour scale to show the amount of each content type:

from probs_models.models import scout
from probs_models.models.scout import OutputContent, OutputAllocation, ContentTransformation

from ipywidgets import VBox

def show_content_sankey(flows, content_type, palette=None):
    """Show a Sankey diagram with color scale showing content."""
    
    # Scale based on value of given content type, normalised by total value
    scale = QuantitativeScale(f"value_{content_type}", 
                              palette=palette,
                              intensity="value",
                              domain=(0, 1))

    return weave(
        sdd,
        flows,
        link_color=scale,
        measures=["value", scale.attr],  # both values need to be aggregated
        link_width="value",
    ).to_widget(debugging=True, width=1000, height=300)


def show_sankeys(flows):
    return VBox([
        show_content_sankey(flows, "Wood", "Greens_9"),
        show_content_sankey(flows, "Metal", "Reds_9"),
        show_content_sankey(flows, "Gas", "Blues_9"),
    ])


def sankey_for_specs(specs):
    flows = scout.flows_with_content(m, X, content_types, specs)
    return show_sankeys(flows)

Iron is 100% “Metal”

Let’s define the first “content spec”: the OutputContent of the “Ironmaking” process, content type “Metal”, going to “Iron” is 100%:

Note

This works by: (a) setting the \(\beta\) coefficient for the process to 0, so that incoming content is reset; (b) setting the \(\delta\) coefficient for the process so that the correct total amount of new content is created; and (c) setting the \(\alpha\) coefficients for the process so the new content is directed to the correct output.

specs = [
    OutputContent("Ironmaking", "Metal", {"Iron": 1.0})
]

flows = scout.flows_with_content(m, X, content_types, specs)

You can see in this extract of the resulting flows table that the Iron output does indeed have a metal content of 1:

flows.iloc[5:10]
source target material value content_Wood content_Metal content_Gas value_Wood value_Metal value_Gas
8 Oxygen Ironmaking Oxygen 0.315774 0.0 0.0 0.0 0.0 0.000000 0.0
9 Oxygen Incineration Oxygen 6.000123 0.0 0.0 0.0 0.0 0.000000 0.0
12 Ironmaking Iron Iron 3.157741 -0.0 1.0 -0.0 -0.0 3.157741 -0.0
13 Iron Steelmaking Iron 3.157753 -0.0 1.0 -0.0 -0.0 3.157753 -0.0
16 Ironmaking CO2 CO2 3.157741 0.0 0.0 0.0 0.0 0.000000 0.0

Three Sankey diagrams, one for each content type:

show_sankeys(flows)

This is a good start. Note that:

  • Imports of steel are assumed to have the same metal content as the steel produced within the system.

  • The metal content is diluted by the addition of scrap and then wood. We’ll fix this in a minute.

Let’s also specify that Wood is 100% “wood”, and Oxygen and CO2 are 100% “gas”:

sankey_for_specs([
    OutputContent("Ironmaking", "Metal", {"Iron": 1.0}),
    OutputContent("WoodProduction", "Wood", {"Wood": 1.0}),
    OutputContent("ProducingOxygen", "Gas", {"Oxygen": 1.0}),
])

Separating material types in Demolition

Currently the outputs of the Demolition process are assumed to be all mixed up together. This dilutes the metal content of the “scrap metal” flows, which we don’t want.

To solve this, we’ll define the allocation of each content type to the different outputs of this process. This is required in general for any process where the inputs are separated into different streams of different compositions.

The specific values here match the recipe for Demolition, i.e. 20% of the output by mass is “scrap wood”, and the input is 50% wood, so to make sure that the content of scrap wood is 100% “wood”, we need to make sure that 40% of the wood inputs to Demolition go to “scrap wood”.

Note

Maybe this calculation can be simplified? Currently it’s slightly indirect.

Note

OutputAllocation works by: setting the \(\alpha\) coefficients for the process so the specified content type is directed to the correct output.

sankey_for_specs([
    OutputContent("Ironmaking", "Metal", {"Iron": 1.0}),
    OutputContent("WoodProduction", "Wood", {"Wood": 1.0}),
    OutputContent("ProducingOxygen", "Gas", {"Oxygen": 1.0}),
    OutputAllocation("Demolition", "Wood", {
        "Scrap wood": 0.4,
        "Mixed waste": 0.6,        
    }),
    OutputAllocation("Demolition", "Metal", {
        "Scrap steel": 0.8,
        "Mixed waste": 0.2,
    }),
])

This is better:

  • Scrap wood coming out of demolition is 100% “wood”

  • Scrap metal coming out of demolition is 100% “metal”, and hence all the flows of metal are 100% metal.

However, the metal and wood content of the “mixed waste” is being propagated into the CO2 output stream. This is because by default processes are assumed to simply mix their inputs without transformation; but in incineration the material type is being transformed.

Transformation during Incineration

To fix this we need the final “spec” building block: ContentTransformation. This specifies the mapping from input content types to output content types within the process.

Note

ContentTransformation works by: setting the \(\beta\) coefficients for the process so that one content type is mapped to another.

sankey_for_specs([
    OutputContent("Ironmaking", "Metal", {"Iron": 1.0}),
    OutputContent("WoodProduction", "Wood", {"Wood": 1.0}),
    OutputContent("ProducingOxygen", "Gas", {"Oxygen": 1.0}),
    OutputAllocation("Demolition", "Wood", {
        "Scrap wood": 0.4,
        "Mixed waste": 0.6,
    }),
    OutputAllocation("Demolition", "Metal", {
        "Scrap steel": 0.8,
        "Mixed waste": 0.2,
    }),
    ContentTransformation("Incineration", {
        "Metal": {"Gas": 1.0},
        "Wood": {"Gas": 1.0}
    })
])

Is “Iron ore” made of “metal”?

You could argue either way, depending on how you define “metal”. So far we’ve been saying no; in the ironmaking process the inputs are transformed to create “metal” that wasn’t there before.

Alternatively you could define “metal” to mean “content of metallic elements”, in which case iron ore should have a (lower) metal content.

We can start to change the model to reflect this by setting the starting metal content of the iron ore, instead of the iron.

sankey_for_specs([
    OutputContent("ProducingIronOre", "Metal", {"Iron ore": 0.5 / 0.8}),
    OutputContent("WoodProduction", "Wood", {"Wood": 1.0}),
    OutputContent("ProducingOxygen", "Gas", {"Oxygen": 1.0}),
    OutputAllocation("Demolition", "Wood", {
        "Scrap wood": 0.4,
        "Mixed waste": 0.6,
    }),
    OutputAllocation("Demolition", "Metal", {
        "Scrap steel": 0.8,
        "Mixed waste": 0.2,
    }),
    ContentTransformation("Incineration", {
        "Metal": {"Gas": 1.0},
        "Wood": {"Gas": 1.0}
    })
])

But this isn’t quite right, because part of that metal is going into the CO2 stream. To fix this, we need to set the allocation for Ironmaking too.

sankey_for_specs([
    OutputContent("ProducingIronOre", "Metal", {"Iron ore": 0.5 / 0.8}),
    OutputContent("WoodProduction", "Wood", {"Wood": 1.0}),
    OutputContent("ProducingOxygen", "Gas", {"Oxygen": 1.0}),
    OutputAllocation("Ironmaking", "Metal", {
        "Iron": 1,
    }),
    OutputAllocation("Demolition", "Wood", {
        "Scrap wood": 0.4,
        "Mixed waste": 0.6,
    }),
    OutputAllocation("Demolition", "Metal", {
        "Scrap steel": 0.8,
        "Mixed waste": 0.2,
    }),
    ContentTransformation("Incineration", {
        "Metal": {"Gas": 1.0},
        "Wood": {"Gas": 1.0}
    })
])

This completes the example.

Conclusion

Most processes in the system don’t fundamentally change the type of material that passes through them. Those processes need no special attention.

However, to correctly track the content of different types of material through the system we do need to pay attention to three situations:

  • The starting point – where by definition the content of a particular content type is first set (by specifying the OutputContent of a process)

  • Where transformations happen within processes (ContentTransformation)

  • Where a process’s inputs are separated into distinct streams (OutputAllocation)

Mostly these should be fairly simple to define, based on the known characteristics of the process. In some cases, as with the Demolition process, it may be harder to write down directly the correct coefficients. In these cases it would be interesting to explore if the content-solver constraints could be adjusted to make it easier to specify (without losing the current guarantee that a unique solution can be found).