Skip to content

Commit 49493a2

Browse files
authored
[Dev 2905] Add load override functionality for hosting capacity work packages (#29)
Signed-off-by: Jimmy Tung <jimmy.tung@zepben.com>
1 parent f36f277 commit 49493a2

4 files changed

Lines changed: 283 additions & 67 deletions

File tree

changelog.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
# EAS Python client
22
## [0.18.0] - UNRELEASED
33
### Breaking Changes
4-
* None.
4+
* Added `load_overrides` to both `FixedTime` and `TimePeriod` which consist of a list of `FixedTimeLoadOverride` and `TimePeriodLoadOverride`
5+
* `WorkPackageConfig` has some of its variables moved into the new classes `ForecastConfig` and `FeederConfig`.
6+
* Moved `feeders`, `years`, `scenarios` and `load_time`.
7+
* `WorkPackageConfig` now has a new variable `syf_config` consist of a Union of `ForecastConfig`, and list of `FeederConfig`.
8+
* This is to support feeder specific load override events
59

610
### New Features
711
* Update `ModelConfig` to contain an optional `transformer_tap_settings` field to specify a set of distribution transformer tap settings to be applied by the model-processor.
812
* Added basic client method to run a hosting capacity calibration and method to query its status.
913
* Added basic client method to run a hosting capacity work package cost estimation.
14+
* Added `FixedTimeLoadOverride` and `TimePeriodLoadOverride` class
1015

1116
### Enhancements
1217
* Added work package config documentation.

src/zepben/eas/client/eas_client.py

Lines changed: 82 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from zepben.eas.client.study import Study
1919
from zepben.eas.client.util import construct_url
20-
from zepben.eas.client.work_package import WorkPackageConfig, FixedTime, TimePeriod
20+
from zepben.eas.client.work_package import WorkPackageConfig, FixedTime, TimePeriod, ForecastConfig, FeederConfigs
2121

2222
__all__ = ["EasClient"]
2323

@@ -211,15 +211,46 @@ async def async_get_work_package_cost_estimation(self, work_package: WorkPackage
211211
"variables": {
212212
"workPackageName": work_package.name,
213213
"input": {
214-
"feeders": work_package.feeders,
215-
"years": work_package.years,
216-
"scenarios": work_package.scenarios,
217-
"fixedTime": work_package.load_time.time.isoformat()
218-
if isinstance(work_package.load_time, FixedTime) else None,
219-
"timePeriod": {
220-
"startTime": work_package.load_time.start_time.isoformat(),
221-
"endTime": work_package.load_time.end_time.isoformat(),
222-
} if isinstance(work_package.load_time, TimePeriod) else None,
214+
"feederConfigs": {
215+
"configs": [
216+
{
217+
"feeder": config.feeder,
218+
"years": config.years,
219+
"scenarios": config.scenarios,
220+
"timePeriod": {
221+
"startTime": config.load_time.start_time.isoformat(),
222+
"endTime": config.load_time.end_time.isoformat(),
223+
"overrides": config.load_time.load_overrides and {
224+
key: value.__dict__
225+
for key, value in config.load_time.load_overrides.items()}
226+
} if isinstance(config.load_time, TimePeriod) else None,
227+
"fixedTime": config.load_time and {
228+
"loadTime": config.load_time.time.isoformat(),
229+
"overrides": config.load_time.load_overrides and {
230+
key: value.__dict__
231+
for key, value in config.load_time.load_overrides.items()}
232+
} if isinstance(config.load_time, FixedTime) else None,
233+
} for config in work_package.syf_config.configs
234+
]
235+
} if isinstance(work_package.syf_config, FeederConfigs) else None,
236+
"forecastConfig": {
237+
"feeders": work_package.syf_config.feeders,
238+
"years": work_package.syf_config.years,
239+
"scenarios": work_package.syf_config.scenarios,
240+
"timePeriod": {
241+
"startTime": work_package.syf_config.load_time.start_time.isoformat(),
242+
"endTime": work_package.syf_config.load_time.end_time.isoformat(),
243+
"overrides": work_package.syf_config.load_time.load_overrides and {
244+
key: value.__dict__
245+
for key, value in work_package.syf_config.load_time.load_overrides.items()}
246+
} if isinstance(work_package.syf_config.load_time, TimePeriod) else None,
247+
"fixedTime": work_package.syf_config.load_time and {
248+
"loadTime": work_package.syf_config.load_time.fetch_load_time.isoformat(),
249+
"overrides": work_package.syf_config.load_time.load_overrides and {
250+
key: value.__dict__
251+
for key, value in work_package.syf_config.load_time.load_overrides.items()}
252+
} if isinstance(work_package.syf_config.load_time, FixedTime) else None
253+
} if isinstance(work_package.syf_config, ForecastConfig) else None,
223254
"qualityAssuranceProcessing": work_package.quality_assurance_processing,
224255
"generatorConfig": work_package.generator_config and {
225256
"model": work_package.generator_config.model and {
@@ -273,7 +304,7 @@ async def async_get_work_package_cost_estimation(self, work_package: WorkPackage
273304
"defaultGenWatts": work_package.generator_config.model.default_gen_watts,
274305
"defaultLoadVar": work_package.generator_config.model.default_load_var,
275306
"defaultGenVar": work_package.generator_config.model.default_gen_var,
276-
"transformerTapSettings": work_package.generator_config.model.transformer_tap_settings
307+
"transformerTapSettings": work_package.generator_config.model.transformer_tap_settings,
277308
},
278309
"solve": work_package.generator_config.solve and {
279310
"normVMinPu": work_package.generator_config.solve.norm_vmin_pu,
@@ -399,15 +430,46 @@ async def async_run_hosting_capacity_work_package(self, work_package: WorkPackag
399430
"variables": {
400431
"workPackageName": work_package.name,
401432
"input": {
402-
"feeders": work_package.feeders,
403-
"years": work_package.years,
404-
"scenarios": work_package.scenarios,
405-
"fixedTime": work_package.load_time.time.isoformat()
406-
if isinstance(work_package.load_time, FixedTime) else None,
407-
"timePeriod": {
408-
"startTime": work_package.load_time.start_time.isoformat(),
409-
"endTime": work_package.load_time.end_time.isoformat(),
410-
} if isinstance(work_package.load_time, TimePeriod) else None,
433+
"feederConfigs": {
434+
"configs": [
435+
{
436+
"feeder": config.feeder,
437+
"years": config.years,
438+
"scenarios": config.scenarios,
439+
"timePeriod": {
440+
"startTime": config.load_time.start_time.isoformat(),
441+
"endTime": config.load_time.end_time.isoformat(),
442+
"overrides": config.load_time.load_overrides and {
443+
key: value.__dict__
444+
for key, value in config.load_time.load_overrides.items()}
445+
} if isinstance(config.load_time, TimePeriod) else None,
446+
"fixedTime": config.load_time and {
447+
"loadTime": config.load_time.time.isoformat(),
448+
"overrides": config.load_time.load_overrides and {
449+
key: value.__dict__
450+
for key, value in config.load_time.load_overrides.items()}
451+
} if isinstance(config.load_time, FixedTime) else None,
452+
} for config in work_package.syf_config.configs
453+
]
454+
} if isinstance(work_package.syf_config, FeederConfigs) else None,
455+
"forecastConfig": {
456+
"feeders": work_package.syf_config.feeders,
457+
"years": work_package.syf_config.years,
458+
"scenarios": work_package.syf_config.scenarios,
459+
"timePeriod": {
460+
"startTime": work_package.syf_config.load_time.start_time.isoformat(),
461+
"endTime": work_package.syf_config.load_time.end_time.isoformat(),
462+
"overrides": work_package.syf_config.load_time.load_overrides and {
463+
key: value.__dict__
464+
for key, value in work_package.syf_config.load_time.load_overrides.items()}
465+
} if isinstance(work_package.syf_config.load_time, TimePeriod) else None,
466+
"fixedTime": work_package.syf_config.load_time and {
467+
"loadTime": work_package.syf_config.load_time.time.isoformat(),
468+
"overrides": work_package.syf_config.load_time.load_overrides and {
469+
key: value.__dict__
470+
for key, value in work_package.syf_config.load_time.load_overrides.items()}
471+
} if isinstance(work_package.syf_config.load_time, FixedTime) else None
472+
} if isinstance(work_package.syf_config, ForecastConfig) else None,
411473
"qualityAssuranceProcessing": work_package.quality_assurance_processing,
412474
"generatorConfig": work_package.generator_config and {
413475
"model": work_package.generator_config.model and {

src/zepben/eas/client/work_package.py

Lines changed: 128 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from dataclasses import dataclass
77
from datetime import datetime
88
from enum import Enum
9-
from typing import List, Optional, Union
9+
from typing import List, Optional, Union, Dict
1010

1111
__all__ = [
1212
"SwitchClass",
@@ -38,6 +38,11 @@
3838
"WriterOutputConfig",
3939
"WriterConfig",
4040
"YearRange",
41+
"FixedTimeLoadOverride",
42+
"TimePeriodLoadOverride",
43+
"ForecastConfig",
44+
"FeederConfig",
45+
"FeederConfigs",
4146
]
4247

4348

@@ -62,14 +67,89 @@ class SwitchMeterPlacementConfig:
6267
"""
6368

6469

70+
@dataclass
71+
class FixedTimeLoadOverride:
72+
73+
load_watts: Optional[float]
74+
"""
75+
The reading to be used to override load watts
76+
"""
77+
78+
gen_watts: Optional[float]
79+
"""
80+
The reading to be used to override gen watts
81+
"""
82+
83+
load_var: Optional[float]
84+
"""
85+
The reading to be used to override load var
86+
"""
87+
88+
gen_var: Optional[float]
89+
"""
90+
The reading to be used to override gen var
91+
"""
92+
93+
# def __str__(self):
94+
95+
96+
@dataclass
97+
class TimePeriodLoadOverride:
98+
99+
load_watts: Optional[List[float]]
100+
"""
101+
A list of readings to be used to override load watts.
102+
Can be either a yearly or daily profile.
103+
The number of entries must match the number of entries in load_var, and the expected number for the configured load_interval_length_hours.
104+
For load_interval_length_hours:
105+
0.25: 96 entries for daily and 35040 for yearly
106+
0.5: 48 entries for daily and 17520 for yearly
107+
1.0: 24 entries for daily and 8760 for yearly
108+
"""
109+
110+
gen_watts: Optional[List[float]]
111+
"""
112+
A list of readings to be used to override gen watts.
113+
Can be either a yearly or daily profile.
114+
The number of entries must match the number of entries in gen_var, and the expected number for the configured load_interval_length_hours.
115+
For load_interval_length_hours:
116+
0.25: 96 entries for daily and 35040 for yearly
117+
0.5: 48 entries for daily and 17520 for yearly
118+
1.0: 24 entries for daily and 8760 for yearly
119+
"""
120+
121+
load_var: Optional[List[float]]
122+
"""
123+
A list of readings to be used to override load var.
124+
Can be either a yearly or daily profile.
125+
The number of entries must match the number of entries in load_watts, and the expected number for the configured load_interval_length_hours.
126+
For load_interval_length_hours:
127+
0.25: 96 entries for daily and 35040 for yearly
128+
0.5: 48 entries for daily and 17520 for yearly
129+
1.0: 24 entries for daily and 8760 for yearly
130+
"""
131+
132+
gen_var: Optional[List[float]]
133+
"""
134+
A list of readings to be used to override gen var.
135+
Can be either a yearly or daily profile.
136+
The number of entries must match the number of entries in gen_watts, and the expected number for the configured load_interval_length_hours.
137+
For load_interval_length_hours:
138+
0.25: 96 entries for daily and 35040 for yearly
139+
0.5: 48 entries for daily and 17520 for yearly
140+
1.0: 24 entries for daily and 8760 for yearly
141+
"""
142+
143+
65144
class FixedTime:
66145
"""
67146
A single point in time to model. Should be precise to the minute, and load data must be
68147
present for the provided time in the load database for accurate results.
69148
"""
70149

71-
def __init__(self, time: datetime):
150+
def __init__(self, time: datetime, load_overrides: Optional[Dict[str, FixedTimeLoadOverride]] = None):
72151
self.time = time.replace(tzinfo=None)
152+
self.load_overrides = load_overrides
73153

74154

75155
class TimePeriod:
@@ -82,11 +162,13 @@ class TimePeriod:
82162
def __init__(
83163
self,
84164
start_time: datetime,
85-
end_time: datetime
165+
end_time: datetime,
166+
load_overrides: Optional[Dict[str, TimePeriodLoadOverride]] = None
86167
):
87168
self._validate(start_time, end_time)
88169
self.start_time = start_time.replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=None)
89170
self.end_time = end_time.replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=None)
171+
self.load_overrides = load_overrides
90172

91173
@staticmethod
92174
def _validate(start_time: datetime, end_time: datetime):
@@ -688,9 +770,7 @@ class InterventionConfig:
688770

689771

690772
@dataclass
691-
class WorkPackageConfig:
692-
""" A data class representing the configuration for a hosting capacity work package """
693-
name: str
773+
class ForecastConfig(object):
694774
feeders: List[str]
695775
"""The feeders to process in this work package"""
696776

@@ -712,6 +792,48 @@ class WorkPackageConfig:
712792
result in inaccurate results.
713793
"""
714794

795+
796+
@dataclass
797+
class FeederConfig(object):
798+
feeder: str
799+
"""The feeder to process in this work package"""
800+
801+
years: List[int]
802+
"""
803+
The years to process for the specified feeders in this work package.
804+
The years should be configured in the input database forecasts for all supplied scenarios.
805+
"""
806+
807+
scenarios: List[str]
808+
"""
809+
The scenarios to model. These should be configured in the input.scenario_configuration table.
810+
"""
811+
812+
load_time: Union[TimePeriod, FixedTime]
813+
"""
814+
The time to use for the base load data. The provided time[s] must be available in the
815+
load database for accurate results. Specifying an invalid time (i.e one with no load data) will
816+
result in inaccurate results.
817+
"""
818+
819+
820+
@dataclass
821+
class FeederConfigs(object):
822+
configs: list[FeederConfig]
823+
"""The feeder to process in this work package"""
824+
825+
826+
@dataclass
827+
class WorkPackageConfig:
828+
""" A data class representing the configuration for a hosting capacity work package """
829+
name: str
830+
syf_config: Union[ForecastConfig, FeederConfigs]
831+
"""
832+
The configuration of the scenario, years, and feeders to run. Use ForecastConfig
833+
for the same scenarios and years applied across all feeders, and the more in depth FeederConfig
834+
if configuration varies per feeder.
835+
"""
836+
715837
quality_assurance_processing: Optional[bool] = None
716838
"""Whether to enable QA processing"""
717839

0 commit comments

Comments
 (0)