Skip to content

Commit edf75c2

Browse files
committed
Batch: refactored to allow new filters; new tests, docs. Updated dependencies.
1 parent c1b7f52 commit edf75c2

File tree

8 files changed

+1517
-54
lines changed

8 files changed

+1517
-54
lines changed

altdss/Batch.py

Lines changed: 128 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from __future__ import annotations
12
import numpy as np
23
from typing import Union, List, AnyStr, Optional, Iterator
34
from dss.enums import DSSJSONFlags
@@ -131,15 +132,102 @@ class DSSBatch(Base, BatchCommon):
131132
'__weakref__',
132133
]
133134

135+
def batch(self, **kwargs) -> DSSBatch:
136+
'''
137+
Filter a batch using integer or float DSS properties, returning a new batch.
138+
139+
For integers, provide a single value to match.
140+
141+
For floats, provide a range as a 2-valued tuple/list (min value, max value), or an exact value to value (not recommended).
142+
143+
Multiple properties can be listed to allow filtering various conditions.
144+
145+
Example for loads:
146+
147+
```python
148+
# Create an initial batch using a regular expression
149+
abc_loads = altdss.Load.batch(re=r'^abc.*$') # a batch of all loads with names starting with "abc"
150+
abc_loads_filtered = abc_loads.batch(Class=1, Phases=1, kV=(0.1, 1.0))
151+
152+
# Create an initial batch, already filtered
153+
abc_loads_filtered = altdss.Load.batch(re=r'^abc.*$', Class=1, Phases=1, kV=(0.1, 1.0))
154+
```
155+
156+
'''
157+
return self.__class__(self._api_util, _clone_from=self, **kwargs)
158+
159+
def _filter(self, _existing=True, **kwargs):
160+
if not kwargs:
161+
return
162+
163+
batch_exists = _existing
164+
while kwargs:
165+
if batch_exists:
166+
# If a batch already exists, get its info
167+
prev_ptr, prev_count = self._get_ptr_cnt()
168+
169+
self._ptrptr = ptrptr = self._ffi.new('void***')
170+
self._countptr = countptr = self._ffi.new('int32_t[4]')
171+
172+
(prop_name, val) = kwargs.popitem()
173+
prop_idx = self._obj_cls._cls_prop_idx.get(prop_name.lower())
174+
if prop_idx is None:
175+
raise ValueError(f'Invalid property name "{prop_name}"')
176+
177+
if isinstance(val, int) and prop_idx in self._obj_cls._cls_int_idx:
178+
# Single integer value for an integer property
179+
if batch_exists:
180+
self._lib.Batch_FilterByInt32Property(ptrptr, countptr, prev_ptr, prev_count, prop_idx, val)
181+
else:
182+
self._lib.Batch_CreateByInt32Property(ptrptr, countptr, self._cls_idx, prop_idx, val)
183+
184+
prop_name = None
185+
elif prop_idx in self._obj_cls._cls_float_idx:
186+
if isinstance(val, LIST_LIKE) and len(val) == 2 and isinstance(val[0], (int, float)):
187+
# Range of values for a float property
188+
if batch_exists:
189+
self._lib.Batch_FilterByFloat64PropertyRange(ptrptr, countptr, prev_ptr, prev_count, prop_idx, *val)
190+
else:
191+
self._lib.Batch_CreateByFloat64PropertyRange(ptrptr, countptr, self._cls_idx, prop_idx, *val)
192+
193+
prop_name = None
194+
elif isinstance(val, (int, float)):
195+
# Single value for a float property
196+
if batch_exists:
197+
self._lib.Batch_FilterByFloat64PropertyRange(ptrptr, countptr, prev_ptr, prev_count, prop_idx, val, val)
198+
else:
199+
self._lib.Batch_CreateByFloat64PropertyRange(ptrptr, countptr, self._cls_idx, prop_idx, val, val)
200+
201+
prop_name = None
202+
203+
if prop_name is not None:
204+
raise ValueError(f'Property "{prop_name}" cannot be used to create a filtered batch with value {repr(val)}.')
205+
206+
self._wrap_ptr(ptrptr, countptr)
207+
self._check_for_error()
208+
batch_exists = True
209+
210+
# Track it only once, the other batches are discarded if temporary, or already tracked somewhere else.
211+
self._api_util.track_batch(self)
212+
213+
134214
def __init__(self, api_util, **kwargs):
135215
begin_edit = kwargs.pop('begin_edit', None)
136216
if begin_edit is None:
137217
begin_edit = True
138218

