lcax_convert 3.4.2

LCAx is an open, machine and human-readable data format for exchanging LCA results.
Documentation
use field_access::FieldAccess;
use lcax_core::country::Country;
use lcax_models::epd::{Standard, SubType, EPD};
use lcax_models::life_cycle_base::{ImpactCategory, ImpactCategoryKey, Impacts, LifeCycleModule};
use lcax_models::shared::{Conversion, Source, Unit};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum Node {
    ConstructionProcess(ConstructionProcess),
    Product(Product),
    Construction(Construction),
    ElementModel(ElementModel),
    ConstructionInstallation(ConstructionInstallation),
    FuelConsumption(FuelConsumption),
    DGNBOperationReference(DGNBOperationReference),
    Element(Element),
    EmbodiedRoot(EmbodiedRoot),
    Building(Building),
    Stage(Stage),
    Operation(Operation),
    ProductTransportRoot(ProductTransportRoot),
    Project(Project),
    Transport(Transport),
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Transport {
    pub id: String,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Element {
    pub id: String,
    pub name: Languages,
    pub source: String,
    pub comment: Languages,
    pub enabled: bool,
    pub excluded_scenarios: Vec<String>,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Construction {
    pub id: String,
    pub name: Languages,
    pub unit: String,
    pub source: String,
    pub comment: Languages,
    pub locked: bool,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct DGNBOperationReference {
    pub id: String,
    pub heat_supplement: f64,
    pub electricity_supplement: f64,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Building {
    pub id: String,
    pub scenario_name: String,
    pub locked: String,
    pub description: Languages,
    pub building_type: String,
    pub heated_floor_area: f64,
    pub gross_area: f64,
    pub integrated_garage: f64,
    pub external_area: f64,
    pub gross_area_above_ground: f64,
    pub person_count: f64,
    pub storeys_above_ground: u16,
    pub storeys_below_ground: u16,
    pub storey_height: f64,
    pub initial_year: u16,
    pub calculation_timespan: u16,
    pub calculation_mode: String,
    pub outside_area: f64,
    pub plot_area: f64,
    pub energy_class: String,
    pub project_type: Option<String>,
}

#[derive(Serialize, Deserialize, FieldAccess, Clone)]
#[serde(rename_all = "UPPERCASE")]
pub struct StageIndicators {
    pub ser: f64,
    pub ep: f64,
    pub odp: f64,
    pub pocp: f64,
    pub per: f64,
    pub adpe: f64,
    pub ap: f64,
    pub gwp: f64,
    pub adpf: f64,
    pub penr: f64,
    pub senr: f64,
}

impl StageIndicators {
    pub fn new() -> Self {
        Self {
            ser: 0.0,
            ep: 0.0,
            odp: 0.0,
            pocp: 0.0,
            per: 0.0,
            adpe: 0.0,
            ap: 0.0,
            gwp: 0.0,
            adpf: 0.0,
            penr: 0.0,
            senr: 0.0,
        }
    }

    pub fn update(&mut self, key: &str, value: &f64) {
        match key {
            "ser" => self.ser = value.clone(),
            "ep" => self.ep = value.clone(),
            "odp" => self.odp = value.clone(),
            "pocp" => self.pocp = value.clone(),
            "per" => self.per = value.clone(),
            "adpe" => self.adpe = value.clone(),
            "ap" => self.ap = value.clone(),
            "gwp" => self.gwp = value.clone(),
            "adpf" => self.adpf = value.clone(),
            "penr" => self.penr = value.clone(),
            "senr" => self.senr = value.clone(),
            _ => log::warn!("Could not find impact key {} in stage indicator", key),
        }
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct ProductTransportRoot {
    pub id: String,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Project {
    pub id: String,
    pub name: Languages,
    pub owner: String,
    pub address: String,
    pub lca_advisor: String,
    pub building_regulation_version: String,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Operation {
    pub id: String,
    pub electricity_usage: f64,
    pub heat_usage: f64,
    pub electricity_production: f64,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct EmbodiedRoot {
    pub id: String,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct FuelConsumption {
    pub id: String,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct ConstructionInstallation {
    pub id: String,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct ElementModel {
    pub id: String,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct ConstructionProcess {
    pub id: String,
}

#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
pub struct Product {
    pub id: String,
    pub name: Languages,
    pub source: String,
    pub comment: Languages,
    pub uncertainty_factor: f64,
}

#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct Languages {
    pub english: Option<String>,
    pub german: Option<String>,
    pub norwegian: Option<String>,
    pub danish: Option<String>,
}

impl Languages {
    pub fn get(&self) -> String {
        if self.english != None {
            self.english.clone().unwrap()
        } else if self.danish != None {
            self.danish.clone().unwrap()
        } else if self.german != None {
            self.german.clone().unwrap()
        } else if self.norwegian != None {
            self.norwegian.clone().unwrap()
        } else {
            "".to_string()
        }
    }
}

#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
pub struct Stage {
    pub id: String,
    pub name: Languages,
    pub comment: Languages,
    pub source: String,
    pub valid_to: Option<String>,
    pub stage: String,
    pub stage_unit: String,
    pub stage_factor: f64,
    pub mass_factor: f64,
    pub scale_factor: f64,
    pub external_source: String,
    pub external_id: String,
    pub external_version: String,
    pub external_url: String,
    pub compliance: String,
    pub data_type: String,
    pub indicators: StageIndicators,
}

impl Stage {
    pub fn new(epd: &EPD, stage_name: &str, impact_key: &str, value: &f64) -> Self {
        let mut indicators = StageIndicators::new();
        indicators.update(impact_key, value);

        Self {
            id: epd.id.clone(),
            name: Languages {
                english: Some(epd.name.clone()),
                german: None,
                norwegian: None,
                danish: None,
            },
            comment: Languages {
                english: epd.comment.clone(),
                german: None,
                norwegian: None,
                danish: None,
            },
            source: "User".to_string(),
            valid_to: Some(epd.valid_until.to_string()),
            stage: stage_name.to_string(),
            stage_unit: epd.declared_unit.to_string(),
            stage_factor: 1.0,
            mass_factor: epd.conversions.to_owned().unwrap().first().unwrap().value,
            scale_factor: 1.0,
            external_source: epd.source.to_owned().unwrap().name,
            external_url: epd.source.to_owned().unwrap().url.unwrap_or("".to_string()),
            external_id: "".to_string(),
            external_version: "".to_string(),
            compliance: to_lcabyg_compliance(&epd.standard),
            data_type: epd.subtype.clone().into(),
            indicators,
        }
    }

    pub fn update_indicator(&mut self, key: &str, value: &f64) {
        self.indicators.update(key, value);
    }
}

fn to_lcabyg_compliance(standard: &Standard) -> String {
    match standard {
        Standard::EN15804A2 => "15804+a2".to_string(),
        Standard::EN15804A1 => "15804".to_string(),
        Standard::UNKNOWN => "unknown".to_string(),
    }
}

pub fn epd_from_lcabyg_stages(stages: &Vec<Stage>) -> EPD {
    let mut impacts = Impacts::from([
        (ImpactCategoryKey::EP, ImpactCategory::new()),
        (ImpactCategoryKey::ODP, ImpactCategory::new()),
        (ImpactCategoryKey::POCP, ImpactCategory::new()),
        (ImpactCategoryKey::PERT, ImpactCategory::new()),
        (ImpactCategoryKey::ADPE, ImpactCategory::new()),
        (ImpactCategoryKey::AP, ImpactCategory::new()),
        (ImpactCategoryKey::GWP, ImpactCategory::new()),
        (ImpactCategoryKey::ADPF, ImpactCategory::new()),
        (ImpactCategoryKey::PENRT, ImpactCategory::new()),
    ]);

    for stage in stages {
        for (category_key, impact_category) in impacts.iter_mut() {
            let mut category_name = category_key.to_string().to_lowercase();
            if category_name == "penrt" {
                category_name = String::from("penr");
            } else if category_name == "pert" {
                category_name = String::from("per");
            }
            match impact_category.get(&LifeCycleModule::try_from(stage.stage.as_str()).unwrap()) {
                None => {
                    impact_category.insert(
                        LifeCycleModule::try_from(stage.stage.as_str()).unwrap(),
                        Some(
                            stage
                                .indicators
                                .field(&category_name)
                                .unwrap()
                                .as_f64()
                                .unwrap(),
                        ),
                    );
                }
                Some(stage_value) => {
                    let value = stage
                        .indicators
                        .field(&category_name)
                        .unwrap()
                        .as_f64()
                        .unwrap();
                    match stage_value {
                        None => impact_category.insert(
                            LifeCycleModule::try_from(stage.stage.as_str()).unwrap(),
                            Some(value),
                        ),
                        Some(_stage_value) => impact_category.insert(
                            LifeCycleModule::try_from(stage.stage.as_str()).unwrap(),
                            Some(value + _stage_value),
                        ),
                    };
                }
            }
        }
    }

    let node = &stages[0];
    let epd = EPD {
        id: node.id.to_string(),
        name: node.name.english.clone().unwrap(),
        declared_unit: Unit::from(&node.stage_unit),
        version: node.external_version.clone(),
        published_date: Default::default(),
        valid_until: Default::default(),
        comment: node.comment.english.clone(),
        source: Some(Source {
            name: node.external_source.clone(),
            url: Some(node.external_url.clone()),
        }),
        subtype: SubType::from(&Some(node.data_type.clone())),
        standard: if node.compliance == "A1" {
            Standard::EN15804A1
        } else {
            Standard::EN15804A2
        },
        impacts,
        reference_service_life: None,
        location: Country::DNK,
        conversions: Some(vec![Conversion {
            to: Unit::KG,
            value: node.mass_factor,
            meta_data: None,
        }]),
        meta_data: None,
    };

    epd
}