Download this example as a Jupyter notebook or a Python script.


Transport phase#

This example shows how to explore the transport phase results of a sustainability summary query.

The following supporting files are required for this example:

For help on constructing an XML BoM, see BoM examples.

Info:

This example requires Granta MI Restricted Substances and Sustainability Reports 2025 R2 or later.

If you would like to run an example of exploring the transport phase results of a summary query for an earlier version of the reports bundle, refer to the version of the documentation that corresponds to that version of the reports bundle.

Run a sustainability summary query#

[1]:
from ansys.grantami.bomanalytics import Connection, queries

MASS_UNIT = "kg"
ENERGY_UNIT = "MJ"
DISTANCE_UNIT = "km"

server_url = "http://my_grantami_server/mi_servicelayer"
cxn = Connection(server_url).with_credentials("user_name", "password").connect()

xml_file_path = "../supporting-files/sustainability-bom-2412.xml"
with open(xml_file_path) as f:
    bom = f.read()

sustainability_summary_query = (
    queries.BomSustainabilitySummaryQuery()
    .with_bom(bom)
    .with_units(mass=MASS_UNIT, energy=ENERGY_UNIT, distance=DISTANCE_UNIT)
)
sustainability_summary = cxn.run(sustainability_summary_query)

Transport phase#

The environmental contribution from the transport phase is summarized in the transport_details property. Results include the individual environmental impact for each transport stage included in the input BoM.

A BoM may include many transport stages, each describing transportation throughout the product lifecycle. Print the first three only.

[2]:
sustainability_summary.transport_details[:3]
[2]:
[<TransportSummaryResult('Component 11A raw material', EE%=3.7006354945461486, CC%=3.5405664742758214)>,
 <TransportSummaryResult('Component 11A as-cast to machining shop', EE%=0.5550953241819223, CC%=0.5310849711413731)>,
 <TransportSummaryResult('Finished component 11A to warehouse', EE%=1.6938432542209843, CC%=1.6205769650676105)>]

Convert all to a DataFrame. To see the distribution of results, use the DataFrame.describe() method.

[3]:
import pandas as pd

EE_HEADER = f"EE [{ENERGY_UNIT}]"
CC_HEADER = f"CC [{MASS_UNIT}]"
DISTANCE_HEADER = f"Distance [{DISTANCE_UNIT}]"

transport_df_full = pd.DataFrame.from_records(
    [
        {
            "Name": item.name,
            DISTANCE_HEADER: item.distance.value,
            "EE%": item.embodied_energy_percentage,
            EE_HEADER: item.embodied_energy.value,
            "CC%": item.climate_change_percentage,
            CC_HEADER: item.climate_change.value,
        }
        for item in sustainability_summary.transport_details
    ]
)
transport_df_full.describe()
[3]:
Distance [km] EE% EE [MJ] CC% CC [kg]
count 23.000000 23.000000 23.000000 23.000000 23.000000
mean 792.391304 4.347826 4.343345 4.347826 0.302918
std 1616.453659 10.551622 10.540748 10.719638 0.746851
min 50.000000 0.013185 0.013171 0.012614 0.000879
25% 150.000000 0.362618 0.362244 0.346933 0.024171
50% 400.000000 1.372837 1.371422 1.313456 0.091510
75% 700.000000 2.505972 2.503390 2.397578 0.167042
max 8000.000000 47.529731 47.480747 48.247186 3.361442

Most of these transport stages contribute little to the overall sustainability impact. To make a visualization more insightful, group all transport stages that contribute less than 5% of embodied energy or climate change in a single ‘Other’ transport stage.

[4]:
# Define the criterion
criterion = (transport_df_full["EE%"] < 5.0) | (transport_df_full["CC%"] < 5.0)

# Aggregate all rows that meet the criterion
transport_df_below_5_pct = transport_df_full.loc[criterion].sum(numeric_only=True).to_frame().T
transport_df_below_5_pct["Name"] = "Other"

