Skip to content

Commit 772d761

Browse files
authored
Merge pull request #46 from cepro/customer-rates-from-db
Support customer rates from database
2 parents 1a10ca1 + 759063b commit 772d761

File tree

11 files changed

+166
-49
lines changed

11 files changed

+166
-49
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "skypro"
3-
version = "1.0.0"
3+
version = "1.1.0"
44
description = "Skyprospector by Cepro"
55
authors = ["damonrand <[email protected]>"]
66
license = "AGPL-3.0"

src/skypro/commands/report/main.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,11 +296,19 @@ def report(
296296
time_index=time_index,
297297
rates_by_category=rates.mkt_fix,
298298
allow_vol_rates=False,
299+
allow_fix_rates=True,
299300
)
300-
customer_fixed_cost_dfs, customer_vol_rates_dfs = get_rates_dfs_by_type(
301+
_, customer_vol_rates_dfs = get_rates_dfs_by_type(
301302
time_index=time_index,
302-
rates_by_category=rates.customer,
303+
rates_by_category=rates.customer_vol,
303304
allow_vol_rates=True,
305+
allow_fix_rates=False,
306+
)
307+
customer_fixed_cost_dfs, _ = get_rates_dfs_by_type(
308+
time_index=time_index,
309+
rates_by_category=rates.customer_fix,
310+
allow_vol_rates=False,
311+
allow_fix_rates=True
304312
)
305313

306314
return Report(

src/skypro/commands/report/microgrid_flow_calcs.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,7 @@ def calculate_missing_net_flows_in_junction(
156156
pct_missing = (nans_df[col_to_predict].sum() / len(nans_df)) * 100
157157
df.loc[rows_to_predict, col_to_predict] = total * col_to_predict_direction
158158
notices.append(Notice(
159-
detail=f"{pct_missing:.1f}% of '{col_to_predict}' fields are missing, but {pct_to_fill:.1f}% can be calculated"
160-
" using redundant microgrid metering data",
159+
detail=f"{pct_missing:.1f}% of '{col_to_predict}' fields are missing, but {pct_to_fill:.1f}% can be calculated using redundant microgrid metering data",
161160
level=pct_to_notice_level(pct_missing)
162161
))
163162

src/skypro/commands/report/rates.py

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from skypro.common.data.get_timeseries import get_timeseries
99
from skypro.common.notice.notice import Notice
1010
from skypro.common.rate_utils.to_dfs import VolRatesForEnergyFlows
11-
from skypro.common.rates.rates import FixedRate, Rate
11+
from skypro.common.rates.rates import FixedRate, VolRate
1212
from skypro.common.timeutils.timeseries import get_steps_per_hh, get_step_size
1313

1414
from skypro.commands.report.config.config import Config
@@ -21,8 +21,9 @@ class ParsedRates:
2121
This is just a container to hold the various rate objects
2222
"""
2323
mkt_vol: VolRatesForEnergyFlows = field(default_factory=VolRatesForEnergyFlows) # Volume-based (p/kWh) market rates for each energy flow, as predicted in real-time
24-
mkt_fix: Dict[str, List[FixedRate]] = field(default_factory=dict) # Fixed p/day rates associated with market/suppliers, keyed by user-specified string which can be used to categorise
25-
customer: Dict[str, List[Rate]] = field(default_factory=dict) # Volume and fixed rates charged to customers, keyed by user-specified string which can be used to categorise
24+
mkt_fix: Dict[str, List[FixedRate]] = field(default_factory=dict) # Fixed p/day rates associated with market/suppliers, keyed by a string which can be used to categorise
25+
customer_vol: Dict[str, List[VolRate]] = field(default_factory=dict) # Volume rates charged to customers, keyed by a string which can be used to categorise
26+
customer_fix: Dict[str, List[FixedRate]] = field(default_factory=dict) # Fixed rates charged to customers, keyed by a string which can be used to categorise
2627

2728

2829
def get_rates_from_config(
@@ -62,27 +63,37 @@ def get_rates_from_config(
6263
notices.extend(missing_data_warnings(imbalance_pricing, "Elexon imbalance data"))
6364

6465
# Rates can either be read from the "rates database" or from local YAML files
65-
if config.reporting.rates.rates_db is not None:
66-
mkt_vol, fixed_import, fixed_export = get_rates_from_db(
67-
supply_points_name=config.reporting.rates.rates_db.supply_points_name,
68-
site_region=config.reporting.rates.rates_db.site_specific.region,
69-
site_bands=config.reporting.rates.rates_db.site_specific.bands,
70-
import_bundle_names=config.reporting.rates.rates_db.import_bundles,
71-
export_bundle_names=config.reporting.rates.rates_db.export_bundles,
66+
db_config = config.reporting.rates.rates_db
67+
if db_config is not None:
68+
db_rates = get_rates_from_db(
69+
supply_points_name=db_config.supply_points_name,
70+
site_region=db_config.site_specific.region,
71+
site_bands=db_config.site_specific.bands,
72+
import_bundle_names=db_config.import_bundles,
73+
export_bundle_names=db_config.export_bundles,
7274
db_engine=rates_db_engine,
7375
imbalance_pricing=imbalance_pricing["imbalance_price"],
7476
import_grid_capacity=config.reporting.grid_connection.import_capacity,
7577
export_grid_capacity=config.reporting.grid_connection.export_capacity,
76-
future_offset=timedelta(seconds=0)
78+
future_offset=timedelta(seconds=0),
79+
customer_import_bundle_names=db_config.customer.import_bundles if db_config.customer is not None else [],
80+
customer_export_bundle_names=db_config.customer.export_bundles if db_config.customer is not None else [],
7781
)
7882

7983
parsed_rates = ParsedRates(
80-
mkt_vol=mkt_vol,
84+
mkt_vol=db_rates.mkt_vol_by_flow,
8185
mkt_fix={
82-
"import": fixed_import,
83-
"export": fixed_export
86+
"import": db_rates.mkt_fix_import,
87+
"export": db_rates.mkt_fix_export,
88+
},
89+
customer_vol={
90+
"import": db_rates.customer_vol_import,
91+
"export": db_rates.customer_vol_export,
92+
},
93+
customer_fix={
94+
"import": db_rates.customer_fix_import,
95+
"export": db_rates.customer_fix_export,
8496
},
85-
customer={} # TODO: read customer rates from DB
8697
)
8798
else: # Read rates from local YAML files...
8899
# Parse the supply points config file:
@@ -117,11 +128,20 @@ def get_rates_from_config(
117128

118129
if exp_config.customer_load_files:
119130
for category_str, files in exp_config.customer_load_files.items():
120-
parsed_rates.customer[category_str] = parse_rate_files(
131+
rates = parse_rate_files(
121132
files=files,
122133
supply_points=supply_points,
123134
imbalance_pricing=None,
124135
file_path_resolver_func=file_path_resolver_func
125136
)
137+
parsed_rates.customer_fix[category_str] = []
138+
parsed_rates.customer_vol[category_str] = []
139+
for rate in rates:
140+
if isinstance(rate, FixedRate):
141+
parsed_rates.customer_fix[category_str].append(rate)
142+
elif isinstance(rate, VolRate):
143+
parsed_rates.customer_vol[category_str].append(rate)
144+
else:
145+
raise ValueError(f"Unknown rate type: {rate}")
126146

127147
return parsed_rates, notices

src/skypro/commands/report/warnings.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ def missing_data_warnings(df: pd.DataFrame, data_name: str) -> List[Notice]:
1515
return [
1616
Notice(
1717
detail=f"{pct:.1f}% of '{data_name}' data is missing ({num_missing} NaN fields)",
18-
level=pct_to_notice_level(pct)
19-
)
18+
level=pct_to_notice_level(pct),
19+
)
2020
]
2121

2222
return []

src/skypro/commands/simulator/main.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from skypro.common.rate_utils.to_dfs import get_vol_rates_dfs, get_rates_dfs_by_type, VolRatesForEnergyFlows
2323
from skypro.common.rate_utils.osam import calculate_osam_ncsp
24-
from skypro.common.rates.rates import FixedRate, Rate, OSAMFlatVolRate
24+
from skypro.common.rates.rates import FixedRate, VolRate, OSAMFlatVolRate
2525
from skypro.common.rate_utils.friendly_summary import get_friendly_rates_summary
2626
from skypro.common.timeutils.math import floor_hh
2727
from skypro.common.timeutils.timeseries import get_step_size
@@ -51,8 +51,9 @@ class ParsedRates:
5151
"""
5252
live_mkt_vol: VolRatesForEnergyFlows = field(default_factory=VolRatesForEnergyFlows) # Volume-based (p/kWh) market/supplier rates for each energy flow, as predicted in real-time
5353
final_mkt_vol: VolRatesForEnergyFlows = field(default_factory=VolRatesForEnergyFlows) # Volume-based (p/kWh) market/supplier rates for each energy flow
54-
mkt_fix: Dict[str, List[FixedRate]] = field(default_factory=dict) # Fixed p/day rates associated with market/suppliers
55-
customer: Dict[str, List[Rate]] = field(default_factory=dict) # Volume and fixed rates charged to customers, in string categories
54+
final_mkt_fix: Dict[str, List[FixedRate]] = field(default_factory=dict) # Fixed p/day rates associated with market/suppliers
55+
final_customer_vol: Dict[str, List[VolRate]] = field(default_factory=dict) # Volume rates charged to customers, in string categories
56+
final_customer_fix: Dict[str, List[FixedRate]] = field(default_factory=dict) # Fixed rates charged to customers, in string categories
5657

5758

5859
def simulate(
@@ -197,13 +198,21 @@ def _run_one_simulation(
197198
# not affect the algorithm, but which are passed through into the output CSV
198199
mkt_fixed_cost_dfs, _ = get_rates_dfs_by_type(
199200
time_index=time_index,
200-
rates_by_category=rates.mkt_fix,
201+
rates_by_category=rates.final_mkt_fix,
201202
allow_vol_rates=False,
203+
allow_fix_rates=True,
202204
)
203-
customer_fixed_cost_dfs, customer_vol_rates_dfs = get_rates_dfs_by_type(
205+
_, customer_vol_rates_dfs = get_rates_dfs_by_type(
204206
time_index=time_index,
205-
rates_by_category=rates.customer,
207+
rates_by_category=rates.final_customer_vol,
206208
allow_vol_rates=True,
209+
allow_fix_rates=False,
210+
)
211+
customer_fix_costs_dfs, _ = get_rates_dfs_by_type(
212+
time_index=time_index,
213+
rates_by_category=rates.final_customer_fix,
214+
allow_vol_rates=False,
215+
allow_fix_rates=True,
207216
)
208217

209218
# Generate an output file if configured to do so
@@ -217,7 +226,7 @@ def _run_one_simulation(
217226
int_live_vol_rates_dfs=None, # These 'live' rates aren't available in the output CSV at the moment as they are
218227
mkt_live_vol_rates_dfs=None, # calculated by the price curve algo internally and not returned
219228
mkt_fixed_costs_dfs=mkt_fixed_cost_dfs,
220-
customer_fixed_cost_dfs=customer_fixed_cost_dfs,
229+
customer_fixed_cost_dfs=customer_fix_costs_dfs,
221230
customer_vol_rates_dfs=customer_vol_rates_dfs,
222231
load_energy_breakdown_df=load_energy_breakdown_df,
223232
aggregate_timebase=simulation_output_config.aggregate,
@@ -255,7 +264,7 @@ def _run_one_simulation(
255264
int_live_vol_rates_dfs=None, # These 'live' rates aren't available in the output CSV at the moment as they are
256265
mkt_live_vol_rates_dfs=None, # calculated by the price curve algo internally and not returned
257266
mkt_fixed_costs_dfs=mkt_fixed_cost_dfs,
258-
customer_fixed_cost_dfs=customer_fixed_cost_dfs,
267+
customer_fixed_cost_dfs=customer_fix_costs_dfs,
259268
customer_vol_rates_dfs=customer_vol_rates_dfs,
260269
load_energy_breakdown_df=load_energy_breakdown_df,
261270
aggregate_timebase="all",
@@ -511,6 +520,8 @@ def read_imbalance_data(source: TimeseriesDataSource, context: str):
511520
import_grid_capacity=0,
512521
export_grid_capacity=0,
513522
future_offset=time_offset_str_to_timedelta(rates_config.live.rates_db.future_offset_str),
523+
customer_import_bundle_names=[],
524+
customer_export_bundle_names=[],
514525
)
515526
parsed_rates.final_mkt_vol, _, _ = get_rates_from_db(
516527
supply_points_name=rates_config.final.rates_db.supply_points_name,
@@ -523,8 +534,9 @@ def read_imbalance_data(source: TimeseriesDataSource, context: str):
523534
import_grid_capacity=0,
524535
export_grid_capacity=0,
525536
future_offset=time_offset_str_to_timedelta(rates_config.final.rates_db.future_offset_str),
537+
customer_import_bundle_names=rates_config.final.rates_db.customer.import_bundles if rates_config.final.rates_db.customer is not None else [],
538+
customer_export_bundle_names=rates_config.final.rates_db.customer.export_bundles if rates_config.final.rates_db.customer is not None else [],
526539
)
527-
# TODO: support fixed and customer costs when reading from the rates DB
528540

529541
else: # Read rates from local YAML files...
530542
final_supply_points = parse_supply_points(
@@ -560,16 +572,25 @@ def read_imbalance_data(source: TimeseriesDataSource, context: str):
560572
for rate in rates:
561573
if not isinstance(rate, FixedRate):
562574
raise ValueError(f"Only fixed rates can be specified in the fixedMarketFiles, got: '{rate.name}'")
563-
parsed_rates.mkt_fix[category_str] = cast(List[FixedRate], rates)
575+
parsed_rates.final_mkt_fix[category_str] = cast(List[FixedRate], rates)
564576

565577
if rates_config.final.experimental.customer_load_files:
566578
for category_str, files in rates_config.final.experimental.customer_load_files.items():
567-
parsed_rates.customer[category_str] = parse_rate_files(
579+
rates = parse_rate_files(
568580
files=files,
569581
supply_points=final_supply_points,
570582
imbalance_pricing=None,
571583
file_path_resolver_func=file_path_resolver_func
572584
)
585+
parsed_rates.final_customer_fix[category_str] = []
586+
parsed_rates.final_customer_vol[category_str] = []
587+
for rate in rates:
588+
if isinstance(rate, FixedRate):
589+
parsed_rates.final_customer_fix[category_str].append(rate)
590+
elif isinstance(rate, VolRate):
591+
parsed_rates.final_customer_vol[category_str].append(rate)
592+
else:
593+
raise ValueError(f"Unknown rate type: {rate}")
573594

574595
return parsed_rates, df
575596

src/skypro/common/config/data_source.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ class ProfileDataSource:
8989
constant_profile_data_source: Optional[ConstantProfileDataSource] = field_with_opts(key="constant")
9090

9191
def __post_init__(self):
92-
enforce_one_option([self.csv_profile_data_source, self.constant_profile_data_source],"'csvProfile', 'constant'")
92+
enforce_one_option([self.csv_profile_data_source, self.constant_profile_data_source], "'csvProfile', 'constant'")
9393

9494

9595
@dataclass

src/skypro/common/config/rates_dataclasses.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ class SiteSpecifier:
1919
bands: List[str]
2020

2121

22+
@dataclass
23+
class CustomerRatesDB:
24+
"""
25+
Configures rates for customers (i.e. domestic homes) to be pulled from a database
26+
"""
27+
import_bundles: List[str] = field_with_opts(key="importBundles") # Names of any import rate bundles to use for the customer load
28+
export_bundles: List[str] = field_with_opts(key="exportBundles") # Names of any export rate bundles to use for the customer export
29+
30+
2231
@dataclass
2332
class RatesDB:
2433
"""
@@ -29,6 +38,7 @@ class RatesDB:
2938
import_bundles: List[str] = field_with_opts(key="importBundles") # Names of any import rate bundles to use in addition to the site specific ones (e.g. Supplier arrangements)
3039
export_bundles: List[str] = field_with_opts(key="exportBundles") # Names of any export rate bundles to use in addition to the site specific ones (e.g. Supplier arrangements).
3140
future_offset_str: Optional[str] = field_with_opts(key="futureOffset") # For simulations, it can be useful to bring the rates forwards in time, for example we might want to use the 2025 rates for a simulation run over 2024
41+
customer: Optional[CustomerRatesDB] # Optionally define rates for customers - these are only really used for reporting purposes as this doesn't affect control algorithms
3242

3343

3444
@dataclass

0 commit comments

Comments
 (0)