139-
if len(kwargs) > 1:
140-
raise ValueError('Exactly one argument is expected.')
219+
self._sync_cls_idx = kwargs.pop('sync_cls_idx', False)
220+
221+
new_batch_args = kwargs.keys() & {'new_names', 'new_count', }
222+
existing_batch_args = kwargs.keys() & {'from_func', 'sync_cls_idx', 'idx', 're', '_clone_from'}
223+
if len(new_batch_args) > 1:
224+
raise ValueError("Multiple ways to create a batch of new elements were provided.")
141225

142-
self._sync_cls_idx = kwargs.get('sync_cls_idx', False)
226+
if len(new_batch_args) > 0 and len(existing_batch_args):
227+
raise ValueError("Mixed batch definitions found. Cannot create new elements and use existing elements at the same time.")
228+
229+
if len(existing_batch_args) > 1:
230+
raise ValueError("Multiple ways to create a batch of existing elements were provided.")
143231

144232
if not self._sync_cls_idx:
145233
Base.__init__(self, api_util)
@@ -148,7 +236,33 @@ def __init__(self, api_util, **kwargs):
148236
self._ptrptr = ptrptr = self._ffi.new('void***')
149237
self._countptr = countptr = self._ffi.new('int32_t[4]')
150238

151-
if len(kwargs) == 0 or (len(kwargs) == 1 and self._sync_cls_idx):
239+
# Clone and filter?
240+
clone_source = kwargs.pop('_clone_from', None)
241+
if clone_source is not None:
242+
self._pointer, self._count = clone_source._get_ptr_cnt()
243+
self._filter(**kwargs)
244+
return
245+
246+
# Create new elements?
247+
248+
new_names = kwargs.pop('new_names', None)
249+
if new_names is not None:
250+
names, names_ptr, names_count = self._prepare_string_array(new_names)
251+
self._lib.Batch_CreateFromNew(ptrptr, countptr, self._cls_idx, names_ptr, names_count, begin_edit)
252+
self._wrap_ptr(ptrptr, countptr)
253+
self._check_for_error()
254+
return
255+
256+
new_count = kwargs.pop('new_count', None)
257+
if new_count is not None:
258+
self._lib.Batch_CreateFromNew(ptrptr, countptr, self._cls_idx, self._ffi.NULL, new_count, begin_edit)
259+
self._wrap_ptr(ptrptr, countptr)
260+
self._check_for_error()
261+
return
262+
263+
# Use a whole collection? Since no more kwargs, no filter is expected
264+
265+
if len(kwargs) == 0 or self._sync_cls_idx:
152266
if not self._sync_cls_idx:
153267
self._lib.Batch_CreateByClass(ptrptr, countptr, self._cls_idx)
154268
self._wrap_ptr(ptrptr, countptr)
@@ -161,72 +275,39 @@ def __init__(self, api_util, **kwargs):
161275
self._check_for_error()
162276
return
163277

164-
api_util.track_batch(self)
165-
from_func = kwargs.get('from_func')
278+
# Create from specified function, regexp, or list of indices?
279+
280+
from_func = kwargs.pop('from_func', None)
166281
if from_func is not None:
167282
func, *func_args = from_func
168283
func(ptrptr, countptr, *func_args)
169284
self._wrap_ptr(ptrptr, countptr)
170285
self._check_for_error()
286+
self._filter(**kwargs)
171287
return
172288

173-
new_names = kwargs.get('new_names')
174-
if new_names is not None:
175-
names, names_ptr, names_count = self._prepare_string_array(new_names)
176-
self._lib.Batch_CreateFromNew(ptrptr, countptr, self._cls_idx, names_ptr, names_count, begin_edit)
177-
self._wrap_ptr(ptrptr, countptr)
178-
self._check_for_error()
179-
return
180-
181-
new_count = kwargs.get('new_count')
182-
if new_count is not None:
183-
self._lib.Batch_CreateFromNew(ptrptr, countptr, self._cls_idx, self._ffi.NULL, new_count, begin_edit)
184-
self._wrap_ptr(ptrptr, countptr)
185-
self._check_for_error()
186-
return
187-
188-
regexp = kwargs.get('re')
289+
regexp = kwargs.pop('re', None)
189290
if regexp is not None:
190291
if not isinstance(regexp, bytes):
191292
regexp = regexp.encode(self._api_util.codec)
192293

