# coding=utf-8
"""Module for visualizing and converting between meteorological and other wind speeds."""
from __future__ import division
import math
from ladybug_geometry.geometry3d import Vector3D, Point3D, Plane, LineSegment3D, \
Polyline3D, Mesh3D
from .datacollection import HourlyContinuousCollection
from .color import Color
[docs]class WindProfile(object):
"""Object for visualizing and converting from meteorological wind speeds.
Args:
terrain: A text string that sets the terrain class associated with the
location that the wind profile represents. (Default: city). Values
must be one the following:
* city - 50% of buildings above 21m over a distance of at least 2000m upwind.
* suburban - suburbs, wooded areas.
* country - open, with scattered objects generally less than 10m high.
* water - flat areas downwind of a large water body (max 500m inland).
meteorological_terrain: A text string that sets the terrain class associated
with the meteorological wind speed. (Default: country, which is typical
of most airports where wind measurements are taken).
meteorological_height: A number for the height above the ground at which
the meteorological wind speed is measured in meters. (Default: 10 meters).
log_law: A boolean to note whether the wind profile should use a logarithmic
law to determine wind speeds instead of the default power law, which
is used by EnergyPlus. (Default: False).
Properties:
* terrain
* meteorological_terrain
* meteorological_height
* log_law
* boundary_layer_height
* power_law_exponent
* roughness_length
* met_boundary_layer_height
* met_power_law_exponent
* met_roughness_length
"""
__slots__ = (
'_terrain', '_meteorological_terrain', '_meteorological_height', '_log_law',
'_boundary_layer_height', '_power_law_exponent', '_roughness_length',
'_met_boundary_layer_height', '_met_power_law_exponent', '_met_roughness_length',
'_met_power_denom', '_met_log_denom')
TERRAINS = ('city', 'suburban', 'country', 'water')
TERRAIN_PARAMETERS = {
'city': (460, 0.33, 1.0),
'suburban': (370, 0.22, 0.5),
'country': (270, 0.14, 0.1),
'water': (210, 0.10, 0.03)
}
BLACK = Color(0, 0, 0)
def __init__(self, terrain='city', meteorological_terrain='country',
meteorological_height=10, log_law=False):
"""Initialize wind profile."""
self._meteorological_height = 10 # set a default to ensure checks pass
self.terrain = terrain
self.meteorological_terrain = meteorological_terrain
self.meteorological_height = meteorological_height
self.log_law = log_law
@property
def terrain(self):
"""Get or set text for the terrain class for the wind profile location.
Setting this will set all of the properties of the boundary layer,
roughness length, etc. Choose from the following options.
* city
* suburban
* country
* water
"""
return self._terrain
@terrain.setter
def terrain(self, value):
value = self._check_terrain(value)
self._terrain = value
self._boundary_layer_height, self._power_law_exponent, self._roughness_length = \
self.TERRAIN_PARAMETERS[value]
@property
def meteorological_terrain(self):
"""Get or set text for the terrain class for the meteorological location.
Setting this will set all of the properties of the boundary layer,
roughness length, etc. Choose from the following options.
* city
* suburban
* country
* water
"""
return self._meteorological_terrain
@meteorological_terrain.setter
def meteorological_terrain(self, value):
value = self._check_terrain(value)
self._meteorological_terrain = value
self._met_boundary_layer_height, self._met_power_law_exponent, \
self._met_roughness_length = self.TERRAIN_PARAMETERS[value]
self._compute_met_power_denom()
self._compute_met_log_denom()
@property
def meteorological_height(self):
"""Get or set the measurement height of the meteorological wind speed [m]."""
return self._meteorological_height
@meteorological_height.setter
def meteorological_height(self, value):
assert isinstance(value, (float, int)), 'Expected number for ' \
'WindProfile meteorological_height. Got {}.'.format(type(value))
assert value > 0, 'WindProfile meteorological_height must be ' \
'greater than 0. Got {}.'.format(value)
self._meteorological_height = value
self._compute_met_power_denom()
self._compute_met_log_denom()
@property
def log_law(self):
"""A boolean to note whether the wind profile should be using a logarithmic law.
"""
return self._log_law
@log_law.setter
def log_law(self, value):
self._log_law = bool(value)
@property
def boundary_layer_height(self):
"""Get or set the boundary layer height of the wind profile location [m]."""
return self._boundary_layer_height
@boundary_layer_height.setter
def boundary_layer_height(self, value):
assert isinstance(value, (float, int)), 'Expected number for ' \
'WindProfile boundary_layer_height. Got {}.'.format(type(value))
assert value > 0, 'WindProfile boundary_layer_height must be ' \
'greater than 0. Got {}.'.format(value)
self._boundary_layer_height = value
@property
def power_law_exponent(self):
"""Get or set the power law exponent of the wind profile location."""
return self._power_law_exponent
@power_law_exponent.setter
def power_law_exponent(self, value):
assert isinstance(value, (float, int)), 'Expected number for ' \
'WindProfile power_law_exponent. Got {}.'.format(type(value))
assert 1 > value > 0, 'WindProfile power_law_exponent must be ' \
'between 0 and 1. Got {}.'.format(value)
self._power_law_exponent = value
@property
def roughness_length(self):
"""Get or set the roughness length of the wind profile location [m]."""
return self._roughness_length
@roughness_length.setter
def roughness_length(self, value):
assert isinstance(value, (float, int)), 'Expected number for ' \
'WindProfile roughness_length. Got {}.'.format(type(value))
assert value > 0, 'WindProfile roughness_length must be ' \
'greater than 0. Got {}.'.format(value)
self._roughness_length = value
@property
def met_boundary_layer_height(self):
"""Get or set the boundary layer height of the meteorological location [m]."""
return self._met_boundary_layer_height
@met_boundary_layer_height.setter
def met_boundary_layer_height(self, value):
assert isinstance(value, (float, int)), 'Expected number for ' \
'WindProfile met_boundary_layer_height. Got {}.'.format(type(value))
assert value > 0, 'WindProfile met_boundary_layer_height must be ' \
'greater than 0. Got {}.'.format(value)
self._met_boundary_layer_height = value
self._compute_met_power_denom()
@property
def met_power_law_exponent(self):
"""Get or set the power law exponent of the meteorological location."""
return self._met_power_law_exponent
@met_power_law_exponent.setter
def met_power_law_exponent(self, value):
assert isinstance(value, (float, int)), 'Expected number for ' \
'WindProfile met_power_law_exponent. Got {}.'.format(type(value))
assert 1 > value > 0, 'WindProfile met_power_law_exponent must be ' \
'between 0 and 1. Got {}.'.format(value)
self._met_power_law_exponent = value
self._compute_met_power_denom()
@property
def met_roughness_length(self):
"""Get or set the roughness length of the meteorological location [m]."""
return self._met_roughness_length
@met_roughness_length.setter
def met_roughness_length(self, value):
assert isinstance(value, (float, int)), 'Expected number for ' \
'WindProfile met_roughness_length. Got {}.'.format(type(value))
assert value > 0, 'WindProfile met_roughness_length must be ' \
'greater than 0. Got {}.'.format(value)
self._met_roughness_length = value
self._compute_met_log_denom()
[docs] def calculate_wind(self, meteorological_wind_speed, height=1):
"""Calculate the wind speed at a given height above the ground.
Args:
meteorological_wind_speed: A number for the meteorological
wind speed [m/s].
height: The height above the ground to be evaluated in
meters. (Default: 1).
"""
if self._log_law:
if height > self._roughness_length:
met_log_num = math.log(height / self._roughness_length)
return meteorological_wind_speed * (met_log_num / self._met_log_denom)
return 0
else:
h_ratio = (height / self._boundary_layer_height) ** self._power_law_exponent
return h_ratio * (meteorological_wind_speed * self._met_power_denom)
[docs] def calculate_wind_data(self, meteorological_wind_data, height=1):
"""Get a data collection of wind speed at a given height above the ground.
Args:
meteorological_wind_data: A data collection of meteorological
wind speed [m/s].
height: The height above the ground to be evaluated in
meters. (Default: 1).
"""
vals = tuple(self.calculate_wind(v, height) for v in meteorological_wind_data)
new_header = meteorological_wind_data.header.duplicate()
new_header.metadata['height'] = '{}m'.format(round(height, 1))
if isinstance(meteorological_wind_data, HourlyContinuousCollection):
return HourlyContinuousCollection(new_header, vals)
else:
dts = meteorological_wind_data.datetimes
return meteorological_wind_data.__class__(new_header, vals, dts)
[docs] def wind_vector(
self, meteorological_wind_speed, height, direction=None,
length_dimension=1, scale_factor=1):
"""Get a Vector3D for a wind profile arrow at a given height above the ground.
Args:
meteorological_wind_speed: A number for the meteorological wind speed [m/s].
height: The height above the ground to be evaluated in meters.
direction: An optional number between 0 and 360 that represents the
cardinal direction that the wind vector is facing in the XY
plane. 0 = North, 90 = East, 180 = South, 270 = West. If None,
the wind vector will simply be placed in the XY plane. (Default: None).
length_dimension: A number to denote the length dimension of a 1 m/s
wind vector in meters. This will be used to set the length of the
wind vector. (Default: 1).
scale_factor: An optional number that will be multiplied by all dimensions
to account for the fact that the wind profile may be displaying in
a units system other than meters. (Default: 1).
Returns:
A ladybug-geometry Vector3D representing the wind vector.
"""
direction = self._flip_direction(direction)
wind_speed = self.calculate_wind(meteorological_wind_speed, height)
vec_mag = wind_speed * length_dimension * scale_factor
if direction is None:
return Vector3D(vec_mag, 0, 0)
else:
base_vec = Vector3D(0, vec_mag, 0)
return base_vec.rotate_xy(-math.radians(direction))
[docs] def profile_polyline3d(
self, meteorological_wind_speed, max_height=30, vector_spacing=2,
direction=None, base_point=Point3D(0, 0, 0), length_dimension=5,
scale_factor=1):
"""Get a Polyline3D for a wind profile curve at a meteorological wind speed.
Args:
meteorological_wind_speed: A number for the meteorological wind speed [m/s].
max_height: A number in meters to specify the maximum height of the
wind profile curve. (Default: 30 meters).
vector_spacing: A number in meters to specify the difference in height
between each of the wind vectors that is used to build the
profile curve. Lower numbers will result in smoother looking
curves. (Default 2 meters).
direction: An optional number between 0 and 360 that represents the
cardinal direction that the wind profile is facing in the XY
plane. 0 = North, 90 = East, 180 = South, 270 = West. If None,
the wind profile will simply be placed in the XY plane. (Default: None).
base_point: A ladybug-geometry Point3D that represents the ground
location of the wind profile. (Default, (0, 0, 0)).
length_dimension: A number to denote the length dimension of a 1 m/s
wind vector in meters. (Default: 5).
scale_factor: An optional number that will be multiplied by all dimensions
to account for the fact that the wind profile may be displaying in
a units system other than meters. (Default: 1).
Returns:
A tuple with three values.
- profile_polyline: A ladybug-geometry Polyline3D representing the
wind profile.
- wind_vectors: A list of ladybug-geometry Vector3D representing the
wind vectors that built the profile.
- anchor_pts: A list of ladybug-geometry Point3D representing the
anchor points for the wind vectors.
"""
self._check_profile_inputs(max_height, vector_spacing)
bp = base_point
profile_pts, wind_vectors, anchor_pts = [bp], [Vector3D(0, 0, 0)], [bp]
m_val = max_height + vector_spacing
for h in self._frange(vector_spacing, m_val, vector_spacing):
a_pt = Point3D(bp.x, bp.y + (h * scale_factor), bp.z) \
if direction is None else Point3D(bp.x, bp.y, bp.z + (h * scale_factor))
w_vec = self.wind_vector(
meteorological_wind_speed, h, direction,
length_dimension, scale_factor)
profile_pts.append(a_pt.move(w_vec))
wind_vectors.append(w_vec)
anchor_pts.append(a_pt)
profile_polyline = Polyline3D(profile_pts)
return profile_polyline, wind_vectors, anchor_pts
[docs] def mesh_arrow(
self, meteorological_wind_speed, height, direction=None,
base_point=Point3D(0, 0, 0), length_dimension=5, height_dimension=1,
scale_factor=1):
"""Get a Mesh3D for an arrow at a given height above the ground.
Args:
meteorological_wind_speed: A number for the meteorological wind speed [m/s].
height: The height above the ground to be evaluated in meters.
direction: An optional number between 0 and 360 that represents the
cardinal direction that the mesh arrow is facing in the XY
plane. 0 = North, 90 = East, 180 = South, 270 = West. If None,
the wind vector and mesh arrow will simply be placed in the XY
plane. (Default: None).
base_point: A ladybug-geometry Point3D that represents the ground
location of the wind profile. (Default, (0, 0, 0)).
length_dimension: A number to denote the length dimension of a 1 m/s
wind vector in meters. This will be used to set the length of the
wind vector arrow. (Default: 5).
height_dimension: A number to denote the height dimension of the
wind vector in meters. (Default: 1).
scale_factor: An optional number that will be multiplied by all dimensions
to account for the fact that the wind profile may be displaying in
a units system other than meters. (Default: 1).
Returns:
A tuple with four values.
- mesh_arrow: A Mesh3D object that represents the wind speed at the
height above the ground.
- wind_vector: A ladybug-geometry Vector3D representing the wind vector.
- anchor_pt: A ladybug-geometry Point3D representing the anchor point
for the wind vector.
- wind_speed: A number for the wind speed associated with the mesh arrow.
"""
direction = self._flip_direction(direction)
# get an anchor point that represents the wind arrow
bp = base_point
if direction is None:
y_val = bp.y + height * scale_factor
anchor_pt = Point3D(bp.x, y_val, bp.z)
else:
z_val = bp.z + height * scale_factor
anchor_pt = Point3D(bp.x, bp.y, z_val)
# calculate the wind speed and vector at the height
wind_speed = self.calculate_wind(meteorological_wind_speed, height)
vec_mag = wind_speed * length_dimension
vm_scaled = vec_mag * scale_factor
# build the mesh vertices and with arrow facing the X-axis from the anchor point
x_vec = Vector3D(vm_scaled, 0, 0)
max_arrow = length_dimension / 2
if vec_mag >= length_dimension:
ar_vec = Vector3D(scale_factor * max_arrow, 0, 0)
ar_x = (vec_mag - max_arrow) * scale_factor
else:
mid_val = ar_x = vm_scaled * 0.5
ar_vec = Vector3D(mid_val, 0, 0)
if direction is None:
ar_pt = Point3D(bp.x + ar_x, bp.y + (height * scale_factor), bp.z)
else:
ar_pt = Point3D(bp.x + ar_x, bp.y, bp.z + (height * scale_factor))
ad = (height_dimension * scale_factor) / 2
hd = (height_dimension * scale_factor) / 4
stem_mvs = (Vector3D(0, hd, hd), Vector3D(0, -hd, hd),
Vector3D(0, -hd, -hd), Vector3D(0, hd, -hd))
ar_mvs = (Vector3D(0, ad, ad), Vector3D(0, -ad, ad),
Vector3D(0, -ad, -ad), Vector3D(0, ad, -ad))
verts = []
for mv in stem_mvs + (x_vec,):
verts.append(anchor_pt.move(mv))
for mv in ar_mvs + (ar_vec,):
verts.append(ar_pt.move(mv))
# rotate the vertices if the direction is specified
if direction is not None:
v_ang, rad90 = -math.radians(direction), math.radians(90)
verts = [v.rotate_xy(v_ang + rad90, base_point) for v in verts]
base_vec = Vector3D(0, vec_mag, 0)
wind_vector = base_vec.rotate_xy(v_ang)
else:
wind_vector = Vector3D(vec_mag, 0, 0)
# specify the faces and return the final Mesh3D
faces = [
(0, 1, 2, 3), (0, 1, 4), (1, 2, 4), (2, 3, 4), (3, 0, 4),
(5, 6, 7, 8), (5, 6, 9), (6, 7, 9), (7, 8, 9), (8, 5, 9)
]
mesh_arrow = Mesh3D(verts, faces)
return mesh_arrow, wind_vector, anchor_pt, wind_speed
[docs] def mesh_arrow_profile(
self, meteorological_wind_speed, max_height=30, vector_spacing=2,
direction=None, base_point=Point3D(0, 0, 0), length_dimension=5,
height_dimension=1, scale_factor=1):
"""Get a Polyline3D for a wind profile curve at a meteorological wind speed.
Args:
meteorological_wind_speed: A number for the meteorological wind speed [m/s].
max_height: A number in meters to specify the maximum height of the
wind profile curve. (Default: 30 meters).
vector_spacing: A number in meters to specify the difference in height
between each of the mesh arrows. (Default 2 meters).
direction: An optional number between 0 and 360 that represents the
cardinal direction that the wind profile is facing in the XY
plane. 0 = North, 90 = East, 180 = South, 270 = West. If None,
the wind profile will simply be placed in the XY plane. (Default: None).
base_point: A ladybug-geometry Point3D that represents the ground
location of the wind profile. (Default, (0, 0, 0)).
length_dimension: A number to denote the length dimension of a 1 m/s
wind vector in meters. (Default: 5).
height_dimension: A number to denote the height dimension of the
wind vector in meters. (Default: 1).
scale_factor: An optional number that will be multiplied by all dimensions
to account for the fact that the wind profile may be displaying in
a units system other than meters. (Default: 1).
Returns:
A tuple with five values
- profile_polyline: A ladybug-geometry Polyline3D representing the
wind profile.
- mesh_arrows: A list of ladybug-geometry Mesh3D objects that
represent the wind speed along with wind profile.
- wind_speeds: A list of numbers for the wind speed associated with
the mesh arrows.
- wind_vectors: A list of ladybug-geometry Vector3D representing the
wind vectors that built the profile.
- anchor_pts: A list of ladybug-geometry Point3D representing the
anchor points for the wind vectors.
"""
self._check_profile_inputs(max_height, vector_spacing)
bp = base_point
mesh_arrows, wind_speeds = [], []
profile_pts, wind_vectors, anchor_pts = [bp], [Vector3D(0, 0, 0)], [bp]
m_val = max_height + vector_spacing
for h in self._frange(vector_spacing, m_val, vector_spacing):
m_ar, w_vec, a_pt, ws = self.mesh_arrow(
meteorological_wind_speed, h, direction,
base_point, length_dimension, height_dimension, scale_factor)
mesh_arrows.append(m_ar)
wind_speeds.append(ws)
profile_pts.append(a_pt.move(w_vec))
wind_vectors.append(w_vec)
anchor_pts.append(a_pt)
profile_polyline = Polyline3D(profile_pts)
return profile_polyline, mesh_arrows, wind_speeds, wind_vectors, anchor_pts
[docs] def speed_axis(
self, max_speed=5, direction=None, base_point=Point3D(0, 0, 0),
length_dimension=5, scale_factor=1, text_height=None):
"""Get a several objects for representing the X axis for wind speed.
Args:
max_speed: A number for the maximum wind speed along the axis
in [m/s]. (Default: 5)
direction: An optional number between 0 and 360 that represents the
cardinal direction that the axis is facing in the XY
plane. 0 = North, 90 = East, 180 = South, 270 = West. If None,
the axis will simply be placed in the XY plane. (Default: None).
base_point: A ladybug-geometry Point3D that represents the origin
of the axis. (Default, (0, 0, 0)).
length_dimension: A number to denote the length dimension of a 1 m/s
wind vector in meters. (Default: 5).
scale_factor: An optional number that will be multiplied by all dimensions
to account for the fact that the wind profile may be displaying in
a units system other than meters. (Default: 1).
text_height: An optional number for the height of the text in the axis
label. If None, the text height will be inferred based on the
length_dimension. (Default: None).
Returns:
A tuple with three values.
- axis_line: A ladybug-geometry Linesegment3D representing the axis.
- axis_arrow: A ladybug-geometry Mesh3D representing the arrow head.
- axis_ticks: A list of ladybug-geometry LineSegment3D representing the
marks of speeds along the axis.
- text_planes: A list of ladybug-geometry Planes for the axis text labels.
- text: A list of text strings tha align with the text_planes for the
text to display in the 3D scene.
"""
# construct the axis line along the world X axis
direction = self._flip_direction(direction)
bp, max_speed = base_point, int(max_speed)
end_x = bp.x + ((max_speed + 1) * length_dimension * scale_factor)
ax_end_pt = Point3D(end_x, bp.y, bp.z)
axis_line = LineSegment3D.from_end_points(bp, ax_end_pt)
step_d = length_dimension * scale_factor
# create the arrow at the end of the axis as a mesh
a_d = step_d / 8
ea_pt = Point3D(end_x + a_d * 3, bp.y, bp.z)
if direction is None:
m_pts = (Point3D(end_x, bp.y + a_d, bp.z),
Point3D(end_x, bp.y - a_d, bp.z), ea_pt)
else:
m_pts = (Point3D(end_x, bp.y, bp.z + a_d),
Point3D(end_x, bp.y, bp.z - a_d), ea_pt)
axis_arrow = Mesh3D(m_pts, ((0, 1, 2),), colors=(self.BLACK,))
# create the axis ticks and text planes
tick_d = step_d / 6
axis_ticks, text_planes, text = [], [], []
for i in range(1, max_speed + 1):
pt_x = bp.x + (step_d * i)
pt_1 = Point3D(pt_x, bp.y, bp.z)
if direction is None:
pt_2 = Point3D(pt_x, bp.y - tick_d, bp.z)
pt_3 = Point3D(pt_x, bp.y - (tick_d * 2), bp.z)
txt_p = Plane(n=Vector3D(0, 0, 1), o=pt_3, x=Vector3D(1, 0, 0))
else:
pt_2 = Point3D(pt_x, bp.y, bp.z - tick_d)
pt_3 = Point3D(pt_x, bp.y, bp.z - (tick_d * 2))
txt_p = Plane(n=Vector3D(0, -1, 0), o=pt_3, x=Vector3D(1, 0, 0))
axis_ticks.append(LineSegment3D.from_end_points(pt_1, pt_2))
text_planes.append(txt_p)
text.append(str(i))
# add a text plane and string for the axis title
txt_height = length_dimension / 2 if text_height is None else text_height
sub_d = (tick_d * 2) + (txt_height * 2)
ti_x = axis_line.midpoint.x
if direction is None:
pt_4 = Point3D(ti_x, bp.y - sub_d, bp.z)
text_planes.append(Plane(n=Vector3D(0, 0, 1), o=pt_4, x=Vector3D(1, 0, 0)))
else:
pt_4 = Point3D(ti_x, bp.y, bp.z - sub_d)
text_planes.append(Plane(n=Vector3D(0, -1, 0), o=pt_4, x=Vector3D(1, 0, 0)))
text.append('Wind Speed (m/s)')
# rotate the whole axis if a direction is specified
if direction is not None:
dir_radians = math.radians(90) - math.radians(direction)
axis_line = axis_line.rotate_xy(dir_radians, bp)
axis_arrow = axis_arrow.rotate_xy(dir_radians, bp)
axis_ticks = [t.rotate_xy(dir_radians, bp) for t in axis_ticks]
text_planes = [p.rotate_xy(dir_radians, bp) for p in text_planes]
return axis_line, axis_arrow, axis_ticks, text_planes, text
[docs] def height_axis(
self, max_height=30, tick_spacing=4, direction=None,
base_point=Point3D(0, 0, 0), scale_factor=1, text_height=None,
feet_labels=False):
"""Get a several objects for representing the Y axis for height above the ground.
Args:
max_height: A number in meters to specify the maximum height of the
wind profile curve. (Default: 30 meters).
tick_spacing: A number in meters to specify the difference between
each of axis ticks. (Default 4 meters).
direction: An optional number between 0 and 360 that represents the
cardinal direction that the wind profile is facing in the XY
plane. 0 = North, 90 = East, 180 = South, 270 = West. If None,
the axis will simply be placed in the XY plane. (Default: None).
base_point: A ladybug-geometry Point3D that represents the ground
location of the wind profile. (Default, (0, 0, 0)).
scale_factor: An optional number that will be multiplied by all dimensions
to account for the fact that the wind profile may be displaying in
a units system other than meters. (Default: 1).
text_height: An optional number for the height of the text in the axis
label. If None, the text height will be inferred based on the
tick_spacing. (Default: None).
feet_labels: A boolean to note whether the text labels should be in
feet (True) or meters (False). (Default: False).
Returns:
A tuple with three values.
- axis_line: A ladybug-geometry Linesegment3D representing the axis.
- axis_arrow: A ladybug-geometry Mesh3D representing the arrow head.
- axis_ticks: A list of ladybug-geometry LineSegment3D representing the
marks of heights along the axis.
- text: A list of text strings tha align with the text_planes for the
text to display in the 3D scene.
"""
# construct the axis line
direction = self._flip_direction(direction)
bp = base_point
end_v = (max_height + (tick_spacing / 2)) * scale_factor
ax_end_pt = Point3D(bp.x, bp.y + end_v, bp.z) if direction is None \
else Point3D(bp.x, bp.y, bp.z + end_v)
axis_line = LineSegment3D.from_end_points(bp, ax_end_pt)
step_d = tick_spacing * scale_factor
# create the arrow at the end of the axis as a mesh
a_d = step_d / 10
if direction is None:
ea_pt = Point3D(bp.x, ax_end_pt.y + (a_d * 3), bp.z)
m_pts = (Point3D(bp.x + a_d, ax_end_pt.y, bp.z),
Point3D(bp.x - a_d, ax_end_pt.y, bp.z), ea_pt)
else:
ea_pt = Point3D(bp.x, bp.y, ax_end_pt.z + (a_d * 3))
m_pts = (Point3D(bp.x + a_d, bp.y, ax_end_pt.z),
Point3D(bp.x - a_d, bp.y, ax_end_pt.z), ea_pt)
axis_arrow = Mesh3D(m_pts, ((0, 1, 2),), colors=(self.BLACK,))
# create the axis ticks and text planes
tick_d = step_d / 6
axis_ticks, text_planes, text = [], [], []
mh = max_height + tick_spacing if max_height % tick_spacing == 0 else max_height
for i in self._frange(0, mh, tick_spacing):
pt_v = i * scale_factor
if direction is None:
pt_1 = Point3D(bp.x, bp.y + pt_v, bp.z)
pt_2 = Point3D(bp.x - tick_d, bp.y + pt_v, bp.z)
pt_3 = Point3D(bp.x - (tick_d * 2), bp.y + pt_v, bp.z)
txt_p = Plane(n=Vector3D(0, 0, 1), o=pt_3, x=Vector3D(1, 0, 0))
else:
pt_1 = Point3D(bp.x, bp.y, bp.z + pt_v)
pt_2 = Point3D(bp.x - tick_d, bp.y, bp.z + pt_v)
pt_3 = Point3D(bp.x - (tick_d * 2), bp.y, bp.z + pt_v)
txt_p = Plane(n=Vector3D(0, -1, 0), o=pt_3, x=Vector3D(1, 0, 0))
axis_ticks.append(LineSegment3D.from_end_points(pt_1, pt_2))
text_planes.append(txt_p)
txt_str = str(int(i)) if not feet_labels else str(int(i * 3.28084))
text.append(txt_str)
# add a text plane and string for the axis title
txt_height = tick_spacing / 2 if text_height is None else text_height
sub_d = (tick_d * 2) + (txt_height * 4)
if direction is None:
ti_v = axis_line.midpoint.y
pt_4 = Point3D(bp.x - sub_d, ti_v, bp.z)
text_planes.append(Plane(n=Vector3D(0, 0, 1), o=pt_4, x=Vector3D(0, 1, 0)))
else:
ti_v = axis_line.midpoint.z
pt_4 = Point3D(bp.x - sub_d, bp.y, ti_v)
text_planes.append(Plane(n=Vector3D(0, -1, 0), o=pt_4, x=Vector3D(0, 0, 1)))
units = 'ft' if feet_labels else 'm'
text.append('Height ({})'.format(units))
# rotate the whole axis if a direction is specified
if direction is not None:
dir_radians = math.radians(90) - math.radians(direction)
axis_line = axis_line.rotate_xy(dir_radians, bp)
axis_arrow = axis_arrow.rotate_xy(dir_radians, bp)
axis_ticks = [t.rotate_xy(dir_radians, bp) for t in axis_ticks]
text_planes = [p.rotate_xy(dir_radians, bp) for p in text_planes]
return axis_line, axis_arrow, axis_ticks, text_planes, text
[docs] def legend_plane(
self, max_speed=5, direction=None, base_point=Point3D(0, 0, 0),
length_dimension=5, scale_factor=1):
"""Get a recommended Plane for the default base plane of the legend.
Args:
max_speed: A number for the maximum wind speed along the axis
in [m/s]. (Default: 5)
direction: An optional number between 0 and 360 that represents the
cardinal direction that the axis is facing in the XY
plane. 0 = North, 90 = East, 180 = South, 270 = West. If None,
the axis will simply be placed in the XY plane. (Default: None).
base_point: A ladybug-geometry Point3D that represents the origin
of the axis. (Default, (0, 0, 0)).
length_dimension: A number to denote the length dimension of a 1 m/s
wind vector in meters. (Default: 5).
scale_factor: An optional number that will be multiplied by all dimensions
to account for the fact that the wind profile may be displaying in
a units system other than meters. (Default: 1).
Returns:
A ladybug-geometry Plane for the recommended legend plane.
"""
direction = self._flip_direction(direction)
bp, max_speed = base_point, int(max_speed)
end_x = bp.x + ((max_speed + 2) * length_dimension * scale_factor)
origin_pt = Point3D(end_x, bp.y, bp.z)
if direction is None:
return Plane(n=Vector3D(0, 0, 1), o=origin_pt, x=Vector3D(1, 0, 0))
else:
st_pl = Plane(n=Vector3D(0, -1, 0), o=origin_pt, x=Vector3D(1, 0, 0))
dir_radians = math.radians(90) - math.radians(direction)
return st_pl.rotate_xy(dir_radians, bp)
[docs] def title_plane(
self, direction=None, base_point=Point3D(0, 0, 0),
length_dimension=5, scale_factor=1, text_height=None):
"""Get a recommended Plane for the default base plane of the title.
Args:
max_speed: A number for the maximum wind speed along the axis
in [m/s]. (Default: 5)
direction: An optional number between 0 and 360 that represents the
cardinal direction that the axis is facing in the XY
plane. 0 = North, 90 = East, 180 = South, 270 = West. If None,
the axis will simply be placed in the XY plane. (Default: None).
base_point: A ladybug-geometry Point3D that represents the origin
of the axis. (Default, (0, 0, 0)).
length_dimension: A number to denote the length dimension of a 1 m/s
wind vector in meters. (Default: 5).
scale_factor: An optional number that will be multiplied by all dimensions
to account for the fact that the wind profile may be displaying in
a units system other than meters. (Default: 1).
text_height: An optional number for the height of the text in the axis
label. If None, the text height will be inferred based on the
length_dimension. (Default: None).
Returns:
A ladybug-geometry Plane for the recommended title plane.
"""
direction = self._flip_direction(direction)
bp = base_point
step_d = length_dimension * scale_factor
tick_d = step_d / 6
txt_height = length_dimension / 2 if text_height is None else text_height
sub_d = (tick_d * 2) + (txt_height * 5)
if direction is None:
origin_pt = Point3D(bp.x, bp.y - sub_d, bp.z)
return Plane(n=Vector3D(0, 0, 1), o=origin_pt, x=Vector3D(1, 0, 0))
else:
origin_pt = Point3D(bp.x, bp.y, bp.z - sub_d)
st_pl = Plane(n=Vector3D(0, -1, 0), o=origin_pt, x=Vector3D(1, 0, 0))
dir_radians = math.radians(90) - math.radians(direction)
return st_pl.rotate_xy(dir_radians, bp)
def _check_terrain(self, value):
"""Check any string input for the terrain."""
clean_input = value.lower()
for key in self.TERRAINS:
if key.lower() == clean_input:
value = key
break
else:
raise ValueError(
'terrain {} is not recognized.\nChoose from the '
'following:\n{}'.format(value, self.TERRAINS))
return value
def _compute_met_power_denom(self):
"""Compute the denominator of the power function."""
h_ratio = self._met_boundary_layer_height / self._meteorological_height
self._met_power_denom = h_ratio ** self._met_power_law_exponent
def _compute_met_log_denom(self):
"""Compute the denominator of the logarithmic function."""
h_ratio = self._meteorological_height / self._met_roughness_length
self._met_log_denom = math.log(h_ratio)
@staticmethod
def _flip_direction(direction):
"""Flip the direction of a wind so it notes the orientation of arrows."""
if direction is not None:
if direction < 180:
return direction + 180
else:
return direction - 180
@staticmethod
def _check_profile_inputs(max_height, vector_spacing):
assert vector_spacing > 0, 'WindProfile vector spacing must be greater ' \
'than zero. Got {}.'.format(vector_spacing)
assert max_height > vector_spacing, 'WindProfile max height [{}] must be ' \
'greater than vector spacing [{}].'.format(max_height, vector_spacing)
@staticmethod
def _frange(start, stop, step):
"""Range function capable of yielding float values."""
while start < stop:
yield start
start += step
[docs] def ToString(self):
"""Overwrite .NET ToString method."""
return self.__repr__()
def __repr__(self):
"""WindProfile representation."""
return "WindProfile (terrain: {})".format(self.terrain)