Skip to content

Actions#

Actions are a way to mutate an object, typically used for defining retrofits for building energy models in this library.

Actions to modify a library object.

Action #

Bases: BaseModel, Generic[T]

An action to modify a library object.

This base class should be inherited by classes that represent actions to modify a library object. It provides an abstract method run that should be implemented by subclasses to perform the modification.

Source code in epinterface\actions.py
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
class Action(BaseModel, Generic[T]):
    """An action to modify a library object.

    This base class should be inherited by classes that represent actions to modify
    a library object. It provides an abstract method `run` that should be implemented
    by subclasses to perform the modification.
    """

    target: ParameterPath[T] = Field(
        ..., description="The path to the parameter to modify."
    )
    priority: Priority | None = Field(
        default=None,
        description="The priority of the action (low will execute if the new value is less than the old value).",
    )

    def run(self, lib: LibT) -> LibT:
        """Run the action to modify the library object.

        Args:
            lib (LibT): The library object to modify.

        Returns:
            lib (LibT): The modified library object.
        """
        new_val = self.new_val(lib)
        original_val = self.get_original_val(lib)
        if self.check_priority(original_val, new_val):
            original_obj = self.get_original_obj(lib)
            key = self.original_key
            set_dict_val_or_attr(original_obj, key, new_val)
        return lib

    def check_priority(self, original: T, new: T) -> bool:
        """Check if the new value should be applied based on the action priority.

        Args:
            original (T): The original value in the library object.
            new (T): The new value to apply.

        Returns:
            apply (bool): True if the new value should be applied, False otherwise.
        """
        if self.priority is None:
            return True

        if not isinstance(original, int | float) or not isinstance(new, int | float):
            msg = "priority comparison only supported for numerical values."
            raise TypeError(msg)

        if self.priority == "low":
            return original > new

        elif self.priority == "high":
            return original < new
        else:
            msg = f"Invalid priority value: {self.priority}"
            raise ValueError(msg)

    def get_original_val(self, lib: LibT) -> T:
        """Retrieve the original value from the library object.

        Args:
            lib (LibT): The library object from which to retrieve the original value.

        Returns:
            val (T): The original value from the library object.
        """
        return self.target.get_lib_val(lib)

    @property
    def original_key(self) -> str | int | ParameterPath:
        """Retrieve the key of the original value in the library object.

        Returns:
            key (str | int | ParameterPath): The key of the original value in the library object.
        """
        # TODO: handle cases where final key is a ParameterPath!!
        return self.target.path[-1]

    def get_original_obj(self, lib: LibT):
        """Retrieve the object containing the original value in the library object.

        Args:
            lib (LibT): The library object from which to retrieve the original object.

        Returns:
            obj (Any): The object containing the original value in the library object.
        """
        return self.target.parent_path.get_lib_val(lib)

    @abstractmethod
    def new_val(self, lib: LibT) -> T:
        """Calculate the new value to apply to the library object.

        NB: This method should be implemented by subclasses to calculate the new value.

        Args:
            lib (LibT): The library object on which to apply the new value.

        Returns:
            val (T): The new value to apply to the library object.
        """
        pass

original_key: str | int | ParameterPath property #

Retrieve the key of the original value in the library object.

Returns:

Name Type Description
key str | int | ParameterPath

The key of the original value in the library object.

check_priority(original, new) #

Check if the new value should be applied based on the action priority.

Parameters:

Name Type Description Default
original T

The original value in the library object.

required
new T

The new value to apply.

required

Returns:

Name Type Description
apply bool

True if the new value should be applied, False otherwise.

Source code in epinterface\actions.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def check_priority(self, original: T, new: T) -> bool:
    """Check if the new value should be applied based on the action priority.

    Args:
        original (T): The original value in the library object.
        new (T): The new value to apply.

    Returns:
        apply (bool): True if the new value should be applied, False otherwise.
    """
    if self.priority is None:
        return True

    if not isinstance(original, int | float) or not isinstance(new, int | float):
        msg = "priority comparison only supported for numerical values."
        raise TypeError(msg)

    if self.priority == "low":
        return original > new

    elif self.priority == "high":
        return original < new
    else:
        msg = f"Invalid priority value: {self.priority}"
        raise ValueError(msg)

get_original_obj(lib) #

Retrieve the object containing the original value in the library object.

