Skip to content

Instantly share code, notes, and snippets.

@mtaptich
Last active June 23, 2016 20:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mtaptich/87f9609aca1f3e87b4c7 to your computer and use it in GitHub Desktop.
Save mtaptich/87f9609aca1f3e87b4c7 to your computer and use it in GitHub Desktop.
EIO-LCA Visualization

Economic Input-Output Life Cycle Assessment (EIO-LCA) is a method to quantify the material, energy, and environmental impacts resulting from activities in our economy. The Economic Input-Output (EIO) method was first formalized by Nobel Prize winning economist, Wassily Leontief, in the 1970s. It took two decades before the computation performance of modern computers was sufficient enough to utilize this model at scale. Since the mid-1990s, EIO-LCA has been used to estimate economy-wide environmental impacts of many products and services, such as automobiles, refrigerators, computers, paper, retail trade, food systems, etc.

In this illustrative example, I model a simple economy consisting of three industrial sectors: steel, electricity, and freight transportation. The interindustry flows of purchases are initially highlighted in grey. Each industry requires finite amounts of materials, products, and/or services in order to function properly. For example, steel needs to purchase different amounts of steel for fabricating equipment (X11), electricity for operating its smelters (X21), and transportation to ship to consumers (X31) in order to sell a unit of steel.

The model first generates a random set of monetary transactions between the industries (Ax) as well as direct demand (y). The sum of the value added (non-interindustry purchases, row-wise) and final demand (column-wise) is referred to as the economy's gross domestic product (GDP). The total output of this economy can be solved using the Leontief inverse. Environmental impacts are then scaled based on a sector-level environmental impact coefficient matrix. For more information, I recommend reviewing the formal derivation provided by the Green Design Institute of Carnegie Mellon University.

Exogenous Factors:

  • This model quantifies the environmental impacts of an additional $100 demand for steel.
  • The arbitrary environmental impacts per dollar spent in each industry are 100 units/$ (steel), 50 units/$ (electricity), and 25 units/$ (transportation). This information is used to generate the sector-level environmental impact coefficient matrix.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
