# Source code for co2mpas.core.model.physical.wheels

# -*- 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 wheels.
"""
import math
import regex
import schedula as sh
from co2mpas.defaults import dfl

dsp = sh.BlueDispatcher(
name='Wheel model', description='It models the wheel dynamics.'
)

"""
Calculates the wheel power [kW].

:param velocities:
Velocity [km/h].
:type velocities: numpy.array | float

:param accelerations:
Acceleration [m/s2].
:type accelerations: numpy.array | float

:param vehicle_mass:
Vehicle mass [kg].
:type vehicle_mass: float

:return:
Power at wheels [kW].
:rtype: numpy.array | float
"""

quadratic_term = f0 + (f1 + f2 * velocities) * velocities

vel = velocities / 3600

return (quadratic_term + 1.03 * vehicle_mass * accelerations) * vel

# noinspection PyIncorrectDocstring,SpellCheckingInspection
def calculate_wheel_torques(wheel_powers, wheel_speeds, coef=30000 / math.pi):
"""
Calculates torque at the wheels [N*m].

:param wheel_powers:
Power at the wheels [kW].
:type wheel_powers: numpy.array | float

:param wheel_speeds:
Rotating speed of the wheel [RPM].
:type wheel_speeds: numpy.array | float

:return:
Torque at the wheels [N*m].
:rtype: numpy.array | float
"""
import numpy as np
with np.errstate(divide='ignore', invalid='ignore'):
torques = np.divide(wheel_powers, wheel_speeds) * coef
return np.where(wheel_speeds, torques, 0)

def calculate_wheel_powers(wheel_torques, wheel_speeds):
"""
Calculates power at the wheels [kW].

:param wheel_torques:
Torque at the wheel [N*m].
:type wheel_torques: numpy.array | float

:param wheel_speeds:
Rotating speed of the wheel [RPM].
:type wheel_speeds: numpy.array | float

:return:
Power at the wheels [kW].
:rtype: numpy.array | float
"""

return wheel_torques * wheel_speeds * (math.pi / 30000.0)

def _compile_speed_function(r_dynamic):
c = 30.0 / (3.6 * math.pi * r_dynamic)

def _wheel_speed(velocities):
return velocities * c

return _wheel_speed

def calculate_wheel_speeds(velocities, r_dynamic):
"""
Calculates rotating speed of the wheels [RPM].

:param velocities:
Vehicle velocity [km/h].
:type velocities: numpy.array | float

:param r_dynamic:
Dynamic radius of the wheels [m].
:type r_dynamic: float

:return:
Rotating speed of the wheel [RPM].
:rtype: numpy.array | float
"""
return _compile_speed_function(r_dynamic)(velocities)

def identify_r_dynamic(
velocity_speed_ratios, gear_box_ratios, final_drive_ratios):
"""
Identifies the dynamic radius of the wheels [m].

:param velocity_speed_ratios:
Constant velocity speed ratios of the gear box [km/(h*RPM)].
:type velocity_speed_ratios: dict[int | float]

:param gear_box_ratios:
Gear box ratios [-].
:type gear_box_ratios: dict[int, float | int]

:param final_drive_ratios:
Final drive ratios [-].
:type final_drive_ratios: dict[int, float | int]

:return:
Dynamic radius of the wheels [m].
:rtype: float
"""
from co2mpas.utils import reject_outliers
from .gear_box.mechanical import calculate_speed_velocity_ratios
svr = calculate_speed_velocity_ratios(
gear_box_ratios, final_drive_ratios, 1
)

r = [svr[k] * vs for k, vs in velocity_speed_ratios.items() if k]

r_dynamic = reject_outliers(r)[0]

return r_dynamic

def identify_r_dynamic_v1(
velocities, gears, gear_box_speeds_in, gear_box_ratios,
final_drive_ratios, stop_velocity):
"""
Identifies the dynamic radius of the wheels [m].

:param velocities:
Vehicle velocity [km/h].
:type velocities: numpy.array

:param gears:
Gear vector [-].
:type gears: numpy.array

:param gear_box_speeds_in:
Gear box speed [RPM].
:type gear_box_speeds_in: numpy.array

:param gear_box_ratios:
Gear box ratios [-].
:type gear_box_ratios: dict[int, float | int]

:param final_drive_ratios:
Final drive ratios [-].
:type final_drive_ratios: dict[int, float | int]

:param stop_velocity:
Maximum velocity to consider the vehicle stopped [km/h].
:type stop_velocity: float

