[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 https://en.wikipedia.org/wiki/Material_flow_analysis%|%this Wikipedia page%|% A somewhat artificial example is this: ======tcl # 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: ======tcl # 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] } } } } ======