# Sort all rows that do not meet the criterion by embodied energy
transport_df_over_5_pct = transport_df_full.loc[~(criterion)].sort_values(by="EE%", ascending=False)

# Concatenate the rows together
transport_df = pd.concat([transport_df_over_5_pct, transport_df_below_5_pct], ignore_index=True)
transport_df
[4]:
Name Distance [km] EE% EE [MJ] CC% CC [kg]
0 Finished component 11B to warehouse 8000.0 47.529731 47.480747 48.247186 3.361442
1 Product from warehouse to distributor (air) 500.0 23.278252 23.254261 23.629634 1.646306
2 Product from warehouse to distributor (truck 1) 350.0 5.273438 5.268003 5.045338 0.351515
3 Other 9375.0 23.918579 23.893929 23.077842 1.607862

This example produces multiple plots which all consist of a pair of pie charts representing the “Embodied Energy” and “Climate Change CO2 equivalent” impacts respectively. Define a helper function to create these plots.

[5]:
import plotly.graph_objects as go
from plotly.subplots import make_subplots


def plot_impact(df, title, textinfo="percent+label", hoverinfo="value+name", labels=True):
    fig = make_subplots(
        rows=1,
        cols=2,
        specs=[[{"type": "domain"}, {"type": "domain"}]],
        subplot_titles=["Embodied Energy", "Climate Change"],
    )
    fig.add_trace(go.Pie(labels=df["Name"], values=df[EE_HEADER], name=ENERGY_UNIT), 1, 1)
    fig.add_trace(go.Pie(labels=df["Name"], values=df[CC_HEADER], name=MASS_UNIT), 1, 2)
    fig.update_layout(title_text=title, legend=dict(orientation="h"))
    if labels:
        fig.update_traces(textposition="inside", textinfo=textinfo, hoverinfo=hoverinfo)
    fig.show()

Use this function to plot the environment impact for all transport stages.

[6]:
plot_impact(transport_df, "Transport stages - environmental impact", labels=False)

Transport impact per unit distance#

In some situations, it might be useful to calculate the environmental impact per distance travelled and add the results as new columns in the dataframe.

[7]:
EE_PER_DISTANCE = f"EE [{ENERGY_UNIT}/{DISTANCE_UNIT}]"
CC_PER_DISTANCE = f"CC [{MASS_UNIT}/{DISTANCE_UNIT}]"
transport_df[EE_PER_DISTANCE] = transport_df.apply(lambda row: row[EE_HEADER] / row[DISTANCE_HEADER], axis=1)
transport_df[CC_PER_DISTANCE] = transport_df.apply(lambda row: row[CC_HEADER] / row[DISTANCE_HEADER], axis=1)
transport_df
[7]:
Name Distance [km] EE% EE [MJ] CC% CC [kg] EE [MJ/km] CC [kg/km]
0 Finished component 11B to warehouse 8000.0 47.529731 47.480747 48.247186 3.361442 0.005935 0.000420
1 Product from warehouse to distributor (air) 500.0 23.278252 23.254261 23.629634 1.646306 0.046509 0.003293
2 Product from warehouse to distributor (truck 1) 350.0 5.273438 5.268003 5.045338 0.351515 0.015051 0.001004
3 Other 9375.0 23.918579 23.893929 23.077842 1.607862 0.002549 0.000172
[8]:
fig = make_subplots(
    rows=1, cols=2, specs=[[{"type": "domain"}, {"type": "domain"}]], subplot_titles=[EE_PER_DISTANCE, CC_PER_DISTANCE]
)
fig.add_trace(
    go.Pie(labels=transport_df["Name"], values=transport_df[EE_PER_DISTANCE], name=f"{ENERGY_UNIT}/{DISTANCE_UNIT}"),
    1,
    1,
)
fig.add_trace(
    go.Pie(labels=transport_df["Name"], values=transport_df[CC_PER_DISTANCE], name=f"{MASS_UNIT}/{DISTANCE_UNIT}"), 1, 2
)
fig.update_layout(
    title_text="Transport stages impact - Relative to distance travelled",
    legend=dict(orientation="h")
)
fig.show()

Transport impact aggregated by category#

The environmental impacts from transportation associated with distribution and manufacturing phases are summarized in the distribution_transport_summary and manufacturing_transport_summary properties.

[9]:
sustainability_summary.distribution_transport_summary
[9]:
<TransportSummaryByCategoryResult(EE%=30.435059969423882, CC%=30.476878034635334)>
[10]:
dist_summary = sustainability_summary.distribution_transport_summary
distribution = {
    "Name": "Distribution",
    DISTANCE_HEADER: dist_summary.distance.value,
    "EE%": dist_summary.embodied_energy_percentage,
    EE_HEADER: dist_summary.embodied_energy.value,
    "CC%": dist_summary.climate_change_percentage,
    CC_HEADER: dist_summary.climate_change.value,
}

manuf_summary = sustainability_summary.manufacturing_transport_summary
manufacturing = {
    "Name": "Manufacturing",
    DISTANCE_HEADER: manuf_summary.distance.value,
    "EE%": manuf_summary.embodied_energy_percentage,
    EE_HEADER: manuf_summary.embodied_energy.value,
    "CC%": manuf_summary.climate_change_percentage,
    CC_HEADER: manuf_summary.climate_change.value,
}

transport_by_category_df = pd.DataFrame.from_records([distribution, manufacturing])
transport_by_category_df
[10]:
Name Distance [km] EE% EE [MJ] CC% CC [kg]
0 Distribution 975.0 30.43506 30.403694 30.476878 2.123362
1 Manufacturing 17250.0 69.56494 69.493247 69.523122 4.843763
[11]:
plot_impact(transport_by_category_df, "Transport impact - grouped by category")

Transport impact aggregated by part#

The environmental contributions from transportation are summarized by the associated part in the transport_details_aggregated_by_part property. This property groups parts that contribute less than 5% embodied energy or climate change automatically.

[12]:
sustainability_summary.transport_details_aggregated_by_part
[12]:
[<TransportSummaryByPartResult('Component 11B', EE%=49.00062821794582, CC%=49.65446009256159)>,
 <TransportSummaryByPartResult('Assembly', EE%=30.435059969423882, CC%=30.476878034635334)>,
 <TransportSummaryByPartResult('Component 1C', EE%=6.475059185865298, CC%=6.194983944301699)>,
 <TransportSummaryByPartResult('Component 11A', EE%=5.949574072949056, CC%=5.692228410484804)>,
 <TransportSummaryByPartResult('Other', EE%=8.139678553815914, CC%=7.981449518016565)>]
[13]:
transport_by_part_df = pd.DataFrame.from_records(
    [
        {
            "Name": item.part_name,
            "Parent part name": item.parent_part_name,
            DISTANCE_HEADER: item.distance.value,
            "EE%": item.embodied_energy_percentage,
            EE_HEADER: item.embodied_energy.value,
            "CC%": item.climate_change_percentage,
            CC_HEADER: item.climate_change.value,
            "Transport types": "; ".join(item.transport_types),
        }
        for item in sustainability_summary.transport_details_aggregated_by_part
    ]
)
transport_by_part_df
[13]:
Name Parent part name Distance [km] EE% EE [MJ] CC% CC [kg] Transport types
0 Component 11B Subassembly 8750.0 49.000628 48.950128 49.654460 3.459488 Aircraft, long haul dedicated-freight; Truck 7...
1 Assembly None 975.0 30.435060 30.403694 30.476878 2.123362 Aircraft, long haul dedicated-freight; Truck 7...
2 Component 1C Assembly 1700.0 6.475059 6.468386 6.194984 0.431612 Truck 7.5-16t, EURO 3
3 Component 11A Subassembly 1650.0 5.949574 5.943442 5.692228 0.396585 Truck 7.5-16t, EURO 3
4 Other None 5150.0 8.139679 8.131290 7.981450 0.556078 Train, diesel; Truck 7.5-16t, EURO 3
[14]:
plot_impact(transport_by_part_df, "Transport impact - grouped by part")