# -*- coding: utf-8 -*-
"""Utilities for Radiance folder structure."""
import os
import json
import re
def _as_posix(path):
return path.replace('\\', '/')
[docs]
class SceneState(object):
"""A state for a dynamic non-aperture geometry.
This object is parallels the ``ShadeState`` class in ``honeybee-radiance``.
Args:
identifier (str): Human-readable identifier for the state.
default (str): Path to file to be used for normal representation of the geometry.
direct (str): Path to file to be used for direct studies.
Properties:
* identifier
* default
* direct
"""
def __init__(self, identifier, default, direct):
self.identifier = identifier
self.default = _as_posix(default)
self.direct = _as_posix(direct)
[docs]
@classmethod
def from_dict(cls, input_dict):
"""Create a state from an input dictionary.
.. code-block:: python
{
"identifier": "grass_covered",
"default": "ground..summer..000.rad",
"direct": "ground..direct..000.rad",
}
"""
for key in ['identifier', 'default', 'direct']:
assert key in input_dict, 'State is missing required key: %s' % key
identifier = input_dict['identifier']
default = _as_posix(os.path.normpath(input_dict['default']))
direct = _as_posix(os.path.normpath(input_dict['direct']))
return cls(identifier, default, direct)
[docs]
def validate(self, folder):
"""Validate files in this state.
Args:
folder: Path to state folder.
"""
assert os.path.isfile(os.path.join(folder, self.default)), \
'Failed to find default file for %s' % self.identifier
assert os.path.isfile(os.path.join(folder, self.direct)), \
'Failed to find direct file for %s' % self.identifier
def __repr__(self):
return 'SceneState: {}'.format(self.identifier)
[docs]
class ApertureState(SceneState):
"""A state for a dynamic aperture from Radiance files.
This object parallels the honeybee-radiance ``SubFaceState`` in
``honeybee-radiance``.
Args:
identifier (str): Optional human-readable identifier for the state. Can be None.
default (str): Path to file to be used for normal representation of the geometry.
direct (str): Path to file to be used for direct studies.
black (str): Path to file for blacking out the window.
tmtx (str): Path to file for transmittance matrix.
vmtx (str): Path to file for transmittance matrix.
dmtx (str): Path to file for transmittance matrix.
Properties:
* identifier
* default
* direct
* black
* tmtx
* vmtx
* dmtx
"""
def __init__(
self, identifier, default, direct, black=None, tmtx=None, vmtx=None,
dmtx=None):
SceneState.__init__(self, identifier, default, direct)
self.black = _as_posix(black) if black is not None else None
self.tmtx = _as_posix(tmtx) if tmtx is not None else None
self.vmtx = _as_posix(vmtx) if vmtx is not None else None
self.dmtx = _as_posix(dmtx) if dmtx is not None else None
[docs]
@classmethod
def from_dict(cls, input_dict):
"""Create a state from an input dictionary.
.. code-block:: python
{
"identifier": "clear",
"default": "./south_window..default..000.rad",
"direct": "./south_window..direct..000.rad",
"black": "./south_window..black.rad",
"tmtx": "clear.xml",
"vmtx": "./south_window..mtx.rad",
"dmtx": "./south_window..mtx.rad"
}
"""
for key in ['identifier', 'default', 'direct']:
assert key in input_dict, 'State is missing required key: %s' % key
identifier = input_dict['identifier']
default = _as_posix(os.path.normpath(input_dict['default']))
direct = _as_posix(os.path.normpath(input_dict['direct']))
try:
black = input_dict['black']
except KeyError:
black = None
try:
tmtx = input_dict['tmtx']
except KeyError:
tmtx = None
try:
vmtx = _as_posix(os.path.normpath(input_dict['vmtx']))
except KeyError:
vmtx = None
try:
dmtx = _as_posix(os.path.normpath(input_dict['dmtx']))
except KeyError:
dmtx = None
return cls(identifier, default, direct, black, tmtx, vmtx, dmtx)
[docs]
def validate(self, folder, bsdf_folder):
"""Validate files in this state.
Args:
folder: Path to dynamic scene folder.
bsdf_folder: Path to BSDF folder.
"""
assert os.path.isfile(os.path.join(folder, self.default)), \
'Failed to find default file for %s' % self.identifier
assert os.path.isfile(os.path.join(folder, self.direct)), \
'Failed to find direct file for %s' % self.identifier
if self.black is not None:
assert os.path.isfile(os.path.join(folder, self.black)), \
'Failed to find black file for %s' % self.identifier
if self.tmtx is not None:
assert os.path.isfile(os.path.join(bsdf_folder, self.tmtx)), \
'Failed to find tmtx file for %s' % self.identifier
if self.vmtx is not None:
assert os.path.isfile(os.path.join(folder, self.vmtx)), \
'Failed to find vmtx file for %s' % self.identifier
if self.dmtx is not None:
assert os.path.isfile(os.path.join(folder, self.dmtx)), \
'Failed to find dmtx file for %s' % self.identifier
def __repr__(self):
return 'ApertureState: {}'.format(self.identifier)
[docs]
class DynamicScene(object):
"""Representation of a Dynamic scene geometry in Radiance folder.
Args:
identifier (str): Text string for a unique dynamic scene group identifier.
This is required and cannot be None.
states(list[SceneState]): A list of scene states.
Properties:
* identifier
* states
* state_count
"""
def __init__(self, identifier, states):
self.identifier = identifier
self.states = states
@property
def state_count(self):
"""Number of states."""
return len(self.states)
[docs]
@classmethod
def from_dict(cls, input_dict):
"""Create a dynamic scene from a dictionary.
Args:
input_dict: An input dictionary.
.. code-block:: python
{
"ground": [
{
"identifier": "grass_covered",
"default": "ground..summer..000.rad",
"direct": "ground..direct..000.rad"
},
{
"identifier": "snow_covered",
"default": "ground..winter..001.rad",
"direct": "ground..direct..000.rad"
}
]
}
"""
keys = list(input_dict.keys())
assert len(keys) == 1, \
'There must be only one dynamic group in input dictionary.'
identifier = keys[0]
states_dict = input_dict[identifier]
states = [SceneState.from_dict(state) for state in states_dict]
return cls(identifier, states)
[docs]
def validate(self, folder):
"""Validate this dynamic geometry.
Args:
folder: Path to dynamic scene folder.
"""
for state in self.states:
state.validate(folder)
def __repr__(self):
return 'DynamicScene: {}'.format(self.identifier)
[docs]
class ApertureGroup(DynamicScene):
"""Representation of a Dynamic aperture in Radiance folder.
Args:
identifier (str): Text string for a unique dynamic aperture group identifier.
This is required and cannot be None.
states: A list of aperture states.
Properties:
* identifier
* states
"""
[docs]
@classmethod
def from_dict(cls, input_dict):
"""Create a dynamic aperture from a dictionary.
.. code-block:: python
{
"south_window": [
{
"identifier": "clear",
"default": "./south_window..default..000.rad",
"direct": "./south_window..direct..000.rad",
"black": "./south_window..black.rad",
"tmtx": "clear.xml",
"vmtx": "./south_window..mtx.rad",
"dmtx": "./south_window..mtx.rad"
},
{
"identifier": "diffuse",
"default": "./south_window..default..001.rad",
"direct": "./south_window..direct..001.rad",
"black": "./south_window..black.rad",
"tmtx": "diffuse50.xml",
"vmtx": "./south_window..mtx.rad",
"dmtx": "./south_window..mtx.rad"
}
]
}
"""
keys = list(input_dict.keys())
assert len(keys) == 1, \
'There must be only one dynamic group in input dictionary.'
identifier = keys[0]
states_dict = input_dict[identifier]
states = [ApertureState.from_dict(state) for state in states_dict]
return cls(identifier, states)
[docs]
def validate(self, folder, bsdf_folder):
"""Validate aperture group.
Args:
folder: Path to dynamic scene folder.
bsdf_folder: Path to BSDF folder.
"""
for state in self.states:
state.validate(folder, bsdf_folder)
def __repr__(self):
return 'ApertureGroup: {} (#{})'.format(self.identifier, len(self.states))
[docs]
def parse_aperture_groups(states_file, validate=True, bsdf_folder=None):
"""Parse dynamic apertures from a states.json file.
Args:
states_file: Path to states JSON file.
validate: Validate the files in states files exist in the folder.
bsdf_folder: Required for validation of tmtx. Not required if validate is set to
False.
Returns:
A list of dynamic apertures
"""
if not os.path.isfile(states_file):
return []
with open(states_file) as inf:
data = json.load(inf)
apertures = [ApertureGroup.from_dict(
{key: value}) for key, value in data.items()]
if validate:
# check for the files to exist
folder = os.path.dirname(states_file)
for aperture in apertures:
aperture.validate(folder, bsdf_folder)
return apertures
[docs]
def parse_dynamic_scene(states_file, validate=True):
"""Parse dynamic nonaperture geometries from a state file.
Args:
states_file: Path to states JSON file.
Returns:
A list of dynamic nonaperture geometries
"""
if not os.path.isfile(states_file):
return []
with open(states_file) as inf:
data = json.load(inf)
geometries = [
DynamicScene.from_dict({key: value})
for key, value in data.items()
]
if validate:
# check for the files to exist
folder = os.path.dirname(states_file)
for geometry in geometries:
geometry.validate(folder)
return geometries
[docs]
def add_output_spec_to_receiver(receiver_file, output_spec, output_file=None):
"""Add output spec to a receiver file.
Args:
receiver_file: Path to a receiver file. You can find these files under the
``aperture_group`` subfolder.
output_spec: A string for receiver output spec.
output_file: An optional output file to write the modified receiver. By default
this function overwrites the original file.
"""
# find and replace
if not os.path.isfile(receiver_file):
raise ValueError('Failed to find %s' % receiver_file)
with open(receiver_file, 'r') as f:
content = f.read()
try:
value = re.search(r'^#@rfluxmtx[\s\S].*$',
content, re.MULTILINE).group(0)
except AttributeError:
raise ValueError(
'%s is not a valid receiver file with '
'RfluxmtxControlParameters.' % receiver_file
)
if 'o=' in value:
raise ValueError('%s already has output_spec' % value)
ctrl_params = value.strip() + ' o=%s' % output_spec
updated_content = re.sub(value, ctrl_params, content)
out_file = output_file or receiver_file
with open(out_file, 'w') as outf:
outf.write(updated_content)
[docs]
def parse_states(states_file):
"""Parse states information from a json file.
This information typically contains the various rad files for each state
such as 'default', 'black', 'direct', as well as matrix files such as
'vmtx', 'tmtx', and 'dmtx'.
Args:
states_file: Path to the states file.
Returns:
A list containing the information about states.
"""
if not os.path.isfile(states_file):
return []
with open(states_file) as inf:
return json.load(inf)
[docs]
def combined_receiver(grid_name, apt_group_folder, apt_groups, target_folder,
add_output_header=True):
"""Write combined receiver file for a grid and aperture groups.
The aperture group folder must be a relative path. Otherwise xform will fail when
using the combined receiver file in simulations.
Arg:
grid_name: A string of the grid name (identifier).
apt_group_folder: Path to aperture group folder.
apt_groups: A list of aperture groups to include in the combined receiver.
target_folder: A path of the target folder to write files to.
add_output_header: If set to True, a header will be added to redirect the
generated view matrix to the path specified through the "o= .." option.
Returns:
The path of the file that was written out as the combined receiver.
"""
file_name = '%s..receiver.rad' % grid_name
apt_group_folder = apt_group_folder.replace('\\', '/')
content = []
content.append('# %s\n' % file_name) # add header
for apt in apt_groups:
if add_output_header:
content.append('#@rfluxmtx o=%s..%s.vmx' % (apt, grid_name))
content.append('!xform ./%s/%s..mtx.rad\n' % (apt_group_folder, apt))
out_file = os.path.join(target_folder, file_name)
with open(out_file, 'w') as outf:
outf.write('\n'.join(content))
return file_name
def _nukedir(target_dir, rmdir=True):
"""Delete all the files inside target_dir.
Usage:
nukedir("c:/ladybug/libs", True)
"""
d = os.path.normpath(target_dir)
if not os.path.isdir(d):
return
files = os.listdir(d)
for f in files:
if f == '.' or f == '..':
continue
path = os.path.join(d, f)
if os.path.isdir(path):
_nukedir(path)
else:
try:
os.remove(path)
except Exception:
print("Failed to remove %s" % path)
if rmdir:
try:
os.rmdir(d)
except Exception:
print("Failed to remove %s" % d)