Selectors
Selectors
identify which part of the building model to modify, and
how to modify it, which lets us automate the changes above.
Their main purpose is to be used automatically inside evaluators, which is described later.
Here we will use them directly to modify the building, get the objects they affect, and read the current value of the field(s) they refer to. (Note that genericSelectors don’t automatically support all of these operations)
import pandas as pd
from besos import eppy_funcs as ef
from besos.evaluator import EvaluatorEP
from besos.parameters import (
FieldSelector,
FilterSelector,
GenericSelector,
Parameter,
)
from besos.problem import EPProblem
building = ef.get_building(mode="json")
Selecting things manually
This example assumes that you know about the layered datastructure of buildings. (classes, objects, and fields). We can get the value of a field like this:
class_name = "Material"
object_name = "1/2IN Gypsum"
field_name = "conductivity"
building[class_name][object_name][field_name]
0.16
Note that we can access any field like this, as long as we know the class, object and field names that describe where the field is.
We can also assign values to the field:
building[class_name][object_name][field_name] = 0.23
print("new value is:", building[class_name][object_name][field_name])
new value is: 0.23
To create a selector, we need the same information that was needed to change the field in the building.
gypsum_selector = FieldSelector(
class_name="Material", object_name="1/2IN Gypsum", field_name="Conductivity",
)
Now, we can use the selector to modify the building, using the .set
method.
gypsum_selector.set(building=building, value=0.05)
Selectors also let us get the current value of the field, using the
.get
method. This will return a list of values, since in some cases,
a selector can refer to more than one field. In this case, the field we
refered to has indeed been set to 0.05
gypsum_selector.get(building=building)
[0.05]
The method .get_objects
retrieves the objects that are affected by
this selector. Since we only set it up to find one object, it returns a
list with only the 1/2IN Gypsum material. Since the json format does not
include the name inside the object, we do not see the object name here,
just the fields.
gypsum_selector.get_objects(building)
[{'conductivity': 0.05,
'density': 784.9,
'idf_max_extensible_fields': 0,
'idf_max_fields': 9,
'idf_order': 49,
'roughness': 'Smooth',
'solar_absorptance': 0.92,
'specific_heat': 830.0,
'thermal_absorptance': 0.9,
'thickness': 0.0127,
'visible_absorptance': 0.92}]
Creating Selectors
There are a few other options when creating selectors.
We can skip using the class name. In this case the selector will search through all of the objects of all classes, and find the ones that match the object name given.
gypsum_selector = FieldSelector(
# class_name="Material", <--- Commented out instead of being used as an input
object_name="1/2IN Gypsum",
field_name="Conductivity",
)
Since there is only one object in this building with the name
1/2IN Gypsum
, this selector affects the same object as before. (We
could also use get/set the same way as before)
gypsum_selector.get_objects(building)
[{'conductivity': 0.05,
'density': 784.9,
'idf_max_extensible_fields': 0,
'idf_max_fields': 9,
'idf_order': 49,
'roughness': 'Smooth',
'solar_absorptance': 0.92,
'specific_heat': 830.0,
'thermal_absorptance': 0.9,
'thickness': 0.0127,
'visible_absorptance': 0.92}]
There is only one object in the example building with the name
1/2IN Gypsum
. If there were multiple objects with the same name, we
would get an error when using a json building. (For idfs, the selector
will guess that you mean the first object with that name)
Setting all fields on objects of a certain class
If you have multiple objects of the same type that all share the same
field to modify, you can modify them all with one selector by setting
object_name
to '*'
.
Our building has several Lights, and all of the lights have a field
called Watts per Zone Floor Area
lights_selector = FieldSelector(
class_name="Lights", object_name="*", field_name="Watts per Zone Floor Area"
)
# this selector affects the following objects
lights_selector.get_objects(building)
[{'design_level_calculation_method': 'Watts/Area',
'end_use_subcategory': 'General',
'fraction_radiant': 0.7,
'fraction_replaceable': 1.0,
'fraction_visible': 0.2,
'idf_max_extensible_fields': 0,
'idf_max_fields': 13,
'idf_order': 137,
'return_air_fraction': 0.0,
'return_air_fraction_calculated_from_plenum_temperature': 'No',
'schedule_name': 'BLDG_LIGHT_SCH',
'watts_per_zone_floor_area': 10.76,
'zone_or_zonelist_name': 'Core_ZN'},
{'design_level_calculation_method': 'Watts/Area',
'end_use_subcategory': 'General',
'fraction_radiant': 0.7,
'fraction_replaceable': 1.0,
'fraction_visible': 0.2,
'idf_max_extensible_fields': 0,
'idf_max_fields': 13,
'idf_order': 138,
'return_air_fraction': 0.0,
'return_air_fraction_calculated_from_plenum_temperature': 'No',
'schedule_name': 'BLDG_LIGHT_SCH',
'watts_per_zone_floor_area': 10.76,
'zone_or_zonelist_name': 'Perimeter_ZN_1'},
{'design_level_calculation_method': 'Watts/Area',
'end_use_subcategory': 'General',
'fraction_radiant': 0.7,
'fraction_replaceable': 1.0,
'fraction_visible': 0.2,
'idf_max_extensible_fields': 0,
'idf_max_fields': 13,
'idf_order': 139,
'return_air_fraction': 0.0,
'return_air_fraction_calculated_from_plenum_temperature': 'No',
'schedule_name': 'BLDG_LIGHT_SCH',
'watts_per_zone_floor_area': 10.76,
'zone_or_zonelist_name': 'Perimeter_ZN_2'},
{'design_level_calculation_method': 'Watts/Area',
'end_use_subcategory': 'General',
'fraction_radiant': 0.7,
'fraction_replaceable': 1.0,
'fraction_visible': 0.2,
'idf_max_extensible_fields': 0,
'idf_max_fields': 13,
'idf_order': 140,
'return_air_fraction': 0.0,
'return_air_fraction_calculated_from_plenum_temperature': 'No',
'schedule_name': 'BLDG_LIGHT_SCH',
'watts_per_zone_floor_area': 10.76,
'zone_or_zonelist_name': 'Perimeter_ZN_3'},
{'design_level_calculation_method': 'Watts/Area',
'end_use_subcategory': 'General',
'fraction_radiant': 0.7,
'fraction_replaceable': 1.0,
'fraction_visible': 0.2,
'idf_max_extensible_fields': 0,
'idf_max_fields': 13,
'idf_order': 141,
'return_air_fraction': 0.0,
'return_air_fraction_calculated_from_plenum_temperature': 'No',
'schedule_name': 'BLDG_LIGHT_SCH',
'watts_per_zone_floor_area': 10.76,
'zone_or_zonelist_name': 'Perimeter_ZN_4'}]
# setting sets all values at once, and getting retrieves the value of the field for each affected object.
lights_selector.set(building, 11)
lights_selector.get(building)
[11, 11, 11, 11, 11]
Field selectors must always have a value for field_name
Filtering Selectors (Set a field on each object from an arbitrary list)
FilterSelectors
allow us to use custom function to select the
objects to modify. Here we define a function that finds all materials
with Insulation
in their name. Then we use this function to modify
the thickness of all these materials.
def insulation_filter_json(building):
return [obj for name, obj in building["Material"].items() if "Insulation" in name]
# This function only works for buildings with a json representation.
insulation_json = FilterSelector(insulation_filter_json, field_name="Thickness")
building_json = ef.get_building(mode="json")
Filtering for .idf
files works the same way, we just need to
re-write the insulation_filter
function so that it can handle idf
objects instead of json ones.
def insulation_filter_idf(building):
return [obj for obj in building.idfobjects["MATERIAL"] if "Insulation" in obj.Name]
insulation_idf = FilterSelector(insulation_filter_idf, field_name="Thickness")
building_idf = ef.get_building(mode="idf")
These two selectors are equivalent, and support get/set/get_objects like the FieldSelectors before. Note that since the json file has objects in a different order than the idf, we get the results in a different order from each of these selectors.
insulation_json.get(building_json)
[0.236804989096202, 0.0495494599433393]
insulation_idf.get(building_idf)
[0.0495494599433393, 0.236804989096202]
# .set() and .get_objects(), also work, try them out here if you want.
GenericSelectors (Descriptor supplied value is taken by a function which can do any changes)
Parameter scripts using a Generic Selector
Parameters can also be created by defining a function that takes an idf
and a value and mutates the idf. These functions can be specific to a
certain idf’s format, and can perform any arbitrary transformation.
Creating these can be more involved. eppy_funcs
contains the
functions one_window
and wwr_all
. one_window
removes windows
from a building until it has only one per wall. wwr_all
takes a
building with one window per wall and adjusts it to have a specific
window to wall ratio.
BESOS also includes some pre-defined parameter scripts:
wwr
for window to wall ratio
Here we define a selector which will modify the window to wall ratio.
For more details on how GenericSelector
s work, and how to write
your own scripts like this, check the Generic Selectors example
notebook.
window_to_wall = GenericSelector(
setup=ef.one_window, # adjusts the building so that each wall has only one window.
set=ef.wwr_all, # adjusts the windows so that they have the requested ratio
)
Sampling and evaluating the design space
Since Selectors
do not describe the values they can take on, only
where those values go, they are not sufficient to explore the design
space.
In order to evaluate several different building configurations, we need
the values to use, Selectors
to apply them to the building, and
Parameters
and an Evaluator
to connect them together.
Breaking down what is happening behind the scenes: We can specify
several samples manually to look at the design space. (Descriptors
and the sampling helper functions can be used to generate samples
automatically) These values will be processed row by row by the
evaluator which will split them up and send one value to each Parameter
in the problem. (These parameters would use their descriptors to
validate the values, but since there are no descriptors, this step is
skipped) Then the selectors get the value from the Parameter, and use it
to modify the building. The building is then run, and results collected.
samples = pd.DataFrame(
{
"Thickness": [x / 10 for x in range(1, 10)] * 2,
"Watts": [8, 10, 12] * 6,
"wwr": [0.25, 0.5] * 9,
}
)
# bundle all of the different selectors into a single list of parameters
parameters = [
Parameter(selector=x) for x in (insulation_idf, lights_selector, window_to_wall)
]
# the inputs to the problem will be the parameters
# default output is the total electricity use (measured by Electricity:Facility)
problem = EPProblem(inputs=parameters)
# The evaluator will take the problem and building file
evaluator = EvaluatorEP(problem, building_idf)
samples
/home/user/.local/lib/python3.7/site-packages/besos/problem.py:152: RuntimeWarning: Duplicate names found. (duplicate, repetitions): [('input', 3)]
Attempting to fix automatically
f"Duplicate names found. (duplicate, repetitions): "
Thickness | Watts | wwr | |
---|---|---|---|
0 | 0.1 | 8 | 0.25 |
1 | 0.2 | 10 | 0.50 |
2 | 0.3 | 12 | 0.25 |
3 | 0.4 | 8 | 0.50 |
4 | 0.5 | 10 | 0.25 |
5 | 0.6 | 12 | 0.50 |
6 | 0.7 | 8 | 0.25 |
7 | 0.8 | 10 | 0.50 |
8 | 0.9 | 12 | 0.25 |
9 | 0.1 | 8 | 0.50 |
10 | 0.2 | 10 | 0.25 |
11 | 0.3 | 12 | 0.50 |
12 | 0.4 | 8 | 0.25 |
13 | 0.5 | 10 | 0.50 |
14 | 0.6 | 12 | 0.25 |
15 | 0.7 | 8 | 0.50 |
16 | 0.8 | 10 | 0.25 |
17 | 0.9 | 12 | 0.50 |
# We can apply some samples to the problem
outputs = evaluator.df_apply(samples, keep_input=True)
outputs
HBox(children=(FloatProgress(value=0.0, description='Executing', max=18.0, style=ProgressStyle(description_wid…
Thickness | Watts | wwr | Electricity:Facility | |
---|---|---|---|---|
0 | 0.1 | 8 | 0.25 | 1.582322e+09 |
1 | 0.2 | 10 | 0.50 | 1.708025e+09 |
2 | 0.3 | 12 | 0.25 | 1.823149e+09 |
3 | 0.4 | 8 | 0.50 | 1.532866e+09 |
4 | 0.5 | 10 | 0.25 | 1.662859e+09 |
5 | 0.6 | 12 | 0.50 | 1.791266e+09 |
6 | 0.7 | 8 | 0.25 | 1.505992e+09 |
7 | 0.8 | 10 | 0.50 | 1.640104e+09 |
8 | 0.9 | 12 | 0.25 | 1.770866e+09 |
9 | 0.1 | 8 | 0.50 | 1.585615e+09 |
10 | 0.2 | 10 | 0.25 | 1.707491e+09 |
11 | 0.3 | 12 | 0.50 | 1.823969e+09 |
12 | 0.4 | 8 | 0.25 | 1.532297e+09 |
13 | 0.5 | 10 | 0.50 | 1.664604e+09 |
14 | 0.6 | 12 | 0.25 | 1.790538e+09 |
15 | 0.7 | 8 | 0.50 | 1.506915e+09 |
16 | 0.8 | 10 | 0.25 | 1.640168e+09 |
17 | 0.9 | 12 | 0.50 | 1.773212e+09 |