Source code for co2mpas.core.model.physical.vehicle

# -*- coding: utf-8 -*-
#
# Copyright 2015-2019 European Commission (JRC);
# Licensed under the EUPL (the 'Licence');
# You may not use this work except in compliance with the Licence.
# You may obtain a copy of the Licence at: http://ec.europa.eu/idabc/eupl
"""
Functions and `dsp` model to model the mechanic of the vehicle.
"""
import numpy as np
import schedula as sh
from co2mpas.defaults import dfl

dsp = sh.BlueDispatcher(
    name='Vehicle free body diagram',
    description='Calculates forces and power acting on the vehicle.'
)


[docs]@sh.add_function(dsp, outputs=['velocities']) def calculate_velocities(times, obd_velocities): """ Filters the obd velocities [km/h]. :param times: Time vector [s]. :type times: numpy.array :param obd_velocities: OBD velocity vector [km/h]. :type obd_velocities: numpy.array :return: Velocity vector [km/h]. :rtype: numpy.array """ from co2mpas.utils import median_filter dt_window = dfl.functions.calculate_velocities.dt_window return median_filter(times, obd_velocities, dt_window, np.mean)
def _integral(x, y, y0=.0): from scipy.interpolate import InterpolatedUnivariateSpline as Spl return Spl(x, y).antiderivative()(x) + y0
[docs]@sh.add_function(dsp, outputs=['distances']) def calculate_distances(times, velocities): """ Calculates the cumulative distance vector [m]. :param times: Time vector [s]. :type times: numpy.array :param velocities: Velocity vector [km/h]. :type velocities: numpy.array :return: Cumulative distance vector [m]. :rtype: numpy.array """ d = _integral(times, velocities / 3.6, 0) d[1:] = np.cumsum(np.maximum(0, np.diff(d))) + d[0] return d
[docs]@sh.add_function( dsp, inputs_kwargs=True, inputs_defaults=True, outputs=['velocities'] ) def calculate_velocities_v1(times, accelerations, initial_velocity=.0): """ Calculates the velocity from acceleration time series [km/h]. :param times: Time vector [s]. :type times: numpy.array :param accelerations: Acceleration vector [m/s2]. :type accelerations: numpy.array :param initial_velocity: Initial velocity [km/h]. :type initial_velocity: float :return: Velocity vector [km/h]. :rtype: numpy.array """ vel = _integral(times, accelerations, initial_velocity / 3.6) * 3.6 return np.maximum(0, vel)
[docs]@sh.add_function(dsp, outputs=['accelerations']) def calculate_accelerations(times, velocities): """ Calculates the acceleration from velocity time series [m/s2]. :param times: Time vector [s]. :type times: numpy.array :param velocities: Velocity vector [km/h]. :type velocities: numpy.array :return: Acceleration vector [m/s2]. :rtype: numpy.array """ from scipy.interpolate import InterpolatedUnivariateSpline as Spl acc = Spl(times, velocities / 3.6).derivative()(times) b = (velocities[:-1] == 0) & (velocities[1:] == velocities[:-1]) acc[:-1][b] = 0 if b[-1]: acc[-1] = 0 return acc
[docs]@sh.add_function(dsp, outputs=['aerodynamic_resistances']) def calculate_aerodynamic_resistances(f2, velocities): """ Calculates the aerodynamic resistances of the vehicle [N]. :param f2: As used in the dyno and defined by respective guidelines [N/(km/h)^2]. :type f2: float :param velocities: Velocity vector [km/h]. :type velocities: numpy.array | float :return: Aerodynamic resistance vector [N]. :rtype: numpy.array | float """ return f2 * velocities ** 2
dsp.add_data('n_passengers', dfl.values.n_passengers) dsp.add_data('passenger_mass', dfl.values.passenger_mass) dsp.add_data('cargo_mass', dfl.values.cargo_mass) dsp.add_data('fuel_mass', dfl.values.fuel_mass)
[docs]@sh.add_function(dsp, outputs=['passengers_mass']) def calculate_passengers_mass(n_passengers, passenger_mass): """ Calculate passengers mass including driver [kg]. :param n_passengers: Number of passengers including driver [-]. :type n_passengers: int :param passenger_mass: Average passenger mass [kg]. :type passenger_mass: float :return: Passengers mass including the driver [kg]. :rtype: float """ return passenger_mass * n_passengers
[docs]@sh.add_function(dsp, outputs=['unladen_mass']) def calculate_unladen_mass(curb_mass, fuel_mass): """ Calculate unladen mass [kg]. :param curb_mass: Curb mass [kg]. :type curb_mass: float :param fuel_mass: Fuel mass [kg]. :type fuel_mass: float :return: Unladen mass [kg]. :rtype: float """ return curb_mass + fuel_mass
[docs]@sh.add_function(dsp, outputs=['unladen_mass']) def calculate_unladen_mass_v1(vehicle_mass, passengers_mass, cargo_mass): """ Calculate unladen mass [kg]. :param vehicle_mass: Vehicle mass [kg]. :type vehicle_mass: float :param passengers_mass: Passengers mass including the driver [kg]. :type passengers_mass: float :param cargo_mass: Cargo mass [kg]. :type cargo_mass: float :return: Unladen mass [kg]. :rtype: float """ return vehicle_mass - passengers_mass - cargo_mass
[docs]@sh.add_function(dsp, outputs=['curb_mass']) def calculate_curb_mass(unladen_mass, fuel_mass): """ Calculate unladen mass [kg]. :param unladen_mass: Unladen mass [kg]. :type unladen_mass: float :param fuel_mass: Fuel mass [kg]. :type fuel_mass: float :return: Curb mass [kg]. :rtype: float """ return unladen_mass - fuel_mass
[docs]@sh.add_function(dsp, outputs=['vehicle_mass']) def calculate_vehicle_mass(unladen_mass, passengers_mass, cargo_mass): """ Calculate vehicle_mass [kg]. :param unladen_mass: Unladen mass [kg]. :type unladen_mass: float :param passengers_mass: Passengers mass including the driver [kg]. :type passengers_mass: float :param cargo_mass: Cargo mass [kg]. :type cargo_mass: float :return: Vehicle mass [kg]. :rtype: float """ return unladen_mass + passengers_mass + cargo_mass
[docs]@sh.add_function(dsp, outputs=['raw_frontal_area']) def calculate_raw_frontal_area(vehicle_height, vehicle_width): """ Calculates raw frontal area of the vehicle [m2]. :param vehicle_height: Vehicle height [m]. :type vehicle_height: float :param vehicle_width: Vehicle width [m]. :type vehicle_width: float :return: Raw frontal area of the vehicle [m2]. :rtype: float """ return vehicle_height * vehicle_width
[docs]@sh.add_function(dsp, outputs=['raw_frontal_area'], weight=5) def calculate_raw_frontal_area_v1(vehicle_mass, vehicle_category): """ Calculates raw frontal area of the vehicle [m2]. :param vehicle_mass: Vehicle mass [kg]. :type vehicle_mass: float :param vehicle_category: Vehicle category (i.e., A, B, C, D, E, F, S, M, and J). :type vehicle_category: str :return: Raw frontal area of the vehicle [m2]. :rtype: float """ from asteval import Interpreter as Interp d = dfl.functions.calculate_raw_frontal_area_v1 expr = d.formulas[vehicle_category.upper()] return Interp(dict(vehicle_mass=vehicle_mass)).eval(expr)
[docs]@sh.add_function(dsp, outputs=['frontal_area']) def calculate_frontal_area(raw_frontal_area): """ Calculates the vehicle frontal area [m2]. :param raw_frontal_area: Raw frontal area of the vehicle [m2]. :type raw_frontal_area: float :return: Frontal area of the vehicle [m2]. :rtype: float """ d = dfl.functions.calculate_frontal_area.projection_factor return raw_frontal_area * d
[docs]@sh.add_function(dsp, outputs=['aerodynamic_drag_coefficient']) def calculate_aerodynamic_drag_coefficient(vehicle_category): """ Calculates the aerodynamic drag coefficient [-]. :param vehicle_category: Vehicle category (i.e., A, B, C, D, E, F, S, M, and J). :type vehicle_category: str :return: Aerodynamic drag coefficient [-]. :rtype: float """ d = dfl.functions.calculate_aerodynamic_drag_coefficient return d.cw[vehicle_category.upper()]
[docs]@sh.add_function(dsp, outputs=['aerodynamic_drag_coefficient']) def calculate_aerodynamic_drag_coefficient_v1(vehicle_body): """ Calculates the aerodynamic drag coefficient [-]. :param vehicle_body: Vehicle body (i.e., cabriolet, sedan, hatchback, stationwagon, suv/crossover, mpv, coupé, bus, bestelwagen, pick-up). :type vehicle_body: str :return: Aerodynamic drag coefficient [-]. :rtype: float """ d = dfl.functions.calculate_aerodynamic_drag_coefficient_v1 return d.cw[vehicle_body.lower()]
dsp.add_data('air_temperature', dfl.values.air_temperature) dsp.add_data('atmospheric_pressure', dfl.values.atmospheric_pressure)
[docs]@sh.add_function(dsp, outputs=['air_density']) def calculate_air_density(air_temperature, atmospheric_pressure): """ Calculates the air density [kg/m3]. :param air_temperature: Air temperature [°C]. :type air_temperature: float :param atmospheric_pressure: Atmospheric pressure [kPa]. :type atmospheric_pressure: float :return: Air density [kg/m3]. :rtype: float """ # http://www.thecartech.com/subjects/auto_eng/Road_loads.htm return 3.48 * atmospheric_pressure / (273.16 + air_temperature)
dsp.add_data('has_roof_box', dfl.values.has_roof_box)
[docs]@sh.add_function(dsp, outputs=['f2'], weight=5) def calculate_f2( air_density, aerodynamic_drag_coefficient, frontal_area, has_roof_box): """ Calculates the f2 coefficient [N/(km/h)^2]. :param air_density: Air density [kg/m3]. :type air_density: float :param aerodynamic_drag_coefficient: Aerodynamic drag coefficient [-]. :type aerodynamic_drag_coefficient: float :param frontal_area: Frontal area of the vehicle [m2]. :type frontal_area: float :param has_roof_box: Has the vehicle a roof box? [-]. :type has_roof_box: bool :return: As used in the dyno and defined by respective guidelines [N/(km/h)^2]. :rtype: float """ c = aerodynamic_drag_coefficient * frontal_area * air_density if has_roof_box: c *= dfl.functions.calculate_f2.roof_box return 0.5 * c / 3.6 ** 2
dsp.add_data('tyre_state', dfl.values.tyre_state) dsp.add_data('road_state', dfl.values.road_state)
[docs]@sh.add_function(dsp, outputs=['static_friction']) def default_static_friction(tyre_state, road_state): """ Returns the default static friction coefficient [-]. :param tyre_state: Tyre state (i.e., new or worm). :type tyre_state: str :param road_state: Road state (i.e., dry, wet, rainfall, puddles, ice). :type road_state: str :return: Static friction coefficient [-]. :rtype: float """ coeff = dfl.functions.default_static_friction.coeff return coeff[tyre_state][road_state]
[docs]@sh.add_function(dsp, outputs=['n_wheel']) def default_n_wheel(n_wheel_drive): """ Returns the default total number of wheels [-]. :param n_wheel_drive: Number of wheel drive [-]. :type n_wheel_drive: int :return: Total number of wheels [-]. :rtype: int """ return max(n_wheel_drive, dfl.functions.default_n_wheel.n_wheel)
[docs]@sh.add_function(dsp, outputs=['wheel_drive_load_fraction']) def calculate_wheel_drive_load_fraction(n_wheel_drive, n_wheel=4): """ Calculate the repartition of the load on wheel drive axles [-]. :param n_wheel_drive: Number of wheel drive [-]. :type n_wheel_drive: int :param n_wheel: Total number of wheels [-]. :type n_wheel: int :return: Repartition of the load on wheel drive axles [-]. :rtype: float """ return n_wheel_drive / n_wheel
def _compile_traction_acceleration_limits( static_friction, wheel_drive_load_fraction): deceleration = -9.81 * static_friction acceleration = -deceleration * wheel_drive_load_fraction def _func(angle_slopes): slope = np.cos(angle_slopes) return deceleration * slope, acceleration * slope return _func
[docs]@sh.add_function( dsp, outputs=['traction_deceleration_limit', 'traction_acceleration_limit'] ) def calculate_traction_acceleration_limits( static_friction, wheel_drive_load_fraction, angle_slopes): """ Calculates the traction acceleration limits [m/s2]. :param static_friction: Static friction coefficient [-]. :type static_friction: float :param wheel_drive_load_fraction: Repartition of the load on wheel drive axles [-]. :type wheel_drive_load_fraction: float :param angle_slopes: Angle slope vector [rad]. :type angle_slopes: numpy.array :return: Traction acceleration limits (i.e., deceleration, acceleration) [m/s2]. :rtype: tuple[numpy.array] """ return _compile_traction_acceleration_limits( static_friction, wheel_drive_load_fraction )(angle_slopes)
dsp.add_data('tyre_class', dfl.values.tyre_class)
[docs]@sh.add_function(dsp, outputs=['rolling_resistance_coeff']) def calculate_rolling_resistance_coeff(tyre_class, tyre_category): """ Calculates the rolling resistance coefficient [-]. :param tyre_class: Tyre class (i.e., C1, C2, and C3). :type tyre_class: str :param tyre_category: Tyre category (i.e., A, B, C, D, E, F, and G). :type tyre_category: str :return: Rolling resistance coefficient [-]. :rtype: float """ coeff = dfl.functions.calculate_rolling_resistance_coeff.coeff return coeff[tyre_class.upper()][tyre_category.upper()]
[docs]@sh.add_function(dsp, outputs=['f0'], weight=5) def calculate_f0(vehicle_mass, rolling_resistance_coeff): """ Calculates rolling resistance [N]. :param vehicle_mass: Vehicle mass [kg]. :type vehicle_mass: float :param rolling_resistance_coeff: Rolling resistance coefficient [-]. :type rolling_resistance_coeff: float :return: Rolling resistance force [N] when angle_slope == 0. :rtype: float """ return vehicle_mass * 9.81 * rolling_resistance_coeff
[docs]@sh.add_function(dsp, outputs=['f1'], weight=5) def calculate_f1(f2): """ Calculates the f1 road load [N/(km/h)]. :param f2: As used in the dyno and defined by respective guidelines [N/(km/h)^2]. :type f2: float :return: Defined by dyno procedure [N/(km/h)]. :rtype: float """ q, m = dfl.functions.calculate_f1.qm return m * f2 + q
dsp.add_data('angle_slope', dfl.values.angle_slope) dsp.add_data('path_elevations', wildcard=True)
[docs]@sh.add_function(dsp, outputs=['slope_model']) @sh.add_function( dsp, inputs=['path_distances', 'path_elevations'], outputs=['slope_model'] ) def define_slope_model(distances, elevations): """ Returns the angle slope model [rad]. :param distances: Cumulative distance vector [m]. :type distances: numpy.array :param elevations: Elevation vector [m]. :type elevations: numpy.array :return: Angle slope model [rad]. :rtype: function """ from scipy.interpolate import InterpolatedUnivariateSpline as Spl i = np.append([0], np.where(np.diff(distances) > dfl.EPS)[0] + 1) func = Spl(distances[i], elevations[i]).derivative() return lambda d: np.arctan(func(d))
[docs]@sh.add_function(dsp, outputs=['slope_model'], weight=5) def define_slope_model_v1(angle_slope): """ Returns the angle slope model [rad]. :param angle_slope: Angle slope [rad]. :type angle_slope: float :return: Angle slope model [rad]. :rtype: function """ return np.vectorize(lambda *args: angle_slope, otypes=[float])
[docs]@sh.add_function(dsp, outputs=['angle_slopes']) def calculate_angle_slopes(slope_model, distances): """ Returns the angle slope vector [rad]. :param slope_model: Angle slope model [rad]. :type slope_model: function :param distances: Cumulative distance vector [m]. :type distances: numpy.array :return: Angle slope vector [rad]. :rtype: numpy.array """ return slope_model(distances)
[docs]@sh.add_function(dsp, outputs=['rolling_resistance']) def calculate_rolling_resistance(f0, angle_slopes): """ Calculates rolling resistance [N]. :param f0: Rolling resistance force [N] when angle_slope == 0. :type f0: float :param angle_slopes: Angle slope vector [rad]. :type angle_slopes: numpy.array :return: Rolling resistance force [N]. :rtype: numpy.array """ return f0 * np.cos(angle_slopes)
[docs]@sh.add_function(dsp, outputs=['velocity_resistances']) def calculate_velocity_resistances(f1, velocities): """ Calculates forces function of velocity [N]. :param f1: Defined by dyno procedure [N/(km/h)]. :type f1: float :param velocities: Velocity vector [km/h]. :type velocities: numpy.array | float :return: Forces function of velocity [N]. :rtype: numpy.array | float """ return f1 * velocities
[docs]@sh.add_function(dsp, outputs=['climbing_force']) def calculate_climbing_force(vehicle_mass, angle_slopes): """ Calculates the vehicle climbing resistance [N]. :param vehicle_mass: Vehicle mass [kg]. :type vehicle_mass: float :param angle_slopes: Angle slope vector [rad]. :type angle_slopes: numpy.array :return: Vehicle climbing resistance [N]. :rtype: numpy.array """ return vehicle_mass * 9.81 * np.sin(angle_slopes)
[docs]@sh.add_function(dsp, outputs=['n_dyno_axes']) def select_default_n_dyno_axes(cycle_type, n_wheel_drive): """ Selects the default number of dyno axes[-]. :param cycle_type: Cycle type (WLTP or NEDC). :type cycle_type: str :param n_wheel_drive: Number of wheel drive [-]. :type n_wheel_drive: int :return: Number of dyno axes [-]. :rtype: int """ par = dfl.functions.select_default_n_dyno_axes try: return par.DYNO_AXES[cycle_type.upper()][n_wheel_drive] except KeyError: return n_wheel_drive // 2
[docs]@sh.add_function(dsp, outputs=['inertial_factor']) def select_inertial_factor(n_dyno_axes): """ Selects the inertia factor [%] according to the number of dyno axes. :param n_dyno_axes: Number of dyno axes [-]. :type n_dyno_axes: int :return: Factor that considers the rotational inertia [%]. :rtype: float """ return 1.5 * n_dyno_axes
[docs]@sh.add_function(dsp, outputs=['rotational_inertia_forces']) def calculate_rotational_inertia_forces( vehicle_mass, inertial_factor, accelerations): """ Calculate rotational inertia forces [N]. :param vehicle_mass: Vehicle mass [kg]. :type vehicle_mass: float :param inertial_factor: Factor that considers the rotational inertia [%]. :type inertial_factor: float :param accelerations: Acceleration vector [m/s2]. :type accelerations: numpy.array | float :return: Rotational inertia forces [N]. :rtype: numpy.array | float """ return vehicle_mass * inertial_factor * accelerations / 100
[docs]@sh.add_function(dsp, outputs=['accelerations']) def calculate_accelerations_v1( vehicle_mass, inertial_factor, motive_forces, climbing_force, aerodynamic_resistances, rolling_resistance, velocity_resistances): """ Calculates the acceleration from motive forces [m/s2]. :param vehicle_mass: Vehicle mass [kg]. :type vehicle_mass: float :param inertial_factor: Factor that considers the rotational inertia [%]. :type inertial_factor: float :param motive_forces: Motive forces [N]. :type motive_forces: numpy.array | float :param climbing_force: Vehicle climbing resistance [N]. :type climbing_force: float | numpy.array :param aerodynamic_resistances: Aerodynamic resistance vector [N]. :type aerodynamic_resistances: numpy.array | float :param rolling_resistance: Rolling resistance force [N]. :type rolling_resistance: float | numpy.array :param velocity_resistances: Forces function of velocity [N]. :type velocity_resistances: numpy.array | float :return: Acceleration vector [m/s2]. :rtype: numpy.array """ acc = motive_forces - climbing_force - aerodynamic_resistances acc -= rolling_resistance + velocity_resistances acc /= vehicle_mass * (1 + inertial_factor / 100) return acc
# noinspection PyPep8Naming
[docs]@sh.add_function(dsp, outputs=['motive_forces']) def calculate_motive_forces( vehicle_mass, accelerations, climbing_force, aerodynamic_resistances, rolling_resistance, velocity_resistances, rotational_inertia_forces): """ Calculate motive forces [N]. :param vehicle_mass: Vehicle mass [kg]. :type vehicle_mass: float :param accelerations: Acceleration vector [m/s2]. :type accelerations: numpy.array | float :param climbing_force: Vehicle climbing resistance [N]. :type climbing_force: float | numpy.array :param rolling_resistance: Rolling resistance force [N]. :type rolling_resistance: float | numpy.array :param aerodynamic_resistances: Aerodynamic resistance vector [N]. :type aerodynamic_resistances: numpy.array | float :param velocity_resistances: Forces function of velocity [N]. :type velocity_resistances: numpy.array | float :param rotational_inertia_forces: Rotational inertia forces [N]. :type rotational_inertia_forces: numpy.array | float :return: Motive forces [N]. :rtype: numpy.array | float """ # namespace shortcuts Frr = rolling_resistance Faero = aerodynamic_resistances Fclimb = climbing_force Fvel = velocity_resistances Finertia = rotational_inertia_forces return vehicle_mass * accelerations + Fclimb + Frr + Faero + Fvel + Finertia
[docs]@sh.add_function(dsp, outputs=['motive_powers']) def calculate_motive_powers(motive_forces, velocities): """ Calculates motive power [kW]. :param motive_forces: Motive forces [N]. :type motive_forces: numpy.array | float :param velocities: Velocity vector [km/h]. :type velocities: numpy.array | float :return: Motive power [kW]. :rtype: numpy.array | float """ return motive_forces * velocities / 3600
[docs]@sh.add_function(dsp, outputs=['motive_forces']) def calculate_motive_forces_v1(motive_powers, velocities): """ Calculate motive forces [N]. :param motive_powers: Motive power [kW]. :type motive_powers: numpy.array | float :param velocities: Velocity vector [km/h]. :type velocities: numpy.array | float :return: Motive forces [N]. :rtype: numpy.array | float """ return motive_powers / velocities * 3600
dsp.add_data( 'road_loads', description='Cycle road loads [N, N/(km/h), N/(km/h)^2].' ) dsp.add_function('grouping', sh.bypass, ['f0', 'f1', 'f2'], ['road_loads']) dsp.add_function('splitting', sh.bypass, ['road_loads'], ['f0', 'f1', 'f2']) dsp.add_data('correct_f0', dfl.values.correct_f0)
[docs]@sh.add_function(dsp, outputs=['f0']) def apply_f0_correction(f0_uncorrected, correct_f0): """ Corrects the rolling resistance force [N] if a different preconditioning cycle was used for WLTP (WLTP precon) and NEDC (NEDC precon). :param f0_uncorrected: Uncorrected rolling resistance force [N] when angle_slope == 0. :type f0_uncorrected: float :param correct_f0: A different preconditioning cycle was used for WLTP and NEDC? :type correct_f0: bool :return: Rolling resistance force [N] when angle_slope == 0. :rtype: float """ if correct_f0: return f0_uncorrected - 6.0 return f0_uncorrected