Parameters:

Name Type Description Default
lib LibT

The library object from which to retrieve the original object.

required

Returns:

Name Type Description
obj Any

The object containing the original value in the library object.

Source code in epinterface\actions.py
192
193
194
195
196
197
198
199
200
201
def get_original_obj(self, lib: LibT):
    """Retrieve the object containing the original value in the library object.

    Args:
        lib (LibT): The library object from which to retrieve the original object.

    Returns:
        obj (Any): The object containing the original value in the library object.
    """
    return self.target.parent_path.get_lib_val(lib)

get_original_val(lib) #

Retrieve the original value from the library object.

Parameters:

Name Type Description Default
lib LibT

The library object from which to retrieve the original value.

required

Returns:

Name Type Description
val T

The original value from the library object.

Source code in epinterface\actions.py
171
172
173
174
175
176
177
178
179
180
def get_original_val(self, lib: LibT) -> T:
    """Retrieve the original value from the library object.

    Args:
        lib (LibT): The library object from which to retrieve the original value.

    Returns:
        val (T): The original value from the library object.
    """
    return self.target.get_lib_val(lib)

new_val(lib) abstractmethod #

Calculate the new value to apply to the library object.

NB: This method should be implemented by subclasses to calculate the new value.

Parameters:

Name Type Description Default
lib LibT

The library object on which to apply the new value.

required

Returns:

Name Type Description
val T

The new value to apply to the library object.

Source code in epinterface\actions.py
203
204
205
206
207
208
209
210
211
212
213
214
215
@abstractmethod
def new_val(self, lib: LibT) -> T:
    """Calculate the new value to apply to the library object.

    NB: This method should be implemented by subclasses to calculate the new value.

    Args:
        lib (LibT): The library object on which to apply the new value.

    Returns:
        val (T): The new value to apply to the library object.
    """
    pass

run(lib) #

Run the action to modify the library object.

Parameters:

Name Type Description Default
lib LibT

The library object to modify.

required

Returns:

Name Type Description
lib LibT

The modified library object.

Source code in epinterface\actions.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
def run(self, lib: LibT) -> LibT:
    """Run the action to modify the library object.

    Args:
        lib (LibT): The library object to modify.

    Returns:
        lib (LibT): The modified library object.
    """
    new_val = self.new_val(lib)
    original_val = self.get_original_val(lib)
    if self.check_priority(original_val, new_val):
        original_obj = self.get_original_obj(lib)
        key = self.original_key
        set_dict_val_or_attr(original_obj, key, new_val)
    return lib

ActionLibrary #

Bases: BaseModel

A library of action sequences, e.g. to represent deep and shallow retrofits for different types of buildings.

Source code in epinterface\actions.py
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
class ActionLibrary(BaseModel):
    """A library of action sequences, e.g. to represent deep and shallow retrofits for different types of buildings."""

    name: str = Field(..., description="The name of the action library.")
    actions: list[ActionSequence] = Field(
        ..., description="A list of action sequences to perform on a library object."
    )

    @model_validator(mode="after")
    def check_action_names_are_unique(self):
        """Check that the names of the action sequences in the action library are unique.

        Raises:
            ValueError: If the names of the action sequences are not unique.
        """
        action_names = self.action_names
        if len(action_names) != len(set(action_names)):
            msg = f"Action names must be unique: {', '.join(action_names)}"
            raise ValueError(msg)
        return self

    @property
    def action_names(self):
        """Return the names of the action sequences in the action library.

        Returns:
            action_names (list[str]): The names of the action sequences in the action
        """
        return [action.name for action in self.actions]

    def get(self, name: str) -> ActionSequence:
        """Retrieve an action sequence by name.

        Args:
            name (str): The name of the action sequence to retrieve.

        Returns:
            action (ActionSequence): The action sequence with the specified name.

        Raises:
            KeyError: If the action sequence with the specified name is not found in the action library.
        """
        if name in self.action_names:
            return self.actions[self.action_names.index(name)]
        else:
            msg = f"Action sequence not found: {name}\nAvailable action sequences: {', '.join(self.action_names)}"
            raise KeyError(msg)

action_names property #

Return the names of the action sequences in the action library.

Returns:

Name Type Description
action_names list[str]

The names of the action sequences in the action

check_action_names_are_unique() #

Check that the names of the action sequences in the action library are unique.

Raises:

Type Description
ValueError

If the names of the action sequences are not unique.

Source code in epinterface\actions.py
335
336
337
338
339
340
341
342
343
344
345
346
@model_validator(mode="after")
def check_action_names_are_unique(self):
    """Check that the names of the action sequences in the action library are unique.

    Raises:
        ValueError: If the names of the action sequences are not unique.
    """
    action_names = self.action_names
    if len(action_names) != len(set(action_names)):
        msg = f"Action names must be unique: {', '.join(action_names)}"
        raise ValueError(msg)
    return self

get(name) #

Retrieve an action sequence by name.

Parameters:

Name Type Description Default
name str

The name of the action sequence to retrieve.

required

Returns:

Name Type Description
action ActionSequence

The action sequence with the specified name.

Raises:

Type Description
KeyError

If the action sequence with the specified name is not found in the action library.

Source code in epinterface\actions.py
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
def get(self, name: str) -> ActionSequence:
    """Retrieve an action sequence by name.

    Args:
        name (str): The name of the action sequence to retrieve.

    Returns:
        action (ActionSequence): The action sequence with the specified name.

    Raises:
        KeyError: If the action sequence with the specified name is not found in the action library.
    """
    if name in self.action_names:
        return self.actions[self.action_names.index(name)]
    else:
        msg = f"Action sequence not found: {name}\nAvailable action sequences: {', '.join(self.action_names)}"
        raise KeyError(msg)

ActionSequence #

Bases: BaseModel

A sequence of actions to perform on a library object.

Source code in epinterface\actions.py
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
class ActionSequence(BaseModel):
    """A sequence of actions to perform on a library object."""

    name: str = Field(..., description="The name of the action sequence.")
    actions: list[
        "DeltaVal | ReplaceWithExisting | ReplaceWithVal | ActionSequence"
    ] = Field(  # TODO: should we allow nested actionsequences?
        ..., description="A sequence of actions to perform on a library object."
    )

    def run(self, lib: LibT) -> LibT:
        """Run the sequence of actions on the library object.

        Args:
            lib (LibT): The library object to modify.

        Returns:
            lib (LibT): The modified library object.
        """
        for action in self.actions:
            lib = action.run(lib)
        return lib

run(lib) #

Run the sequence of actions on the library object.

Parameters:

Name Type Description Default
lib LibT

The library object to modify.

required

Returns:

Name Type Description
lib LibT

The modified library object.

Source code in epinterface\actions.py
313
314
315
316
317
318
319
320
321
322
323
324
def run(self, lib: LibT) -> LibT:
    """Run the sequence of actions on the library object.

    Args:
        lib (LibT): The library object to modify.

    Returns:
        lib (LibT): The modified library object.
    """
    for action in self.actions:
        lib = action.run(lib)
    return lib

DeltaVal #

Bases: Action[Numeric]

Add a value to a parameter in a library object.

Source code in epinterface\actions.py
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
class DeltaVal(Action[Numeric]):
    """Add a value to a parameter in a library object."""

    delta: Numeric = Field(
        ..., description="The value to modify to the original value."
    )
    op: Operation = Field(
        ..., description="The operation to perform on the original value."
    )

    def new_val(self, lib: LibT) -> Numeric:
        """Calculate a new value by combining the original value from the given library with a delta.

        Args:
            lib (LibT): The library from which to retrieve the original value.

        Returns:
            new_val (Numeric): The new value obtained by combining the original value with the delta.
        """
        original_val = self.get_original_val(lib)

        return self.combine(original_val, self.delta)

    @property
    def combine(self) -> Callable[[Numeric, Numeric], Numeric]:
        """Combines two numeric values based on the specified operation.

        Supported operations:
            - "+": Addition
            - "*": Multiplication

        Returns:
            fn (Callable[[Numeric, Numeric], Numeric]): A function that takes two numeric arguments and returns a numeric result.

        Raises:
            ValueError: If the operation specified by `self.op` is not supported.

        """
        if self.op == "+":
            return lambda x, y: x + y
        elif self.op == "*":
            return lambda x, y: x * y
        else:
            msg = f"Invalid operation: {self.op}"
            raise ValueError(msg)

combine: Callable[[Numeric, Numeric], Numeric] property #