width: 1024px;
margin-top: 0;
margin: auto;
font-family: "Lato", "PT Serif", serif;
color: #222222;
padding: 0;
font-weight: 300;
line-height: 33px;
-webkit-font-smoothing: antialiased;
}
text {
pointer-events: none;
}
.tile {
stroke: #000;
stroke-width: 1px;
fill: #ecf0f1;
-webkit-font-smoothing: antialiased;
}
.indenity rect, .ys rect, .impact rect {
stroke: #000;
stroke-width: 1px;
stroke-dasharray: 4px, 4px;
fill: #fff;
-webkit-font-smoothing: antialiased;
}
.empty{
visibility: hidden;
stroke: #000;
stroke-width: 1px;
stroke-dasharray: 4px, 4px;
fill: #fff;
-webkit-font-smoothing: antialiased;
}
.result{
stroke: #000;
stroke-width: 1px;
stroke-dasharray: 2px, 4px;
fill: #fff;
-webkit-font-smoothing: antialiased;
}
.label{
fill: #000;
}
.cols, .rows{
fill: #000;
font-size: 18px;
}
.legend{
font-size: 20px;
}
.display{
font-size: 50px;
}
text {
pointer-events: none;
}
.guide{
fill: none;
stroke: red;
stroke-width: 12px;
opacity: 0.3;
-webkit-font-smoothing: antialiased;
}
.fixed{
position: absolute;
top: 550px;
left: 830px;
}
button {
background-color: #95a5a6;
border: none;
color: white;
padding: 7.5px 18px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 14px;
border-radius: 4px;
}
button:hover{
background-color: #f39c12;
}
button:active{
background-color: #e67e22;
}
</style>
<body>
<button type="button" class='fixed' onclick="model()">New Problem</button>
</body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="http://labratrevenge.com/d3-tip/javascripts/d3.tip.v0.6.3.js"></script>
<script src="invert.js"></script>
<script>
var margin = { 'top': 50, 'right': 20, 'bottom': 50, 'left': 100 },
width = 960 - margin.left - margin.right,
height = 593 - margin.top - margin.bottom,
nitems = 5,
sw = 600/(nitems),
sh = 470/(nitems),
translate_speed = 500;
var model = function(){
var A_invert, rtiles, _R
d3.selectAll('svg').remove()
var svg = d3.select('body').append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
buildI()
var data = computeMatrix()
var rs = ['Steel', 'Electric', 'Transport', 'Val. Added', 'Input'],
cs = ['Steel', 'Electric', 'Transport', 'Demand', 'Output']
var cols = svg.selectAll('.cols')
.data(cs).enter().append('text')
.attr('class', function(d, i){ return i>2 ? 'extra cols' : 'need cols'})
.attr('transform', function(d,i){
return 'translate('+ ((sw)*i + sw/2 - 5) +','+ (-20) +')'
})
.html(function(d,i){ return d})
.style('text-anchor', 'middle')
var rows = svg.selectAll('.rs')
.data(rs).enter().append('text')
.attr('class', function(d, i){ return i>2 ? 'extra cols' : 'need cols'})
.attr('transform', function(d,i){
return 'translate('+ (-10) +','+ (i*(sh+10) + 10 + sh/2) +')'
})
.style('text-anchor', 'end')
.text(function(d){ return d})
tiles = svg.selectAll('.tiles')
.data(data).enter().append('g')
.selectAll('.tile')
.data(function(d){ return d}).enter()
.append('g').attr('class', function(d){ return d[4]+' g_'+d[0]+'_'+d[1]})
var rects = tiles.append('rect')
.attr('transform', function(d){ return 'translate('+ ( d[1] * (sw ) ) +','+ ( d[0] * (sh + 10) ) +')' })
.attr('width', sw-10)
.attr('height', sh)
.attr('rx', 3)
.attr('ry', 3)
.attr('class', function(d, i){ return d[3]+' _'+d[0]+'_'+d[1]})
tiles.append('text')
.attr('transform', function(d){ return 'translate('+ ( d[1] * (sw ) ) +','+ ( d[0] * (sh + 10) ) +')' })
.attr('x', sw/2)
.attr('y', sh/2)
.attr('dy', 7)
.attr('dx', -5)
.attr('class', function(d){ return 'r'+d[0]+'_'+d[1]})
.style('text-anchor', 'middle')
.style('font-size', '24')
.text(function(d){ return d[2]});
function buildI(){
var is = d3.range(3).map(function(d){
return d3.range(3).map(function(e){ return d==e ? [d,e,1] : [d,e,0] })
})
var itiles = svg.selectAll('.indenities')
.data(is).enter().append('g').attr('class', 'indenity').style('visibility', 'hidden')
.selectAll('.indenity')
.data(function(d){ return d}).enter()
itiles.append('rect')
.attr('transform', function(d){ return 'translate('+ ( d[1] * (sw ) ) +','+ ( d[0] * (sh + 10) ) +')' })
.attr('width', sw-10)
.attr('height', sh)
.attr('rx', 3)
.attr('ry', 3)
itiles.append('text')
.attr('transform', function(d){ return 'translate('+ ( d[1] * (sw ) ) +','+ ( d[0] * (sh + 10) ) +')' })
.attr('x', sw/2)
.attr('y', sh/2)
.attr('dy', 7)
.attr('dx', -5)
.style('text-anchor', 'middle')
.style('font-size', '24')
.text(function(d){ return d[2]})
var impact_mag = [100, 50, 25]
var rs = d3.range(3).map(function(d){
return d3.range(3).map(function(e){ return d==e ? [d,e,impact_mag[d]] : [d,e,0] })
})
rtiles = svg.selectAll('.impacts')
.data(rs).enter().append('g').attr('class', 'impact').style('visibility', 'hidden')
.selectAll('.impact')
.data(function(d){ return d}).enter()
rtiles.append('rect')
.attr('transform', function(d){ return 'translate('+ ( d[1] * (sw ) ) +','+ ( d[0] * (sh + 10) ) +')' })
.attr('width', sw-10)
.attr('height', sh)
.attr('rx', 3)
.attr('ry', 3)
rtiles.append('text')
.attr('transform', function(d){ return 'translate('+ ( d[1] * (sw ) ) +','+ ( d[0] * (sh + 10) ) +')' })
.attr('x', sw/2)
.attr('y', sh/2)
.attr('dy', 7)
.attr('dx', -5)
.style('text-anchor', 'middle')
.style('font-size', '24')
.text(function(d){ return d[2]})
}
function buildY(){
var ys = d3.range(3).map(function(d,i){
return d==0 ? [i,100] : [i,0]
})
var itiles = svg.selectAll('.ys')
.data(ys).enter().append('g').attr('class', 'ys')
itiles.append('rect')
.attr('transform', function(d){ return 'translate('+ ( 3.5 * (sw ) ) +','+ ( d[0] * (sh + 10) ) +')' })
.attr('width', sw-10)
.attr('height', sh)
.attr('rx', 3)
.attr('ry', 3)
itiles.append('text')
.attr('transform', function(d){ return 'translate('+ ( 3.5 * (sw ) ) +','+ ( d[0] * (sh + 10) ) +')' })
.attr('x', sw/2)
.attr('y', sh/2)
.attr('dy', 7)
.attr('dx', -5)
.style('text-anchor', 'middle')
.style('font-size', '24')
.text(function(d){ return d[1]})
itiles.transition().duration(1500)
.attr('transform', function(d){
return 'translate('+ ( (d[0]- 3.5) * (sw ) ) +','+ ( -d[0] * (sh + 10) ) +')'
})
.transition().duration(2000)
.attr('transform', function(d){
return 'translate('+ ( (d[0]- 3.5) * (sw ) ) +','+ ( (3.5-d[0]) * (sh + 10) ) +')'
})
.transition().duration(1000)
.style('opacity',0);
d3.selectAll('.essential text').transition().duration(1000).delay(3500)
.text(function(d){
var val = d3.select(this).text(),
c = d3.select(this).data()[0][1];
return c==0 ? Math.round(val*100): ''
})
d3.selectAll('.essential').transition().duration(1000).delay(3500)
.style('opacity', function(d){
var c = d3.select(this).data()[0][1];
return c==0 ? 1: 0
})
.transition().duration(1000).delay(5000)
.attr('transform', function(d){
return 'translate('+ ( 4.5*sh ) +','+ ( 0 ) +')'
})
.each('end', function(d){
if(d[0]==d[1] && d[0]==2) buildR()
})
}
function buildR(){
d3.selectAll('.impact').transition().duration(1000)
.style('visibility','visible')
.transition().duration(1000).delay(3500)
.attr('transform', function(d, i){
return 'translate(' + (650 - i*sw) + ',' + 0 + ')'
})
d3.selectAll('.impact text').transition().duration(1000).delay(4000)
.text(function(d){
return d[1]==d[0] ? Math.round(_R[d[0]]*d[2]/100)*100 : ''
})
d3.selectAll('.impact rect')
.transition().duration(1000).delay(3500)
.style('fill', function(d){
return d[0]==d[1] ? '#f1c40f' : 'none'
})
.style('stroke', 'none')
d3.selectAll('.essential').transition().duration(1500)
.attr('transform', function(d){
return 'translate('+ ( (d[0]) * (sw ) ) +','+ ( -d[0] * (sh + 10) ) +')'
})
.transition().duration(2000)
.attr('transform', function(d){
return 'translate('+ ( (d[0]) * (sw ) ) +','+ ( (3.5-d[0]) * (sh + 10) ) +')'
})
.transition().duration(1000)
.style('opacity',0.5)
.attr('transform','translate(0,0)')
d3.selectAll('.essential text').transition().duration(1500).delay(4000)
.text(function(d){ return d[2]});
}
function computeMatrix(){
var data = d3.range(nitems).map(function(d){
return d3.range(nitems).map(function(e){ return [d,e, +Math.ceil(Math.random()*8)*100, 'tile', 'essential']})
})
for (var i = 0; i < nitems; i++) {
data[i][4][2] = data[i][0][2] + data[i][1][2] + data[i][2][2] + data[i][3][2]
data[4][i][2] = data[i][4][2]
data[3][i][2] = data[4][i][2] - (data[0][i][2] + data[1][i][2] + data[2][i][2])
if (data[3][i][2] < 0) return computeMatrix()
if (i > 2) {
data[0][i][3] = 'result'
data[1][i][3] = 'result'
data[2][i][3] = 'result'
data[i][0][3] = 'result'
data[i][1][3] = 'result'
data[i][2][3] = 'result'
data[0][i][4] = 'extra'
data[1][i][4] = 'extra'
data[2][i][4] = 'extra'
data[i][0][4] = 'extra'
data[i][1][4] = 'extra'
data[i][2][4] = 'extra'
}
}
data[3][3] = [3,3,'', 'empty', 'extra']
data[3][4] = [3,4,'', 'empty', 'extra']
data[4][3] = [4,3,'', 'empty', 'extra']
data[4][4] = [4,4,'', 'empty', 'extra']
return data
}
function normalize(callback){
var A = [[0,0,0], [0,0,0], [0,0,0]];
for (var i = 0; i < 3; i++) {
d3.select('.g_4_'+i).transition().duration(1800)
.style('opacity', '0.98')
.attr('transform', 'translate(' + (0) + ',' + (-4*(sh+10)) + ')')
.transition().duration(1000).delay(2000)
.style('opacity', '1')
.attr('transform', 'translate(' + (0) + ',' + (0) + ')');
var norm = d3.select('._4_'+i).data()[0][2],
c1 = d3.select('._'+i+'_0').data()[0][2],
c2 = d3.select('._'+i+'_1').data()[0][2],
c3 = d3.select('._'+i+'_2').data()[0][2];
d3.select('.r'+i+'_0').transition().duration(1000).delay(3000).text(Math.round(c1/norm*100)/100)
d3.select('.r'+i+'_1').transition().duration(1000).delay(3000).text(Math.round(c2/norm*100)/100)
d3.select('.r'+i+'_2').transition().duration(1000).delay(3000).text(Math.round(c3/norm*100)/100)
A[i][0] = i==0 ? 1 - (c1/norm) : -c1/norm
A[i][1] = i==1 ? 1 - (c2/norm) : -c2/norm
A[i][2] = i==2 ? 1 - (c3/norm) : -c3/norm
}
var A_ = matrix_invert(A),
fin_R = [A_[0][0]*100, A_[1][0]*100, A_[1][0]*100]
callback(A_, fin_R)
}
function run(){
normalize(function(A_, fin_R){
A_invert = A_,
_R = fin_R
d3.selectAll('.extra').transition().duration(1000).delay(3500)
.style('opacity',0)
.transition().duration(1000).delay(25500)
.style('opacity',0.5)
d3.selectAll('.need').transition().duration(1000).delay(3500)
.style('opacity',0)
.transition().duration(1000).delay(14500)
.style('opacity',1)
.transition().duration(1000).delay(33000)
.style('opacity',0.5)
d3.selectAll('.indenity').transition().duration(1000).delay(4000)
.style('visibility','visible')
.transition().duration(1000).delay(6000)
.attr('transform', 'translate(' + 400 + ',' + 0 + ')')
.transition().duration(1000).delay(8000)
.style('opacity',0)
// Start next chain of events
d3.selectAll('.essential').transition().duration(1000).delay(3500)
.each("end", function(){
d3.select(this).transition().duration(1000)
.attr('transform', 'translate(' + 400 + ',' + 0 + ')')
.each("end",updateI)
})
svg.append('text')
.attr('x', width*0.70)
.attr('y', height*0.85)
.text('Normalize by Total Input')
.style('font-size', '32')
.style('text-anchor', 'middle')
.style('opacity',1)
.transition().duration(1000).delay(4000)
.text('Subtract from Identity')
.transition().duration(1000).delay(9000)
.text('Invert Matrix')
.transition().duration(1000).delay(12000)
.style('opacity',0)
.transition().duration(1000).delay(13500)
.style('opacity',1)
.text('Multiply by Added Demand')
.transition().duration(1000).delay(20500)
.text('Multiply Activity Vector...')
.transition().duration(1000).delay(22000)
.text('... by the Impact Matrix')
.transition().duration(1000).delay(24000)
.text('Ans: Impacts by Sector')
})
}
function updateI(){
var val = d3.select(this).text(),
r = d3.select(this).data()[0][0],
c = d3.select(this).data()[0][1]
d3.select(this).select('text').transition().duration(1000).delay(2500)
.text(r==c ? Math.round((1-val)*100)/100: Math.round((-val)*100)/100)
.transition().duration(1000).delay(5000)
.text(Math.round((A_invert[r][c])*100)/100) ;
d3.select(this).transition().duration(2000).delay(7000)
.attr('transform', 'translate(' + 0 + ',' + 0 + ')')
.each('end', function(d){
if(d[0]==d[1] && d[0]==2) buildY()
})
}
run()
}
model()
d3.select(self.frameElement).style("height", 620 + "px");
</script>
function matrix_invert(M){
// I use Guassian Elimination to calculate the inverse:
// (1) 'augment' the matrix (left) by the identity (on the right)
// (2) Turn the matrix on the left into the identity by elemetry row ops
// (3) The matrix on the right is the inverse (was the identity matrix)
// There are 3 elemtary row ops: (I combine b and c in my code)
// (a) Swap 2 rows
// (b) Multiply a row by a scalar
// (c) Add 2 rows
//if the matrix isn't square: exit (error)
if(M.length !== M[0].length){return;}
//create the identity matrix (I), and a copy (C) of the original
var i=0, ii=0, j=0, dim=M.length, e=0, t=0;
var I = [], C = [];
for(i=0; i<dim; i+=1){
// Create the row
I[I.length]=[];
C[C.length]=[];
for(j=0; j<dim; j+=1){
//if we're on the diagonal, put a 1 (for identity)
if(i==j){ I[i][j] = 1; }
else{ I[i][j] = 0; }
// Also, make the copy of the original
C[i][j] = M[i][j];
}
}
// Perform elementary row operations
for(i=0; i<dim; i+=1){
// get the element e on the diagonal
e = C[i][i];
// if we have a 0 on the diagonal (we'll need to swap with a lower row)
if(e==0){
//look through every row below the i'th row
for(ii=i+1; ii<dim; ii+=1){
//if the ii'th row has a non-0 in the i'th col
if(C[ii][i] != 0){
//it would make the diagonal have a non-0 so swap it
for(j=0; j<dim; j++){
e = C[i][j]; //temp store i'th row
C[i][j] = C[ii][j];//replace i'th row by ii'th
C[ii][j] = e; //repace ii'th by temp
e = I[i][j]; //temp store i'th row
I[i][j] = I[ii][j];//replace i'th row by ii'th
I[ii][j] = e; //repace ii'th by temp
}
//don't bother checking other rows since we've swapped
break;
}
}
//get the new diagonal
e = C[i][i];
//if it's still 0, not invertable (error)
if(e==0){return}
}
// Scale this row down by e (so we have a 1 on the diagonal)
for(j=0; j<dim; j++){
C[i][j] = C[i][j]/e; //apply to original matrix
I[i][j] = I[i][j]/e; //apply to identity
}
// Subtract this row (scaled appropriately for each row) from ALL of
// the other rows so that there will be 0's in this column in the
// rows above and below this one
for(ii=0; ii<dim; ii++){
// Only apply to other rows (we want a 1 on the diagonal)
if(ii==i){continue;}
// We want to change this element to 0
e = C[ii][i];
// Subtract (the row above(or below) scaled by e) from (the
// current row) but start at the i'th column and assume all the
// stuff left of diagonal is 0 (which it should be if we made this
// algorithm correctly)
for(j=0; j<dim; j++){
C[ii][j] -= e*C[i][j]; //apply to original matrix
I[ii][j] -= e*I[i][j]; //apply to identity
}
}
}
//we've done all operations, C should be the identity
//matrix I should be the inverse:
return I;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment