Analysis Example

Multiple Objectives for Residential PV.

Import packages.

import os
import sys
sys.path.insert(0, os.path.abspath("../src"))
import numpy             as np
import matplotlib.pyplot as pl
import pandas            as pd
import seaborn           as sb
import tyche             as ty

from copy            import deepcopy
from IPython.display import Image

Load data.

The data should be stored in a set of comma-separated value files in a sub-directory of the technology folder, as shown in the directory structure diagram (Fig. 1)

designs = ty.Designs("data/pv_residential_simple")
investments = ty.Investments("data/pv_residential_simple")

Compile the production and metric functions for each technology in the dataset.

designs.compile()

Examine the data.

The functions table specifies where the Python code for each technology resides.

designs.functions
Style Module Capital Fixed Production Metrics Notes
Technology
Residential PV numpy pv_residential_simple capital_cost fixed_cost production metrics

Right now, only the style numpy is supported.

The indices table defines the subscripts for variables.

designs.indices
Offset Description Notes
Technology Type Index
Residential PV Capital BoS 2 balance of system
Inverter 1 system inverters
Module 0 system module
Fixed System 0 whole system
Input NaN 0 no inputs
Metric GHG 2 reduction in GHGs
LCOE 0 reduction in levelized cost of energy
Labor 1 increase in spending on wages
Output Electricity 0 electricity generated

The designs table contains the cost, input, efficiency, and price data resulting from a Tranche.

designs.designs
Value Units Notes
Technology Tranche Variable Index
Residential PV 2015 Actual Input NaN 0 1 no inputs
Input efficiency NaN 1 1 no inputs
Input price NaN 0 1 no inputs
Lifetime BoS 1 system-lifetime per-lifetime computations
Inverter 1 system-lifetime per-lifetime computations
... ... ... ... ... ...
Module Slow Progress Lifetime Inverter 1 system-lifetime per-lifetime computations
Module 1 system-lifetime per-lifetime computations
Output efficiency Electricity 1 W/W see parameter table for individual efficiencies
Output price Electricity 0 $/kWh not tracking electricity price
Scale NaN 1 system/system no scaling

90 rows × 3 columns

The parameters table contains additional techno-economic parameters for each technology.

designs.parameters
Offset Value Units Notes
Technology Tranche Parameter
Residential PV 2015 Actual Customer Acquisition 19 st.triang(0.5, loc=2000, scale=0.2) $/system BCA
DC-to-AC Ratio 15 st.triang(0.5, loc=1.4, scale=0.00014) 1 IDC
Direct Labor 17 st.triang(0.5, loc=2000, scale=0.2) $/system BLR
Discount Rate 0 0.07 1/year DR
Hardware Capital 16 st.triang(0.5, loc=80, scale=0.008) $/m^2 BCC
... ... ... ... ... ...
Module Slow Progress Module Lifetime 4 st.triang(0.5, loc=26, scale=1) yr MLT
Module O&M Fixed 7 st.triang(0.5, loc=19, scale=0.5) $/kWyr MOM
Module Soiling Loss 10 st.triang(0.5, loc=0.05, scale=10E-06) 1 MSL
Permitting 18 st.triang(0.5, loc=600, scale=0.06) $/system BPR
System Size 2 36 m^2 SSZ

210 rows × 4 columns

The results table specifies the units of measure for results of computations.

designs.results
Units Notes
Technology Variable Index
Residential PV Cost Cost $/system
Metric GHG ΔgCO2e/system
LCOE Δ$/kWh
Labor Δ$/system
Output Electricity kWh

The tranches table specifies multually exclusive possibilities for investments: only one Tranch may be selected for each Category.

investments.tranches
Amount Notes
Category Tranche
BoS R&D BoS High R&D BoS Fast Progress 900000.0
BoS Low R&D BoS Slow Progress 300000.0
BoS Medium R&D BoS Moderate Progress 600000.0
Inverter R&D Inverter High R&D Inverter Fast Progress 3000000.0
Inverter Low R&D Inverter Slow Progress 1000000.0
Inverter Medium R&D Inverter Moderate Progress 2000000.0
Module R&D Module High R&D Module Fast Progress 4500000.0
Module Low R&D Module Slow Progress 1500000.0
Module Medium R&D Module Moderate Progress 3000000.0

The investments table bundles a consistent set of tranches (one per category) into an overall investment.

investments.investments
Notes
Investment Category Tranche
High R&D BoS R&D BoS High R&D
Inverter R&D Inverter High R&D
Module R&D Module High R&D
Low R&D BoS R&D BoS Low R&D
Inverter R&D Inverter Low R&D
Module R&D Module Low R&D
Medium R&D BoS R&D BoS Medium R&D
Inverter R&D Inverter Medium R&D
Module R&D Module Medium R&D

Evaluate the Tranches in the dataset.

tranche_results = designs.evaluate_tranches(sample_count=50)
tranche_results.xs(1, level="Sample", drop_level=False)
Value Units
Technology Tranche Sample Variable Index
Residential PV 2015 Actual 1 Cost Cost 19541.835826 $/system
Metric GHG -0.001761 ΔgCO2e/system
LCOE -0.000019 Δ$/kWh
Labor -0.001281 Δ$/system
Output Electricity 184107.032791 kWh
BoS Fast Progress 1 Cost Cost 17524.525245 $/system
Metric GHG -0.004254 ΔgCO2e/system
LCOE 0.010936 Δ$/kWh
Labor -545.200985 Δ$/system
Output Electricity 184101.481909 kWh
BoS Moderate Progress 1 Cost Cost 17960.467902 $/system
Metric GHG -0.001253 ΔgCO2e/system
LCOE 0.008571 Δ$/kWh
Labor -331.852654 Δ$/system
Output Electricity 184108.162865 kWh
BoS Slow Progress 1 Cost Cost 19022.884313 $/system
Metric GHG 0.000327 ΔgCO2e/system
LCOE 0.002802 Δ$/kWh
Labor -148.230849 Δ$/system
Output Electricity 184111.682213 kWh
Inverter Fast Progress 1 Cost Cost 18059.997438 $/system
Metric GHG 2.601021 ΔgCO2e/system
LCOE 0.011024 Δ$/kWh
Labor -0.031111 Δ$/system
Output Electricity 189903.145647 kWh
Inverter Moderate Progress 1 Cost Cost 18713.047656 $/system
Metric GHG 2.537671 ΔgCO2e/system
LCOE 0.007512 Δ$/kWh
Labor -0.034240 Δ$/system
Output Electricity 189762.072909 kWh
Inverter Slow Progress 1 Cost Cost 19224.862899 $/system
Metric GHG 2.435100 ΔgCO2e/system
LCOE 0.004693 Δ$/kWh
Labor 0.056486 Δ$/system
Output Electricity 189533.659025 kWh
Module Fast Progress 1 Cost Cost 18935.973204 $/system
Metric GHG 51.490235 ΔgCO2e/system
LCOE 0.042746 Δ$/kWh
Labor 0.013583 Δ$/system
Output Electricity 298774.134685 kWh
Module Moderate Progress 1 Cost Cost 18952.058689 $/system
Metric GHG 41.216046 ΔgCO2e/system
LCOE 0.037432 Δ$/kWh
Labor 0.029792 Δ$/system
Output Electricity 275894.626758 kWh
Module Slow Progress 1 Cost Cost 19656.198525 $/system
Metric GHG 14.794693 ΔgCO2e/system
LCOE 0.015567 Δ$/kWh
Labor -0.007250 Δ$/system
Output Electricity 217057.134731 kWh

Save results.

tranche_results.to_csv("output/pv_residential_simple/example-analysis.csv")

Plot GHG metric.

g = sb.boxplot(
    x="Tranche",
    y="Value",
    data=tranche_results.xs(
        ["Metric", "GHG"],
        level=["Variable", "Index"]
    ).reset_index()[["Tranche", "Value"]],
    order=[
        "2015 Actual"              ,
        "Module Slow Progress"      ,
        "Module Moderate Progress"  ,
        "Module Fast Progress"      ,
        "Inverter Slow Progress"    ,
        "Inverter Moderate Progress",
        "Inverter Fast Progress"    ,
        "BoS Slow Progress"         ,
        "BoS Moderate Progress"     ,
        "BoS Fast Progress"         ,
    ]
)
g.set(ylabel="GHG Reduction [gCO2e / system]")
g.set_xticklabels(g.get_xticklabels(), rotation=30);
_images/output_35_0.png

Plot LCOE metric.

g = sb.boxplot(
    x="Tranche",
    y="Value",
    data=tranche_results.xs(
        ["Metric", "LCOE"],
        level=["Variable", "Index"]
    ).reset_index()[["Tranche", "Value"]],
    order=[
        "2015 Actual"              ,
        "Module Slow Progress"      ,
        "Module Moderate Progress"  ,
        "Module Fast Progress"      ,
        "Inverter Slow Progress"    ,
        "Inverter Moderate Progress",
        "Inverter Fast Progress"    ,
        "BoS Slow Progress"         ,
        "BoS Moderate Progress"     ,
        "BoS Fast Progress"         ,
    ]
)
g.set(ylabel="LCOE Reduction [USD / kWh]")
g.set_xticklabels(g.get_xticklabels(), rotation=30);
_images/output_37_0.png

Plot labor metric.

g = sb.boxplot(
    x="Tranche",
    y="Value",
    data=tranche_results.xs(
        ["Metric", "Labor"],
        level=["Variable", "Index"]
    ).reset_index()[["Tranche", "Value"]],
    order=[
        "2015 Actual"              ,
        "Module Slow Progress"      ,
        "Module Moderate Progress"  ,
        "Module Fast Progress"      ,
        "Inverter Slow Progress"    ,
        "Inverter Moderate Progress",
        "Inverter Fast Progress"    ,
        "BoS Slow Progress"         ,
        "BoS Moderate Progress"     ,
        "BoS Fast Progress"         ,
    ]
)
g.set(ylabel="Labor Increase [USD / system]")
g.set_xticklabels(g.get_xticklabels(), rotation=15);
_images/output_39_0.png

Evaluate the investments in the dataset.

investment_results = investments.evaluate_investments(designs, sample_count=50)

Costs of investments.

investment_results.amounts
Amount
Investment
High R&D 8400000.0
Low R&D 2800000.0
Medium R&D 5600000.0

Benefits of investments.

investment_results.metrics.xs(1, level="Sample", drop_level=False)
Value Units
Investment Category Tranche Sample Technology Index
High R&D BoS R&D BoS High R&D BoS Fast Progress 1 Residential PV GHG 0.001646 ΔgCO2e/system
LCOE 0.009871 Δ$/kWh
Labor -484.675917 Δ$/system
Medium R&D BoS R&D BoS Medium R&D BoS Moderate Progress 1 Residential PV GHG -0.005431 ΔgCO2e/system
LCOE 0.009181 Δ$/kWh
Labor -350.111301 Δ$/system
Low R&D BoS R&D BoS Low R&D BoS Slow Progress 1 Residential PV GHG -0.000623 ΔgCO2e/system
LCOE 0.002863 Δ$/kWh
Labor -165.967402 Δ$/system
High R&D Inverter R&D Inverter High R&D Inverter Fast Progress 1 Residential PV GHG 2.366737 ΔgCO2e/system
LCOE 0.011084 Δ$/kWh
Labor 0.034014 Δ$/system
Medium R&D Inverter R&D Inverter Medium R&D Inverter Moderate Progress 1 Residential PV GHG 2.385654 ΔgCO2e/system
LCOE 0.007551 Δ$/kWh
Labor 0.016533 Δ$/system
Low R&D Inverter R&D Inverter Low R&D Inverter Slow Progress 1 Residential PV GHG 2.562178 ΔgCO2e/system
LCOE 0.004598 Δ$/kWh
Labor 0.081408 Δ$/system
High R&D Module R&D Module High R&D Module Fast Progress 1 Residential PV GHG 50.680545 ΔgCO2e/system
LCOE 0.043544 Δ$/kWh
Labor -0.014162 Δ$/system
Medium R&D Module R&D Module Medium R&D Module Moderate Progress 1 Residential PV GHG 41.065128 ΔgCO2e/system
LCOE 0.037053 Δ$/kWh
Labor -0.010921 Δ$/system
Low R&D Module R&D Module Low R&D Module Slow Progress 1 Residential PV GHG 12.916316 ΔgCO2e/system
LCOE 0.013848 Δ$/kWh
Labor 0.057653 Δ$/system
investment_results.summary.xs(1, level="Sample", drop_level=False)
Value Units
Investment Sample Index
High R&D 1 GHG 53.048928 ΔgCO2e/system
LCOE 0.064500 Δ$/kWh
Labor -484.656066 Δ$/system
Medium R&D 1 GHG 43.445350 ΔgCO2e/system
LCOE 0.053785 Δ$/kWh
Labor -350.105690 Δ$/system
Low R&D 1 GHG 15.477872 ΔgCO2e/system
LCOE 0.021309 Δ$/kWh
Labor -165.828341 Δ$/system

