# This file is part of FAST-OAD_CS23-HE : A framework for rapid Overall Aircraft Design of Hybrid
# Electric Aircraft.
# Copyright (C) 2022 ISAE-SUPAERO
import os
import pathlib
from collections import OrderedDict
from typing import List, Union, Dict, Tuple
import numpy as np
import plotly
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import fastoad.api as oad
from fastga_he.exceptions import ImpactUnavailableForPlotError
from ..models.environmental_impacts.resources.constants import LCA_PREFIX
COLS = plotly.colors.DEFAULT_PLOTLY_COLORS
HASH = ["/", "x", "-", "|", "+", ".", "", "\\"]
AIRFRAME_ASSOCIATED_COMPONENTS = [
"wing",
"fuselage",
"horizontal_tail",
"vertical_tail",
"landing_gear",
"flight_controls",
"assembly",
]
[docs]
def lca_impacts_sun_breakdown(
aircraft_file_path: Union[Union[str, pathlib.Path], List[Union[str, pathlib.Path]]],
full_burst: bool = False,
name_aircraft: Union[str, List[str]] = None,
rel: str = "absolute",
) -> go.FigureWidget:
"""
Give a breakdown of the single score impact of the aircraft under the form of a sun breakdown.
:param aircraft_file_path: path to the output file that contains the weighted and aggregated
impacts
:param full_burst: boolean to display all levels of impacts
:param name_aircraft: name of the aircraft
:param rel: string to display impacts in a relative form. By default, it is not done. Can be
relative to the "single_score" or relative to the "parent".
"""
title_text = "Single score breakdown"
if rel == "single_score":
title_text += "<br>expressed as a percentage of the total score"
elif rel == "parent":
title_text += "<br>expressed as a percentage of parent category"
if isinstance(aircraft_file_path, str) or isinstance(aircraft_file_path, pathlib.Path):
fig = go.Figure()
fig.add_trace(_get_impact_sunburst(aircraft_file_path, rel))
if name_aircraft:
fig.update_layout(title_text=name_aircraft + " " + title_text, title_x=0.5)
else:
fig.update_layout(title_text=title_text, title_x=0.5)
elif len(aircraft_file_path) == 1:
fig = go.Figure()
fig.add_trace(_get_impact_sunburst(aircraft_file_path[0], rel))
if name_aircraft[0]:
fig.update_layout(title_text=name_aircraft[0] + " " + title_text, title_x=0.5)
else:
fig.update_layout(title_text=title_text, title_x=0.5)
else:
fig = make_subplots(
1,
cols=len(aircraft_file_path),
specs=[[{"type": "domain"}, {"type": "domain"}]],
subplot_titles=name_aircraft,
)
fig_counter = 1
for idx, curr_aircraft_file_path in enumerate(aircraft_file_path):
fig.add_trace(_get_impact_sunburst(curr_aircraft_file_path, rel=rel), 1, fig_counter)
fig.update_traces(row=1, col=fig_counter, name=name_aircraft[idx])
fig_counter += 1
fig.update_layout(title_text=title_text, title_x=0.5)
if full_burst:
fig.update_traces(selector=dict(type="sunburst"))
else:
fig.update_traces(maxdepth=2, selector=dict(type="sunburst"))
fig = go.FigureWidget(fig)
return fig
def _get_impact_variable_list(
aircraft_file_path: Union[str, pathlib.Path], impact_step: str = "weighted"
) -> list:
"""
Returns a list of the name of the variable associated with the weighted impacts and available
in the output file.
:param aircraft_file_path: path to the output file path.
:param impact_step: step of the LCIA to consider, by default weighted impacts are considered,
can also be "normalized" or "raw" results.
:return: a list of all weighted impact available in the output file path.
"""
datafile = oad.DataFile(aircraft_file_path)
names = datafile.names()
names_variables_lca = []
for name in names:
if LCA_PREFIX in name:
if impact_step == "weighted":
if "weighted" in name or "single_score" in name:
names_variables_lca.append(name)
elif impact_step == "normalized":
if "normalized" in name:
names_variables_lca.append(name)
else:
if (
"weighted" not in name
and "normalized" not in name
and "aircraft_per_fu" not in name
and "flight_per_fu" not in name
and "single_score" not in name
):
names_variables_lca.append(name)
return names_variables_lca
def _get_impact_dict(
aircraft_file_path: Union[str, pathlib.Path], impact_step: str = "weighted"
) -> Tuple[dict, dict]:
"""
Returns a dict of impacts categories available in the output file and their value. By default,
the weighted impacts are given, but normalized results and raw results can also be returned.
Also returns a dict of the units used for each category for which the information is in the
variable description.
:param aircraft_file_path: path to the output file path.
:param impact_step: step of the LCIA to consider, by default weighted impacts are considered,
can also be "normalized" or "raw" results.
:return: a dict of all weighted impact available in the output file path and their value.
"""
names_variable_lca = _get_impact_variable_list(aircraft_file_path, impact_step=impact_step)
names_impact_categories = {}
units_impact_categories = {}
datafile = oad.DataFile(aircraft_file_path)
for name_variable_lca in names_variable_lca:
if _depth_lca_detail(name_variable_lca) <= 2:
impact_score = datafile[name_variable_lca].value[0]
variable_name_for_unit = name_variable_lca.replace("_weighted", "").replace(
"_normalized", ""
)
impact_name = name_variable_lca.replace(LCA_PREFIX, "")
if impact_step == "weighted":
impact_name = impact_name.replace("_weighted:sum", "")
elif impact_step == "normalized":
impact_name = impact_name.replace("_normalized:sum", "")
else:
impact_name = impact_name.replace(":sum", "")
impact_unit = datafile[variable_name_for_unit].description.split(
" for the whole process"
)[0]
units_impact_categories[impact_name] = impact_unit
names_impact_categories[impact_name] = impact_score
return names_impact_categories, units_impact_categories
def _get_impact_sunburst(
aircraft_file_path: Union[str, pathlib.Path], rel: str = "absolute"
) -> go.Sunburst:
names_variables_lca = _get_impact_variable_list(aircraft_file_path)
datafile = oad.DataFile(aircraft_file_path)
if len(names_variables_lca) == 0:
sunburst = go.Sunburst()
return sunburst
# Because it's the earliest parent ;)
label_ancestor = _get_ancestor_label(datafile)
figure_labels = [label_ancestor]
figure_parents = [""]
figure_color = [None]
color_dict = {}
if rel == "single_score" or rel == "parent":
figure_values = [100.0] # In percent
else:
figure_values = [datafile[LCA_PREFIX + "single_score"].value[0]]
names_variables_lca.remove(LCA_PREFIX + "single_score")
for name in names_variables_lca:
figure_labels.append(_name_to_label(name, datafile, rel=rel))
figure_parents.append(_get_parent_label(name, datafile, rel=rel))
if rel == "single_score" or rel == "parent":
figure_values.append(
datafile[name].value[0] / datafile[LCA_PREFIX + "single_score"].value[0] * 100.0
)
else:
figure_values.append(datafile[name].value[0])
figure_color.append(_get_color(name, color_dict))
return go.Sunburst(
labels=figure_labels,
parents=figure_parents,
values=figure_values,
branchvalues="total",
sort=False,
marker={"colors": figure_color},
)
def _depth_lca_detail(name_variable: str) -> int:
if "single_score" in name_variable:
return 1
tmp_name = name_variable.replace(LCA_PREFIX, "")
depth_lca = len(tmp_name.split(":"))
if "sum" not in tmp_name:
depth_lca += 1
return depth_lca
def _name_to_label(name_variable: str, datafile: oad.DataFile, rel: str = "absolute") -> str:
if name_variable == LCA_PREFIX + "single_score":
return _get_ancestor_label(datafile)
if "sum" not in name_variable:
depth = -1
else:
depth = -2
clean_name = name_variable.split(":")[depth].replace("_weighted", "").replace("_", "<br>")
if rel == "single_score":
value = (
datafile[name_variable].value[0]
/ datafile[LCA_PREFIX + "single_score"].value[0]
* 100.0
)
label = clean_name + "<br> " + str(_round_value(value)) + " %"
elif rel == "parent":
parent_value = _get_parent_score(name_variable, datafile)
value = datafile[name_variable].value[0] / parent_value * 100.0
label = clean_name + "<br> " + str(_round_value(value)) + " %"
else:
value = datafile[name_variable].value[0]
label = clean_name + "<br> " + str(_round_value(value)) + " pt"
return label
def _get_parent_label(name_variable: str, datafile: oad.DataFile, rel: str = "absolute") -> str:
parent_name = _get_parent_name(name_variable)
return _name_to_label(parent_name, datafile, rel=rel)
def _get_parent_score(name_variable: str, datafile: oad.DataFile) -> float:
parent_name = _get_parent_name(name_variable)
parent_score = datafile[parent_name].value[0]
return parent_score
def _get_parent_name(name_variable: str) -> str:
if _depth_lca_detail(name_variable) == 2:
return LCA_PREFIX + "single_score"
if "sum" not in name_variable:
parent_name = ":".join(name_variable.split(":")[:-1]) + ":sum"
else:
parent_name = ":".join(name_variable.split(":")[:-2]) + ":sum"
return parent_name
def _get_ancestor_label(datafile: oad.DataFile) -> str:
return (
"single_score <br> "
+ str(_round_value(datafile[LCA_PREFIX + "single_score"].value[0]))
+ " pt"
)
def _round_value(value: float) -> float:
if value == 0.0:
return value
else:
# For some very obscure reason, if you put a value which is too small here. The graph simply
# won't display ...
return round(value, int(np.ceil(abs(np.log10(value))) + 5))
def _get_first_parent_name(name_variable: str) -> str:
if _depth_lca_detail(name_variable) <= 2:
return name_variable
else:
return _get_first_parent_name(_get_parent_name(name_variable))
def _get_color(name_variable: str, color_dict: dict) -> str:
first_parent = _get_first_parent_name(name_variable)
if first_parent in color_dict:
return color_dict[first_parent]
else:
color = COLS[len(color_dict) % len(COLS)]
color_dict[name_variable] = color
return color
[docs]
def lca_score_sensitivity_simple(
results_folder_path: Union[str, pathlib.Path],
prefix: str,
name: str = None,
impact_to_plot: str = "single_score",
fig: go.Figure = None,
) -> go.Figure:
"""
Displays the evolution of the impacts of an aircraft with respect to its lifespan. This
method is a bit sensitive to use as it requires the results to be stored under the form of
FAST-OAD output files, all in the same folder and all with the same prefix. It also requires
the user to know and input said prefix. Results can be superimposed to an existing figure, but
it is recommended to only put results computed on the same lifespan.
:param results_folder_path: path to the folder that contains the output files that contains
the results.
:param prefix: prefix of the output file for the aircraft.
:param impact_to_plot: Name of the impact to plot.
:param name: name of the aircraft, to be displayed on the figure.
:param fig: figure with existing results.
:return: plotly figure with the evolution of the impact as a function of the lifespan.
"""
aircraft_lifespan_list = []
impact_list = []
names_variables_lca = []
for dirpath, _, filenames in os.walk(results_folder_path):
for filename in filenames:
if filename.startswith(prefix):
if not names_variables_lca:
# Fetch the name of available impacts for plotting
names_variables_lca = list(
_get_impact_dict(os.path.join(dirpath, filename))[0].keys()
)
# Check that the impact we request exists to make it fail as soon as possible
# if it needs to fail
if impact_to_plot not in names_variables_lca:
raise ImpactUnavailableForPlotError(
"Impact "
+ impact_to_plot
+ " unavailable in the output file. Available impacts include: "
+ ", ".join(names_variables_lca)
)
datafile = oad.DataFile(os.path.join(dirpath, filename))
aircraft_lifespan = datafile["data:TLAR:max_airframe_hours"].value[0]
aircraft_lifespan_list.append(aircraft_lifespan)
if impact_to_plot == "single_score":
variable_name = LCA_PREFIX + "single_score"
else:
variable_name = LCA_PREFIX + impact_to_plot + "_weighted:sum"
impact_score = datafile[variable_name].value[0]
impact_list.append(impact_score)
aircraft_lifespan_list, impact_list = zip(*sorted(zip(aircraft_lifespan_list, impact_list)))
if fig is None:
orig_fig = True
fig = go.Figure()
else:
orig_fig = False
scatter = go.Scatter(x=aircraft_lifespan_list, y=impact_list, name=name, showlegend=True)
beautified_impact_score = impact_to_plot.replace("_", " ")
fig.add_trace(scatter)
if orig_fig:
fig.update_layout(
title_text="Evolution of the "
+ beautified_impact_score
+ " impact with life expectancy of the aircraft",
xaxis_title="Airframe hours [h]",
yaxis_title="Single score [-]",
)
_update_fig_axis(fig)
return fig
[docs]
def lca_score_sensitivity_advanced_impact_category(
results_folder_path: Union[str, pathlib.Path],
prefix: str,
cutoff_criteria: float,
name: str = None,
) -> go.Figure:
"""
Displays the evolution of the impacts of an aircraft in terms of single score with respect to
its lifespan by stacking the contributing impact category. This method is a bit sensitive to
use as it requires the results to be stored under the form of FAST-OAD output files,
all in the same folder and all with the same prefix. It also requires the user to know and
input said prefix. In order not to overload the diagram, we'll allow the user to set a cutoff
criteria below which not to plot the contribution of the impact. The rest will be aggregated
into others.
:param results_folder_path: path to the folder that contains the output files that contains
the results.
:param prefix: prefix of the output file for the aircraft.
:param name: name of the aircraft, to be displayed on the figure.
:param cutoff_criteria: cutoff criteria, in % of the single score on the last year (e.g. enter
5 for 5% percent not 0.05)
:return: plotly figure with the evolution of all the impact contributing ot the single score
as a function of the lifespan.
"""
aircraft_lifespan_list = []
impact_variations = {}
for dirpath, _, filenames in os.walk(results_folder_path):
for filename in filenames:
if filename.startswith(prefix):
impact_score_dict, _ = _get_impact_dict(os.path.join(dirpath, filename))
impact_score_dict.pop("single_score")
datafile = oad.DataFile(os.path.join(dirpath, filename))
aircraft_lifespan = datafile["data:TLAR:max_airframe_hours"].value[0]
aircraft_lifespan_list.append(aircraft_lifespan)
for impact, impact_score in impact_score_dict.items():
_safe_add_to_dict_of_list(impact_variations, impact, impact_score)
new_impact_variation = _sort_and_cut_off(
impact_variations, aircraft_lifespan_list, cutoff_criteria
)
fig = _prep_lasagna_plot(new_impact_variation, aircraft_lifespan_list)
fig.update_layout(
title_text="Evolution of the contribution of each impact to the single score of the "
+ name,
xaxis_title="Airframe hours [h]",
yaxis_title="Single score [-]",
)
_update_fig_axis(fig)
return fig
[docs]
def lca_score_sensitivity_advanced_components(
results_folder_path: Union[str, pathlib.Path],
prefix: str,
cutoff_criteria: float,
name: str = None,
) -> go.Figure:
"""
Displays the evolution of the contribution to the single score of each component of the
aircraft as a function of the estimated lifespan of the aircraft. This method is a bit
sensitive to use as it requires the results to be stored under the form of FAST-OAD output
files, all in the same folder and all with the same prefix. It also requires the user to know
and input said prefix. In order not to overload the diagram, we'll allow the user to set a
cutoff criteria. The rest will be aggregated into others.
:param results_folder_path: path to the folder that contains the output files that contains
the results.
:param prefix: prefix of the output file for the aircraft.
:param name: name of the aircraft, to be displayed on the figure.
:param cutoff_criteria: cutoff criteria, in % of the single score on the last year (e.g. enter
5 for 5% percent not 0.05)
:return: plotly figure with the evolution of all the components contributing ot the single score
as a function of the lifespan.
"""
aircraft_lifespan_list = []
contributing_components_and_variables = {}
components_contribution = {}
for dirpath, _, filenames in os.walk(results_folder_path):
for filename in filenames:
if filename.startswith(prefix):
datafile = oad.DataFile(os.path.join(dirpath, filename))
aircraft_lifespan = datafile["data:TLAR:max_airframe_hours"].value[0]
aircraft_lifespan_list.append(aircraft_lifespan)
if not contributing_components_and_variables:
# In that context, by components, we mean all that contributes to the different
# impacts.
contributing_components_and_variables = (
_get_list_contributing_components_and_variables(
os.path.join(dirpath, filename)
)
)
for component, variables in contributing_components_and_variables.items():
impact_this_component_this_year = 0.0
for variable in variables:
impact_this_component_this_year += datafile[variable].value[0]
_safe_add_to_dict_of_list(
components_contribution, component, impact_this_component_this_year
)
new_component_variation = _sort_and_cut_off(
components_contribution, aircraft_lifespan_list, cutoff_criteria
)
fig = _prep_lasagna_plot(new_component_variation, aircraft_lifespan_list)
fig.update_layout(
title_text="Evolution of the contribution of each component to the single score of the "
+ name,
xaxis_title="Airframe hours [h]",
yaxis_title="Single score [-]",
)
_update_fig_axis(fig)
return fig
[docs]
def lca_score_sensitivity_advanced_components_and_phase(
results_folder_path: Union[str, pathlib.Path],
prefix: str,
cutoff_criteria: float,
name: str = None,
force_order: list = None,
) -> go.Figure:
"""
Displays the evolution of the contribution to the single score of each component of the
aircraft as a function of the estimated lifespan of the aircraft and separates them by phase.
This method is a bit sensitive to use as it requires the results to be stored under the form
of FAST-OAD output files, all in the same folder and all with the same prefix. It also
requires the user to know and input said prefix. In order not to overload the diagram,
we'll allow the user to set a cutoff criteria. The rest will be aggregated into others. For
components whose total contribution is greater than the cutoff we'll highlight if their
biggest impact comes from the production phase or the use phase
:param results_folder_path: path to the folder that contains the output files that contains
the results.
:param prefix: prefix of the output file for the aircraft.
:param name: name of the aircraft, to be displayed on the figure.
:param cutoff_criteria: cutoff criteria, in % of the single score on the last year (e.g. enter
5 for 5% percent not 0.05)
:param force_order: for values that aren't cutoff, forces an order of display from bottom to
top.
:return: plotly figure with the evolution of all the components contributing ot the single score
as a function of the lifespan.
"""
aircraft_lifespan_list = []
contributing_components_and_variables = {}
components_contribution_total = {}
components_contribution_production = {}
components_contribution_use = {}
components_contribution_other = {}
for dirpath, _, filenames in os.walk(results_folder_path):
for filename in filenames:
if filename.startswith(prefix):
datafile = oad.DataFile(os.path.join(dirpath, filename))
aircraft_lifespan = datafile["data:TLAR:max_airframe_hours"].value[0]
aircraft_lifespan_list.append(aircraft_lifespan)
if not contributing_components_and_variables:
# In that context, by components, we mean all that contributes to the different
# impacts.
contributing_components_and_variables = (
_get_list_contributing_components_and_variables(
os.path.join(dirpath, filename)
)
)
for component, variables in contributing_components_and_variables.items():
impact_this_component_this_year = 0.0
# Based on what the LCA conf file looks like at the time this was written, the
# only life cycle phases where we can do a breakdown of components is the
# production and the use phase. This is not generic
impact_this_component_production_this_year = 0.0
impact_this_component_use_this_year = 0.0
impact_this_component_other_this_year = 0.0
for variable in variables:
impact_this_component_this_year += datafile[variable].value[0]
if ":production:" in variable:
impact_this_component_production_this_year += datafile[variable].value[
0
]
elif ":operation:" in variable:
impact_this_component_use_this_year += datafile[variable].value[0]
else:
impact_this_component_other_this_year += datafile[variable].value[0]
_safe_add_to_dict_of_list(
components_contribution_total, component, impact_this_component_this_year
)
if impact_this_component_other_this_year != 0.0:
_safe_add_to_dict_of_list(
components_contribution_other,
component,
impact_this_component_other_this_year,
)
if impact_this_component_production_this_year != 0.0:
_safe_add_to_dict_of_list(
components_contribution_production,
component,
impact_this_component_production_this_year,
)
if impact_this_component_use_this_year != 0.0:
_safe_add_to_dict_of_list(
components_contribution_use,
component,
impact_this_component_use_this_year,
)
# To get them sorted and cut off
new_component_variation = _sort_and_cut_off(
components_contribution_total, aircraft_lifespan_list, cutoff_criteria, force_order
)
fig = go.Figure()
cumulated_impact = np.zeros_like(aircraft_lifespan_list)
for idx, contributor in enumerate(new_component_variation):
component_color = COLS[idx % len(COLS)]
beautified_impact_score = contributor.replace("_", " ")
if contributor in components_contribution_production:
cumulated_impact += np.array(list(components_contribution_production[contributor]))
scatter = go.Scatter(
x=aircraft_lifespan_list,
y=cumulated_impact,
name=beautified_impact_score + ": production",
showlegend=True,
line=dict(color="rgb(50,50,50)", width=3),
fill="tonexty",
fillpattern=dict(shape="/"),
fillcolor=component_color,
legendgroup=beautified_impact_score,
)
fig.add_trace(scatter)
if contributor in components_contribution_use:
cumulated_impact += np.array(list(components_contribution_use[contributor]))
scatter = go.Scatter(
x=aircraft_lifespan_list,
y=cumulated_impact,
name=beautified_impact_score + ": operation",
showlegend=True,
line=dict(color="rgb(50,50,50)", width=3),
fill="tonexty",
fillpattern=dict(shape="x"),
fillcolor=component_color,
legendgroup=beautified_impact_score,
)
fig.add_trace(scatter)
if contributor in components_contribution_other:
cumulated_impact += np.array(list(components_contribution_other[contributor]))
scatter = go.Scatter(
x=aircraft_lifespan_list,
y=cumulated_impact,
name=beautified_impact_score + ": other phases",
line=dict(color="rgb(50,50,50)", width=3),
showlegend=True,
fill="tonexty",
fillpattern=dict(shape="o"),
fillcolor=component_color,
legendgroup=beautified_impact_score,
)
fig.add_trace(scatter)
separator_scatter = go.Scatter(
x=aircraft_lifespan_list,
y=cumulated_impact,
line=dict(color="rgb(0,0,0)", width=2),
showlegend=False,
)
fig.add_trace(separator_scatter)
cumulated_impact += new_component_variation["Others"]
others_scatter = go.Scatter(
x=aircraft_lifespan_list,
y=cumulated_impact,
name="Others",
line=dict(color="rgb(50,50,50)", width=3),
showlegend=True,
fill="tonexty",
fillcolor=COLS[len(new_component_variation) % len(COLS)],
legendgroup="others",
)
fig.add_trace(others_scatter)
scatter = go.Scatter(
x=aircraft_lifespan_list,
y=cumulated_impact,
name="Single score",
line=dict(color="black", width=5),
showlegend=True,
)
fig.add_trace(scatter)
fig.update_layout(
title_text="Evolution of the contribution of each component to the single score of the "
+ name,
xaxis_title="Airframe hours [h]",
yaxis_title="Single score [-]",
)
_update_fig_axis(fig)
return fig
def _prep_lasagna_plot(
treated_data_dict: Dict[str, list], aircraft_lifespan_list: list
) -> go.Figure:
"""
Prepares the lasagna plot by stacking impacts on top of one another and filling below. Having
contributors sorted from biggest to smallest is recommended. Thanks, @ScottDelbecq for the name
suggestions
:param treated_data_dict: dictionary containing the data to plot
:param aircraft_lifespan_list: list containing the lifespan at which data were computed.
"""
fig = go.Figure()
cumulated_impact = np.zeros_like(aircraft_lifespan_list)
for contributor, contributor_score in treated_data_dict.items():
cumulated_impact += np.array(list(contributor_score))
beautified_impact_score = contributor.replace("_", " ")
scatter = go.Scatter(
x=aircraft_lifespan_list,
y=cumulated_impact,
name=beautified_impact_score,
showlegend=True,
fill="tonexty",
)
fig.add_trace(scatter)
scatter = go.Scatter(
x=aircraft_lifespan_list,
y=cumulated_impact,
name="Single score",
line=dict(color="black", width=5),
showlegend=True,
)
fig.add_trace(scatter)
return fig
def _sort_and_cut_off(
untreated_dict: Dict[str, list],
lifespan_list: List[float],
cutoff_criteria: float,
force_order: list = None,
) -> Dict[str, list]:
"""
For a lot of figures, we will only plot the most significant contributors. This function sorts
the contributor from biggest to smallest, cuts off the smallest ones based on the criteria and
aggregates the others. It also ensures that the data are sorted starting from the one that were
computed with the smallest lifespan.
:param untreated_dict: dictionary containing the untreated value, stored as a dict where key
are the contributor and the items are the evolution of the contributor.
:param lifespan_list: list containing the untreated list containing the value of the aircraft
lifespan at which the analysis was conducted.
:param cutoff_criteria: value of the cutoff criteria, in percent.
:param force_order: for values that aren't cutoff, forces an order of display from bottom to
top.
"""
for contributor_name, contributor_value in untreated_dict.items():
aircraft_lifespan, sorted_impact = zip(*sorted(zip(lifespan_list, contributor_value)))
untreated_dict[contributor_name] = sorted_impact
# In order to not overload the diagram, we'll only display a limited number of impacts.
contributor_score_last_year = []
contributor_name = []
aggregated_score_last_year = 0.0
for contributor, contributor_score in untreated_dict.items():
contributor_score_last_year.append(contributor_score[-1])
contributor_name.append(contributor)
aggregated_score_last_year += contributor_score[-1]
# Ensure the biggest impacts are inserted first so that they are at the bottom of the graph
last_output_score, last_output_name = zip(
*sorted(zip(contributor_score_last_year, contributor_name))
)
# This way they should be inserted starting from the biggest down to the smallest up plus the
# other, after the order forcing is done
if force_order:
biggest_to_smallest_pre_force_order = list(reversed(list(last_output_name)))
biggest_to_smallest = force_order.copy()
for component in biggest_to_smallest_pre_force_order:
if component not in force_order:
biggest_to_smallest.append(component)
else:
biggest_to_smallest = list(reversed(list(last_output_name)))
treated_dict = OrderedDict()
other = np.zeros_like(lifespan_list)
for contributor in biggest_to_smallest:
# We only take the biggest one
contributor_score = untreated_dict[contributor]
if contributor_score[-1] / aggregated_score_last_year > cutoff_criteria / 100.0:
treated_dict[contributor] = contributor_score
else:
other += np.array(list(contributor_score))
treated_dict["Others"] = other
return treated_dict
def _update_fig_axis(fig: go.Figure):
"""
Utility function that updates the aspect of the axis so that all figures have the same aspect.
:param fig: figure whose layout is to be updated.
"""
fig.update_layout(
plot_bgcolor="white",
title_x=0.5,
title_font=dict(size=20),
legend_font=dict(size=20),
)
fig.update_xaxes(
mirror=True,
ticks="outside",
showline=True,
linecolor="black",
gridcolor="lightgrey",
title_font=dict(size=20),
tickfont=dict(size=20),
)
fig.update_yaxes(
mirror=True,
ticks="outside",
showline=True,
linecolor="black",
gridcolor="lightgrey",
range=[0, None],
title_font=dict(size=20),
tickfont=dict(size=20),
side="right",
)
# You may wonder why I set the y-axis to the right, well that's because if it's on the left
# changing the tick font changes the range !
# You could try to solve that problem, but if you don't manage to update the counter below:
# hours_wasted: 1.75
def _get_list_contributing_components_and_variables(
datafile_path: Union[str, pathlib.Path],
) -> dict:
"""
Gets a list of variables names that contribute to the single score as well as the component
they are linked to.
"""
datafile = oad.DataFile(datafile_path)
names = datafile.names()
contributing_components_and_variables = {}
for name in names:
if LCA_PREFIX in name and "_weighted:" in name:
if _depth_lca_detail(name) >= 4:
component_name = _get_component_from_variable_name(name)
_safe_add_to_dict_of_list(
contributing_components_and_variables, component_name, name
)
# This isn't very generic, but I can't find another way to do it, maybe check that there
# aren't any other subprocesses ?
# TODO: Update if we add any life phase to the LCA analysis that aren't detailed
elif "manufacturing:sum" in name:
_safe_add_to_dict_of_list(
contributing_components_and_variables, "manufacturing", name
)
elif "distribution:sum" in name:
_safe_add_to_dict_of_list(
contributing_components_and_variables, "distribution", name
)
return contributing_components_and_variables
def _safe_add_to_dict_of_list(
dict_to_update: Dict[str, list], dict_key: str, element_to_add: Union[str, float]
):
"""
For dictionaries where items are meant to be lists, this function checks if the key exists.
If it does, it appends to the list, otherwise it creates the lists.
:param dict_to_update: dictionary in which to add element.
:param dict_key: dictionary key at which the element is meant to be added.
:param dict_key: dictionary key at which the element is meant to be added.
:param element_to_add: element to add to the dictionary.
"""
if dict_to_update.get(dict_key):
dict_to_update[dict_key].append(element_to_add)
else:
dict_to_update[dict_key] = [element_to_add]
def _get_component_from_variable_name(variable_name: str) -> str:
"""
Gets the name of the component or process based on the name of the variable it is associated to.
We will aggregate all process related to the airframe in "airframe". It is possible because
they are included in the analysis regardless of the propulsion chain.
"""
component = variable_name.split(":")[4 - _depth_lca_detail(variable_name) - 1]
# These names are assured to be in the LCA conf file regardless of the propulsion chain.
if component in AIRFRAME_ASSOCIATED_COMPONENTS:
return "airframe"
else:
return component
[docs]
def lca_impacts_bar_chart_simple(
aircraft_file_paths: Union[Union[str, pathlib.Path], List[Union[str, pathlib.Path]]],
names_aircraft: Union[str, List[str]] = None,
impact_step: str = "weighted",
graph_title: str = None,
impact_filter_list: list = None,
) -> go.FigureWidget:
"""
Give a bar chart that compares multiples aircraft designs across all categories. This comparison
is done relative to the first design given in the inputs. Can be used with only one design but
is pointless since it will compare an aircraft to itself.
:param aircraft_file_paths: paths to the output file that contains the impacts.
:param names_aircraft: names of the aircraft.
:param impact_step: step of the LCIA to consider, by default weighted impacts are considered,
can also be "normalized" or "raw" results.
:param graph_title: title of the graph, if None are specified one is created based on the
aircraft names
:param impact_filter_list: filter to only show impact in the list in output graph. By default
everything is plotted.
"""
fig = go.Figure()
reference_value, _ = _get_impact_dict(aircraft_file_paths[0], impact_step=impact_step)
if impact_step == "weighted":
reference_value.pop("single_score")
for aircraft_file_path, name_aircraft in zip(aircraft_file_paths, names_aircraft):
impact_score_dict, _ = _get_impact_dict(aircraft_file_path, impact_step=impact_step)
if impact_step == "weighted":
impact_score_dict.pop("single_score")
impact_scores = []
beautified_impact_names = []
for impact_name, impact_score in impact_score_dict.items():
beautified_impact_name = impact_name.replace("_", " ")
impact_scores.append(impact_score / reference_value[impact_name] * 100.0)
beautified_impact_names.append(beautified_impact_name)
if impact_filter_list:
filtered_impact_names = []
filtered_impact_scores = []
for tl_impact in impact_filter_list:
if tl_impact in beautified_impact_names:
filtered_impact_names.append(tl_impact)
filtered_impact_scores.append(
impact_scores[beautified_impact_names.index(tl_impact)]
)
else:
filtered_impact_names = beautified_impact_names
filtered_impact_scores = impact_scores
bar_chart = go.Bar(name=name_aircraft, x=filtered_impact_names, y=filtered_impact_scores)
fig.add_trace(bar_chart)
if graph_title:
title = graph_title
else:
title = (
"Relative score of "
+ ", ".join(names_aircraft[1:])
+ " with respect to "
+ names_aircraft[0]
)
fig.update_layout(
barmode="group",
plot_bgcolor="white",
title_font=dict(size=20),
legend_font=dict(size=20),
title_x=0.5,
title_text=title,
)
fig.update_xaxes(
ticks="outside",
title_font=dict(size=20),
tickfont=dict(size=20),
showline=True,
linecolor="black",
linewidth=3,
)
fig.update_yaxes(
ticks="outside",
showline=True,
linecolor="black",
gridcolor="lightgrey",
linewidth=3,
tickfont=dict(size=20),
title="Relative score [%]",
)
fig.update_yaxes(
title_font=dict(size=20),
)
return go.FigureWidget(fig)
[docs]
def lca_impacts_bar_chart_normalised(
aircraft_file_paths: Union[Union[str, pathlib.Path], List[Union[str, pathlib.Path]]],
names_aircraft: Union[str, List[str]] = None,
) -> go.FigureWidget:
"""
Give a bar chart that compares multiples aircraft designs across all categories. This comparison
is done in terms of normalized results. Can be used with only one design.
:param aircraft_file_paths: paths to the output file that contains the impacts.
:param names_aircraft: names of the aircraft.
"""
fig = go.Figure()
for aircraft_file_path, name_aircraft in zip(aircraft_file_paths, names_aircraft):
impact_score_dict, _ = _get_impact_dict(aircraft_file_path, impact_step="normalized")
impact_scores = []
beautified_impact_names = []
for impact_name, impact_score in impact_score_dict.items():
beautified_impact_name = impact_name.replace("_", " ")
# There has to be a smarter way of doing it ^^'
impact_scores.append(impact_score)
beautified_impact_names.append(beautified_impact_name)
bar_chart = go.Bar(name=name_aircraft, x=beautified_impact_names, y=impact_scores)
fig.add_trace(bar_chart)
if len(names_aircraft) == 1:
title = "Normalized score for " + names_aircraft[0]
else:
title = (
"Normalized score for " + ", ".join(names_aircraft[:-1]) + " and " + names_aircraft[-1]
)
fig.update_layout(
barmode="group",
plot_bgcolor="white",
title_font=dict(size=20),
legend_font=dict(size=20),
title_x=0.5,
title_text=title,
)
fig.update_xaxes(
ticks="outside",
title_font=dict(size=20),
tickfont=dict(size=20),
showline=True,
linecolor="black",
linewidth=3,
)
fig.update_yaxes(
ticks="outside",
showline=True,
linecolor="black",
gridcolor="lightgrey",
linewidth=3,
tickfont=dict(size=20),
title="Normalized results [eq-person]",
)
fig.update_yaxes(
title_font=dict(size=20),
)
return go.FigureWidget(fig)
[docs]
def lca_raw_impact_comparison(
aircraft_file_paths: Union[Union[str, pathlib.Path], List[Union[str, pathlib.Path]]],
names_aircraft: Union[str, List[str]] = None,
impact_category: str = None,
) -> go.FigureWidget:
"""
Plots, on bar chart, the simple comparison in one impact category of one or more designs.
:param aircraft_file_paths: paths to the output file that contains the impacts.
:param names_aircraft: names of the aircraft.
:param impact_category: impact category to plot, by default the first one alphabetically will
be plotted
"""
fig = go.Figure()
for aircraft_file_path, name_aircraft in zip(aircraft_file_paths, names_aircraft):
impact_score_dict, impact_unit_dict = _get_impact_dict(
aircraft_file_path, impact_step="raw"
)
beautified_impact_names_and_scores = {}
beautified_impact_names_and_units = {}
for impact_name, impact_score in impact_score_dict.items():
beautified_impact_name = impact_name.replace("_", " ")
beautified_impact_names_and_scores[beautified_impact_name] = impact_score
beautified_impact_names_and_units[beautified_impact_name] = impact_unit_dict[
impact_name
]
if impact_category is None:
impact_to_plot = list(beautified_impact_names_and_scores.keys())[0]
elif impact_category in beautified_impact_names_and_scores:
impact_to_plot = impact_category
else:
raise ImpactUnavailableForPlotError(
"Impact "
+ impact_category
+ " unavailable in the output file. Available impacts include: "
+ ", ".join(list(beautified_impact_names_and_scores.keys()))
)
bar_chart = go.Bar(
name=name_aircraft,
x=[impact_to_plot],
y=[beautified_impact_names_and_scores[impact_to_plot]],
)
fig.add_trace(bar_chart)
fig.update_layout(
barmode="group",
plot_bgcolor="white",
title_font=dict(size=20),
legend_font=dict(size=20),
title_x=0.5,
title_text="Comparison of impact in category: " + impact_to_plot,
)
fig.update_xaxes(
ticks="outside",
title_font=dict(size=20),
tickfont=dict(size=20),
showline=True,
linecolor="black",
linewidth=3,
)
fig.update_yaxes(
ticks="outside",
showline=True,
linecolor="black",
gridcolor="lightgrey",
linewidth=3,
tickfont=dict(size=20),
title=beautified_impact_names_and_units[impact_to_plot] + " per FU",
)
fig.update_yaxes(
title_font=dict(size=20),
)
return go.FigureWidget(fig)
[docs]
def lca_raw_impact_comparison_advanced(
aircraft_file_paths: Union[Union[str, pathlib.Path], List[Union[str, pathlib.Path]]],
names_aircraft: Union[str, List[str]] = None,
impact_category: str = None,
aggregate_and_sort_contributor: Dict[str, Union[str, List[str]]] = None,
) -> go.FigureWidget:
"""
Plots, on bar chart, the comparison in one impact category of one or more designs with a detail
of each contributor
:param aircraft_file_paths: paths to the output file that contains the impacts.
:param names_aircraft: names of the aircraft.
:param impact_category: impact category to plot, by default the first one alphabetically will
be plotted
:param aggregate_and_sort_contributor: dict of contributor to aggregate and name under which to
aggregate them. Keys are new names and items are a list of old names. The order in which new
names are given will also serve as the order in which we plot contributors starting from the
bottom.
"""
fig = go.Figure()
total_impact_score_dict, total_impact_unit_dict = _get_impact_dict(
aircraft_file_paths[0], impact_step="raw"
)
beautified_impact_names_and_scores = {}
beautified_impact_names_and_units = {}
datafile = oad.DataFile(aircraft_file_paths[0])
for impact_name, impact_score in total_impact_score_dict.items():
beautified_impact_name = impact_name.replace("_", " ")
beautified_impact_names_and_scores[beautified_impact_name] = impact_score
beautified_impact_names_and_units[beautified_impact_name] = total_impact_unit_dict[
impact_name
]
if impact_category is None:
impact_to_plot = list(beautified_impact_names_and_scores.keys())[0]
elif impact_category in beautified_impact_names_and_scores:
impact_to_plot = impact_category
else:
raise ImpactUnavailableForPlotError(
"Impact "
+ impact_category
+ " unavailable in the output file. Available impacts include: "
+ ", ".join(list(beautified_impact_names_and_scores.keys()))
)
un_beautified_impact = impact_to_plot.replace(" ", "_")
component_contribution_on_each_aircraft = {}
for aircraft_file_path, name_aircraft in zip(aircraft_file_paths, names_aircraft):
# This can't fetch raw results but with a small cross product everything will be fine !
available_components_and_contribution = _get_component_and_contribution(
aircraft_file_path, impact_step="normalized"
)
normalization_coefficient = datafile[
LCA_PREFIX + un_beautified_impact + ":normalization_factor"
].value[0]
for available_component, contribution in available_components_and_contribution.items():
component_contribution = contribution[un_beautified_impact] * normalization_coefficient
if available_component not in component_contribution_on_each_aircraft:
component_contribution_on_each_aircraft[available_component] = {
name_aircraft: component_contribution
}
else:
component_contribution_on_each_aircraft[available_component][name_aircraft] = (
component_contribution
)
if aggregate_and_sort_contributor:
# Now we aggregate the contribution ...
component_contribution_aggregated_sorted = {}
component_contribution_unsorted = {}
# But before anything we presort the sorted dict so that it appear in the order we want
for name_for_aggregation in aggregate_and_sort_contributor:
blank_dict = {}
for aircraft in names_aircraft:
blank_dict[aircraft] = 0.0
component_contribution_aggregated_sorted[name_for_aggregation] = blank_dict
for (
component,
contribution_on_each_aircraft_dict,
) in component_contribution_on_each_aircraft.items():
component_to_be_sorted = False
for (
name_for_aggregation,
components_to_aggregate,
) in aggregate_and_sort_contributor.items():
# This should correspond to the case where we just rename a contributor
if components_to_aggregate is str:
if component == components_to_aggregate:
component_to_be_sorted = True
component_contribution_aggregated_sorted[name_for_aggregation] = (
contribution_on_each_aircraft_dict
)
# Components can't be aggregated in more than one place so if it is here it
# can't be elsewhere.
break
# The type of input we expect should ensure this. Also in that case it means
# component contribution can be summed, so it means the contribution can already
# exist.
else:
if component in components_to_aggregate:
component_to_be_sorted = True
# It has already been added so we must sum
if name_for_aggregation in component_contribution_aggregated_sorted:
for aircraft in contribution_on_each_aircraft_dict:
component_contribution_aggregated_sorted[name_for_aggregation][
aircraft
] += contribution_on_each_aircraft_dict[aircraft]
break
# If the component is not to be aggregated or just renamed, we put it in a different
# dict to plot it later in the order.
if not component_to_be_sorted:
component_contribution_unsorted[component] = contribution_on_each_aircraft_dict
else:
component_contribution_aggregated_sorted = {}
component_contribution_unsorted = component_contribution_on_each_aircraft
component_counter = 0
for dict_to_plot in [component_contribution_aggregated_sorted, component_contribution_unsorted]:
for (
component,
contribution_on_each_aircraft_dict,
) in dict_to_plot.items():
contribution_on_each_aircraft_list = []
for aircraft in names_aircraft:
if aircraft in contribution_on_each_aircraft_dict:
contribution_on_each_aircraft_list.append(
contribution_on_each_aircraft_dict[aircraft]
)
else:
contribution_on_each_aircraft_list.append(0.0)
bar_chart = go.Bar(
name=component,
x=names_aircraft,
y=contribution_on_each_aircraft_list,
marker=dict(
pattern_shape=HASH[component_counter // len(HASH)],
color=COLS[component_counter % len(COLS)],
),
)
fig.add_trace(bar_chart)
component_counter += 1
fig.update_layout(
plot_bgcolor="white",
title_font=dict(size=20),
legend_font=dict(size=20),
title_x=0.5,
title_text="Comparison of impact in category: " + impact_to_plot,
barmode="stack",
)
fig.update_xaxes(
ticks="outside",
title_font=dict(size=20),
tickfont=dict(size=20),
showline=True,
linecolor="black",
linewidth=3,
)
fig.update_yaxes(
ticks="outside",
showline=True,
linecolor="black",
gridcolor="lightgrey",
linewidth=3,
tickfont=dict(size=20),
title=beautified_impact_names_and_units[impact_to_plot] + " per FU",
)
fig.update_yaxes(
title_font=dict(size=20),
)
return go.FigureWidget(fig)
[docs]
def lca_impacts_bar_chart_normalised_weighted(
aircraft_file_paths: Union[Union[str, pathlib.Path], List[Union[str, pathlib.Path]]],
names_aircraft: Union[str, List[str]] = None,
impact_filter_list: list = None,
) -> go.FigureWidget:
"""
Give a bar chart that compares multiples aircraft designs across all categories. This comparison
is done relative to the first design given in the inputs. Can be used with only one design but
is pointless since it will compare an aircraft to itself.
:param aircraft_file_paths: paths to the output file that contains the impacts.
:param names_aircraft: names of the aircraft.
:param impact_filter_list: filter to only show impact in the list in output graph. By default
everything is plotted.
"""
fig = go.Figure()
for aircraft_file_path, name_aircraft in zip(aircraft_file_paths, names_aircraft):
impact_score_dict, _ = _get_impact_dict(aircraft_file_path)
impact_scores = []
beautified_impact_names = []
for impact_name, impact_score in impact_score_dict.items():
beautified_impact_name = impact_name.replace("_", " ")
impact_scores.append(impact_score)
beautified_impact_names.append(beautified_impact_name)
if impact_filter_list:
filtered_impact_names = []
filtered_impact_scores = []
for tl_impact in impact_filter_list:
if tl_impact in beautified_impact_names:
filtered_impact_names.append(tl_impact)
filtered_impact_scores.append(
impact_scores[beautified_impact_names.index(tl_impact)]
)
else:
filtered_impact_names = beautified_impact_names
filtered_impact_scores = impact_scores
bar_chart = go.Bar(name=name_aircraft, x=filtered_impact_names, y=filtered_impact_scores)
fig.add_trace(bar_chart)
if len(names_aircraft) == 1:
title_text = "Normalized and weighted scores for " + names_aircraft[0]
else:
title_text = (
"Normalized and weighted scores for "
+ ", ".join(names_aircraft[0:-1])
+ " and "
+ names_aircraft[-1]
)
fig.update_layout(
barmode="group",
plot_bgcolor="white",
title_font=dict(size=20),
legend_font=dict(size=20),
title_x=0.5,
title_text=title_text,
)
fig.update_xaxes(
ticks="outside",
title_font=dict(size=20),
tickfont=dict(size=20),
showline=True,
linecolor="black",
linewidth=3,
)
fig.update_yaxes(
ticks="outside",
showline=True,
linecolor="black",
gridcolor="lightgrey",
linewidth=3,
tickfont=dict(size=20),
title="Points [-]",
)
fig.update_yaxes(
title_font=dict(size=20),
)
return go.FigureWidget(fig)
def _get_component_and_contribution(
aircraft_file_path: Union[str, pathlib.Path],
detailed_component_contributions: bool = False,
aggregate_phase: list = None,
impact_step: str = "weighted",
) -> dict:
"""
Returns a dict of the components and their impact in each category. Also return a dict with the
total value for each impact.
:param aircraft_file_path: path to the output file path.
:return: a dict of the components with their contribution to each impact category.
:param detailed_component_contributions: by default, the contribution in each phase of a
components are summed together and only the total is shown, this allows to see the contribution
in each phase of each component
:param aggregate_phase: for compactness, it may be preferable to aggregate the contribution
of all components to a phase. This options is a list of phases to aggregate. Please note that
the aggregation of the manufacturing and distribution can't be changed (see the documentation).
:param impact_step: step of the LCIA to consider, by default weighted impacts are considered,
can also be "normalized" results.
"""
datafile = oad.DataFile(aircraft_file_path)
names = datafile.names()
component_and_impacts = {}
filter_tag = "_" + impact_step
for name in names:
# We can focus on the weighted value since it'll be relative anyway
if LCA_PREFIX in name and filter_tag in name:
if _depth_lca_detail(name) >= 4:
component_name = _get_component_from_variable_name(name)
phase_name = name.split(":")[-2]
impact_name = name.replace(LCA_PREFIX, "").split(filter_tag)[0]
contribution = datafile[name].value[0]
if aggregate_phase and phase_name in aggregate_phase:
if contribution != 0.0:
key_name = phase_name
if key_name in component_and_impacts:
if impact_name in component_and_impacts[key_name]:
component_and_impacts[key_name][impact_name] += contribution
else:
component_and_impacts[key_name][impact_name] = contribution
else:
component_and_impacts[key_name] = {impact_name: contribution}
else:
if detailed_component_contributions:
key_name = component_name + ": " + phase_name
else:
key_name = component_name
if contribution != 0.0:
if key_name in component_and_impacts:
if impact_name in component_and_impacts[key_name]:
component_and_impacts[key_name][impact_name] += contribution
else:
component_and_impacts[key_name][impact_name] = contribution
else:
component_and_impacts[key_name] = {impact_name: contribution}
elif "manufacturing:sum" in name:
component_name = "manufacturing"
impact_name = name.replace(LCA_PREFIX, "").split(filter_tag)[0]
contribution = datafile[name].value[0]
if contribution != 0.0:
if component_name in component_and_impacts:
# Manufacturing and distribution only contribute once each time
component_and_impacts[component_name][impact_name] = contribution
else:
component_and_impacts[component_name] = {impact_name: contribution}
elif "distribution:sum" in name:
component_name = "distribution"
impact_name = name.replace(LCA_PREFIX, "").split(filter_tag)[0]
contribution = datafile[name].value[0]
if contribution != 0.0:
if component_name in component_and_impacts:
# Manufacturing and distribution only contribute once each time
component_and_impacts[component_name][impact_name] = contribution
else:
component_and_impacts[component_name] = {impact_name: contribution}
return component_and_impacts
[docs]
def lca_impacts_bar_chart_with_contributors(
aircraft_file_path: Union[str, pathlib.Path],
name_aircraft: str = None,
detailed_component_contributions: bool = False,
legend_rename: dict = None,
aggregate_phase: list = None,
impact_filter_list: list = None,
impact_step: str = "weighted",
aggregate_and_sort_contributor: Dict[str, Union[str, List[str]]] = None,
) -> go.FigureWidget:
"""
Give a bar chart that plot the impact of an aircraft in each category and how each component
contributes to it in relative terms.
:param aircraft_file_path: path to the output file that contains the results of the LCA
:param name_aircraft: name of the aircraft.
:param detailed_component_contributions: by default, the contribution in each phase of a
components are summed together and only the total is shown, this allows to see the contribution
in each phase of each component
:param legend_rename: legend names are set by the code by default, if any renaming is to be
done, pass here the legend to be renamed as key and how to rename it as item.
:param aggregate_phase: for compactness, it may be preferable to aggregate the contribution
of all components to a phase. This options is a list of phases to aggregate. Please note that
the aggregation of the manufacturing and distribution can't be changed (see the documentation).
:param impact_filter_list: filter to only show impact in the list in output graph
:param impact_step: step of the LCIA to consider, by default weighted impacts are considered,
can also be "normalized" results.
:param aggregate_and_sort_contributor: dict of contributor to aggregate and name under which to
aggregate them. Keys are new names and items are a list of old names. The order in which new
names are given will also serve as the order in which we plot contributors starting from the
bottom.
"""
component_and_contribution = _get_component_and_contribution(
aircraft_file_path,
detailed_component_contributions,
aggregate_phase,
impact_step=impact_step,
)
fig = go.Figure()
impact_score_dict, _ = _get_impact_dict(aircraft_file_path, impact_step=impact_step)
if impact_step == "weighted":
impact_score_dict.pop("single_score")
impact_list = list(impact_score_dict.keys())
if impact_filter_list is None:
impact_filter_list = impact_list
component_counter = 0
current_contribution = dict(zip(impact_list, np.zeros(len(impact_list))))
# This is where we should sort and aggregate
if aggregate_and_sort_contributor:
# Now we aggregate the contribution ...
component_contribution_aggregated_sorted = {}
component_contribution_unsorted = {}
# But before anything we presort the sorted dict so that it appear in the order we want
for name_for_aggregation in aggregate_and_sort_contributor:
blank_dict = {}
for aircraft in impact_list:
blank_dict[aircraft] = 0.0
component_contribution_aggregated_sorted[name_for_aggregation] = blank_dict
for (
component,
contribution_on_each_impact_dict,
) in component_and_contribution.items():
component_to_be_sorted = False
for (
name_for_aggregation,
components_to_aggregate,
) in aggregate_and_sort_contributor.items():
# This should correspond to the case where we just rename a contributor
if components_to_aggregate is str:
if component == components_to_aggregate:
component_to_be_sorted = True
component_contribution_aggregated_sorted[name_for_aggregation] = (
contribution_on_each_impact_dict
)
# Components can't be aggregated in more than one place so if it is here it
# can't be elsewhere.
break
else:
if component in components_to_aggregate:
component_to_be_sorted = True
# It has already been added so we must sum
if name_for_aggregation in component_contribution_aggregated_sorted:
for aircraft in contribution_on_each_impact_dict:
component_contribution_aggregated_sorted[name_for_aggregation][
aircraft
] += contribution_on_each_impact_dict[aircraft]
break
# If the component is not to be aggregated or just renamed, we put it in a different
# dict to plot it later in the order.
if not component_to_be_sorted:
component_contribution_unsorted[component] = contribution_on_each_impact_dict
else:
component_contribution_aggregated_sorted = {}
component_contribution_unsorted = component_and_contribution
for dict_to_plot in (component_contribution_aggregated_sorted, component_contribution_unsorted):
for component, impacts in dict_to_plot.items():
impact_contributions = []
beautified_impact_names = []
if detailed_component_contributions:
component_name = component.split(":")[0]
beautified_component_name = component_name.replace("_", " ")
else:
component_name = component
beautified_component_name = component.replace("_", " ")
final_name = component.replace(component_name, beautified_component_name)
if legend_rename and final_name in legend_rename:
final_name = legend_rename[final_name]
for impact_name in impact_filter_list:
# Failsafe
if impact_name in impacts:
contribution = impacts[impact_name]
else:
contribution = 0.0
beautified_impact_name = impact_name.replace("_", " ")
beautified_impact_names.append(beautified_impact_name)
rel_contribution = contribution / impact_score_dict[impact_name] * 100.0
impact_contributions.append(rel_contribution)
current_contribution[impact_name] += rel_contribution
bar_chart = go.Bar(
name=final_name,
x=beautified_impact_names,
y=impact_contributions,
marker=dict(
pattern_shape=HASH[component_counter // len(HASH)],
color=COLS[component_counter % len(COLS)],
),
)
fig.add_trace(bar_chart)
component_counter += 1
title_text = (
"Relative contribution of each component to each impact category for " + name_aircraft
)
fig.update_layout(
plot_bgcolor="white",
title_font=dict(size=20),
legend_font=dict(size=20),
title_x=0.5,
title_text=title_text,
barmode="stack",
)
fig.update_xaxes(
ticks="outside",
title_font=dict(size=20),
tickfont=dict(size=20),
showline=True,
linecolor="black",
linewidth=3,
)
fig.update_yaxes(
ticks="outside",
showline=True,
linecolor="black",
gridcolor="lightgrey",
linewidth=3,
tickfont=dict(size=20),
title="Relative contribution [%]",
)
fig.update_yaxes(
title_font=dict(size=20),
)
return go.FigureWidget(fig)
[docs]
def lca_impacts_bar_chart_with_components_absolute(
aircraft_file_path: Union[str, pathlib.Path],
name_aircraft: str = None,
detailed_component_contributions: bool = False,
legend_rename: dict = None,
aggregate_phase: list = None,
cutoff_criteria: float = None,
) -> go.FigureWidget:
"""
Provide a bar chart of the weighted impacts of an aircraft, showing the absolute value of each
component's contribution across all impact categories.
:param aircraft_file_path: path to the output file that contains the results of the LCA
:param name_aircraft: name of the aircraft
:param detailed_component_contributions: by default, all contribution of one component,
regardless of the phase is aggregated, this segregates them.
:param legend_rename: legend names are set by the code by default, if any renaming is to be
done, pass here the legend to be renamed as key and how to rename it as item.
:param aggregate_phase: by default only the manufacturing and distribution are aggregated.
Additional phase specified here can be aggregated.
:param cutoff_criteria: value of the cutoff criteria, in percent of the single score.
"""
component_and_contribution = _get_component_and_contribution(
aircraft_file_path, detailed_component_contributions, aggregate_phase
)
fig = go.Figure()
impact_score_dict, _ = _get_impact_dict(aircraft_file_path)
single_score = impact_score_dict["single_score"]
impact_score_dict.pop("single_score")
component_counter = 0
components_type = {}
for component_name in component_and_contribution:
if detailed_component_contributions:
beautified_component_name = component_name.split(":")[0].replace("_", " ")
else:
beautified_component_name = component_name.replace("_", " ")
if beautified_component_name[-1].isdigit():
component_type = beautified_component_name[:-2]
if component_type in components_type:
existing_component_of_that_type = components_type[component_type]
# To avoid duplication
existing_component_of_that_type.append(beautified_component_name)
components_type[component_type] = list(set(existing_component_of_that_type))
else:
components_type[component_type] = [beautified_component_name]
else:
components_type[beautified_component_name] = [beautified_component_name]
# Here we filter to only show the impact whose contribution to the single score is greater than
# the set value in inputs
if cutoff_criteria:
component_and_contribution_with_cutoff = {}
contribution_others = {}
for component, impacts in component_and_contribution.items():
if np.sum(np.array(list(impacts.values()))) < single_score * cutoff_criteria / 100.0:
if not contribution_others:
contribution_others = impacts
else:
for impact, contribution in impacts.items():
contribution_others[impact] = contribution
else:
component_and_contribution_with_cutoff[component] = impacts
if contribution_others:
component_and_contribution_with_cutoff["Others"] = contribution_others
component_and_contribution = component_and_contribution_with_cutoff
for component, impacts in component_and_contribution.items():
impact_contributions = []
beautified_impact_names = []
for impact_name, contribution in impacts.items():
beautified_impact_name = impact_name.replace("_", " ")
beautified_impact_names.append(beautified_impact_name)
impact_contributions.append(contribution)
# If there are only one component of each type, we don't put the number
if detailed_component_contributions:
component_name = component.split(":")[0]
beautified_component_name = component_name.replace("_", " ")
else:
component_name = component
beautified_component_name = component_name.replace("_", " ")
if beautified_component_name[-1].isdigit():
component_type = " ".join(beautified_component_name.split(" ")[:-1])
if len(components_type[component_type]) == 1:
final_name = component.replace(component_name, component_type)
else:
final_name = component.replace(component_name, beautified_component_name)
else:
final_name = component.replace(component_name, beautified_component_name)
if legend_rename and final_name in legend_rename:
final_name = legend_rename[final_name]
bar_chart = go.Bar(
name=final_name,
x=beautified_impact_names,
y=impact_contributions,
marker=dict(
pattern_shape=HASH[component_counter // len(HASH)],
color=COLS[component_counter % len(COLS)],
),
)
fig.add_trace(bar_chart)
component_counter += 1
title_text = (
"Absolute contribution of each component to each impact category for " + name_aircraft
)
fig.update_layout(
plot_bgcolor="white",
title_font=dict(size=20),
legend_font=dict(size=20),
title_x=0.5,
title_text=title_text,
barmode="stack",
)
fig.update_xaxes(
ticks="outside",
title_font=dict(size=20),
tickfont=dict(size=20),
showline=True,
linecolor="black",
linewidth=3,
)
fig.update_yaxes(
ticks="outside",
showline=True,
linecolor="black",
gridcolor="lightgrey",
linewidth=3,
tickfont=dict(size=20),
title="Normalised and weighted contribution [-]",
)
fig.update_yaxes(
title_font=dict(size=20),
)
return go.FigureWidget(fig)
[docs]
def lca_impacts_search_table(
aircraft_file_path: Union[str, pathlib.Path],
impact_criteria: List[str],
phase_criteria: List[str],
component_criteria: List[str],
rel: bool = False,
) -> list:
"""
Can be used as a very simple search engine of impacts and their contribution to the single
score. Can give criteria on what impacts/phases/components to consider. If an asterisk is used
the sum of all variable name that matches will be used. Can also be returned as a percent of the
total score rather than an absolute value.
:param aircraft_file_path: path to the output file that contains the results of the LCA
:param impact_criteria: criterion on impacts to consider
:param phase_criteria: criterion on phases to consider
:param component_criteria: criterion on components to consider
:param rel: boolean to return the variable as a percentage
"""
datafile = oad.DataFile(aircraft_file_path)
available_impacts = list(_get_impact_dict(aircraft_file_path)[0].keys())
available_impacts.remove("single_score")
# For now won't be likely to change a lot, so we will do it like this
available_phases = ["distribution", "manufacturing", "operation", "production"]
available_components = list(_get_component_and_contribution(aircraft_file_path).keys())
available_components.remove("airframe")
available_components += AIRFRAME_ASSOCIATED_COMPONENTS
# At this point there might some phase till left in the components, so we remove them first
# and put them in a separate list
un_detailed_phases = []
for phase in available_phases:
if phase in available_components:
available_components.remove(phase)
un_detailed_phases.append(phase)
single_score = datafile[LCA_PREFIX + "single_score"].value[0]
impacts = []
for impact, phase, component in zip(impact_criteria, phase_criteria, component_criteria):
if impact != "*":
if impact not in available_impacts:
impacts.append(np.nan)
continue
else:
impacts_to_browse = [impact]
else:
impacts_to_browse = available_impacts
if phase != "*":
if phase not in available_phases:
impacts.append(np.nan)
continue
else:
phases_to_browse = [phase]
else:
phases_to_browse = available_phases
if component != "*":
if component not in available_components:
impacts.append(np.nan)
continue
else:
components_to_browse = [component]
else:
components_to_browse = available_components
impact_value = 0.0
for impact_to_browse in impacts_to_browse:
for phase_to_browse in phases_to_browse:
if phase_to_browse in un_detailed_phases and component == "*":
variable_name = (
LCA_PREFIX + impact_to_browse + "_weighted:" + phase_to_browse + ":sum"
)
impact_value += datafile[variable_name].value[0]
continue
else:
for component_to_browse in components_to_browse:
variable_name = (
LCA_PREFIX
+ impact_to_browse
+ "_weighted:"
+ phase_to_browse
+ ":"
+ component_to_browse
)
# Only adds variable that exist
if variable_name in datafile.names():
impact_value += datafile[variable_name].value[0]
if rel:
impacts.append(impact_value / single_score * 100.0)
else:
impacts.append(impact_value)
return impacts