Source code for skijumpdesign.functions

import os
import logging

import numpy as np
from scipy.interpolate import interp1d

# TODO : Might be better to use:
# import matplotlib
# matplotlib.use('Agg')
# so that it doesn't try to use tk on heroku.
if 'ONHEROKU' in os.environ:
    plt = None
else:
    import matplotlib.pyplot as plt

from .skiers import Skier
from .surfaces import (FlatSurface, HorizontalSurface, TakeoffSurface,
                       LandingTransitionSurface, LandingSurface)
from .utils import InvalidJumpError, vel2speed


[docs]def snow_budget(parent_slope, takeoff, landing, landing_trans): """Returns the jump's cross sectional snow budget area of the EFH jump. Parameters ========== parent_slope : FlatSurface A FlatSurface that spans before and after the jump. takeoff : TakeoffSurface The clothiod-circle-clothiod-flat takeoff surface. landing : LandingSurface The EFH landing surface. landing_trans: LandingTransitionSurface The EFH landing transition surface. Returns ======= float The cross sectional snow budget (area between the parent slope and jump curve) in meters squared. """ # TODO : Make this function more robust, may need to handle jumps that are # above the x axis. if (np.any(takeoff.y > 0.0) or np.any(landing.y > 0.0) or np.any(landing_trans.y > 0.0)): logging.warn('Snowbudget invalid since jump about X axis.') A = parent_slope.area_under(x_start=takeoff.start[0], x_end=landing_trans.end[0]) B = takeoff.area_under() + landing.area_under() + landing_trans.area_under() return np.abs(A - B)
[docs]def make_jump(slope_angle, start_pos, approach_len, takeoff_angle, fall_height, plot=False): """Returns a set of surfaces and output values that define the equivalent fall height jump design and the skier's flight trajectory. Parameters ========== slope_angle : float The parent slope angle in degrees. Counter clockwise is positive and clockwise is negative. start_pos : float The distance in meters along the parent slope from the top (x=0, y=0) to where the skier starts skiing. approach_len : float The distance in meters along the parent slope the skier travels before entering the takeoff. takeoff_angle : float The angle in degrees at end of the takeoff ramp. Counter clockwise is positive and clockwise is negative. fall_height : float The desired equivalent fall height of the landing surface in meters. plot : boolean If True a matplotlib figure showing the jump will appear. Returns ======= slope : FlatSurface The parent slope starting at (x=0, y=0) until a meter after the jump. approach : FlatSurface The slope the skier travels on before entering the takeoff. takeoff : TakeoffSurface The circle-clothoid-circle-flat takeoff ramp. landing : LandingSurface The equivalent fall height landing surface. landing_trans : LandingTransitionSurface The minimum exponential landing transition. flight : Trajectory The maximum velocity flight trajectory. outputs : dictionary A dictionary of output values with keys: ``Takeoff Speed``, ``Flight Time``, and ``Snow Budget``. """ # TODO : function is too long! outputs = {'Takeoff Speed': None, 'Flight Time': None, 'Snow Budget': None} logging.info('Calling make_jump({}, {}, {}, {}, {})'.format( slope_angle, start_pos, approach_len, takeoff_angle, fall_height)) skier = Skier() if takeoff_angle >= 90.0 or takeoff_angle <= slope_angle: msg = 'Invalid takeoff angle. Enter value between {} and 90 degrees' raise InvalidJumpError(msg.format(slope_angle)) slope_angle = np.deg2rad(slope_angle) takeoff_angle = np.deg2rad(takeoff_angle) # The approach is the flat slope that the skier starts from rest on to gain # speed before reaching the takeoff ramp. init_pos = (start_pos * np.cos(slope_angle), start_pos * np.sin(slope_angle)) approach = FlatSurface(slope_angle, approach_len, init_pos=init_pos) # The takeoff surface is the combined circle-clothoid-circle-flat. # TODO : If there is not enough speed, then this method will run forever # because the skier can't make the jump. Need to raise an error if this is # the case. takeoff_entry_speed = skier.end_speed_on(approach) takeoff = TakeoffSurface(skier, slope_angle, takeoff_angle, takeoff_entry_speed, init_pos=approach.end) # The skier becomes airborne after the takeoff surface and the trajectory # is computed until the skier contacts the parent slope. takeoff_vel = skier.end_vel_on(takeoff, init_speed=takeoff_entry_speed) msg = 'Takeoff speed: {:1.3f} [m/s]' takeoff_speed = vel2speed(*takeoff_vel)[0] outputs['Takeoff Speed'] = takeoff_speed logging.info(msg.format(takeoff_speed)) slope = FlatSurface(slope_angle, 100 * approach_len) flight = skier.fly_to(slope, init_pos=takeoff.end, init_vel=takeoff_vel) # The landing transition curve transfers the max velocity skier from their # landing point smoothly to the parent slope. landing_trans = LandingTransitionSurface(slope, flight, fall_height, skier.tolerable_landing_acc) slope = FlatSurface(slope_angle, np.sqrt(landing_trans.end[0]**2 + landing_trans.end[1]**2) + 1.0) land_trans_contact = HorizontalSurface(landing_trans.start[1], 50.0, start=landing_trans.start[0] - 10.0) flight = skier.fly_to(land_trans_contact, init_pos=takeoff.end, init_vel=takeoff_vel) outputs['Flight Time'] = flight.duration outputs['Flight Distance'] = flight.pos[-1, 0] - flight.pos[0, 0] logging.info('Flight time: {:1.3f} [s]'.format(flight.duration)) # The landing surface ensures an equivalent fall height for any skiers that # do not reach maximum velocity. landing = LandingSurface(skier, takeoff.end, takeoff_angle, landing_trans.start, fall_height, surf=slope) logging.info("Num points in landing surface: {}".format(len(landing.x))) if landing.y[0] < slope.interp_y(landing.x[0]): raise InvalidJumpError('Fall height is too large.') x_at_highest = flight.interp_pos_wrt_slope(0.0)[0] y_at_highest = flight.interp_pos_wrt_x(x_at_highest)[1] outputs['Flight Height'] = y_at_highest - landing.interp_y(x_at_highest) budget = snow_budget(slope, takeoff, landing, landing_trans) outputs['Snow Budget'] = budget logging.info('Snow budget: {} m^2'.format(budget)) if plot: plot_jump(slope, approach, takeoff, landing, landing_trans, flight) plt.show() return slope, approach, takeoff, landing, landing_trans, flight, outputs
[docs]def plot_jump(slope, approach, takeoff, landing, landing_trans, flight): """Returns a matplotlib axes with the jump and flight plotted given the surfaces created by ``make_jump()``.""" ax = slope.plot(linestyle='dashed', color='black', label='Slope') ax = approach.plot(ax=ax, linewidth=2, label='Approach') ax = takeoff.plot(ax=ax, linewidth=2, label='Takeoff') ax = landing.plot(ax=ax, linewidth=2, label='Landing') ax = landing_trans.plot(ax=ax, linewidth=2, label='Landing Transition') ax = flight.plot(ax=ax, linestyle='dotted', label='Flight') ax.grid() ax.legend() return ax
[docs]def plot_efh(surface, takeoff_angle, takeoff_point, show_knee_collapse_line=True, skier=None, increment=0.2, ax=None): """Returns a matplotlib axes containing a plot of the surface and its corresponding equivalent fall height. Parameters ========== surface : Surface A Surface for a 2D curve expressed in a standard Cartesian coordinate system. takeoff_angle : float Takeoff angle in degrees. takeoff_point : 2-tuple of floats x and y coordinates relative to the surface's coordinate system of the point at which the skier leaves the takeoff ramp. show_knee_collapse_line : bool, optional Displays a line on the EFH plot indicating the EHF above which even elite ski jumpers are unable to prevent knee collapse. This value is taken from [Minetti]_. skier : Skier, optional A skier instance. This is passed to ``calculate_efh``. increment : float, optional x increment in meters between each calculated landing location. This is passed to ``calculate_efh``. ax : array of Axes, shape(2,), optional An existing matplotlib axes to plot to - ax[0] equivalent fall height, ax[1] surface profile. References ========== .. [Minetti] Minetti AE, Ardigo LP, Susta D, Cotelli F (2010) Using leg muscles as shock absorbers: theoretical predictions and experimental results of drop landing performance. Ergonomics 41(12):1771–1791 """ if skier is None: skier = Skier() takeoff_ang = np.deg2rad(takeoff_angle) dist, efh, speeds = surface.calculate_efh(takeoff_ang, takeoff_point, skier, increment) if ax is None: _, ax = plt.subplots(2, 1, sharex=True) prof_ax = ax[0] efh_ax = ax[1] surf_line_kwargs = {'color': 'black', 'linewidth': 2, 'label': 'Surface Profile'} prof_ax.plot(surface.x, surface.y, **surf_line_kwargs) prof_ax.scatter(*zip(takeoff_point), label='Takeoff Point', color='C1') prof_ax.set_ylabel('Vertical Position [m]') prof_ax.grid(True) prof_ax.legend() efh_bar_kwargs = {'color': 'black', 'align': 'center', 'width': increment/2, 'label': None} rects = efh_ax.bar(dist, efh, **efh_bar_kwargs) for rect, si in list(zip(rects, speeds))[::2]: height = rect.get_height() efh_ax.text(rect.get_x() + rect.get_width()/2., 1.05*height, '{:1.1f}'.format(si), fontsize='xx-small', ha='center', va='bottom', rotation=90) knee_line_kwargs = {'color': 'C1', 'label': 'Knee Collapse EFH, 1.5m', 'linestyle': ':'} if show_knee_collapse_line: knee_collapse_efh = 1.5 efh_ax.axhline(knee_collapse_efh, **knee_line_kwargs) efh_ax.set_xlabel('Horizontal Position [m]') efh_ax.set_ylabel('Equivalent Fall Height [m]') efh_ax.grid(True) efh_ax.legend() return ax
[docs]def cartesian_from_measurements(distances, angles, takeoff_distance=None): """Returns the Cartesian coordinates of a surface given measurements of distance along the surface and angle measurements at each distance measure along with the takeoff point and takeoff angle. Parameters ========== distances : array_like, shape(n,) Distances measured from an origin location on a jump surface along the surface of the jump. angles : array_like, shape(n,) Angle of the slope surface at the distance measures in radians. Positive about a right handed z axis. takeoff_distance : float Distance value where the takeoff is located (only if the takeoff is on the surface on the measured portion of the surface). Returns ======= x : ndarray, shape(n-1,) Longitudinal coordinates of the surface. y : ndarray, shape(n-1,) Vertical coordinates of the surface. takeoff_point : tuple of floats (x, y) coordinates of the takeoff point. takeoff_angle : float Angle in radians at the takeoff point. """ del_d = np.diff(distances) avg_ang = (angles[:-1] + angles[1:]) / 2.0 del_x = del_d*np.cos(avg_ang) del_y = del_d*np.sin(avg_ang) x = np.hstack((0.0, np.cumsum(del_x))) y = np.hstack((0.0, np.cumsum(del_y))) if takeoff_distance is None: # assumes takeoff is first point takeoff_x = x[0] takeoff_y = y[0] takeoff_angle = angles[0] else: if takeoff_distance < distances[0]: msg = 'Takeoff distance must be larger than {:1.3f}' raise ValueError(msg.format(distances[0])) interp_func = interp1d(distances, angles) takeoff_angle = interp_func(takeoff_distance) idx = np.argmin(np.abs(distances - takeoff_distance)) if distances[idx] > takeoff_distance: idx = idx - 1 takeoff_del_d = takeoff_distance - distances[idx] takeoff_del_x = takeoff_del_d*np.cos((takeoff_angle + angles[idx])/2.0) takeoff_del_y = takeoff_del_d*np.sin((takeoff_angle + angles[idx])/2.0) takeoff_x = x[idx] + takeoff_del_x takeoff_y = y[idx] + takeoff_del_y return x, y, (takeoff_x, takeoff_y), takeoff_angle