Combines two numeric values based on the specified operation.

Supported operations
  • "+": Addition
  • "*": Multiplication

Returns:

Name Type Description
fn Callable[[Numeric, Numeric], Numeric]

A function that takes two numeric arguments and returns a numeric result.

Raises:

Type Description
ValueError

If the operation specified by self.op is not supported.

new_val(lib) #

Calculate a new value by combining the original value from the given library with a delta.

Parameters:

Name Type Description Default
lib LibT

The library from which to retrieve the original value.

required

Returns:

Name Type Description
new_val Numeric

The new value obtained by combining the original value with the delta.

Source code in epinterface\actions.py
266
267
268
269
270
271
272
273
274
275
276
277
def new_val(self, lib: LibT) -> Numeric:
    """Calculate a new value by combining the original value from the given library with a delta.

    Args:
        lib (LibT): The library from which to retrieve the original value.

    Returns:
        new_val (Numeric): The new value obtained by combining the original value with the delta.
    """
    original_val = self.get_original_val(lib)

    return self.combine(original_val, self.delta)

ParameterPath #

Bases: BaseModel, Generic[T]

Pathing to find a parameter in a library/object.

ParameterPath is a generic class that represents a path consisting of strings, integers, or other ParameterPath instances. It provides methods to resolve the path and retrieve values from a given library.

Source code in epinterface\actions.py
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
class ParameterPath(BaseModel, Generic[T]):
    """Pathing to find a parameter in a library/object.

    ParameterPath is a generic class that represents a path consisting of strings, integers,
    or other ParameterPath instances. It provides methods to resolve the path and retrieve
    values from a given library.
    """

    path: list["str | int | ParameterPath[str] | ParameterPath[int]"] = Field(
        ..., description="The path to the parameter to select."
    )

    def resolved_path(self, lib: LibT):
        """Resolve the path to the parameter in the library.

        Args:
            lib (LibT): The library to search for the parameter.

        Returns:
            path (list[Any]): The resolved path to the parameter in the library.
        """
        return [
            p if isinstance(p, str | int) else p.get_lib_val(lib) for p in self.path
        ]

    def get_lib_val(self, lib: LibT) -> T:
        """Retrieves a value from a nested dictionary or object attribute path.

        Args:
            lib (LibT): The library object from which to retrieve the value.

        Returns:
            val (T): The value retrieved from the nested dictionary or object attribute path.
        """
        return cast(T, reduce(get_dict_val_or_attr, self.resolved_path(lib), lib))

    @property
    def parent_path(self):
        """Returns the parent path of the current path.

        Returns:
            parent_path (ParameterPath): The parent path of the current path.
        """
        # TODO: how can we type-narrow the generic parameterpath here?
        # get the parent using the similar reduction technique as before
        return ParameterPath[Any](path=self.path[:-1])

parent_path property #

Returns the parent path of the current path.

Returns:

Name Type Description
parent_path ParameterPath

The parent path of the current path.

get_lib_val(lib) #

Retrieves a value from a nested dictionary or object attribute path.

Parameters:

Name Type Description Default
lib LibT

The library object from which to retrieve the value.

required

Returns:

Name Type Description
val T

The value retrieved from the nested dictionary or object attribute path.

Source code in epinterface\actions.py
86
87
88
89
90
91
92
93
94
95
def get_lib_val(self, lib: LibT) -> T:
    """Retrieves a value from a nested dictionary or object attribute path.

    Args:
        lib (LibT): The library object from which to retrieve the value.

    Returns:
        val (T): The value retrieved from the nested dictionary or object attribute path.
    """
    return cast(T, reduce(get_dict_val_or_attr, self.resolved_path(lib), lib))

resolved_path(lib) #

Resolve the path to the parameter in the library.

Parameters:

Name Type Description Default
lib LibT

The library to search for the parameter.

required

Returns:

Name Type Description
path list[Any]

The resolved path to the parameter in the library.

Source code in epinterface\actions.py
73
74
75
76
77
78
79
80
81
82
83
84
def resolved_path(self, lib: LibT):
    """Resolve the path to the parameter in the library.

    Args:
        lib (LibT): The library to search for the parameter.

    Returns:
        path (list[Any]): The resolved path to the parameter in the library.
    """
    return [
        p if isinstance(p, str | int) else p.get_lib_val(lib) for p in self.path
    ]