:return:
Dynamic radius of the wheels [m].
:rtype: float
"""
import numpy as np
from co2mpas.utils import reject_outliers
from .gear_box import mechanical as gb_mec
svr = gb_mec.calculate_speed_velocity_ratios(
gear_box_ratios, final_drive_ratios, 1.0
)

vsr = gb_mec.calculate_velocity_speed_ratios(svr)

speed_x_r_dyn_ratios = gb_mec.calculate_gear_box_speeds_in(
gears, velocities, vsr, stop_velocity
)
with np.errstate(divide='ignore', invalid='ignore'):
r_dynamic = speed_x_r_dyn_ratios / gear_box_speeds_in
r_dynamic = r_dynamic[np.isfinite(r_dynamic)]
r_dynamic = reject_outliers(r_dynamic)[0]
return r_dynamic

def identify_r_dynamic_v2(
velocities, gears, engine_speeds_out, gear_box_ratios,
final_drive_ratios, stop_velocity, on_engine):
"""
Identifies the dynamic radius of the wheels [m].

:param velocities:
Vehicle velocity [km/h].
:type velocities: numpy.array

:param gears:
Gear vector [-].
:type gears: numpy.array

:param engine_speeds_out:
Engine speed [RPM].
:type engine_speeds_out: numpy.array

:param gear_box_ratios:
Gear box ratios [-].
:type gear_box_ratios: dict[int, float | int]

:param final_drive_ratios:
Final drive ratios [-].
:type final_drive_ratios: dict[int, float | int]

:param stop_velocity:
Maximum velocity to consider the vehicle stopped [km/h].
:type stop_velocity: float

:param on_engine:
If the engine is on [-].
:type on_engine: numpy.array

:return:
Dynamic radius of the wheels [m].
:rtype: float
"""
return identify_r_dynamic_v1(
velocities[on_engine], gears[on_engine], engine_speeds_out[on_engine],
gear_box_ratios, final_drive_ratios, stop_velocity
)

def identify_r_dynamic_v3(
times, velocities, accelerations, r_wheels, gear_box_speeds_in,
gear_box_ratios, final_drive_ratios, idle_engine_speed, stop_velocity,
plateau_acceleration, change_gear_window_width):
"""
Identifies the dynamic radius of the wheels [m].

:param times:
Time vector [s].
:type times: numpy.array

:param velocities:
Vehicle velocity [km/h].
:type velocities: numpy.array

:param accelerations:
Vehicle acceleration [m/s2].
:type accelerations: numpy.array

:param r_wheels:
:type r_wheels: float

:param gear_box_speeds_in:
Gear box speed [RPM].
:type gear_box_speeds_in: numpy.array

:param gear_box_ratios:
Gear box ratios [-].
:type gear_box_ratios: dict[int, float | int]

:param final_drive_ratios:
Final drive ratios [-].
:type final_drive_ratios: dict[int, float | int]

:param idle_engine_speed:
Engine speed idle median and std [RPM].
:type idle_engine_speed: (float, float)

:param stop_velocity:
Maximum velocity to consider the vehicle stopped [km/h].
:type stop_velocity: float

:param plateau_acceleration:
Maximum acceleration to be at constant velocity [m/s2].
:type plateau_acceleration: float

:param change_gear_window_width:
Time window used to apply gear change filters [s].
:type change_gear_window_width: float

:return:
Dynamic radius of the wheels [m].
:rtype: float
"""
from .gear_box import mechanical as gb_mec
svr = gb_mec.calculate_speed_velocity_ratios(
gear_box_ratios, final_drive_ratios, r_wheels
)

gears = gb_mec.identify_gears(
times, velocities, accelerations, gear_box_speeds_in,
gb_mec.calculate_velocity_speed_ratios(svr), stop_velocity,
plateau_acceleration, change_gear_window_width, idle_engine_speed
)

r_dynamic = identify_r_dynamic_v1(
velocities, gears, gear_box_speeds_in, gear_box_ratios,
final_drive_ratios, stop_velocity
)

return r_dynamic

def identify_r_dynamic_v4(
times, velocities, accelerations, r_wheels, engine_speeds_out,
gear_box_ratios, final_drive_ratios, idle_engine_speed, stop_velocity,
plateau_acceleration, change_gear_window_width, on_engine):
"""
Identifies the dynamic radius of the wheels [m].

:param times:
Time vector [s].
:type times: numpy.array

:param velocities:
Vehicle velocity [km/h].
:type velocities: numpy.array

:param accelerations:
Vehicle acceleration [m/s2].
:type accelerations: numpy.array

:param r_wheels:
:type r_wheels: float

