Material flow analysis

Arjen Markus (6 november 2019) Material flow analysis (MFA) is a technique to map the use and reuse of products and materials within, especially, technical systems. While it is often fairly simple to conceptualise the mass flows, the real problem is to quantify these flows.

The assumption in MFA is that such material flows can be described via transfer coefficients: a certain, fixed, fraction of material moves from one compartment (or reservoir as it is called in the code below) to the next. By simulating the network that is constructed in this way over time, you can determine all manner of properties.

The code below does so and allows slightly more than merely transfer coefficients. Note that it is not complete: I would like to add more features, such as keeping track of the amount that passes between two compartments, sanity checks, facilities to easily change the properties of the compartments and the connections between, perhaps also visualisation techniques such as the so-called Sankey diagrams.

A useful description can be found at this Wikipedia page

A somewhat artificial example is this:

# mfa_batteries.tcl --
#     Example of modelling a particular problem using Material Flow Analysis:
#     Modern households have a large number of devices that contain batteries
#     of all sorts. These devices have a limited lifespan, among others because
#     the batteries have. Since batteries invariably contain toxic substances,
#     or at the very least substances we would like to use again, it is important
#     to recycle those materials and to keep them from entering the environment.
#
#     This example tries to model the material flows involved with batteries.
#     No particular attempt is made to make it very realistic and especially
#     the numbers are the result of my imagination. It hopefully illustrates
#     the sort of problems you can use MFA for. (A more refined method exploits
#     Monte Carlo simulations to get a feeling for the uncertainties involved.)
#
#     The model is based on the following assumptions:
#     - Households continuously acquire devices holding batteries. The devices
#       have a lifespan of roughly four years. This is modelled using a
#       set of four "battery" groups.
#     - Batteries in each group can break down and then end up in the environment
#       or in the recycling industry.
#     - The recycling industry has a limited capacity, if that is exceeded,
#       the batteries are first stored in a storage facility and if that is
#       full, they end up in the environment after all.
#
#     The time step is one year (which is implicit)
#
source simulate_mfa.tcl

::mfa::Simulator create sim

sim define reservoir new_products

sim define reservoir batteries_1
sim define reservoir batteries_2
sim define reservoir batteries_3
sim define reservoir batteries_4

sim define reservoir recycling
sim define reservoir environment
#
# Now define the recycling facility
#
sim define reservoir recycling -maximum 1000
sim define reservoir batteries_storage -initial 2500

#
# 20% of the batteries in each group fails within a year
# Half of that is released into the environment, the rest goes into recycling
#
sim define transfer batteries_1 environment -fraction 0.1
sim define transfer batteries_1 recycling   -fraction 0.1
sim define transfer batteries_1 batteries_2 -fraction 0.8  ;# At the end of the year: next group
sim define transfer batteries_2 environment -fraction 0.1
sim define transfer batteries_2 recycling   -fraction 0.1
sim define transfer batteries_2 batteries_3 -fraction 0.8  ;# At the end of the year: next group
sim define transfer batteries_3 environment -fraction 0.1
sim define transfer batteries_3 recycling   -fraction 0.1
sim define transfer batteries_3 batteries_4 -fraction 0.8  ;# At the end of the year: next group
sim define transfer batteries_4 environment -fraction 0.5  ;# The last group: evenly split over recycling and environment
sim define transfer batteries_4 recycling   -fraction 0.5

#
# Transferring batteries for recycling
#
sim define transfer recycling new_products -fraction 0.5 -maximum 800
sim define surplus recycling batteries_storage

sim define surplus batteries_storage environment

#
# Now the amount of new devices/batteries
#
sim define input new_products -amount 200
sim define transfer new_products batteries_1 -fraction 1.0

#
# The model has been set up, now simulate it over a period of 20 years
#
for {set year 0} {$year < 20} {incr year} {
    #
    # Calculate the new situation
    #
    sim nextstep

    #
    # Report: amount of batteries "alive", how much recycled, how much in storage, how much released into the environment
    #
    set total_batteries [expr {[sim amount batteries_1] + [sim amount batteries_2] + [sim amount batteries_3] + [sim amount batteries_4]}]
    puts [format "%5d %10.4f %10.4f %10.4f %10.4f" $year $total_batteries [sim amount new_products] [sim amount batteries_storage] [sim amount environment]]
}

The code (to be stored in a file "simulate_Mfa.tcl") that defines the "engine" is here:

# simulate_mfa.tcl --
#     Simulation of systems inspired by "Material Flow Analysis"
#
#     TODO:
#     Loads of things
#     - set up a sanity check:
#       - surplus available if necessary
#       - no large transfer than available (sum of coefficients <= 1)
#       - reservoirs properly defined
#
#     Notes:
#     - Version without "groups"
#     - Reservoirs may have a required minimum and maximum capacity
#     - Transfers may have a maximum capacity or even a fixed amount
#     - Time scales are important - while a year may be typical, we need to use
#       smaller steps to make sure within a time step you get an equilibrium,
#       but not all reservoirs may attain an equilibrium - accumulation can occur
#       Maybe best to define "output" and "input" (not production).
#     - Think of useful metrics for reservoirs and transfers
#
#     I have thought more about it, I think the following properties should be
#     implemented:
#     - Reservoirs have either an unlimited capacity or some maximum. They
#       can also have a minimum requirement. This works as follows:
#       - If the reservoir has a maximum capacity, there has to be a
#         special transfer, called "surplus". If in the time step the capacity
#         is exceeded, the surplus is immediately redirected to the receiver
#         of the surplus transfer.
#       - If the reservoir has a minimum requirement, it can transfer material
#         via the transfers, but only if the content is not below the minimum
#         yet. If it is, nothing happens, but the content may become lower than
#         the minimum (this is in contrast to the maximum).
#     - Transfers are defined via a transfer coefficient and may have a maximum.
#       If the transfer would exceed that maximum, then it is truncated to
#       that maximum.
#     - A special type of transfer is the surplus. It is required that reservoirs
#       that have a maximum capacity, have exactly one surplus transfer. This
#       is never limited, so that the material that causes the maximum to be
#       exceeded can be moved to the next stage (note: a cascade is possible,
#       if the receiving reservoir also has a maximum!).
#     - I thought hard about the time step issue and the transfer of material
#       through the system. I have decided that it should be up to the user.
#       The material is only passed on to the next reservoir - so only one step.
#       If this is too slow, then the user should define the "time step" in
#       a more suitable way, for instance instead of mass per year, use mass
#       per month and monitor the outcome once every twelve time steps.
#       If there is no reservoir with a maximum or minimum and all have at least
#       one output transfer, then an equilibrium is possible. But it is up to
#       the user to take care of that. I want the system to be a bit more
#       flexible.
#     - An output node (besides an input) should collect the material leaving
#       the system. This is an opportunity to monitor the total mass.
#