ReplaceWithExisting #

Bases: Action[T]

Replace a value in a library object with a value from another location in the library.

Source code in epinterface\actions.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
class ReplaceWithExisting(Action[T]):
    """Replace a value in a library object with a value from another location in the library."""

    source: ParameterPath[T]

    def new_val(self, lib: LibT) -> T:
        """Retrieve the value from the source path to replace the target value.

        Args:
            lib (LibT): The library object from which to retrieve the new value.

        Returns:
            val (T): The new value to replace the target value.
        """
        return self.source.get_lib_val(lib)

new_val(lib) #

Retrieve the value from the source path to replace the target value.

Parameters:

Name Type Description Default
lib LibT

The library object from which to retrieve the new value.

required

Returns:

Name Type Description
val T

The new value to replace the target value.

Source code in epinterface\actions.py
223
224
225
226
227
228
229
230
231
232
def new_val(self, lib: LibT) -> T:
    """Retrieve the value from the source path to replace the target value.

    Args:
        lib (LibT): The library object from which to retrieve the new value.

    Returns:
        val (T): The new value to replace the target value.
    """
    return self.source.get_lib_val(lib)

ReplaceWithVal #

Bases: Action[T]

Replace a value in a library object with a new value.

Source code in epinterface\actions.py
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
class ReplaceWithVal(Action[T]):
    """Replace a value in a library object with a new value."""

    val: T

    def new_val(self, lib: LibT) -> T:
        """Returns the current value of the instance to use for updating.

        Args:
            lib (LibT): A library instance of type LibT.

        Returns:
            val (T): The current value of the instance.
        """
        return self.val

new_val(lib) #

Returns the current value of the instance to use for updating.

Parameters:

Name Type Description Default
lib LibT

A library instance of type LibT.

required

Returns:

Name Type Description
val T

The current value of the instance.

Source code in epinterface\actions.py
240
241
242
243
244
245
246
247
248
249
def new_val(self, lib: LibT) -> T:
    """Returns the current value of the instance to use for updating.

    Args:
        lib (LibT): A library instance of type LibT.

    Returns:
        val (T): The current value of the instance.
    """
    return self.val

get_dict_val_or_attr(obj, key) #

Retrieve a value from a dictionary or list, or an attribute from an object.

Parameters:

Name Type Description Default
obj Union[dict, list, Any]

The object from which to retrieve the value or attribute.

required
key Any

The key or attribute name to retrieve.

required

Returns:

Name Type Description
val Any

The value associated with the key if obj is a dictionary or list, or the attribute value if obj is an object.

Source code in epinterface\actions.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def get_dict_val_or_attr(obj, key):
    """Retrieve a value from a dictionary or list, or an attribute from an object.

    Args:
        obj (Union[dict, list, Any]): The object from which to retrieve the value or attribute.
        key (Any): The key or attribute name to retrieve.

    Returns:
        val (Any): The value associated with the key if `obj` is a dictionary or list,
             or the attribute value if `obj` is an object.
    """
    if isinstance(obj, dict | list):
        return obj[key]
    else:
        return getattr(obj, key)

set_dict_val_or_attr(obj, key, val) #

Sets a value in a dictionary or list, or sets an attribute on an object.

If the provided object is a dictionary or list, the function sets the value at the specified key or index. If the object is not a dictionary or list, the function sets an attribute on the object with the specified key and value.

Parameters:

Name Type Description Default
obj Union[dict, list, object]

The object to modify.

required
key Union[str, int]

The key or attribute name to set.

required
val Any

The value to set.

required

Raises:

Type Description
TypeError

If the object is a list and the key is not an integer.

Source code in epinterface\actions.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def set_dict_val_or_attr(obj, key, val):
    """Sets a value in a dictionary or list, or sets an attribute on an object.

    If the provided object is a dictionary or list, the function sets the value
    at the specified key or index. If the object is not a dictionary or list,
    the function sets an attribute on the object with the specified key and value.

    Args:
        obj (Union[dict, list, object]): The object to modify.
        key (Union[str, int]): The key or attribute name to set.
        val (Any): The value to set.

    Raises:
        TypeError: If the object is a list and the key is not an integer.
    """
    if isinstance(obj, dict | list):
        obj[key] = val
    else:
        setattr(obj, key, val)