Save results.

investment_results.amounts.to_csv("output/pv_residential_simple/example-investment-amounts.csv")
investment_results.metrics.to_csv("output/pv_residential_simple/example-investment-metrics.csv")

Plot GHG metric.

g = sb.boxplot(
    x="Investment",
    y="Value",
    data=investment_results.metrics.xs(
        "GHG",
        level="Index"
    ).reset_index()[["Investment", "Value"]],
    order=[
        "Low R&D"   ,
        "Medium R&D",
        "High R&D"  ,
    ]
)
g.set(ylabel="GHG Reduction [gCO2e / system]")
g.set_xticklabels(g.get_xticklabels(), rotation=15);
_images/output_51_0.png

Plot LCOE metric.

g = sb.boxplot(
    x="Investment",
    y="Value",
    data=investment_results.metrics.xs(
        "LCOE",
        level="Index"
    ).reset_index()[["Investment", "Value"]],
    order=[
        "Low R&D"   ,
        "Medium R&D",
        "High R&D"  ,
    ]
)
g.set(ylabel="LCOE Reduction [USD / kWh]")
g.set_xticklabels(g.get_xticklabels(), rotation=15);
_images/output_53_0.png

Plot labor metric.

g = sb.boxplot(
    x="Investment",
    y="Value",
    data=investment_results.metrics.xs(
        "Labor",
        level="Index"
    ).reset_index()[["Investment", "Value"]],
    order=[
        "Low R&D"   ,
        "Medium R&D",
        "High R&D"  ,
    ]
)
g.set(ylabel="Labor Increase [USD / system]")
g.set_xticklabels(g.get_xticklabels(), rotation=15);
_images/output_55_0.png

Multi-objective decision analysis.

Compute costs and metrics for tranches.

Tranches are atomic units for building investment portfolios. Evaluate all of the tranches, so we can assemble them into investments (portfolios).

tranche_results = investments.evaluate_tranches(designs, sample_count=50)

Display the cost of each tranche.

tranche_results.amounts
Amount
Category Tranche
BoS R&D BoS High R&D 900000.0
BoS Low R&D 300000.0
BoS Medium R&D 600000.0
Inverter R&D Inverter High R&D 3000000.0
Inverter Low R&D 1000000.0
Inverter Medium R&D 2000000.0
Module R&D Module High R&D 4500000.0
Module Low R&D 1500000.0
Module Medium R&D 3000000.0

Display the metrics for each tranche.

tranche_results.summary
Value Units
Category Tranche Sample Index
BoS R&D BoS High R&D 1 GHG -0.004062 ΔgCO2e/system
LCOE 0.009967 Δ$/kWh
Labor -490.859314 Δ$/system
2 GHG 0.001960 ΔgCO2e/system
LCOE 0.010154 Δ$/kWh
... ... ... ... ... ...
Module R&D Module Low R&D 49 LCOE 0.016198 Δ$/kWh
Labor 0.039788 Δ$/system
50 GHG 13.654483 ΔgCO2e/system
LCOE 0.014910 Δ$/kWh
Labor -0.015539 Δ$/system

1350 rows × 2 columns

Save the results.

tranche_results.amounts.to_csv("output/pv_residential_simple/example-tranche-amounts.csv")
tranche_results.summary.to_csv("output/pv_residential_simple/example-tranche-summary.csv")

Fit a response surface to the results.

The response surface interpolates between the discrete set of cases provided in the expert elicitation. This allows us to study funding levels intermediate between the pre-defined Tranches.

evaluator = ty.Evaluator(investments.tranches, tranche_results.summary)

Here are the categories of investment and the maximum amount that could be invested in each:

evaluator.max_amount
Amount
Category
BoS R&D 900000.0
Inverter R&D 3000000.0
Module R&D 4500000.0

Here are the metrics and their units of measure:

evaluator.units
Units
Index
GHG ΔgCO2e/system
LCOE Δ$/kWh
Labor Δ$/system

Example interpolation.

Let’s evaluate the case where each category is invested in at half of its maximum amount.

example_investments = evaluator.max_amount / 2
example_investments
Amount
Category
BoS R&D 450000.0
Inverter R&D 1500000.0
Module R&D 2250000.0
evaluator.evaluate(example_investments)
Category    Index  Sample
BoS R&D     GHG    1         -0.0010586097518157094
                   2          7.493162517135921e-05
                   3           0.001253893601450784
                   4           -0.00398626797827717
                   5          -0.005572343870333896
                                      ...
Module R&D  Labor  46          0.014371009324918305
                   47          0.011128728287076228
                   48         0.0039832773605894545
                   49          0.006026680267950724
                   50          0.028844695933457842
Name: Value, Length: 450, dtype: object

Let’s evaluate the mean instead of outputing the whole distribution.

evaluator.evaluate_statistic(example_investments, np.mean)
Index
GHG       30.156830
LCOE       0.038160
Labor   -246.843027
Name: Value, dtype: float64

Here is the standard deviation:

evaluator.evaluate_statistic(example_investments, np.std)
Index
GHG       1.410956
LCOE      0.000850
Labor    16.070395
Name: Value, dtype: float64

A risk-averse decision maker might be interested in the 10% percentile:

evaluator.evaluate_statistic(example_investments, lambda x: np.quantile(x, 0.1))
Index
GHG       28.573627
LCOE       0.037140
Labor   -268.059699
Name: Value, dtype: float64

ε-Constraint multiobjective optimization

optimizer = ty.EpsilonConstraintOptimizer(evaluator)

In order to meaningfully map the decision space, we need to know the maximum values for each of the metrics.

metric_max = optimizer.max_metrics()
metric_max
GHG      49.429976
LCOE      0.062818
Labor     0.049555
Name: Value, dtype: float64

Example optimization.

Limit spending to $3M.

investment_max = 3e6

Require that the GHG reduction be at least 40 gCO2e/system and that the Labor wages not decrease.

metric_min = pd.Series([40, 0], name = "Value", index = ["GHG", "Labor"])
metric_min
GHG      40
Labor     0
Name: Value, dtype: int64

Compute the ε-constrained maximum for the LCOE.

optimum = optimizer.maximize(
    "LCOE"                       ,
    total_amount = investment_max,
    min_metric   = metric_min    ,
    statistic    = np.mean       ,
)
optimum.exit_message
'Optimization terminated successfully.'

Here are the optimal spending levels:

np.round(optimum.amounts)
Category
BoS R&D               0.0
Inverter R&D          0.0
Module R&D      3000000.0
Name: Amount, dtype: float64

Here are the three metrics at that optimum:

optimum.metrics
Index
GHG      41.627691
LCOE      0.037566
Labor     0.028691
Name: Value, dtype: float64

Thus, by putting all of the investment into Module R&D, we can expected to achieve a mean 3.75 ¢/kWh reduction in LCOE under the GHG and Labor constraints.

It turns out that there is no solution for these constraints if we evaluate the 10th percentile of the metrics, for a risk-averse decision maker.

optimum = optimizer.maximize(
    "LCOE"                       ,
    total_amount = investment_max,
    min_metric   = metric_min    ,
    statistic    = lambda x: np.quantile(x, 0.1),
)
optimum.exit_message
'Iteration limit exceeded'

Let’s try again, but with a less stringent set of constraints, only constraining GHG somewhat but not Labor at all.

optimum = optimizer.maximize(
    "LCOE"                                                         ,
    total_amount = investment_max                                  ,
    min_metric   = pd.Series([30], name = "Value", index = ["GHG"]),
    statistic    = lambda x: np.quantile(x, 0.1)                   ,
)
optimum.exit_message
'Optimization terminated successfully.'
np.round(optimum.amounts)
Category
BoS R&D               0.0
Inverter R&D          0.0
Module R&D      3000000.0
Name: Amount, dtype: float64
optimum.metrics
Index
GHG      39.046988
LCOE      0.036463
Labor    -0.019725
Name: Value, dtype: float64

Pareto surfaces.

Metrics constrained by total investment.

pareto_amounts = None
for investment_max in np.arange(1e6, 9e6, 0.5e6):
    metrics = optimizer.max_metrics(total_amount = investment_max)
    pareto_amounts = pd.DataFrame(
        [metrics.values]                                         ,
        columns = metrics.index.values                           ,
        index   = pd.Index([investment_max / 1e6], name = "Investment [M$]"),
    ).append(pareto_amounts)
pareto_amounts
GHG LCOE Labor
Investment [M$]
8.5 49.429976 0.062818 0.049555
8.0 49.429976 0.061848 0.049555
7.5 49.429976 0.060635 0.049555
7.0 49.429976 0.059423 0.049555
6.5 49.429976 0.057592 0.049560
6.0 49.426992 0.055608 0.049545
5.5 49.424007 0.053976 0.049104
5.0 48.278589 0.052171 0.048930
4.5 47.133172 0.050431 0.047878
4.0 45.298011 0.048243 0.046810
3.5 43.462851 0.045006 0.042130
3.0 41.627691 0.037569 0.037450
2.5 32.453455 0.030129 0.032769
2.0 23.279219 0.023166 0.027886
1.5 14.104983 0.018081 0.023003
1.0 9.403322 0.010170 0.018119
sb.relplot(
    x         = "Investment [M$]",
    y         = "Value"          ,
    col       = "Metric"         ,
    kind      = "line"           ,
    facet_kws = {'sharey': False},
    data      = pareto_amounts.reset_index().melt(id_vars = "Investment [M$]", var_name = "Metric", value_name = "Value")
)
<seaborn.axisgrid.FacetGrid at 0x7f9da11752b0>
_images/output_108_1.png

We see that the LCOE metric saturates more slowly than the GHG and Labor ones.

GHG vs LCOE, constrained by total investment.

investment_max = 3
pareto_ghg_lcoe = None
for lcoe_min in 0.95 * np.arange(0.5, 0.9, 0.05) * pareto_amounts.loc[investment_max, "LCOE"]:
    optimum = optimizer.maximize(
        "GHG",
        max_amount   = pd.Series([0.9e6, 3.0e6, 1.0e6], name = "Amount", index = ["BoS R&D", "Inverter R&D", "Module R&D"]),
        total_amount = investment_max * 1e6                                 ,
        min_metric   = pd.Series([lcoe_min], name = "Value", index = ["LCOE"]),
    )
    pareto_ghg_lcoe = pd.DataFrame(
        [[investment_max, lcoe_min, optimum.metrics["LCOE"], optimum.metrics["GHG"], optimum.exit_message]],
        columns = ["Investment [M$]", "LCOE (min)", "LCOE", "GHG", "Result"]                               ,
    ).append(pareto_ghg_lcoe)
pareto_ghg_lcoe = pareto_ghg_lcoe.set_index(["Investment [M$]", "LCOE (min)"])
pareto_ghg_lcoe
LCOE GHG Result
Investment [M$] LCOE (min)
3 0.030337 0.025037 11.691901 Positive directional derivative for linesearch
0.028553 0.025037 11.691901 Positive directional derivative for linesearch
0.026768 0.025037 11.691901 Positive directional derivative for linesearch
0.024983 0.024983 11.692188 Optimization terminated successfully.
0.023199 0.023199 11.693916 Optimization terminated successfully.
0.021414 0.021414 11.694230 Optimization terminated successfully.
0.019630 0.019630 11.694544 Optimization terminated successfully.
0.017845 0.017845 11.699478 Optimization terminated successfully.
sb.relplot(
    x = "LCOE",
    y = "GHG",
    kind = "scatter",
    data = pareto_ghg_lcoe#[pareto_ghg_lcoe.Result == "Optimization terminated successfully."]
)
<seaborn.axisgrid.FacetGrid at 0x7f9da13ae630>
_images/output_112_1.png

The three types of investment are too decoupled to make an interesting pareto frontier, and we also need a better solver if we want to push to lower right.

Run the interactive explorer for the decision space.

Make sure the the tk package is installed on your machine. Here is the Anaconda link: https://anaconda.org/anaconda/tk.

w = ty.DecisionWindow(evaluator)
w.mainloop()

A new window should open that looks like the image below. Moving the sliders will cause a recomputation of the boxplots.

Image("pv_residential_simple_gui.png")
_images/output_118_0.png