Skip to content

Commit baf0fef

Browse files
authored
Add consumer formula generator not accounting for phantom loads (#31)
Meters with successors can still have loads not represented in the component graph. These are called phantom loads. Tracking phantom loads requires larger formulas and expects all meters to be available. This PR implements a consumer formula generator as it was implemented in the SDK - exclude producer and battery powers from the grid power, or when there is no grid meter, add all the non battery and producer meters. This PR also changes a number of default settings: - Prefer consumer power without phantom loads, the original approach is still available through a config flag. - Prefer meters for all component category formulas
2 parents 27778d3 + e12b153 commit baf0fef

File tree

13 files changed

+839
-207
lines changed

13 files changed

+839
-207
lines changed

RELEASE_NOTES.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
# Frequenz Component Graph Release Notes
22

3-
## Summary
3+
## Upgrading
44

5-
<!-- Here goes a general summary of what this release is about -->
5+
- The PV, battery, CHP, EV and Wind Turbine formulas now prefer meters as the primary components and fallback to inverters or other corresponding components only when the meters are not available.
66

7-
## Upgrading
7+
This makes a big difference in performance when there are multiple PV inverters behind a single meter, for example.
88

9-
<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->
9+
This behaviour can be changed with the newly introduced meter preference config flags.
1010

11-
## New Features
11+
- Consumer formulas don't consider phantom loads by default anymore. The original behaviour is still available through a config flag.
1212

13-
<!-- Here goes the main new features and examples or instructions on how to use them -->
13+
## New Features
1414

15-
## Bug Fixes
15+
- This introduces a new consumer formula generator that doesn't consider phantom loads.
1616

17-
<!-- Here goes notable bug fixes that are worth a special mention or explanation -->
17+
Meters with successors can still have loads not represented in the component graph. These are called phantom loads.

src/config.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,42 @@ pub struct ComponentGraphConfig {
2323
/// Whether to disable fallback components in generated formulas. When this
2424
/// is `true`, the formulas will not include fallback components.
2525
pub disable_fallback_components: bool,
26+
27+
/// Meters with successors can still have loads not represented in the
28+
/// component graph. These are called phantom loads.
29+
///
30+
/// When this is `true`, phantom loads are included in formulas by excluding
31+
/// the measurements of successor meters from the measurements of their
32+
/// predecessor meters.
33+
///
34+
/// When `false`, consumer formula is generated by excluding production
35+
/// and battery components from the grid measurements.
36+
pub include_phantom_loads_in_consumer_formula: bool,
37+
38+
/// Whether to prefer PV inverters when generating PV formulas. When this
39+
/// is `true`, PV inverters will be the primary source and PV meters will be
40+
/// the fallback. When `false`, PV meters will be the primary source.
41+
pub prefer_inverters_in_pv_formula: bool,
42+
43+
/// Whether to prefer battery inverters when generating Battery formulas.
44+
/// When this is `true`, battery inverters will be the primary source and
45+
/// battery meters will be secondary. When `false`, battery meters will be
46+
/// the primary source.
47+
pub prefer_inverters_in_battery_formula: bool,
48+
49+
/// Whether to prefer CHP when generating CHP formulas. When this is
50+
/// `true`, CHP units will be the primary source and CHP meters will be
51+
/// secondary. When `false`, CHP meters will be the primary source.
52+
pub prefer_chp_in_chp_formula: bool,
53+
54+
/// Whether to prefer EV chargers when generating EV formulas. When this
55+
/// is `true`, EV chargers will be the primary source and EV meters will be
56+
/// secondary. When `false`, EV meters will be the primary source.
57+
pub prefer_ev_chargers_in_ev_formula: bool,
58+
59+
/// Whether to prefer wind turbines when generating Wind formulas. When
60+
/// this is `true`, wind turbines will be the primary source and wind meters
61+
/// will be secondary. When `false`, wind meters will be the primary
62+
/// source.
63+
pub prefer_wind_turbines_in_wind_formula: bool,
2664
}

src/graph/formulas/fallback.rs

Lines changed: 117 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,26 @@ use std::collections::BTreeSet;
99

1010
use super::expr::Expr;
1111

12+
#[derive(Default)]
1213
pub(crate) struct FallbackExpr {
13-
pub(crate) prefer_meters: bool,
14-
pub(crate) meter_fallback_for_meters: bool,
14+
prefer_meters: bool,
15+
meter_fallback_for_meters: bool,
16+
}
17+
18+
impl FallbackExpr {
19+
pub fn new() -> Self {
20+
Self::default()
21+
}
22+
23+
pub fn prefer_meters(mut self, prefer: bool) -> Self {
24+
self.prefer_meters = prefer;
25+
self
26+
}
27+
28+
pub fn meter_fallback_for_meters(mut self, enable: bool) -> Self {
29+
self.meter_fallback_for_meters = enable;
30+
self
31+
}
1532
}
1633

1734
impl FallbackExpr {
@@ -84,6 +101,21 @@ impl FallbackExpr {
84101
return Ok(Some(Expr::component(component_id)));
85102
}
86103

104+
// If a meter fallback for a meter exists, make sure it is not a component meter.
105+
if self.meter_fallback_for_meters && has_successor_meters {
106+
// we've already established that if there are successor meters,
107+
// then there's only one successor.
108+
let successor = graph
109+
.successors(component_id)?
110+
.find(|node| node.is_meter())
111+
.ok_or(Error::internal(
112+
"Can't find successor meter of component with successor meters.",
113+
))?;
114+
if graph.is_component_meter(successor.component_id())? {
115+
return Ok(Some(Expr::component(component_id)));
116+
}
117+
}
118+
87119
let mut coalesced = Expr::component(component_id);
88120

89121
if !self.prefer_meters {
@@ -203,76 +235,56 @@ mod tests {
203235
assert_eq!(meter_bat_chain.component_id(), 2);
204236

205237
let graph = builder.build(None)?;
206-
let expr = FallbackExpr {
207-
prefer_meters: true,
208-
meter_fallback_for_meters: true,
209-
}
210-
.generate(&graph, BTreeSet::from([1]))?;
211-
assert_eq!(expr.to_string(), "COALESCE(#1, #2)");
212-
213-
let expr = FallbackExpr {
214-
prefer_meters: true,
215-
meter_fallback_for_meters: true,
216-
}
217-
.generate(&graph, BTreeSet::from([1, 2]))?;
218-
assert_eq!(expr.to_string(), "COALESCE(#1, #2) + COALESCE(#2, #3, 0.0)");
238+
let expr = FallbackExpr::new()
239+
.prefer_meters(true)
240+
.meter_fallback_for_meters(true)
241+
.generate(&graph, BTreeSet::from([1]))?;
242+
assert_eq!(expr.to_string(), "#1");
243+
244+
let expr = FallbackExpr::new()
245+
.prefer_meters(true)
246+
.meter_fallback_for_meters(true)
247+
.generate(&graph, BTreeSet::from([1, 2]))?;
248+
assert_eq!(expr.to_string(), "#1 + COALESCE(#2, #3, 0.0)");
219249

220-
let expr = FallbackExpr {
221-
prefer_meters: false,
222-
meter_fallback_for_meters: false,
223-
}
224-
.generate(&graph, BTreeSet::from([1, 2]))?;
250+
let expr = FallbackExpr::new().generate(&graph, BTreeSet::from([1, 2]))?;
225251
assert_eq!(expr.to_string(), "#1 + COALESCE(#3, #2, 0.0)");
226252

227-
let expr = FallbackExpr {
228-
prefer_meters: true,
229-
meter_fallback_for_meters: false,
230-
}
231-
.generate(&graph, BTreeSet::from([1, 2]))?;
253+
let expr = FallbackExpr::new()
254+
.prefer_meters(true)
255+
.generate(&graph, BTreeSet::from([1, 2]))?;
232256
assert_eq!(expr.to_string(), "#1 + COALESCE(#2, #3, 0.0)");
233257

234-
let expr = FallbackExpr {
235-
prefer_meters: true,
236-
meter_fallback_for_meters: false,
237-
}
238-
.generate(&graph, BTreeSet::from([3]))?;
258+
let expr = FallbackExpr::new()
259+
.prefer_meters(true)
260+
.generate(&graph, BTreeSet::from([3]))?;
239261
assert_eq!(expr.to_string(), "COALESCE(#2, #3, 0.0)");
240-
let expr = FallbackExpr {
241-
prefer_meters: true,
242-
meter_fallback_for_meters: true,
243-
}
244-
.generate(&graph, BTreeSet::from([3]))?;
262+
let expr = FallbackExpr::new()
263+
.prefer_meters(true)
264+
.meter_fallback_for_meters(true)
265+
.generate(&graph, BTreeSet::from([3]))?;
245266
assert_eq!(expr.to_string(), "COALESCE(#2, #3, 0.0)");
246-
let expr = FallbackExpr {
247-
prefer_meters: true,
248-
meter_fallback_for_meters: true,
249-
}
250-
.generate(&graph, BTreeSet::from([2]))?;
267+
let expr = FallbackExpr::new()
268+
.prefer_meters(true)
269+
.meter_fallback_for_meters(true)
270+
.generate(&graph, BTreeSet::from([2]))?;
251271
assert_eq!(expr.to_string(), "COALESCE(#2, #3, 0.0)");
252272

253273
let graph = builder.build(Some(ComponentGraphConfig {
254274
disable_fallback_components: true,
255275
..Default::default()
256276
}))?;
257-
let expr = FallbackExpr {
258-
prefer_meters: false,
259-
meter_fallback_for_meters: false,
260-
}
261-
.generate(&graph, BTreeSet::from([1, 2]))?;
277+
let expr = FallbackExpr::new().generate(&graph, BTreeSet::from([1, 2]))?;
262278
assert_eq!(expr.to_string(), "#1 + #2");
263279

264-
let expr = FallbackExpr {
265-
prefer_meters: true,
266-
meter_fallback_for_meters: false,
267-
}
268-
.generate(&graph, BTreeSet::from([1, 2]))?;
280+
let expr = FallbackExpr::new()
281+
.prefer_meters(true)
282+
.generate(&graph, BTreeSet::from([1, 2]))?;
269283
assert_eq!(expr.to_string(), "#1 + #2");
270284

271-
let expr = FallbackExpr {
272-
prefer_meters: true,
273-
meter_fallback_for_meters: false,
274-
}
275-
.generate(&graph, BTreeSet::from([3]))?;
285+
let expr = FallbackExpr::new()
286+
.prefer_meters(true)
287+
.generate(&graph, BTreeSet::from([3]))?;
276288
assert_eq!(expr.to_string(), "#3");
277289

278290
// Add a battery meter with three inverter and three batteries
@@ -282,11 +294,7 @@ mod tests {
282294
assert_eq!(meter_bat_chain.component_id(), 5);
283295

284296
let graph = builder.build(None)?;
285-
let expr = FallbackExpr {
286-
prefer_meters: false,
287-
meter_fallback_for_meters: false,
288-
}
289-
.generate(&graph, BTreeSet::from([3, 5]))?;
297+
let expr = FallbackExpr::new().generate(&graph, BTreeSet::from([3, 5]))?;
290298
assert_eq!(
291299
expr.to_string(),
292300
concat!(
@@ -299,11 +307,9 @@ mod tests {
299307
)
300308
);
301309

302-
let expr = FallbackExpr {
303-
prefer_meters: true,
304-
meter_fallback_for_meters: false,
305-
}
306-
.generate(&graph, BTreeSet::from([2, 5]))?;
310+
let expr = FallbackExpr::new()
311+
.prefer_meters(true)
312+
.generate(&graph, BTreeSet::from([2, 5]))?;
307313
assert_eq!(
308314
expr.to_string(),
309315
concat!(
@@ -312,11 +318,9 @@ mod tests {
312318
)
313319
);
314320

315-
let expr = FallbackExpr {
316-
prefer_meters: true,
317-
meter_fallback_for_meters: false,
318-
}
319-
.generate(&graph, BTreeSet::from([2, 6, 7, 8]))?;
321+
let expr = FallbackExpr::new()
322+
.prefer_meters(true)
323+
.generate(&graph, BTreeSet::from([2, 6, 7, 8]))?;
320324
assert_eq!(
321325
expr.to_string(),
322326
concat!(
@@ -325,11 +329,9 @@ mod tests {
325329
)
326330
);
327331

328-
let expr = FallbackExpr {
329-
prefer_meters: true,
330-
meter_fallback_for_meters: false,
331-
}
332-
.generate(&graph, BTreeSet::from([2, 7, 8]))?;
332+
let expr = FallbackExpr::new()
333+
.prefer_meters(true)
334+
.generate(&graph, BTreeSet::from([2, 7, 8]))?;
333335
assert_eq!(
334336
expr.to_string(),
335337
"COALESCE(#2, #3, 0.0) + COALESCE(#7, 0.0) + COALESCE(#8, 0.0)"
@@ -339,32 +341,22 @@ mod tests {
339341
disable_fallback_components: true,
340342
..Default::default()
341343
}))?;
342-
let expr = FallbackExpr {
343-
prefer_meters: false,
344-
meter_fallback_for_meters: false,
345-
}
346-
.generate(&graph, BTreeSet::from([3, 5]))?;
344+
let expr = FallbackExpr::new().generate(&graph, BTreeSet::from([3, 5]))?;
347345
assert_eq!(expr.to_string(), "#3 + #5");
348346

349-
let expr = FallbackExpr {
350-
prefer_meters: true,
351-
meter_fallback_for_meters: false,
352-
}
353-
.generate(&graph, BTreeSet::from([2, 5]))?;
347+
let expr = FallbackExpr::new()
348+
.prefer_meters(true)
349+
.generate(&graph, BTreeSet::from([2, 5]))?;
354350
assert_eq!(expr.to_string(), "#2 + #5");
355351

356-
let expr = FallbackExpr {
357-
prefer_meters: true,
358-
meter_fallback_for_meters: false,
359-
}
360-
.generate(&graph, BTreeSet::from([2, 6, 7, 8]))?;
352+
let expr = FallbackExpr::new()
353+
.prefer_meters(true)
354+
.generate(&graph, BTreeSet::from([2, 6, 7, 8]))?;
361355
assert_eq!(expr.to_string(), "#2 + #6 + #7 + #8");
362356

363-
let expr = FallbackExpr {
364-
prefer_meters: true,
365-
meter_fallback_for_meters: false,
366-
}
367-
.generate(&graph, BTreeSet::from([2, 7, 8]))?;
357+
let expr = FallbackExpr::new()
358+
.prefer_meters(true)
359+
.generate(&graph, BTreeSet::from([2, 7, 8]))?;
368360
assert_eq!(expr.to_string(), "#2 + #7 + #8");
369361

370362
let meter = builder.meter();
@@ -379,11 +371,9 @@ mod tests {
379371
assert_eq!(pv_inverter.component_id(), 14);
380372

381373
let graph = builder.build(None)?;
382-
let expr = FallbackExpr {
383-
prefer_meters: true,
384-
meter_fallback_for_meters: false,
385-
}
386-
.generate(&graph, BTreeSet::from([5, 12]))?;
374+
let expr = FallbackExpr::new()
375+
.prefer_meters(true)
376+
.generate(&graph, BTreeSet::from([5, 12]))?;
387377
assert_eq!(
388378
expr.to_string(),
389379
concat!(
@@ -392,11 +382,7 @@ mod tests {
392382
)
393383
);
394384

395-
let expr = FallbackExpr {
396-
prefer_meters: false,
397-
meter_fallback_for_meters: false,
398-
}
399-
.generate(&graph, BTreeSet::from([7, 14]))?;
385+
let expr = FallbackExpr::new().generate(&graph, BTreeSet::from([7, 14]))?;
400386
assert_eq!(expr.to_string(), "COALESCE(#7, 0.0) + COALESCE(#14, 0.0)");
401387

402388
Ok(())
@@ -423,4 +409,30 @@ mod tests {
423409
let expr = graph.pv_formula(None).unwrap().to_string();
424410
assert_eq!(expr, "COALESCE(#1, 0.0) + COALESCE(#2, 0.0)");
425411
}
412+
413+
/// Test meters with meter fallback
414+
#[test]
415+
fn test_meters_with_meter_fallback() -> Result<(), Error> {
416+
let mut builder = ComponentGraphBuilder::new();
417+
let grid = builder.grid();
418+
419+
let meter1 = builder.meter();
420+
let meter2 = builder.meter();
421+
let bat_chain = builder.meter_bat_chain(1, 1);
422+
let pv_chain = builder.meter_pv_chain(1);
423+
424+
builder.connect(grid, meter1);
425+
builder.connect(meter1, meter2);
426+
builder.connect(meter2, bat_chain);
427+
builder.connect(meter2, pv_chain);
428+
429+
let graph = builder.build(None)?;
430+
let expr = FallbackExpr::new()
431+
.prefer_meters(true)
432+
.meter_fallback_for_meters(true)
433+
.generate(&graph, BTreeSet::from([meter1.component_id()]))?;
434+
assert_eq!(expr.to_string(), "COALESCE(#1, #2)");
435+
436+
Ok(())
437+
}
426438
}

src/graph/formulas/formula.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ where
4343
// evaluator.
4444
#[derive(Debug, Clone, PartialEq)]
4545
pub struct AggregationFormula {
46-
expr: Expr,
46+
pub(crate) expr: Expr,
4747
}
4848

4949
impl From<Expr> for AggregationFormula {

0 commit comments

Comments
 (0)