diff --git a/honeybee_schema/_base.py b/honeybee_schema/_base.py index b0ec8c9..c14a3a1 100644 --- a/honeybee_schema/_base.py +++ b/honeybee_schema/_base.py @@ -4,7 +4,7 @@ class NoExtraBaseModel(BaseModel): """Base class for all objects that are not extensible with additional keys. - + This effectively includes all objects except for the Properties classes that are assigned to geometry objects. """ diff --git a/honeybee_schema/radiance/_base.py b/honeybee_schema/radiance/_base.py new file mode 100644 index 0000000..5a5d127 --- /dev/null +++ b/honeybee_schema/radiance/_base.py @@ -0,0 +1,37 @@ +"""Base class used by radiance schema objects.""" +from pydantic import Field, validator, BaseModel +import re +from .._base import NoExtraBaseModel + + +class IDdRadianceBaseModel(NoExtraBaseModel): + """Base class for all objects requiring a valid Radiance identifier.""" + + identifier: str = Field( + ..., + description='Text string for a unique Radiance object. Must not contain spaces ' + 'or special characters. This will be used to identify the object ' + 'across a model and in the exported Radiance files.' + ) + + @validator('identifier') + def valid_rad_string(cls, value): + """Check that a string is valid for Radiance. + + This method is modified from the honeybee-core.typing.valid_rad_string method. + """ + try: + illegal_match = re.search(r'[^.A-Za-z0-9_-]', value) + except TypeError: + raise TypeError('Identifier must be a text string. Got {}: {}.'.format( + type(value), value)) + assert illegal_match is None, \ + 'Illegal character "{}" found in identifier'.format(illegal_match.group(0)) + assert len(value) > 0, \ + 'Input identifier "{}" contains no characters.'.format(value) + return value + + display_name: str = Field( + default=None, + description='Display name of the object with no character restrictions.' + ) \ No newline at end of file diff --git a/honeybee_schema/radiance/material.py b/honeybee_schema/radiance/modifier.py similarity index 95% rename from honeybee_schema/radiance/material.py rename to honeybee_schema/radiance/modifier.py index a4bbe0d..a05687c 100644 --- a/honeybee_schema/radiance/material.py +++ b/honeybee_schema/radiance/modifier.py @@ -1,9 +1,8 @@ -# TODO: change to Modifers Schema? -"""Material Schema""" +"""Modfier Schema""" from pydantic import Field, constr, validator, root_validator -from typing import List, Union +from typing import List, Union, Optional -from .._base import IDdBaseModel, BaseModel +from ._base import IDdRadianceBaseModel, BaseModel class Void(BaseModel): @@ -12,13 +11,13 @@ class Void(BaseModel): type: constr(regex='^void$') = 'void' -class ModifierBase(IDdBaseModel): +class ModifierBase(IDdRadianceBaseModel): """Base class for Radiance Modifiers""" modifier: Union[ Void, 'Plastic', 'Glass', 'BSDF', 'Glow', 'Light', 'Trans' ] = Field( - default=Void(), + default=None, description='Material modifier (default: Void).' ) @@ -111,8 +110,8 @@ def check_sum_fractions(cls, values): g_refl = values.get('g_reflectance') b_refl = values.get('b_reflectance') identifier = values.get('identifier') - summed = trans_diff + trans_spec + r_refl + g_refl + b_refl - assert summed <= 1, 'The sum of the transmitted diffuse and specular light ' \ + summed = trans_diff + trans_spec + (r_refl + g_refl + b_refl) / 3.0 + assert summed <= 1, 'The sum of the transmitted and reflected ' \ 'fractions cannot be greater than 1, but is {} for modifier {}.'.format( summed, identifier) return values @@ -147,10 +146,9 @@ class Glass(ModifierBase): '(default: 0).' ) - refraction_index: float = Field( + refraction_index: Optional[float] = Field( default=1.52, ge=0, - le=1, description='A value between 0 and 1 for the index of refraction ' '(default: 1.52).' ) diff --git a/honeybee_schema/radiance/modifierset.py b/honeybee_schema/radiance/modifierset.py new file mode 100644 index 0000000..90c3b76 --- /dev/null +++ b/honeybee_schema/radiance/modifierset.py @@ -0,0 +1,303 @@ +"""ModifierSet Schema""" +from pydantic import Field, constr +from typing import List, Union, Optional + +from .._base import NoExtraBaseModel +from ._base import IDdRadianceBaseModel +from .modifier import Plastic, Glass, BSDF, Glow, Light, Trans, Void + +# Unioned Modifier Schema objects defined for type reference +_REFERENCE_UNION_MODIFIERS = Union[Plastic, Glass, BSDF, Glow, Light, Trans, Void] + + +class BaseModifierSetAbridged(NoExtraBaseModel): + """Base class for the abridged modfier sets assigned to Faces.""" + + exterior_modifier: str = Field( + default=None, + description='Identifier for a radiance modifier object for faces with an ' + ' Outdoors boundary condition.' + ) + + interior_modifier: str = Field( + default=None, + description='Identifier for a radiance modifier object for faces with a ' + 'boundary condition other than Outdoors.' + ) + + +class WallModifierSetAbridged(BaseModifierSetAbridged): + """Abridged set containing radiance modifiers needed for a model's Walls.""" + + type: constr(regex='^WallModifierSetAbridged$') = 'WallModifierSetAbridged' + + +class FloorModifierSetAbridged(BaseModifierSetAbridged): + """Abridged set containing radiance modifiers needed for a model's Floors.""" + + type: constr(regex='^FloorModifierSetAbridged$') = 'FloorModifierSetAbridged' + + +class RoofCeilingModifierSetAbridged(BaseModifierSetAbridged): + """Abridged set containing radiance modifiers needed for a model's Roofs.""" + + type: constr(regex='^RoofCeilingModifierSetAbridged$') = \ + 'RoofCeilingModifierSetAbridged' + + +class ShadeModifierSetAbridged(BaseModifierSetAbridged): + """Abridged set containing radiance modifiers needed for a model's Shade.""" + + type: constr(regex='^ShadeModifierSetAbridged$') = 'ShadeModifierSetAbridged' + + +class ApertureModifierSetAbridged(NoExtraBaseModel): + """Abridged set containing radiance modifiers needed for a model's Apertures.""" + + type: constr(regex='^ApertureModifierSetAbridged$') = 'ApertureModifierSetAbridged' + + window_modifier: str = Field( + default=None, + description='Identifier of modifier object for apertures with an Outdoors ' + 'boundary condition, False is_operable property, ' + 'and Wall parent Face.' + ) + + interior_modifier: str = Field( + default=None, + description='Identifier of modifier object for apertures with a Surface ' + 'boundary condition.' + ) + + skylight_modifier: str = Field( + default=None, + description='Identifier of modifier object for apertures with an Outdoors ' + 'boundary condition, False is_operable property, and a ' + 'RoofCeiling or Floor face type for their parent face.' + ) + + operable_modifier: str = Field( + default=None, + description='Identifier of modifier object for apertures with an Outdoors ' + 'boundary condition and a True is_operable property.' + ) + + +class DoorModifierSetAbridged(BaseModifierSetAbridged): + """Abridged set containing radiance modifiers needed for a model's Doors.""" + + type: constr(regex='^DoorModifierSetAbridged$') = 'DoorModifierSetAbridged' + + interior_glass_modifier: str = Field( + default=None, + description='Identifier of modifier object for glass with a Surface ' + 'boundary condition.' + ) + + exterior_glass_modifier: str = Field( + default=None, + description='Identifier of modifier object for glass with an Outdoors ' + 'boundary condition.' + ) + + overhead_modifier: str = Field( + default=None, + description='Identifier of a modifier object for doors with an ' + 'Outdoors boundary condition and a RoofCeiling or Floor ' + 'face type for their parent face.' + ) + + +class ModifierSetAbridged(IDdRadianceBaseModel): + """Abridged set containing all modifiers needed to create a radiance model.""" + + type: constr(regex='^ModifierSetAbridged$') = 'ModifierSetAbridged' + + wall_set: WallModifierSetAbridged = Field( + default=None, + description='Optional WallModifierSet object for this ' + 'ModifierSet (default: None).' + ) + + floor_set: FloorModifierSetAbridged = Field( + default=None, + description='Optional FloorModifierSet object for ' + 'this ModifierSet (default: None).' + ) + + roof_ceiling_set: RoofCeilingModifierSetAbridged = Field( + default=None, + description='Optional RoofCeilingModifierSet object for this ' + 'ModifierSet (default: None).' + ) + + aperture_set: ApertureModifierSetAbridged = Field( + default=None, + description='Optional ApertureModifierSet object for this ' + 'ModifierSet (default: None).' + ) + + door_set: DoorModifierSetAbridged = Field( + default=None, + description='Optional DoorModifierSet object for this ' + 'ModifierSet (default: None).' + ) + + shade_set: ShadeModifierSetAbridged = Field( + default=None, + description='Optional ShadeModifierSet object for this ' + 'ModifierSet (default: None).' + ) + + air_boundary_modifier: str = Field( + default=None, + description='Optional Modifier to be used for all Faces ' + 'with an AirBoundary face type. If None, it will be the ' + 'honyebee generic air wall modifier.' + ) + + +class BaseModifierSet(NoExtraBaseModel): + """Base class for the modifier sets assigned to Faces.""" + + exterior_modifier: _REFERENCE_UNION_MODIFIERS = Field( + default=None, + description='A radiance modifier object for faces with an Outdoors boundary ' + 'condition.' + ) + + interior_modifier: _REFERENCE_UNION_MODIFIERS = Field( + default=None, + description='A radiance modifier object for faces with a boundary condition ' + 'other than Outdoors.' + ) + + +class WallModifierSet(BaseModifierSet): + """Set containing radiance modifiers needed for a model's Walls.""" + + type: constr(regex='^WallModifierSet$') = 'WallModifierSet' + + +class FloorModifierSet(BaseModifierSet): + """Set containing radiance modifiers needed for a model's Floors.""" + + type: constr(regex='^FloorModifierSet$') = 'FloorModifierSet' + + +class RoofCeilingModifierSet(BaseModifierSet): + """Set containing radiance modifiers needed for a model's roofs.""" + + type: constr(regex='^RoofCeilingModifierSet$') = 'RoofCeilingModifierSet' + + +class ShadeModifierSet(BaseModifierSet): + """Set containing radiance modifiers needed for a model's Shade.""" + + type: constr(regex='^ShadeModifierSet$') = 'ShadeModifierSet' + + +class ApertureModifierSet(NoExtraBaseModel): + """Set containing radiance modifiers needed for a model's Apertures.""" + + type: constr(regex='^ApertureModifierSet$') = 'ApertureModifierSet' + + window_modifier: _REFERENCE_UNION_MODIFIERS = Field( + default=None, + description='A modifier object for apertures with an Outdoors ' + 'boundary condition, False is_operable property, ' + 'and Wall parent Face.' + ) + + interior_modifier: _REFERENCE_UNION_MODIFIERS = Field( + default=None, + description='A modifier object for apertures with a Surface ' + 'boundary condition.' + ) + + skylight_modifier: _REFERENCE_UNION_MODIFIERS = Field( + default=None, + description='A modifier object for apertures with an Outdoors ' + 'boundary condition, False is_operable property, and a ' + 'RoofCeiling or Floor face type for their parent face.' + ) + + operable_modifier: _REFERENCE_UNION_MODIFIERS = Field( + default=None, + description='A modifier object for apertures with an Outdoors boundary ' + 'condition and a True is_operable property.' + ) + + +class DoorModifierSet(BaseModifierSet): + """Set containing radiance modifiers needed for a model's Doors.""" + + type: constr(regex='^DoorModifierSet$') = 'DoorModifierSet' + + interior_glass_modifier: _REFERENCE_UNION_MODIFIERS = Field( + default=None, + description='A modifier object for glass with a Surface ' + 'boundary condition.' + ) + + exterior_glass_modifier: _REFERENCE_UNION_MODIFIERS = Field( + default=None, + description='A modifier object for glass with an Outdoors ' + 'boundary condition.' + ) + + overhead_modifier: _REFERENCE_UNION_MODIFIERS = Field( + default=None, + description='A window modifier object for doors with an Outdoors boundary ' + 'condition and a RoofCeiling or Floor face type for their ' + 'parent face.' + ) + + +class ModifierSet(IDdRadianceBaseModel): + """Set containing all radiance modifiers needed to create a radiance model.""" + + type: constr(regex='^ModifierSet$') = 'ModifierSet' + + wall_set: WallModifierSet = Field( + default=None, + description='An optional WallModifierSet object for this ModifierSet. ' + '(default: None).' + ) + + floor_set: FloorModifierSet = Field( + default=None, + description='An optional FloorModifierSet object for this ModifierSet. ' + '(default: None).' + ) + + roof_ceiling_set: RoofCeilingModifierSet = Field( + default=None, + description='An optional RoofCeilingModifierSet object for this ModifierSet. ' + '(default: None).' + ) + + aperture_set: ApertureModifierSet = Field( + default=None, + description='An optional ApertureModifierSet object for this ModifierSet. ' + '(default: None).' + ) + + door_set: DoorModifierSet = Field( + default=None, + description='An optional DoorModifierSet object for this ModifierSet. ' + '(default: None).' + ) + + shade_set: ShadeModifierSet = Field( + default=None, + description='An optional ShadeModifierSet object for this ModifierSet. ' + '(default: None).' + ) + + air_boundary_modifier: _REFERENCE_UNION_MODIFIERS = Field( + default=None, + description='An optional Modifier to be used for all Faces with an AirBoundary ' + 'face type. If None, it will be the honyebee generic air wall ' + 'modifier.' + ) \ No newline at end of file diff --git a/honeybee_schema/radiance/properties.py b/honeybee_schema/radiance/properties.py new file mode 100644 index 0000000..4fc93a2 --- /dev/null +++ b/honeybee_schema/radiance/properties.py @@ -0,0 +1,94 @@ +"""Properties Schema""" +from pydantic import Field, constr, validator, root_validator +from typing import List, Union, Optional + +from .modifier import Plastic, Glass, BSDF, Glow, Light, Trans, Void +from .modifierset import ModifierSet, ModifierSetAbridged +from .._base import NoExtraBaseModel + +# Unioned Modifier Schema objects defined for type reference +_REFERENCE_UNION_MODIFIERS = Union[Plastic, Glass, BSDF, Glow, Light, Trans, Void] + + +class _PropertiesBaseAbridged(NoExtraBaseModel): + """Base class of Abridged Radiance Properties.""" + + modifier: str = Field( + default=None, + description='A string for a Honeybee Radiance Modifier.' + ) + + modifier_blk: str = Field( + default=None, + description='A string for a Honeybee Radiance Modifier to be used ' + 'in direct solar simulations and in isolation studies (assessing' + 'the contribution of individual objects).' + ) + + +class ApertureRadiancePropertiesAbridged(_PropertiesBaseAbridged): + """Radiance Properties for Honeybee Aperture Abridged.""" + + type: constr(regex='^ApertureRadiancePropertiesAbridged$') = \ + 'ApertureRadiancePropertiesAbridged' + + +class DoorRadiancePropertiesAbridged(_PropertiesBaseAbridged): + """Radiance Properties for Honeybee Door Abridged.""" + + type: constr(regex='^DoorRadiancePropertiesAbridged$') = \ + 'DoorRadiancePropertiesAbridged' + + +class FaceRadiancePropertiesAbridged(_PropertiesBaseAbridged): + """Radiance Properties for Honeybee Face Abridged.""" + + type: constr(regex='^FaceRadiancePropertiesAbridged$') = \ + 'FaceRadiancePropertiesAbridged' + + +class ShadeRadiancePropertiesAbridged(_PropertiesBaseAbridged): + """Radiance Properties for Honeybee Shade Abridged.""" + + type: constr(regex='^ShadeRadiancePropertiesAbridged$') = \ + 'ShadeRadiancePropertiesAbridged' + + +class RoomRadiancePropertiesAbridged(NoExtraBaseModel): + """Abridged Radiance Properties for Honeybee Room.""" + + type: constr(regex='^RoomRadiancePropertiesAbridged$') = \ + 'RoomRadiancePropertiesAbridged' + + modifier_set: str = Field( + default=None, + description='An identifier for a unique Room-Assigned ModifierSet ' + '(default: None).' + ) + + +class ModelRadianceProperties(NoExtraBaseModel): + """Radiance Properties for Honeybee Model.""" + + type: constr(regex='^ModelRadianceProperties$') = 'ModelRadianceProperties' + + modifiers: List[_REFERENCE_UNION_MODIFIERS] = Field( + default=[], + description='A list of all unique modifiers in the model. ' + 'This includes modifiers across all Faces, Apertures, Doors, Shades, ' + 'Room ModifierSets, and the global_modifier_set. (default: []).' + ) + + modifier_sets: List[Union[ModifierSet, ModifierSetAbridged]] = Field( + default=[], + description='A list of all unique Room-Assigned ModifierSets in the Model ' + '(default: []).' + ) + + global_modifier_set: str = Field( + default=None, + description='Identifier of a ModifierSet or ModifierSetAbridged object to be used as ' + 'as a default object for all unassigned objects in the Model ' + '(default: None).' + ) + diff --git a/tests/test_modifier.py b/tests/test_modifier.py new file mode 100644 index 0000000..8b20fb6 --- /dev/null +++ b/tests/test_modifier.py @@ -0,0 +1,96 @@ +from honeybee_schema.radiance.modifier import Plastic, Glass, BSDF, Glow, Light, Trans +from copy import copy +from pydantic import ValidationError +import pytest +import os +import json + +# target folder where all of the samples live +root = os.path.dirname(os.path.dirname(__file__)) +target_folder = os.path.join(root, 'samples', 'modifier') + + +def test_modifier_plastic_black(): + file_path = os.path.join(target_folder, 'modifier_plastic_black.json') + Plastic.parse_file(file_path) + + +def test_modifier_plastic_generic_ceiling(): + file_path = os.path.join(target_folder, 'modifier_plastic_generic_ceiling.json') + Plastic.parse_file(file_path) + + +def test_modifier_plastic_generic_wall(): + file_path = os.path.join(target_folder, 'modifier_plastic_generic_wall.json') + Plastic.parse_file(file_path) + + +def test_plastic_wrong(): + file_path = os.path.join(target_folder, 'modifier_plastic_black.json') + with open(file_path) as json_file: + plastic_modifier = json.load(json_file) + + # Test wrong type + plastic_modifier_test = copy(plastic_modifier) + plastic_modifier_test["type"] = 'glass' + with pytest.raises(ValidationError): + Plastic.parse_obj(plastic_modifier_test) + + # Test illegal character in identifier + plastic_modifier_test = copy(plastic_modifier) + plastic_modifier_test["identifier"] = '$#^&///' + with pytest.raises(ValidationError): + Plastic.parse_obj(plastic_modifier_test) + + # Test reflectance range + plastic_modifier_test = copy(plastic_modifier) + plastic_modifier_test["r_reflectance"] = -1 + with pytest.raises(ValidationError): + Plastic.parse_obj(plastic_modifier_test) + + +def test_glass_generic_exterior_window(): + file_path = os.path.join(target_folder, + 'modifier_glass_generic_exterior_window.json') + Glass.parse_file(file_path) + + +def test_glass_air_boundary(): + file_path = os.path.join(target_folder, + 'modifier_glass_air_boundary.json') + Glass.parse_file(file_path) + + +# TODO: Unfinished json +# def test_bsdf_klemsfull(): +# file_path = os.path.join(target_folder, 'modifier_bsdf_klemsfull.json') +# BSDF.parse_file(file_path) + + +def test_modifier_glow_white(): + file_path = os.path.join(target_folder, 'modifier_glow_white.json') + Glow.parse_file(file_path) + + +def test_modifier_light_green_spotlight(): + file_path = os.path.join(target_folder, 'modifier_light_green_spotlight.json') + Light.parse_file(file_path) + + +def test_trans_tree_foliage(): + file_path = os.path.join(target_folder, 'modifier_trans_tree_foliage.json') + Trans.parse_file(file_path) + + +def test_trans_tree_foliage_wrong(): + file_path = os.path.join(target_folder, 'modifier_trans_tree_foliage.json') + with open(file_path) as json_file: + trans_modifier = json.load(json_file) + + # Test wrong sum + trans_modifier_test = copy(trans_modifier) + trans_modifier_test["transmitted_diff"] = 0.6 + trans_modifier_test["transmitted_spec"] = 0.6 + with pytest.raises(ValidationError): + Trans.parse_obj(trans_modifier_test) + diff --git a/tests/test_modifierset.py b/tests/test_modifierset.py new file mode 100644 index 0000000..4a31770 --- /dev/null +++ b/tests/test_modifierset.py @@ -0,0 +1,27 @@ +from honeybee_schema.radiance.modifierset import ModifierSetAbridged, ModifierSet + +import os + +# target folder where all of the samples live +root = os.path.dirname(os.path.dirname(__file__)) +target_folder = os.path.join(root, 'samples', 'modifier_set') + + +def test_modifierset_abridged_complete(): + file_path = os.path.join(target_folder, 'modifierset_abridged_complete.json') + ModifierSetAbridged.parse_file(file_path) + + +def test_modifierset_abridged_partial_exterior(): + file_path = os.path.join(target_folder, 'modifierset_abridged_partial_exterior.json') + ModifierSetAbridged.parse_file(file_path) + + +def test_modifierset_complete(): + file_path = os.path.join(target_folder, 'modifierset_complete.json') + ModifierSet.parse_file(file_path) + + +def test_modifierset_partial_exterior(): + file_path = os.path.join(target_folder, 'modifierset_partial_exterior.json') + ModifierSet.parse_file(file_path) diff --git a/tests/test_properties.py b/tests/test_properties.py new file mode 100644 index 0000000..0b4e524 --- /dev/null +++ b/tests/test_properties.py @@ -0,0 +1,31 @@ +from honeybee_schema.radiance.properties import ApertureRadiancePropertiesAbridged, \ + DoorRadiancePropertiesAbridged, FaceRadiancePropertiesAbridged, \ + ShadeRadiancePropertiesAbridged, RoomRadiancePropertiesAbridged, \ + ModelRadianceProperties + +import os +import json + +# target folder where all of the samples live +root = os.path.dirname(os.path.dirname(__file__)) +target_folder = os.path.join(root, 'samples', 'model') + + +file_path = os.path.join(target_folder, 'model_complete_office_floor.json') +with open(file_path) as json_file: + office_model = json.load(json_file) + + +def test_model_radiance_properties(): + model_rad_props = office_model['properties']['radiance'] + ModelRadianceProperties.parse_obj(model_rad_props) + + +def test_room_radiance_properties(): + room_prop_abridged = office_model['rooms'][0]['properties']['radiance'] + RoomRadiancePropertiesAbridged.parse_obj(room_prop_abridged) + + +def test_face_radiance_properties(): + face_prop_abridged = office_model['rooms'][0]['faces'][0]['properties']['radiance'] + FaceRadiancePropertiesAbridged.parse_obj(face_prop_abridged)