# mfa --
#     Private namespace holding the variables that are needed
#
namespace eval ::mfa {
    ::oo::class create Simulator {
         variable reservoir
         variable transfer
         variable input
         variable output
         variable surplus

         constructor {} {
             variable reservoir
             variable transfer
             variable input
             variable output
             variable surplus

             set reservoir  {}
             set transfer   {}
             set input      {}
             set output     {}
             set surplus    {}
         }

         method define {type args} {
             variable reservoir
             variable transfer
             variable input
             variable output

             switch -- $type {
                 "reservoir" {
                     set name       [lindex $args 0]
                     set initial    0.0
                     set minimum    0.0
                     set maximum    Inf
                     foreach {key value} [lrange $args 1 end] {
                         switch -- $key {
                             "-initial" {
                                  set initial $value
                             }
                             "-maximum" {
                                  set maximum $value
                             }
                             "-minimum" {
                                  set minimum $value
                             }
                             default {
                                 return -code error "Unknown keyword: $key"
                             }
                         }
                     }

                     dict set reservoir $name initial     $initial
                     dict set reservoir $name value       $initial
                     dict set reservoir $name minimum     $minimum
                     dict set reservoir $name maximum     $maximum
                     dict set reservoir $name newValue    $initial
                     dict set reservoir $name input       0.0
                     dict set reservoir $name output      0.0
                     dict set transfer  $name to {}       ;# TODO: Reservoir must be defined before other things
                     dict set input     $name amount      0.0
                     dict set output    $name fraction    0.0
                     dict set output    $name maximum     Inf
                     dict set output    $name minimum    -Inf
                 }
                 "transfer" {
                     set fraction   0.0
                     set minimum   -Inf
                     set maximum    Inf
                     foreach {key value} [lrange $args 2 end] {
                         switch -- $key {
                             "-fraction" {
                                  set fraction $value
                             }
                             "-maximum" {
                                  set maximum $value
                             }
                             "-minimum" {
                                  set minimum $value
                             }
                             default {
                                 return -code error "Unknown keyword: $key"
                             }
                         }
                     }
                     if { $fraction < 0.0 || $fraction > 1.0 } {
                         return -code error "The fraction must be between 0 and 1"
                     }

                     lassign $args from to

                     if { ![dict exists $reservoir $from] || ![dict exists $reservoir $to] } {
                         return -code error "Both reservoirs should be defined first - $from and $to"
                     }

                     dict set transfer $from to [concat [dict get $transfer $from to] $to]
                     dict set transfer $from fraction $to $fraction
                     dict set transfer $from minimum  $to $minimum
                     dict set transfer $from maximum  $to $maximum
                 }
                 "surplus" {
                     lassign $args from to
                     dict set surplus $from $to
                 }
                 "input" {
                     set amount 0.0
                     foreach {key value} [lrange $args 1 end] {
                         switch -- $key {
                             "-amount" {
                                  set amount $value
                             }
                             default {
                                 return -code error "Unknown keyword: $key"
                             }
                         }
                     }
                     lassign $args to
                     dict set input $to amount $amount
                 }
                 "output" {
                     set fraction 0.0
                     set minimum  0.0
                     set maximum  Inf
                     foreach {key value} [lrange $args 1 end] {
                         switch -- $key {
                             "-fraction" {
                                  set fraction $value
                             }
                             "-maximum" {
                                  set maximum $value
                             }
                             "-minimum" {
                                  set minimum $value
                             }
                             default {
                                 return -code error "Unknown keyword: $key"
                             }
                         }
                     }
                     set from [lindex $args 0]
                     dict set output $from fraction $fraction
                     dict set output $from maximum  $maximum
                     dict set output $from minimum  $minimum
                 }
                 default {
                     return -code error "Unknown type: $type"
                 }
             }
         }

         method reset {} {
             variable reservoir

             foreach rv [dict keys $reservoir] {
                 dict set reservoir $rv value  [dict get $reservoir $rv initial]
                 dict set reservoir $rv input  0.0
                 dict set reservoir $rv output 0.0
             }
         }

         method set {...} {
             variable reservoir

             TODO: set reservoir/transfer/... individual values
         }

         method sanitycheck {} {
             # Provide a check on the model definition:
             # - reservoirs have at least an input/output/transfer connected to them
             # - the sum of transfer coefficients does not exceed 1
             #
             # Note: such a check only provides advice, nothing more
             #
             # TODO
         }

         # Methods to get properties
         method amount {rv} {
             variable reservoir

             dict get $reservoir $rv value
         }
         method totalInput {rv} {
             variable reservoir

             dict get $reservoir $rv input
         }
         method totalOutput {rv} {
             variable reservoir

             dict get $reservoir $rv output
         }

         method report {rv type} {
             variable reservoir
             variable transfer
             variable production

             if { $type eq "total" } {
                 set total 0.0
                 foreach v [dict get $reservoir $rv value] {
                     set total [expr {$total + $v}]
                 }
                 return $total
             } else {
                 return -code error "Unknown report type: $type"
             }
         }

         method nextstep {} {
             variable reservoir
             variable transfer
             variable input
             variable output
             variable surplus

             foreach rv [dict keys $reservoir] {
                 ##set oldValue     [dict get $reservoir $rv value]
                 set oldValue     [dict get $reservoir $rv newValue]    ;# Beacause of updating ...
                 set minReservoir [dict get $reservoir $rv minimum]
                 set maxReservoir [dict get $reservoir $rv maximum]

                 if { $oldValue < $minReservoir } {
                     continue
                 }

                 set newValue     [expr {$oldValue + [dict get $input $rv amount]}]

                 foreach to [dict get $transfer $rv to] {
                     set fraction [dict get $transfer $rv fraction $to]
                     set minimum  [dict get $transfer $rv minimum  $to]
                     set maximum  [dict get $transfer $rv maximum  $to]

                     set amount   [expr {$oldValue * $fraction}]
                     if { $amount < $minimum } {
                         set amount $minimum      ;# Really meaningful?
                     }
                     if { $amount > $maximum } {
                         set amount $maximum
                     }

                     dict set reservoir $to newValue [expr {$amount + [dict get $reservoir $to newValue]}]
                     set newValue [expr {$newValue - $amount}]
                     #puts "$rv -- $to -- $newValue -- [dict get $reservoir $to newValue] -- $amount"
                 }

                 # Handle the output connection

                 set oldValue [dict get $reservoir $rv value   ]
                 set fraction [dict get $output    $rv fraction]
                 set minimum  [dict get $output    $rv minimum ]
                 set maximum  [dict get $output    $rv maximum ]

                 set amount   [expr {$oldValue * $fraction}]
                 if { $amount < $minimum } {
                     set amount $minimum      ;# Really meaningful?
                 }
                 if { $amount > $maximum } {
                     set amount $maximum
                 }

                 set newValue [expr {$newValue - $amount}]

                 dict set reservoir $rv newValue    $newValue
             }

             foreach rv [dict keys $reservoir] {
                 set newValue [dict get $reservoir $rv newValue]
                 #puts "$rv -- $newValue"
                 dict set reservoir $rv value $newValue
                 dict set reservoir $rv newValue $newValue
             }

             # Handle the maximum value via the surpluses
             foreach rv [dict keys $reservoir] {
                 set value    [dict get $reservoir $rv value]
                 set maxValue [dict get $reservoir $rv maximum]
                 if { $value > $maxValue } {
                     set to [dict get $surplus $rv]
                     set current [dict get $reservoir $to value]
                     dict set reservoir $to value [expr {$current + $value - $maxValue}]
                     dict set reservoir $rv value $maxValue
                 }
             }
         }

         method quickreport {} {
             variable reservoir
             variable transfer
             variable production

             foreach rv [dict keys $reservoir] {
                 puts [dict get $reservoir $rv value]
                 #puts [dict get $reservoir $rv loss]
                 #puts [dict get $production $rv]
             }
         }
     }
}