Skip to main content

cteepbd/
components.rs

1// Copyright (c) 2018-2019  Ministerio de Fomento
2//                          Instituto de Ciencias de la Construcción Eduardo Torroja (IETcc-CSIC)
3
4// Permission is hereby granted, free of charge, to any person obtaining a copy
5// of this software and associated documentation files (the "Software"), to deal
6// in the Software without restriction, including without limitation the rights
7// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8// copies of the Software, and to permit persons to whom the Software is
9// furnished to do so, subject to the following conditions:
10
11// The above copyright notice and this permission notice shall be included in
12// all copies or substantial portions of the Software.
13
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20// SOFTWARE.
21
22// Author(s): Rafael Villar Burke <pachi@ietcc.csic.es>,
23//            Daniel Jiménez González <dani@ietcc.csic.es>,
24//            Marta Sorribes Gil <msorribes@ietcc.csic.es>
25
26/*!
27Componentes energéticos
28=======================
29
30Define el tipo Components (lista de componentes + metadatos) y sus traits.
31
32Los componentes modelizan el uso y producción de energía en el periodo de cálculo.
33
34Hipótesis:
35
36- Se completa automáticamente el consumo de energía procedente del medioambiente con una producción
37- No se permite la producción de electricidad a usos concretos (se asume NDEF) (XXX: se podría eliminar)
38*/
39
40use std::collections::HashSet;
41use std::fmt;
42use std::str;
43
44use serde::{Deserialize, Serialize};
45
46use crate::{
47    error::EpbdError,
48    types::{CSubtype, CType, Carrier, Component, Meta, MetaVec, Service},
49    vecops::{veclistsum, vecvecdif, vecvecmin, vecvecmul, vecvecsum},
50};
51
52/// Lista de datos de componentes con sus metadatos
53///
54/// List of component data bundled with its metadata
55///
56/// #META CTE_AREAREF: 100.5
57/// ELECTRICIDAD,CONSUMO,EPB,16.39,13.11,8.20,7.38,4.10,4.92,6.56,5.74,4.10,6.56,9.84,13.11
58/// ELECTRICIDAD,PRODUCCION,INSITU,8.20,6.56,4.10,3.69,2.05,2.46,3.28,2.87,2.05,3.28,4.92,6.56
59#[derive(Debug, Default, Clone, Serialize, Deserialize)]
60pub struct Components {
61    /// Component list
62    pub cmeta: Vec<Meta>,
63    /// Metadata
64    pub cdata: Vec<Component>,
65}
66
67impl MetaVec for Components {
68    fn get_metavec(&self) -> &Vec<Meta> {
69        &self.cmeta
70    }
71    fn get_mut_metavec(&mut self) -> &mut Vec<Meta> {
72        &mut self.cmeta
73    }
74}
75
76impl fmt::Display for Components {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        let metalines = self
79            .cmeta
80            .iter()
81            .map(|v| format!("{}", v))
82            .collect::<Vec<_>>()
83            .join("\n");
84        let datalines = self
85            .cdata
86            .iter()
87            .map(|v| format!("{}", v))
88            .collect::<Vec<_>>()
89            .join("\n");
90        write!(f, "{}\n{}", metalines, datalines)
91    }
92}
93
94impl str::FromStr for Components {
95    type Err = EpbdError;
96
97    fn from_str(s: &str) -> Result<Components, Self::Err> {
98        let s_nobom = if s.starts_with("\u{feff}") {
99            &s[3..]
100        } else {
101            s
102        };
103        let lines: Vec<&str> = s_nobom.lines().map(str::trim).collect();
104        let metalines = lines
105            .iter()
106            .filter(|l| l.starts_with("#META") || l.starts_with("#CTE_"));
107        let datalines = lines
108            .iter()
109            .filter(|l| !(l.starts_with('#') || l.starts_with("vector,") || l.is_empty()));
110        let cmeta = metalines
111            .map(|e| e.parse())
112            .collect::<Result<Vec<Meta>, _>>()?;
113        let cdata = datalines
114            .map(|e| e.parse())
115            .collect::<Result<Vec<Component>, _>>()?;
116        {
117            let cdata_lens: Vec<_> = cdata.iter().map(|e| e.values.len()).collect();
118            if cdata_lens.iter().max().unwrap() != cdata_lens.iter().min().unwrap() {
119                return Err(EpbdError::ParseError(s.into()));
120            }
121        }
122        Ok(Components { cmeta, cdata })
123    }
124}
125
126impl Components {
127    /// Corrige los componentes de consumo y producción
128    ///
129    /// - Asegura que la energía MEDIOAMBIENTE consumida tiene su producción correspondiente
130    /// - Asegura que la energía eléctrica producida no tiene un uso que no sea NDEF
131    ///
132    /// Los metadatos, servicios y coherencia de los vectores se aseguran ya en el parsing
133    pub fn normalize(mut self) -> Self {
134        self.force_ndef_use_for_electricity_production();
135        self.compensate_env_use();
136        self
137    }
138
139    /// Filtra Componentes relacionados con un servicio EPB
140    ///
141    /// 1. Se seleccionan todos los consumos y producciones asignados al servicio
142    /// 2. Se toman las producciones eléctricas
143    /// 3. Reparto de las producciones eléctricas en proporción al consumo del servicio respecto al consumo EPB
144    ///
145    /// *Nota*: los componentes deben estar normalizados (ver método normalize) para asegurar que:
146    /// - los consumos de MEDIOAMBIENTE de un servicio ya están equilibrados
147    /// - las producciones eléctricas no pueden ser asignadas a un servicio
148    #[allow(non_snake_case)]
149    pub fn filter_by_epb_service(&self, service: Service) -> Self {
150        let num_steps = self.cdata[0].values.len(); // Pasos de cálculo
151        let cdata = self.cdata.iter(); // Componentes
152
153        // 1. Consumos y producciones del servicio, salvo la producción eléctrica
154        let mut cdata_srv: Vec<_> = cdata
155            .clone()
156            .filter(|c| {
157                c.service == service
158                    && !(c.carrier == Carrier::ELECTRICIDAD && c.ctype == CType::PRODUCCION)
159            })
160            .cloned()
161            .collect();
162
163        // 2. Producción eléctrica
164        let E_pr_el_t = cdata
165            .clone()
166            .filter(|c| c.carrier == Carrier::ELECTRICIDAD && c.ctype == CType::PRODUCCION);
167        let E_pr_el_an: f32 = E_pr_el_t.clone().flat_map(|c| c.values.iter()).sum();
168
169        // 3. Reparto de la producción electrica en proporción al consumo de usos EPB
170        // Energía eléctrica consumida en usos EPB
171        let E_EPus_el_t = cdata.clone().filter(|c| {
172            c.carrier == Carrier::ELECTRICIDAD
173                && c.ctype == CType::CONSUMO
174                && c.csubtype == CSubtype::EPB
175        });
176
177        // Energía eléctrica consumida en el servicio srv
178        let E_srv_el_t = E_EPus_el_t.clone().filter(|c| c.service == service);
179        let E_srv_el_an: f32 = E_srv_el_t.clone().flat_map(|c| c.values.iter()).sum();
180
181        // Si hay consumo y producción de electricidad, se reparte el consumo
182        if E_srv_el_an > 0.0 && E_pr_el_an > 0.0 {
183            // Energía eléctrica consumida en usos EPB
184            let E_EPus_el_t_tot = E_EPus_el_t
185                .clone()
186                .fold(vec![0.0; num_steps], |acc, e| vecvecsum(&acc, &e.values));
187
188            // Fracción del consumo EPB que representa el servicio srv
189            let E_srv_el_t_tot = E_srv_el_t
190                .clone()
191                .fold(vec![0.0; num_steps], |acc, e| vecvecsum(&acc, &e.values));
192            let f_srv_t = E_srv_el_t_tot
193                .iter()
194                .zip(&E_EPus_el_t_tot)
195                .map(|(v, t)| if v.abs() < f32::EPSILON { 0.0 } else { v / t })
196                .collect::<Vec<_>>();
197
198            // Repartimos la producción eléctrica
199
200            // Energía eléctrica producida y consumida en usos EPB, corregida por f_match_t
201            let f_match_t = vec![1.0; num_steps]; // TODO: implementar f_match_t
202            let E_pr_el_t_tot = E_pr_el_t
203                .clone()
204                .fold(vec![0.0; num_steps], |acc, e| vecvecsum(&acc, &e.values));
205            let E_pr_el_used_EPus_t =
206                vecvecmul(&f_match_t, &vecvecmin(&E_EPus_el_t_tot, &E_pr_el_t_tot));
207
208            // Para cada generador i
209            for mut E_pr_el_i in E_pr_el_t.cloned() {
210                // Fracción de la producción total que corresponde al generador i
211                let f_pr_el_i: f32 = E_pr_el_i.values.iter().sum::<f32>() / E_pr_el_an;
212
213                // Reparto proporcional a la producción del generador i y al consumo del servicio srv
214                E_pr_el_i.values = (&E_pr_el_used_EPus_t)
215                    .iter()
216                    .zip(&f_srv_t)
217                    .map(|(v, f_srv)| v * f_pr_el_i * f_srv)
218                    .collect();
219                E_pr_el_i.service = service;
220                E_pr_el_i.comment = format!(
221                    "{} Producción eléctrica reasignada al servicio",
222                    E_pr_el_i.comment
223                );
224                cdata_srv.push(E_pr_el_i);
225            }
226        }
227
228        let cmeta = self.cmeta.clone();
229        let mut newcomponents = Self {
230            cdata: cdata_srv,
231            cmeta,
232        };
233        newcomponents.set_meta("CTE_SERVICIO", &service.to_string());
234
235        newcomponents
236    }
237
238    /// Asegura que la energía eléctrica producida no tiene un uso que no sea NDEF
239    ///
240    /// Esta restricción es propia de la implementación y de cómo hace el reparto de la producción,
241    /// solamente en base al consumo de cada servicio y sin tener en cuenta si se define un destino
242    ///XXX: *Esta restricción debería eliminarse*
243    fn force_ndef_use_for_electricity_production(&mut self) {
244        // Localiza componentes de energía procedente del medioambiente
245        for component in &mut self.cdata {
246            if component.carrier == Carrier::ELECTRICIDAD && component.ctype == CType::PRODUCCION {
247                component.service = Service::NDEF
248            }
249        }
250    }
251
252    /// Asegura que la energía MEDIOAMBIENTE consumida está equilibrada por una producción in situ
253    ///
254    /// Completa el balance de las producciones in situ de energía procedente del medioambiente
255    /// cuando el consumo de esos vectores supera la producción. Es solamente una comodidad, para no
256    /// tener que declarar las producciones de MEDIOAMBIENTE, solo los consumos.
257    fn compensate_env_use(&mut self) {
258        // Localiza componentes de energía procedente del medioambiente
259        let envcomps: Vec<_> = self
260            .cdata
261            .iter()
262            .cloned()
263            .filter(|c| c.carrier == Carrier::MEDIOAMBIENTE)
264            .collect();
265        // Identifica servicios
266        let services: HashSet<_> = envcomps.iter().map(|c| c.service).collect();
267
268        // Asegura que la producción eléctrica no tiene un uso definido (es NDEF)
269
270        // Genera componentes de consumo no compensados con producción
271        let mut balancecomps: Vec<Component> = services
272            .iter()
273            .map(|&service| {
274                // Componentes para el servicio
275                let ecomps = envcomps.iter().filter(|c| c.service == service);
276                // Componentes de consumo del servicio
277                let consumed: Vec<_> = ecomps
278                    .clone()
279                    .filter(|c| c.ctype == CType::CONSUMO)
280                    .collect();
281                // Si no hay consumo que compensar con producción retornamos None
282                if consumed.is_empty() {
283                    return None;
284                };
285                // Consumos no compensados con producción
286                let mut unbalanced_values = veclistsum(
287                    &consumed
288                        .iter()
289                        .map(|&v| v.values.as_slice())
290                        .collect::<Vec<_>>(),
291                );
292                // Componentes de producción del servicio
293                let produced: Vec<_> = ecomps
294                    .clone()
295                    .filter(|c| c.ctype == CType::PRODUCCION)
296                    .collect();
297                // Descontamos la producción existente de los consumos
298                if !produced.is_empty() {
299                    let totproduced = veclistsum(
300                        &produced
301                            .iter()
302                            .map(|&v| v.values.as_slice())
303                            .collect::<Vec<_>>(),
304                    );
305                    unbalanced_values = vecvecdif(&unbalanced_values, &totproduced)
306                        .iter()
307                        .map(|&v| if v > 0.0 { v } else { 0.0 })
308                        .collect();
309                }
310                // Si no hay desequilibrio retornamos None
311                if unbalanced_values.iter().sum::<f32>() == 0.0 {
312                    return None;
313                };
314
315                // Si hay desequilibrio agregamos un componente de producción
316                Some(Component {
317                    carrier: Carrier::MEDIOAMBIENTE,
318                    ctype: CType::PRODUCCION,
319                    csubtype: CSubtype::INSITU,
320                    service,
321                    values: unbalanced_values,
322                    comment: "Equilibrado de consumo sin producción declarada".into(),
323                })
324            })
325            .filter(std::option::Option::is_some)
326            .collect::<Option<Vec<_>>>()
327            .unwrap_or_else(Vec::new);
328        // Agrega componentes no compensados
329        self.cdata.append(&mut balancecomps);
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336    use pretty_assertions::assert_eq;
337
338    const TCOMPS1: &str = "#META CTE_AREAREF: 100.5
339ELECTRICIDAD, PRODUCCION, INSITU, CAL, 8.20, 6.56, 4.10, 3.69, 2.05, 2.46, 3.28, 2.87, 2.05, 3.28, 4.92, 6.56
340ELECTRICIDAD, CONSUMO, EPB, REF, 16.39, 13.11, 8.20, 7.38, 4.10, 4.92, 6.56, 5.74, 4.10, 6.56, 9.84, 13.11
341ELECTRICIDAD, CONSUMO, EPB, CAL, 16.39, 13.11, 8.20, 7.38, 4.10, 4.92, 6.56, 5.74, 4.10, 6.56, 9.84, 13.11
342MEDIOAMBIENTE, CONSUMO, EPB, CAL, 6.39, 3.11, 8.20, 17.38, 4.10, 4.92, 6.56, 5.74, 4.10, 6.56, 9.84, 3.11";
343
344    // Se han puesto las producciones eléctricas a servicio NDEF y compensado consumos de MEDIOAMBIENTE
345    const TCOMPSRES1: &str = "#META CTE_AREAREF: 100.5
346ELECTRICIDAD, PRODUCCION, INSITU, NDEF, 8.20, 6.56, 4.10, 3.69, 2.05, 2.46, 3.28, 2.87, 2.05, 3.28, 4.92, 6.56
347ELECTRICIDAD, CONSUMO, EPB, REF, 16.39, 13.11, 8.20, 7.38, 4.10, 4.92, 6.56, 5.74, 4.10, 6.56, 9.84, 13.11
348ELECTRICIDAD, CONSUMO, EPB, CAL, 16.39, 13.11, 8.20, 7.38, 4.10, 4.92, 6.56, 5.74, 4.10, 6.56, 9.84, 13.11
349MEDIOAMBIENTE, CONSUMO, EPB, CAL, 6.39, 3.11, 8.20, 17.38, 4.10, 4.92, 6.56, 5.74, 4.10, 6.56, 9.84, 3.11
350MEDIOAMBIENTE, PRODUCCION, INSITU, CAL, 6.39, 3.11, 8.20, 17.38, 4.10, 4.92, 6.56, 5.74, 4.10, 6.56, 9.84, 3.11 # Equilibrado de consumo sin producción declarada";
351
352    // La producción se debe repartir al 50% entre los usos EPB
353    const TCOMPSRES2: &str = "#META CTE_AREAREF: 100.5
354#META CTE_SERVICIO: CAL
355ELECTRICIDAD, CONSUMO, EPB, CAL, 16.39, 13.11, 8.20, 7.38, 4.10, 4.92, 6.56, 5.74, 4.10, 6.56, 9.84, 13.11
356MEDIOAMBIENTE, CONSUMO, EPB, CAL, 6.39, 3.11, 8.20, 17.38, 4.10, 4.92, 6.56, 5.74, 4.10, 6.56, 9.84, 3.11
357MEDIOAMBIENTE, PRODUCCION, INSITU, CAL, 6.39, 3.11, 8.20, 17.38, 4.10, 4.92, 6.56, 5.74, 4.10, 6.56, 9.84, 3.11 # Equilibrado de consumo sin producción declarada
358ELECTRICIDAD, PRODUCCION, INSITU, CAL, 4.10, 3.28, 2.05, 1.85, 1.02, 1.23, 1.64, 1.43, 1.02, 1.64, 2.46, 3.28 #  Producción eléctrica reasignada al servicio";
359
360    // La producción se debe repartir al 50% entre los usos EPB y sin excesos
361    const TCOMPS2: &str = "#META CTE_AREAREF: 1.0
362ELECTRICIDAD, PRODUCCION, INSITU, NDEF, 2.00, 6.00, 2.00
363ELECTRICIDAD, CONSUMO, EPB, REF, 1.00, 1.00, 1.00
364ELECTRICIDAD, CONSUMO, EPB, CAL, 1.00, 2.00, 1.00
365MEDIOAMBIENTE, CONSUMO, EPB, CAL, 2.00, 2.00, 2.00";
366
367    const TCOMPSRES3: &str = "#META CTE_AREAREF: 1.0
368#META CTE_SERVICIO: CAL
369ELECTRICIDAD, CONSUMO, EPB, CAL, 1.00, 2.00, 1.00
370MEDIOAMBIENTE, CONSUMO, EPB, CAL, 2.00, 2.00, 2.00
371MEDIOAMBIENTE, PRODUCCION, INSITU, CAL, 2.00, 2.00, 2.00 # Equilibrado de consumo sin producción declarada
372ELECTRICIDAD, PRODUCCION, INSITU, CAL, 1.00, 2.00, 1.00 #  Producción eléctrica reasignada al servicio";
373
374    #[test]
375    fn tcomponents_parse() {
376        let tcomps = TCOMPS1.parse::<Components>().unwrap();
377        // roundtrip building from/to string
378        assert_eq!(tcomps.to_string(), TCOMPS1);
379    }
380
381    #[test]
382    fn tcomponents_normalize() {
383        let tcompsnorm = TCOMPS1.parse::<Components>().unwrap().normalize();
384        assert_eq!(tcompsnorm.to_string(), TCOMPSRES1);
385    }
386
387    #[test]
388    fn tcomponents_filter_by_epb_service() {
389        let tcompsnormfilt = TCOMPS1
390            .parse::<Components>()
391            .unwrap()
392            .normalize()
393            .filter_by_epb_service(Service::CAL);
394        assert_eq!(tcompsnormfilt.to_string(), TCOMPSRES2);
395    }
396
397    #[test]
398    fn tcomponents_filter_by_epb_service_prod_excess() {
399        let tcompsnormfilt = TCOMPS2
400            .parse::<Components>()
401            .unwrap()
402            .normalize()
403            .filter_by_epb_service(Service::CAL);
404        assert_eq!(tcompsnormfilt.to_string(), TCOMPSRES3);
405    }
406}