:param engine_speeds_out:
Engine speed [RPM].
:type engine_speeds_out: numpy.array

:param gear_box_ratios:
Gear box ratios [-].
:type gear_box_ratios: dict[int, float | int]

:param final_drive_ratios:
Final drive ratios [-].
:type final_drive_ratios: dict[int, float | int]

:param idle_engine_speed:
Engine speed idle median and std [RPM].
:type idle_engine_speed: (float, float)

:param stop_velocity:
Maximum velocity to consider the vehicle stopped [km/h].
:type stop_velocity: float

:param plateau_acceleration:
Maximum acceleration to be at constant velocity [m/s2].
:type plateau_acceleration: float

:param change_gear_window_width:
Time window used to apply gear change filters [s].
:type change_gear_window_width: float

:param on_engine:
If the engine is on [-].
:type on_engine: numpy.array

:return:
Dynamic radius of the wheels [m].
:rtype: float
"""
return identify_r_dynamic_v3(
times[on_engine], velocities[on_engine], accelerations[on_engine],
r_wheels, engine_speeds_out[on_engine], gear_box_ratios,
final_drive_ratios, idle_engine_speed, stop_velocity,
plateau_acceleration, change_gear_window_width

)

_re_tyre_code_iso = regex.compile(
r"""
^(?P<use>([a-z]){1,2})?\s*
(?P<nominal_section_width>(\d){3})\s*
\/\s*
(?P<aspect_ratio>(\d){2,3})?
((\s*(?P<carcass>[a-z])\s*)|\s+)
(?P<rim_diameter>(\d){1,2}(\.(\d){1,2})?)
(\s+(?P<use>C))?
(?P<speed_rating>($$[a-z]$$|[a-z]\d?)))?\s*
(\s+(?P<additional_marks>.*))?$""", regex.IGNORECASE | regex.X | regex.DOTALL) _re_tyre_code_numeric = regex.compile( r""" ^((?P<diameter>(\d){2})\s*x\s*)? (?P<nominal_section_width>(\d){1,2}(\.(\d){1,2})?)\s* ((\s*(?P<carcass>([a-z]|-))\s*)|\s+) (?P<rim_diameter>(\d){2}(\.(\d){1,2})?)\s* (?P<use>(LT|C))\s* ((?P<load_index>(\d){2,3}(/(\d){2,3})?)\s* (?P<speed_rating>($$[a-z]$$|[a-z]\d?)))?\s* (\s*((?P<load_range>[a-z])(^| )))? (\s+(?P<additional_marks>.*))?$
""", regex.IGNORECASE | regex.X | regex.DOTALL)

_re_tyre_code_pax = regex.compile(
r"""
^(?P<use>([a-z]){1,2})?\s*
(?P<nominal_section_width>(\d){3})\s*-\s*
(?P<diameter>(\d){2,3})
((\s*(?P<carcass>[a-z])\s*)|\s+)
(?P<rim_diameter>(\d){2,3})
(?P<speed_rating>[a-z]))?\s*
""", regex.IGNORECASE | regex.X | regex.DOTALL)

def _format_tyre_dimensions(tyre_dimensions):
import schema
frt = schema.Schema({
schema.Optional('aspect_ratio'): schema.Use(float),
schema.Optional('carcass'): schema.Use(str),
'rim_diameter': schema.Use(float),
schema.Optional('diameter'): schema.Use(float),
'nominal_section_width': schema.Use(float),
schema.Optional('speed_rating'): schema.Use(str),
schema.Optional('use'): schema.Use(str),
schema.Optional('code'): schema.Use(str),
})
m = {k: v for k, v in tyre_dimensions.items() if v is not None}
return frt.validate(m)

def calculate_tyre_dimensions(tyre_code):
"""
Calculates the tyre dimensions from the tyre code.

:param tyre_code:
Tyre code (e.g.,P225/70R14).
:type tyre_code: str

