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 GenericSelectors 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