def incline_leaf(shape, inclin, relative_angle=True): """ transform a xysr tuple representing leaf shape to get a given angle at leaf base. - angle the desired angle (deg) - if relative_angle == True, angle is interpreted as a multiplier to original shape angle """ Linc = inclin x, y = shape[0], shape[1] init_angle = pgl.angle((x[1] - x[0], y[1] - y[0]), (0, 1)) if relative_angle: angle = Linc * init_angle angle = min(pi, angle) else: angle = radians(Linc) rotation_angle = init_angle - angle # rotation of the midrib cos_a = cos(rotation_angle) sin_a = sin(rotation_angle) x1 = x[0] + cos_a * x - sin_a * y y1 = y[0] + sin_a * x + cos_a * y leaf = x1, y1, shape[2], shape[3] return leaf
def incline_leaf(shape, inclin, relative_angle = True): """ transform a xysr tuple representing leaf shape to get a given angle at leaf base. - angle the desired angle (deg) - if relative_angle == True, angle is interpreted as a multiplier to original shape angle """ Linc = inclin x, y = shape[0], shape[1] init_angle = pgl.angle((x[1]-x[0], y[1]-y[0]),(0,1)) if relative_angle: angle = Linc * init_angle angle = min(pi, angle) else: angle = radians(Linc) rotation_angle = init_angle - angle # rotation of the midrib cos_a = cos(rotation_angle); sin_a = sin(rotation_angle) x1 = x[0] + cos_a*x - sin_a*y y1 = y[0] + sin_a*x + cos_a*y leaf= x1, y1, shape[2], shape[3] return leaf
def _mesh(self, leaf_rank, seed, total_length, length, s_base, s_top, radius_max, *args): db = self.database rank_max = max(map(int,db.keys())) rank = leaf_rank rank = min(rank, rank_max) #choisi la liste de leaves du rang, ou rang + 1 si clef absente ourag -1 ou liste vide sinon leaves = db.get(str(rank), db.get(str(rank+1), db.get(str(rank-1), []))) n = len(leaves) if n == 0: dbk = ' '.join(db.keys()) raise AdelParameterisationError("Leaf curvature index %d not found in database, available indices are:\n%s"%(rank,dbk)) if self.seed is None: random.seed(seed) else: random.seed(self.seed) if args and len(args) > 1 and args[1] >= 0: i = args[1] - 1 #R index starts at 1 else: i = random.randint(0,n-1) leaf = leaves[i] # Rotation of the midrib of the leaf to set the insertion angle a relative fraction of the angle (reference beeing the vertical) if args and args[0] >= 0: Linc = args[0] x, y = leaf[0], leaf[1] init_angle = pgl.angle((x[1]-x[0], y[1]-y[0]),(0,1)) if self.relative_angle: angle = Linc * init_angle angle = min(math.pi, angle) else: angle = math.radians(Linc) rotation_angle = init_angle - angle # rotation of the midrib cos_a = cos(rotation_angle); sin_a = sin(rotation_angle) x1 = x[0] + cos_a*x - sin_a*y y1 = y[0] + sin_a*x + cos_a*y leaf = (x1, y1) + leaf[2:] leaf_mesh = fitting.mesh4(leaf, total_length, length, s_base, s_top, radius_max) if leaf_mesh: pts, ind = leaf_mesh if len(ind) < 2: mesh = None else: mesh = fitting.plantgl_shape(pts, ind) else: mesh = None return mesh
def _set_axis(self, axis): """Property helper method. """ if axis != self._axis: self._axis = axis rotation_axis = pgl.cross(pgl.Vector3.OZ, self._axis) rotation_angle = pgl.angle(self._axis, pgl.Vector3.OZ) self.rotated.axis = rotation_axis self.rotated.angle = rotation_angle
def _set_axis( self, axis ): """Property helper method. """ if axis != self._axis: self._axis = axis rotation_axis = pgl.cross( pgl.Vector3.OZ, self._axis ) rotation_angle = pgl.angle( self._axis, pgl.Vector3.OZ ) self.rotated.axis = rotation_axis self.rotated.angle = rotation_angle
def __init__(self, pos=ASHAPE3D_STANDARD_POS, axis=ASHAPE3D_STANDARD_AXIS, roll=ASHAPE3D_STANDARD_ROLL, scale=ASHAPE3D_STANDARD_SCALE, material=ASHAPE3D_STANDARD_MATERIAL, geometry=None, **keys): """Default constructor. Parameters: pos : Vector3 convertable pos of the object (look below), axis : Vector3 convertable main axis of the object, should be defined to describe the rotation. The Z of the primitive geometry would point to axis, roll : Real Property: rotation of the object around main axis, scale : Vector3 convertable to use while resizing the object. While scaling the Z corresponds to main axis of the object, material : pgl.Material describes the appearance of the object, geometry : pgl.Geometry describes the geometry of the object. """ if not geometry: raise Exception("AShape3D: geometry not defined.") self.geometry = geometry #TODO check for custom rotation, scale, transformation objects (shared between shapes) self.scaled = pgl.Scaled(scale, self.geometry) # roll related self._roll = roll #: to keep internal roll self.rolled = pgl.AxisRotated(pgl.Vector3.OZ, self._roll, self.scaled) # axis related (even the object which do not have intuitive axis need to have predefined axis self._axis = axis #: to keep internal axis vector rotation_axis = pgl.cross(pgl.Vector3.OZ, self._axis) rotation_angle = pgl.angle(self._axis, pgl.Vector3.OZ) self.rotated = pgl.AxisRotated(rotation_axis, rotation_angle, self.rolled) # position related self.translated = pgl.Translated(pos, self.rotated) # apperance related self.shape = pgl.Shape(self.translated, material)
def __init__( self, pos=ASHAPE3D_STANDARD_POS, axis=ASHAPE3D_STANDARD_AXIS, roll=ASHAPE3D_STANDARD_ROLL, scale=ASHAPE3D_STANDARD_SCALE, material=ASHAPE3D_STANDARD_MATERIAL, geometry=None, **keys ): """Default constructor. Parameters: pos : Vector3 convertable pos of the object (look below), axis : Vector3 convertable main axis of the object, should be defined to describe the rotation. The Z of the primitive geometry would point to axis, roll : Real Property: rotation of the object around main axis, scale : Vector3 convertable to use while resizing the object. While scaling the Z corresponds to main axis of the object, material : pgl.Material describes the appearance of the object, geometry : pgl.Geometry describes the geometry of the object. """ if not geometry: raise Exception( "AShape3D: geometry not defined." ) self.geometry = geometry #TODO check for custom rotation, scale, transformation objects (shared between shapes) self.scaled = pgl.Scaled( scale, self.geometry ) # roll related self._roll = roll #: to keep internal roll self.rolled = pgl.AxisRotated(pgl.Vector3.OZ, self._roll, self.scaled) # axis related (even the object which do not have intuitive axis need to have predefined axis self._axis = axis #: to keep internal axis vector rotation_axis = pgl.cross( pgl.Vector3.OZ, self._axis ) rotation_angle = pgl.angle( self._axis, pgl.Vector3.OZ ) self.rotated = pgl.AxisRotated( rotation_axis, rotation_angle, self.rolled ) # position related self.translated = pgl.Translated( pos, self.rotated ) # apperance related self.shape = pgl.Shape( self.translated, material )
def arrange_leaf(leaf, stem_diameter=0, inclination=1, relative=True): """Arrange a leaf to be placed along a stem with a given inclination. Args: leaf: a x, y, s, r tuple describing leaf shape stem_diameter: the diameter of the sem at the leaf insertion point inclination: if relative=False, the leaf basal inclination (deg). A multiplier to leaf basal inclination angle otherwise relative: (bool) controls the meaning of inclination parameter Returns: a modified x, y, s, r tuple """ x, y, s, r = map(numpy.array, leaf) if relative and inclination == 1: x1, y1 = x, y else: basal_inclination = pgl.angle((x[1] - x[0], y[1] - y[0]), (0, 1)) if relative: angle = inclination * basal_inclination angle = min(pi, angle) else: angle = radians(inclination) rotation_angle = basal_inclination - angle # rotation of the midrib cos_a = cos(rotation_angle) sin_a = sin(rotation_angle) x1 = x[0] + cos_a * x - sin_a * y y1 = y[0] + sin_a * x + cos_a * y leaf = x1 + stem_diameter / 2., y1, s, r return leaf
def ucinsertionangle(uc, m): ucldir = ucdir(uc, m) ucpdir = ucdir(m.parent(uc), m) if ucldir is None or ucpdir is None: return None return degrees(angle(ucldir, ucpdir))
def __call__(self, g, v, turtle): geometry = g.property('geometry') # 1. retrieve the node n = g.node(v) axis = n.complex_at_scale(scale=2).label az_axis = n.complex_at_scale(scale=2).azimuth prev_axis = turtle.context.get("axis", axis) metamer = n.complex_at_scale(scale=3) # Go to plant position if first plant element if n.parent() is None: #this is a new plant base p = n.complex_at_scale(scale=1) if 'position' in p.properties(): #print p.label, 'moving to ', p.position turtle.move(map(float, p.position)) else: turtle.move(0, 0, 0) #initial position to be compatible with canMTG positioning turtle.setHead(0, 0, 1, -1, 0, 0) if 'azimuth' in p.properties(): turtle.rollR(p.azimuth) #print prev_axis, 'stop' #print 'new MS start' turtle.context.update({ 'MS_top': turtle.getFrame(), 'tiller_base': turtle.getFrame(), 'top': turtle.getFrame(), 'is_axis_first_StemElement': True }) if axis != prev_axis: if metamer.edge_type() == '+': turtle.context['is_axis_first_StemElement'] = True if prev_axis == 'MS': #this is the begining of the first tiller #print axis, 'start' #top of mainstem saved turtle.context['MS_top'] = turtle.context['top'] turtle.context['tiller_base'] = turtle.context['top'] else: # this is a new tiller attached to the same point thatn the previous tiller #print prev_axis, 'stop' #print axis, 'start' # return to tilller base turtle.context['top'] = turtle.context['tiller_base'] else: #this is the continuation of MS #print prev_axis, 'stop' #print axis, 'continue' turtle.context['top'] = turtle.context['MS_top'] #hypothesis that inclin is to be applied at the base of the visible elements #if n.offset > 0: # turtle.f(n.offset) turtle.setFrame(turtle.context['top']) #incline turtle at the base of stems, if n.label.startswith('Stem'): inclin = float(n.inclination) if n.inclination else 0. azim = float(n.azimuth) if n.azimuth else 0. if inclin: #print 'node', n._vid, 'inclin', inclin # incline along curent azimuth for ramification (tiller bases) or plant base if turtle.context['is_axis_first_StemElement']: #print 'axis',axis, 'prev_axis', prev_axis,' node ', n._vid, 'edge', n.edge_type(),'up before inclin ', turtle.getUp(), 'inclin', inclin if prev_axis != 'MS': #new tiller attached to the same position than the firt turtle.rollR(az_axis) turtle.context['tiller_base'] = turtle.getFrame() turtle.down(inclin) turtle.context['is_axis_first_StemElement'] = False #print 'up after inclin', turtle.getUp() # if not incline towardss vertical else: up = turtle.getUp() zleft = turtle.getLeft()[2] turtle.rollToVert() #print 'up after rollToVert', turtle.getUp() angle = degrees(pgl.angle(up, turtle.getUp())) dzl = zleft - turtle.getLeft()[2] turtle.down(inclin) #replace turtle in original azimuth plane #print 'angle ', angle, 'dzl', dzl if dzl < 0: angle = -angle turtle.rollR(-angle) if azim: #print 'node', n._vid, 'azim ', azim turtle.rollR(azim) if n.label.startswith('Leaf') or n.label.startswith('Stem'): # update geometry of elements mesh = None if n.length > 0: mesh = compute_element(n, self.leaves, self.classic) if mesh: n.geometry = turtle.transform(mesh, face_up=self.face_up and n.label.startswith('Leaf')) n.anchor_point = turtle.getPosition() else: if v in geometry: # delete existing geometry geometry.pop(v) # 3. Update the turtle and context turtle.setId(v) if n.label.startswith('Stem'): if n.length > 0: turtle.f(n.length) turtle.context.update({'top': turtle.getFrame()}) if n.label.startswith('Leaf'): if n.lrolled > 0: turtle.f(n.lrolled) turtle.context.update({'top': turtle.getFrame()}) turtle.context.update({'axis': axis})
""" Gather different strategies for modeling dispersal of fungus propagules.
def _mesh(self, leaf_rank, seed, total_length, length, s_base, s_top, radius_max, *args): db = self.database rank_max = max(map(int, db.keys())) rank = leaf_rank rank = min(rank, rank_max) #choisi la liste de leaves du rang, ou rang + 1 si clef absente ourag -1 ou liste vide sinon leaves = db.get(str(rank), db.get(str(rank + 1), db.get(str(rank - 1), []))) n = len(leaves) if n == 0: dbk = ' '.join(db.keys()) raise AdelParameterisationError( "Leaf curvature index %d not found in database, available indices are:\n%s" % (rank, dbk)) if self.seed is None: random.seed(seed) else: random.seed(self.seed) if args and len(args) > 1 and args[1] >= 0: i = args[1] - 1 #R index starts at 1 else: i = random.randint(0, n - 1) leaf = leaves[i] # Rotation of the midrib of the leaf to set the insertion angle a relative fraction of the angle (reference beeing the vertical) if args and args[0] >= 0: Linc = args[0] x, y = leaf[0], leaf[1] init_angle = pgl.angle((x[1] - x[0], y[1] - y[0]), (0, 1)) if self.relative_angle: angle = Linc * init_angle angle = min(math.pi, angle) else: angle = math.radians(Linc) rotation_angle = init_angle - angle # rotation of the midrib cos_a = cos(rotation_angle) sin_a = sin(rotation_angle) x1 = x[0] + cos_a * x - sin_a * y y1 = y[0] + sin_a * x + cos_a * y leaf = (x1, y1) + leaf[2:] leaf_mesh = fitting.mesh4(leaf, total_length, length, s_base, s_top, radius_max) if leaf_mesh: pts, ind = leaf_mesh if len(ind) < 2: mesh = None else: mesh = fitting.plantgl_shape(pts, ind) else: mesh = None return mesh
def disperse(self, g, dispersal_units, time_control = None): """ Compute distribution of dispersal units by rain splash. 1. Upward dispersal: For each source of dispersal units, create a semi-sphere of dispersal normal to the surface of source leaf. In the semi-sphere, target leaves are sorted according to the distance from the source. Then distribute dispersal units from the closer target to the more far. The number of dispersal units by target leaf is computed as in Robert et al. 2008 2. Downward dispersal: Get leaves in a cylinder whose dimensions are related to the dimesions of the semi-sphere in step 1. Then distribute dispersal units from top to bottom. Parameters ---------- g: MTG MTG representing the canopy (and the soil) dispersal_units : dict Dispersal units emitted by the lesions on leaves Returns ------- deposits : dict Dispersal units deposited on new position on leaves """ try: dt = time_control.dt except: dt = 1 deposits = {} if dt>0: from alinea.astk.plantgl_utils import get_area_and_normal from alinea.alep.architecture import get_leaves from openalea.plantgl import all as pgl from collections import OrderedDict from math import exp, pi, cos, sin, tan from random import shuffle import numpy as np from copy import copy dmax = self.distance_max tesselator = pgl.Tesselator() bbc = pgl.BBoxComputer(tesselator) leaves = get_leaves(g, label=self.label) centroids = g.property('centroid') geometries = g.property('geometry') _, norm = get_area_and_normal(geometries) areas = g.property('area') def centroid(vid): if is_iterable(geometries[vid]): bbc.process(pgl.Scene(geometries[vid])) else: bbc.process(pgl.Scene([pgl.Shape(geometries[vid])])) center = bbc.result.getCenter() centroids[vid] = center for source, dus in dispersal_units.iteritems(): nb_tri = len(norm[source]) borders = np.linspace(0,1,num=nb_tri) dus_by_tri = {k:filter(lambda x: borders[k]<x.position[0]<=borders[k+1], dus) for k in range(nb_tri-1) if len(filter(lambda x: borders[k]<x.position[0]<=borders[k+1], dus))>0.} for k,v in dus_by_tri.iteritems(): source_normal = norm[source][k] ## UPWARD ## # All leaves except the source are potential targets targets = list(leaf for leaf in leaves if leaf in geometries.iterkeys()) targets.remove(source) # Compute centroids centroid(source) for vid in targets: centroid(vid) # Sort the vids based on the direction # TODO : modify source angle Origin = centroids[source] vects = {vid:(centroids[vid]-Origin) for vid in targets if (centroids[vid]-Origin)*source_normal >= 0} # Sort the vids based on the distance distances = {vid:pgl.norm(vects[vid]) for vid in vects if pgl.norm(vects[vid])<dmax} distances = OrderedDict(sorted(distances.iteritems(), key=lambda x: x[1])) # Distribute the dispersal units if len(distances.values())>0: shuffle(v) n = len(v) sphere_area = 2*pi*distances.values()[-1]**2 for leaf_id in distances: area_factor = areas[leaf_id]/sphere_area distance_factor = exp(-self.k * distances[leaf_id]) qc = min(n, (n * area_factor * distance_factor)) deposits[leaf_id] = v[:int(qc)] del v[:int(qc)] # if len(dus) < 1 or len(deposits[leafid]) < 1: if len(v) < 1: for d in v: d.disable() # break ## DOWNWARD ## vects2 = {vid:(centroids[vid]-Origin) for vid in targets if not vid in vects} projection = {} alpha = pgl.angle(source_normal, (1,0,0)) if alpha>=pi/2. or (alpha<pi/2. and source_normal[2]>=0): alpha+=pi/2. beta = pgl.angle(source_normal, (0,0,1)) a = dmax b = dmax*cos(beta) for leaf in vects2: if (centroids[leaf]-Origin)*(source_normal[0], source_normal[1], 0) >= 0: # Big side of the projection semi circle copy_centroid = copy(centroids[leaf]) copy_origin = copy(Origin) copy_centroid[2] = 0. copy_origin[2] = 0. if pgl.norm(copy_centroid-copy_origin) < dmax: projection[leaf] = vects2[leaf] else: # Small side of the projection semi ellipse x = vects2[leaf][0] y = vects2[leaf][1] x2 = x*cos(alpha)+y*sin(alpha) y2 = -x*sin(alpha)+y*cos(alpha) if (x2**2)/(a**2) + (y2**2)/(b**2) < 1 : projection[leaf] = vects2[leaf] projection = OrderedDict(sorted(projection.items(), key=lambda x:x[1][2], reverse=True)) if len(projection)>0: shuffle(v) n = len(v) n_big = int(n*(beta+pi/2.)/pi) n_small = n - n_big for leaf in projection: copy_centroid = copy(centroids[leaf]) copy_origin = copy(Origin) copy_centroid[2] = 0. copy_origin[2] = 0. if (centroids[leaf]-Origin)*(source_normal[0],source_normal[1],0) >= 0: area_factor = areas[leaf]/(pi*dmax**2/2.) dist = pgl.norm(copy_centroid-copy_origin) distance_factor = exp(-self.k * dist) qc = min(n_big, (n_big * area_factor * distance_factor)) g.node(leaf).color = (0, 180, 0) else: area_factor = areas[leaf]/(pi*a*b/2.) dist = pgl.norm(copy_centroid-copy_origin)/abs(cos(pgl.angle(source_normal, (1,0,0))+pi/2.)) distance_factor = exp(-self.k * dist) qc = min(n_small, (n_small * area_factor * distance_factor)) g.node(leaf).color = (0, 0, 180) # import pdb # pdb.set_trace() deposits[leaf] = v[:int(qc)] del v[:int(qc)] for leaf in distances: g.node(leaf).color = (180, 0, 0) # Temp # from alinea.adel.mtg_interpreter import plot3d # from openalea.plantgl.all import Viewer # g.node(source).color=(230, 62, 218) # scene = plot3d(g) # Viewer.display(scene) # import pdb # pdb.set_trace() return deposits
def disperse(self, g, dispersal_units, time_control = None): """ Compute dispersal of spores of powdery mildew by wind in a cone. For each source of dispersal units, create a cone of dispersal in which target leaves are sorted: 1. according to the wind direction 2. according to the angle a0 3. according to the distance from the source Then distribute dispersal units from the closer target to the more far. The number of dispersal units by target leaf is computed as in Calonnec et al. 2008. Parameters ---------- g: MTG MTG representing the canopy (and the soil) dispersal_units : dict Dispersal units emitted by the lesions on leaves Returns ------- deposits : dict Dispersal units deposited on new position on leaves """ try: dt = time_control.dt except: dt = 1 deposits = {} if dt > 0: from alinea.alep.architecture import get_leaves from openalea.plantgl import all as pgl from random import shuffle from math import degrees, exp, tan, pi, radians from collections import OrderedDict geometries = g.property('geometry') centroids = g.property('centroid') areas = g.property('area') wind_directions = g.property('wind_direction') tesselator = pgl.Tesselator() bbc = pgl.BBoxComputer(tesselator) leaves = get_leaves(g, label=self.label) def centroid(vid): if is_iterable(geometries[vid]): bbc.process(pgl.Scene(geometries[vid])) else: bbc.process(pgl.Scene([pgl.Shape(geometries[vid])])) center = bbc.result.getCenter() centroids[vid] = center def area(vid): # areas[vid] = pgl.surface(geometries[vid][0])*1000 areas[vid] = pgl.surface(geometries[vid][0]) for source, dus in dispersal_units.iteritems(): # TODO: Special computation for interception by source leaf # All other leaves are potential targets targets = list(leaf for leaf in leaves if leaf in geometries.iterkeys()) targets.remove(source) # Compute centroids centroid(source) for vid in targets: centroid(vid) # surface(vid) # Sort the vids based on the direction Origin = centroids[source] vects = {vid:(centroids[vid]-Origin) for vid in targets if (centroids[vid]-Origin)*wind_directions[source] >= 0} # Sort the vids based on the angle angles = {vid:degrees(pgl.angle(vect, wind_directions[source])) for vid, vect in vects.iteritems() if degrees(pgl.angle(vect, wind_directions[source]))<= self.a0} # Sort the vids based on the distance distances = {vid:pgl.norm(vects[vid]) for vid in angles} distances = OrderedDict(sorted(distances.iteritems(), key=lambda x: x[1])) # Beer law inside cone to take into account leaf coverage shuffle(dus) n = len(dus) if len(distances.values())>0: for leaf in distances: # qc = min(n, (n * (areas[leaf]/self.reduction) * # exp(-self.cid * distances[leaf]) * # (self.a0 - angles[leaf])/self.a0)) surf_base_cone = pi*(tan(radians(self.a0))*distances[leaf])**2 area_factor = min(1, areas[leaf]/surf_base_cone) # import pdb # pdb.set_trace() qc = min(n, (n * area_factor * exp(-self.cid * distances[leaf]) * (self.a0 - angles[leaf])/self.a0)) # if qc < 1: # for d in dus: # d.disable() # break deposits[leaf] = dus[:int(qc)] del dus[:int(qc)] # if len(dus) < 1 or len(deposits[leaf]) < 1: if len(dus) < 1: for d in dus: d.disable() break return deposits
def wind_speed_on_leaf(wind_speed=0., leaf_height=0., canopy_height=0., lai=0., lc=0.2, cd=0.3, is_in_rows=True, row_direction=(1, 0, 0), wind_direction=(1, 0, 0), param_reduc=0.5): """ Calculate the wind speed on a given leaf according to its height in the canopy. This very simple model makes the assumption that the canopy is completely homogeneous. The particular case of interrows is not treated. Also it does not take into account the wind direction. The equation comes from appendix C in the following article: TUZET, A., PERRIER, A. and LEUNING, R. (2003), A coupled model of stomatal conductance, photosynthesis and transpiration. Plant, Cell and Environment, 26: 1097-1116. doi: 10.1046/j.1365-3040.2003.01035.x link : http://onlinelibrary.wiley.com/doi/10.1046/j.1365-3040.2003.01035.x/abstract Parameters ---------- wind_speed: float Wind speed (in m.s-1) leaf_height: float Leaf height in the canopy (in m) canopy_heigth: float Height of the highest leaf (in m) lai: float Leaf Area Index of the canopy (in m2 of leaf / m2 of ground) lc: float Canopy mixing length (in m) cd: float Leaf drag coefficient (dimensionless) is_in_rows: True or False Indicate if there are rows between plants row_direction: tuple(x,y,z) Direction of vine rows wind_direction: tuple(x,y,z) Wind direction param_reduc: float Maximal reduction of eta for winds parallels to row direction Returns ------- wind_speed_on_leaf: float Wind speed on the given leaf """ from math import exp from openalea.plantgl import all as pgl eta = canopy_height * ((cd * lai / canopy_height) / (2 * lc**2))**(1. / 3) if is_in_rows: angle = degrees(pgl.angle(row_direction, wind_direction)) reduction = param_reduc * (1 - angle / 90) return wind_speed * exp( max(0., eta - reduction) * ((leaf_height / canopy_height) - 1)) else: return wind_speed * exp(eta * ((leaf_height / canopy_height) - 1))