:return:
Tyre dimensions.
:rtype: dict
"""
import schema
it = [
('iso', _re_tyre_code_iso),
('numeric', _re_tyre_code_numeric),
('pax', _re_tyre_code_pax)
]
for c, _r in it:
try:
m = _r.match(tyre_code).groupdict()
m['code'] = c
if c == 'numeric' and 'aspect_ratio' not in m:
b = m['nominal_section_width'].split('.')[-1][-1] == '5'
m['aspect_ratio'] = '82' if b else '92'
return _format_tyre_dimensions(m)
except (AttributeError, schema.SchemaError):
pass
raise ValueError('Invalid tyre code: %s', tyre_code)

# noinspection PyUnusedLocal
def _format_tyre_code(
nominal_section_width, rim_diameter, aspect_ratio=0, use='', carcass='',
code='iso', diameter=None, **kw):
if code == 'iso':
parts = (
'%s%d/%d%s%d' % (use, nominal_section_width, aspect_ratio,
carcass or ' ', rim_diameter),
)
elif code == 'numeric':
diameter = '%.2fx' % diameter if diameter is not None else ''
parts = (
'%s%.2f%s%.2f %s' % (diameter, nominal_section_width,
carcass or ' ', rim_diameter, use),
)
else:
parts = (
'%d-%d%s%d%s' % (nominal_section_width, diameter, carcass or ' ',
)

parts += (
)
return ' '.join(p for p in parts if p)

def define_tyre_code(tyre_dimensions):
"""
Returns the tyre code from the tyre dimensions.

:param tyre_dimensions:
Tyre dimensions.

.. note:: The fields are : use, nominal_section_width, aspect_ratio,
:type tyre_dimensions: dict

:return:
Tyre code (e.g.,P225/70R14).
:rtype: str
"""
return _format_tyre_code(**tyre_dimensions)

def default_tyre_code(r_dynamic):
"""
Return one of the most popular tyre code according to the r dynamic.

:param r_dynamic:
Dynamic radius of the wheels [m].
:type r_dynamic: float

:return:
Tyre code (e.g.,P225/70R14).
:rtype: str
"""

pop = (
'165/65R13', '155/70R13', '165/70R13', '165/60R14', '185/60R14',
'155/65R14', '165/65R14', '175/65R14', '185/65R14', '165/70R14',
'175/70R14', '195/50R15', '185/55R15', '195/55R15', '185/60R15',
'195/60R15', '205/60R15', '175/65R15', '185/65R15', '195/65R15',
'195/70R15', '195/45R16', '205/45R16', '205/50R16', '195/55R16',
'205/55R16', '215/55R16', '205/60R16', '215/60R16', '215/65R16',
'205/40R17', '245/40R17', '205/45R17', '215/45R17', '225/45R17',
'235/45R17', '205/50R17', '215/50R17', '225/50R17', '215/55R17',
'225/55R17', '215/60R17', '225/65R17', '235/65R17', '225/40R18',
'235/40R18', '245/40R18', '225/45R18', '235/60R18', '255/35R19'
)

def _key_func(c):
r = calculate_r_wheels(calculate_tyre_dimensions(c))
return r <= r_dynamic, (r - r_dynamic) ** 2

return min(pop, key=_key_func)

def calculate_r_wheels(tyre_dimensions):
"""
Calculates the radius of the wheels [m] from the tyre dimensions.

:param tyre_dimensions:
Tyre dimensions.

.. note:: The fields are : use, nominal_section_width, aspect_ratio,
:type tyre_dimensions: dict

:return:
:rtype: float
"""
if 'diameter' in tyre_dimensions:
if tyre_dimensions['code'] == 'pax':
return tyre_dimensions['diameter'] / 2000  # Diameter is in mm.
return tyre_dimensions['diameter'] * 0.0254  # Diameter is in inches.
a = tyre_dimensions['aspect_ratio'] / 100  # Aspect ratio is Height/Width.
w = tyre_dimensions['nominal_section_width']
if tyre_dimensions.get('code', 'iso') == 'iso':
w /= 1000  # Width is in mm.
else:
w *= 0.0254  # Width is in inches.

dr = tyre_dimensions['rim_diameter'] * 0.0254  # Rim is in inches.
return a * w + dr / 2

data_id='tyre_dynamic_rolling_coefficient', initial_dist=50,
default_value=dfl.values.tyre_dynamic_rolling_coefficient,
)

def calculate_r_dynamic(r_wheels, tyre_dynamic_rolling_coefficient):
"""
Calculates the dynamic radius of the wheels [m].

:param r_wheels:
:type r_wheels: float

:param tyre_dynamic_rolling_coefficient:
Dynamic rolling coefficient [-].
:type tyre_dynamic_rolling_coefficient: float

:return:
Dynamic radius of the wheels [m].
:rtype: float
"""

return tyre_dynamic_rolling_coefficient * r_wheels

def identify_tyre_dynamic_rolling_coefficient(r_wheels, r_dynamic):
"""
Identifies the dynamic rolling coefficient [-].

:param r_wheels:
:type r_wheels: float

:param r_dynamic:
Dynamic radius of the wheels [m].
:type r_dynamic: float

:return:
Dynamic rolling coefficient [-].
:rtype: float
"""

return r_dynamic / r_wheels