193294
self._lib.Batch_CreateByRegExp(ptrptr, countptr, self._cls_idx, regexp)
194295
self._wrap_ptr(ptrptr, countptr)
195296
self._check_for_error()
297+
self._filter(**kwargs)
196298
return
197299

198-
idx = kwargs.get('idx')
300+
idx = kwargs.pop('idx', None)
199301
if idx is not None:
200302
idx, idx_ptr, idx_cnt = self._prepare_int32_array(np.asarray(idx) + 1)
201303
self._lib.Batch_CreateByIndex(ptrptr, countptr, self._cls_idx, idx_ptr, idx_cnt)
202304
self._wrap_ptr(ptrptr, countptr)
203305
self._check_for_error()
306+
self._filter(**kwargs)
204307
return
205-
206-
(prop_name, val), = kwargs.items()
207-
prop_idx = self._obj_cls._cls_prop_idx.get(prop_name.lower())
208-
if prop_idx is None:
209-
raise ValueError(f'Invalid property name "{prop_name}"')
210308

211-
if isinstance(val, int) and prop_idx in self._obj_cls._cls_int_idx:
212-
# Single integer value for an integer property
213-
self._lib.Batch_CreateByInt32Property(ptrptr, countptr, self._cls_idx, prop_idx, val)
214-
kwargs = None
215-
elif prop_idx in self._obj_cls._cls_float_idx:
216-
if isinstance(val, LIST_LIKE) and len(val) == 2 and isinstance(val[0], (int, float)):
217-
# Range of values for a float property
218-
self._lib.Batch_CreateByFloat64PropertyRange(ptrptr, countptr, self._cls_idx, prop_idx, *val)
219-
kwargs = None
220-
elif isinstance(val, (int, float)):
221-
# Single value for a float property
222-
self._lib.Batch_CreateByFloat64PropertyRange(ptrptr, countptr, self._cls_idx, prop_idx, val, val)
223-
kwargs = None
224-
225-
if kwargs is not None:
226-
raise ValueError(f'Property "{prop_name}" cannot be used to create a filtered batch with value {repr(val)}.')
227-
228-
self._wrap_ptr(ptrptr, countptr)
229-
self._check_for_error()
309+
# Apply filters on the base collection
310+
self._filter(_existing=False, **kwargs)
230311

231312
def begin_edit(self) -> None:
232313
'''

docs/Architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Most of the object lifetime rules also applies to buses, with the caveat that bu
1818

1919
## Batches
2020

21-
Batches of DSS objects and buses generalize some aspects of the API to get or set values in bulk.
21+
Batches of DSS objects and buses generalize some aspects of the API to get or set values in bulk. Check the [Batches example](#examples/Batches) for a walk-through.
2222

2323
### Array proxies
2424

docs/changelog.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ relevant. See [AltDSS/DSS C-API's repository](https://github.com/dss-extensions/
55

66
## 0.2.2
77

8-
This release includes a lot more tests, reaching 100% coverage of the *Python* code considering both the public and private tests. The engine was updated to AltDSS/DSS C-API 0.14.3 to include a couple of new features and fixes found during the tests.
8+
This release includes a lot more tests, reaching 100% coverage of the *Python* code of several important modules, considering both the public and private tests. The engine was updated to AltDSS/DSS C-API 0.14.3 to include a couple of new features and fixes found during the tests.
99

1010
- SystemY: in `AltDSS.SystemY`, return the matrix as a SciPy sparse matrix directly.
1111
- BusBatch: Add missing `Name` function (the bus names were already exposed in the individual objects and at circuit level in `AltDSS.BusNames`).
1212
- Batch, creation function `.batch(...)`:
1313
- Fix by list of indices (`...batch(idx=[0,1,10]`)
1414
- Add some basic type checks
1515
- Add support for selecting items by float property (`...batch(kW=0)` or `...batch(kV=[0.0, 1.0])`)
16+
- Add support for filtering multiple properties (`...batch(Phases=2, kV=[0.0, 1.0])`), and filtering existing batches.
1617
- CircuitElement/CircuitElementBatch: Complement doc strings; fix some type hints.
1718
- CircuitElementBatch:
1819
- Fix `MaxCurrent`. This will require the backend to be updated to v0.14.3.

0 commit comments

Comments
 (0)