#! python
#-*- coding: utf-8 -*-
#
# Copyright 2013-2014 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
'''The core that accepts a vehicle-model and wltc-classes, runs the simulation and updates the model with results (downscaled velocity & gears-profile).
.. Attention:: The documentation of this core module has several issues and needs work.
Notation
--------
* ALL_CAPITAL variables denote *vectors* over the velocity-profile (the cycle),
* ALL_CAPITAL starting with underscore (`_`) denote *matrices* (gears x time).
For instance, GEARS is like that::
[0, 0, 1, 1, 1, 2, 2, ... 1, 0, 0]
<---- cycle time-steps ---->
and _GEARS is like that::
t:||: 0 1 2 3
---+-------------
g1:|[[ 1, 1, 1, 1, ... 1, 1
g2:| 2, 2, 2, 2, ... 2, 2
g3:| 3, 3, 3, 3, ... 3, 3
g4:| 4, 4, 4, 4, ... 4, 4 ]]
Major vectors & matrices
------------------------
V: floats (#cycle_steps)
The wltp-class velocity profile.
_GEARS: integers (#gears X #cycle_steps)
One row for each gear (starting with 1 to #gears).
_N_GEARS: floats (#gears X #cycle_steps)
One row per gear with the Engine-revolutions required to follow the V-profile (unfeasable revs included),
produced by multiplying ``V * gear-rations``.
_GEARS_YES: boolean (#gears X #cycle_steps)
One row per gear having ``True`` wherever gear is possible for each step.
.. Seealso:: :mod:`model` for in/out schemas
'''
from __future__ import division, unicode_literals
import logging
import re
import sys
import numpy as np
import pandas as pd
from . import model
log = logging.getLogger(__name__)
def _shapes(*arrays):
import operator
op_shape = operator.attrgetter('shape')
return list(map(op_shape, arrays))
def _dtypes(*arrays):
import operator
op_shape = operator.attrgetter('dtype')
return list(map(op_shape, arrays))
[docs]class Experiment(object):
'''Runs the vehicle and cycle data describing a WLTC experiment.
See :mod:`wltp.experiment` for documentation.
'''
[docs] def __init__(self, model, skip_model_validation=False, validate_wltc_data=False):
"""
:param model: trees (formed by dicts & lists) holding the experiment data.
:param skip_model_validation: when true, does not validate the model.
"""
self.dtype = np.float64
self._set_model(model, skip_validation=skip_model_validation, validate_wltc_data=validate_wltc_data)
self.wltc = self._model['params']['wltc_data']
[docs] def run(self):
'''Invokes the main-calculations and extracts/update Model values!
@see: Annex 2, p 70
'''
model = self._model
vehicle = model['vehicle']
params = model['params']
## Prepare results
#
results = model.get('cycle_run')
if results is None:
results = pd.DataFrame()
else:
results = pd.DataFrame(results)
log.info('Found forced `cycle-run` table(%ix%i).', results.shape[0], results.shape[1])
model['cycle_run'] = results
## Ensure Time-steps start from 0 (not 1!).
#
results.reset_index()
results.index.name='t'
## Extract vehicle attributes from model.
#
test_mass = vehicle['test_mass']
unladen_mass = vehicle.get('unladen_mass') or test_mass - params['driver_mass']
p_rated = vehicle['p_rated']
n_rated = vehicle['n_rated']
n_idle = vehicle['n_idle']
n_min_drive = vehicle['n_min']
v_max = vehicle['v_max']
gear_ratios = vehicle['gear_ratios']
res_coeffs = vehicle.get('resistance_coeffs')
if res_coeffs:
(f0, f1, f2) = res_coeffs
else:
(f0, f1, f2) = calc_default_resistance_coeffs(test_mass, params['resistance_coeffs_regression_curves'])
if (v_max is None):
v_max = n_rated / gear_ratios[-1]
is_velocity_forced = any(col in results for col in ('v_class', 'v_target'))
if (is_velocity_forced):
forced_v_column = 'v_class' if 'v_class' in results else 'v_target'
log.info("Found forced velocity(%s).", forced_v_column)
V = results[forced_v_column].values
SLOPE = results.get('slope')
if not SLOPE is None:
SLOPE = np.asarray(SLOPE)
results['v_class'] = V
results['v_target'] = V
else:
## Decide WLTC-class.
#
wltc_class = vehicle.get('wltc_class')
if wltc_class is None:
p_m_ratio = (1000 * p_rated / unladen_mass)
vehicle['pmr'] = p_m_ratio
wltc_class = decideClass(self.wltc, p_m_ratio, v_max)
vehicle['wltc_class'] = wltc_class
else:
log.info('Found forced wltc_class(%s).', wltc_class)
class_data = self.wltc['classes'][wltc_class]
V = np.asarray(class_data['cycle'], dtype=self.dtype)
SLOPE = None
results['v_class'] = V
## NOTE: Improved Acceleration calc on central-values with gradient.
# The pure_load 2nd-part of the P_REQ from start-to-stop is 0, as it should.
#
#A = np.gradient(V) ## TODO: Enable gradient acceleration-calculation.
A = np.diff(V); A = np.append(A, 0) # Restore element lost by diff().
A = A / 3.6
## Required-Power needed early-on by Downscaling.
#
f_inertial = params.get('f_inertial', 1.1)
P_REQ = calcPower_required(V, A, SLOPE, test_mass, f0, f1, f2, f_inertial)
if (not is_velocity_forced):
## Downscale velocity-profile.
#
f_downscale = params.get('f_downscale')
if not f_downscale:
f_downscale_threshold = params.get('f_downscale_threshold', 0.01)
dsc_data = class_data['downscale']
phases = dsc_data['phases']
p_max_values = dsc_data['p_max_values']
downsc_coeffs = dsc_data['factor_coeffs']
dsc_v_split = dsc_data.get('v_max_split', None)
f_downscale = calcDownscaleFactor(P_REQ,
p_max_values, downsc_coeffs, dsc_v_split,
p_rated, v_max,
f_downscale_threshold
)
params['f_downscale'] = f_downscale
if (f_downscale > 0):
V = downscaleCycle(V, f_downscale, phases)
results['v_target'] = V
## Run cycle to find internal matrices for all gears
# and (optionally) gearshifts.
#
load_curve = vehicle['full_load_curve']
(GEARS_ORIG, CLUTCH,
_GEAR_RATIOS, _N_GEARS,
_P_AVAILS, _N_NORMS,
driveability_issues) = run_cycle(V, A, P_REQ, gear_ratios,
n_idle, n_min_drive, n_rated,
p_rated, load_curve, params)
results['clutch'] = CLUTCH # TODO: Allow overridde clutch, etc.
if ('gears_orig' in results):
forced_gears = results['gears_orig'].values
log.info('Found forced gears(x%i).', forced_gears.size)
if (GEARS_ORIG.max() != len(gear_ratios)):
raise ValueError('Forced gears(%s) specify gears(%i) > num_of_gears(%i)'%(forced_gears.shape, GEARS_ORIG.max(), len(gear_ratios)))
GEARS_ORIG = forced_gears
else:
results['gears_orig'] = GEARS_ORIG
## Apply Driveability-rules.
#
GEARS = GEARS_ORIG.copy()
applyDriveabilityRules(V, A, GEARS, CLUTCH, driveability_issues)
## Calculate Real quantities.
#
P_AVAIL = _P_AVAILS[GEARS - 1, range(len(V))]
N_NORM = _N_NORMS[GEARS - 1, range(len(V))]
RPM = _N_GEARS[GEARS - 1, range(len(V))]
V_REAL = RPM / _GEAR_RATIOS[GEARS - 1, range(len(V))]
results['gears'] = GEARS
results['v_real'] = V_REAL
results['p_available'] = P_AVAIL
results['p_required'] = P_REQ
results['rpm'] = RPM
results['rpm_norm'] = N_NORM
results['driveability'] = driveability_issues
return model
#######################
## MODEL ##
#######################
@property
def model(self):
return self._model
def _set_model(self, mdl, skip_validation=False, validate_wltc_data=False):
from wltp.model import _get_model_base, merge
merged_model = _get_model_base()
merge(merged_model, mdl)
if not skip_validation:
model.validate_model(merged_model, validate_wltc_data=validate_wltc_data)
self._model = merged_model
def driveability_report(self):
cycle = self._model.get('cycle_run')
if (not cycle is None):
issues = []
drv = cycle['driveability']
pt = -1
for t in drv.nonzero()[0]:
if (pt+1 < t):
issues += ['...']
issues += ['{:>4}: {}'.format(t, drv[t])]
pt = t
return '\n'.join(issues)
return None
#######################
## PURE CALCULATIONS ##
## Separate for ##
## testability! ##
#######################
# resistance_coeffs_regression_curves
def calc_default_resistance_coeffs(test_mass, regression_curves):
a = regression_curves
f0 = a[0][0] * test_mass + a[0][1]
f1 = a[1][0] * test_mass + a[1][1]
f2 = a[2][0] * test_mass + a[2][1]
return (f0, f1, f2)
def addDriveabilityMessage(time_step, msg, driveability_issues):
old = driveability_issues[time_step]
driveability_issues[time_step] = old + msg
def addDriveabilityProblems(_GEARS_BAD, reason, driveability_issues):
failed_steps = _GEARS_BAD.nonzero()[0]
if (failed_steps.size != 0):
log.info('%i %s: %s', failed_steps.size, reason, failed_steps)
for step in failed_steps:
addDriveabilityMessage(step, reason, driveability_issues)
[docs]def decideClass(wltc_data, p_m_ratio, v_max):
'''
@see: Annex 1, p 19
'''
class_limits = {cl: (cd['pmr_limits'], cd.get('velocity_limits')) for (cl, cd) in wltc_data['classes'].items()}
for (cls, ((pmr_low, pmr_high), v_limits)) in class_limits.items():
if pmr_low < p_m_ratio <= pmr_high and \
(not v_limits or v_limits[0] <= v_max < v_limits[1]):
wltc_class = cls
break
else:
raise ValueError("Cannot determine wltp-class for PMR(%s)!\n Class-limits(%s)" %(p_m_ratio, class_limits))
return wltc_class
[docs]def calcDownscaleFactor(P_REQ, p_max_values, downsc_coeffs, dsc_v_split, p_rated, v_max, f_downscale_threshold):
'''Check if downscaling required, and apply it.
:return: (float) the factor
@see: Annex 1-7, p 68
'''
## Max required power
#
p_req_max = P_REQ[p_max_values[0]]
r_max = (p_req_max / p_rated)
## Cycle r0
#
if dsc_v_split is not None:
assert len(downsc_coeffs) == 2, downsc_coeffs
downsc_coeffs = downsc_coeffs[0] if (v_max <= dsc_v_split) else downsc_coeffs[1]
if (downsc_coeffs is None):
f_downscale = 0
else:
(r0, a1, b1) = downsc_coeffs
if (r_max < r0):
f_downscale = 0
else:
f_downscale = a1 * r_max + b1
f_downscale = round(f_downscale, 1)
if (f_downscale <= f_downscale_threshold):
f_downscale = 0
return f_downscale
[docs]def downscaleCycle(V, f_downscale, phases):
'''Downscale just by scaling the 2 phases demarked by the 3 time-points with different factors,
no recursion as implied by the specs.
@see: Annex 1-7, p 64-68
'''
(t0, t1, t2) = phases
## Accelaration phase
#
ix_acc = np.arange(t0, t1 + 1)
offset_acc = V[t0]
acc_scaled = (1 - f_downscale) * (V[ix_acc] - offset_acc) + offset_acc
assert acc_scaled[0] == V[t0], ('smooth-start: ', acc_scaled[0], V[t0])
## Decelaration phase
#
ix_dec = np.arange(t1 + 1, t2 + 1)
offset_dec = V[t2]
f_corr = (acc_scaled[-1] - offset_dec) / (V[t1] - offset_dec)
dec_scaled = f_corr * (V[ix_dec] - offset_dec) + offset_dec
assert dec_scaled[-1] == V[t2], ('smooth-finish: ', dec_scaled[-1], V[t2])
scaled = np.hstack((acc_scaled, dec_scaled))
assert (1 - f_downscale) * abs(scaled[t1 - t0] - scaled[t1 - t0 + 1]) <= abs(V[t1] - V[t1 + 1]), \
('smooth-tip: ', scaled[t1 - t0], scaled[t1 - t0 + 1], V[t1], V[t1 + 1])
V_DSC = np.hstack((V[:t0], scaled, V[t2 + 1:]))
assert V.shape == V_DSC.shape, _shapes(V, V_DSC)
return V_DSC
[docs]def calcEngineRevs_required(V, gear_ratios, n_idle, v_stopped_threshold):
'''Calculates the required engine-revolutions to achieve target-velocity for all gears.
:return: array: _N_GEARS: a (#gears X #velocity) float-array, eg. [3, 150] --> gear(3), time(150)
:rtype: array: _GEARS: a (#gears X #velocity) int-array, eg. [3, 150] --> gear(3), time(150)
@see: Annex 2-3.2, p 71
'''
assert V.ndim == 1, (V.shape, gear_ratios, n_idle)
nG = len(gear_ratios)
nV = len(V)
_GEARS = np.tile(np.arange(0, nG, dtype='int8')+ 1, (nV, 1)).T
assert _GEARS.shape == (nG, nV), (_GEARS.shape, gear_ratios, nV)
_GEAR_RATIOS = np.tile(gear_ratios, (nV, 1)).T ## TODO: Prepend row for idle-gear in _N_GEARS
_N_GEARS = np.tile(V, (nG, 1)) * _GEAR_RATIOS
assert _GEARS.shape == _GEAR_RATIOS.shape == _N_GEARS.shape, _shapes(_GEARS, _GEAR_RATIOS, _N_GEARS, V)
stopped_steps = (V == 0) ## (Annex 2-3.2 & Annex 2-4(a), p72)
_N_GEARS [:, stopped_steps] = n_idle
_GEARS [:, stopped_steps] = 0
return (_N_GEARS, _GEARS, _GEAR_RATIOS)
[docs]def possibleGears_byEngineRevs(V, A, _N_GEARS,
ngears, n_idle,
n_min_drive, n_min_gear2, n_max,
v_stopped_threshold,
driveability_issues):
'''
Calculates the engine-revolutions limits for all gears and returns for which they are accepted.
My interpratation for Gear2 ``n_min`` limit::
_____________ ______________
///INVALID///| CLUTCHED | GEAR-2-OK
EngineRevs(N): 0-----------------------+---------------------------->
for Gear-2 | | +--> n_clutch_gear2 := n_idle + MAX(
| | 0.15% * n_idle,
| | 3% * n_range)
| +---------> n_idle
+-----------------> n_min_gear2 := 90% * n_idle
:return: _GEARS_YES: possibibilty for all the gears on each cycle-step
(eg: [0, 10] == True --> gear(1) is possible for t=10)
:rtype: list(booleans, nGears x CycleSteps)
@see: Annex 2-3.2, p 71
'''
## Identify impossible-gears by n_MAX.
#
GEARS_YES_MAX = (_N_GEARS <= n_max)
_GEARS_BAD = (~GEARS_YES_MAX).all(axis=0)
addDriveabilityProblems(_GEARS_BAD, 'g%i: Revolutions too high!' % ngears, driveability_issues)
## Replace impossibles with max-gear & revs.
#
GEARS_YES_MAX[ngears - 1, _GEARS_BAD] = True
_N_GEARS[ngears - 1, _GEARS_BAD] = n_max
## Identify impossible-gears by n_MIN.
#
## TODO: Construct a matrix of n_min_drive for all gears, including exceptions for gears 1 & 2.
#
GEARS_YES_MIN = (_N_GEARS >= n_min_drive)
GEARS_YES_MIN[0, :] = (_N_GEARS[0, :] >= n_idle) | (V <= v_stopped_threshold) # FIXME: move V==0 into own gear.
## NOTE: "interpratation" of specs for Gear-2
# and FIXME: NOVATIVE rule: "Clutching gear-2 only when Decelerating.".
N_GEARS2 = _N_GEARS[1, :]
GEARS_YES_MIN[1, :] = ((N_GEARS2 >= n_min_gear2) & (A <= 0)) | ((N_GEARS2 >= n_min_gear2) & (A > 0))
## Revert impossibles to min-gear, n_min & clutched.
#
_GEARS_BAD = CLUTCH = (~GEARS_YES_MIN).all(axis=0) ## TODO: Clutch on gear2 (f=will be fixed when add also CLUTCH_GEARS)
GEARS_YES_MIN[0, _GEARS_BAD] = True # Revert to min-gear.
#addDriveabilityProblems(_GEARS_BAD, 'g1: Revolutions too low!', driveability_issues)
_GEARS_YES = (GEARS_YES_MIN & GEARS_YES_MAX)
return (_GEARS_YES, CLUTCH)
[docs]def calcPower_required(V, A, SLOPE, test_mass, f0, f1, f2, f_inertial):
'''
@see: Annex 2-3.1, p 71
'''
gee = 9.81
VV = V * V
VVV = VV * V
assert V.shape == VV.shape == VVV.shape == A.shape, _shapes(V, VV, VVV, A)
if SLOPE is None:
P_REQ = (
f0 * V + f1 * VV + f2 * VVV +
f_inertial * A * V * test_mass
) / 3600.0
else:
assert V.shape == SLOPE.shape, _shapes(V, SLOPE)
P_REQ = (
f0 * V * np.cos(SLOPE) + f1 * VV + f2 * VVV +
f_inertial * A * V * test_mass +
test_mass * np.sin(SLOPE) * gee * V
) / 3600.0
assert V.shape == P_REQ.shape, _shapes(V, P_REQ)
return P_REQ
[docs]def calcPower_available(_N_GEARS, n_idle, n_rated, p_rated, load_curve, p_safety_margin):
'''
@see: Annex 2-3.2, p 72
'''
_N_NORMS = (_N_GEARS - n_idle) / (n_rated - n_idle)
_P_WOTS = np.interp(_N_NORMS, load_curve['n_norm'], load_curve['p_norm']) # When outside of load_curve, accept max-min gear.
# from scipy.interpolate import interp1d
# intrerp_f = interp1d(load_curve[0], load_curve[1], kind='linear', bounds_error=False, fill_value=0, copy=False)
# P_WOT = intrerp_f(_N_NORMS)
_P_AVAILS = _P_WOTS * p_rated * p_safety_margin
return (_P_AVAILS, _N_NORMS)
[docs]def possibleGears_byPower(_N_GEARS, P_REQ,
n_idle, n_rated, p_rated, load_curve, p_safety_margin,
driveability_issues):
'''
@see: Annex 2-3.1 & 3.3, p 71 & 72
'''
(_P_AVAILS, _N_NORMS) = calcPower_available(_N_GEARS, n_idle, n_rated, p_rated, load_curve, p_safety_margin)
assert _N_GEARS.shape == _P_AVAILS.shape, \
_shapes(P_REQ, _N_GEARS, _P_AVAILS)
_GEARS_YES = _P_AVAILS >= P_REQ
_GEARS_BAD = (~_GEARS_YES).all(axis=0)
addDriveabilityProblems(_GEARS_BAD, 'Insufficient power!', driveability_issues)
return (_GEARS_YES, _P_AVAILS, _N_NORMS)
def selectGears(_GEARS, _G_BY_N, _G_BY_P, driveability_issues):
assert _G_BY_N.shape == _G_BY_P.shape, _shapes(_G_BY_N, _G_BY_P)
assert _G_BY_N.dtype == _G_BY_P.dtype == 'bool', _dtypes(_G_BY_N, _G_BY_P)
_GEARS_YES = _G_BY_N & _G_BY_P
_GEARS[~_GEARS_YES] = -1 ## NOTE: Invalid gears will be smoothed-away in extra_rule(1).
assert _G_BY_N.dtype == _G_BY_P.dtype == 'bool', _dtypes(_G_BY_N, _G_BY_P)
GEARS = _GEARS.max(axis=0)
return GEARS
_escape_char = 128
_regex_gears2regex = re.compile(br'\\g(\d+)')
PY2 = sys.version_info[0] < 3
if PY2:
def dec_byte_repl(m):
return chr(_escape_char + int(m.group(1)))
else:
def dec_byte_repl(m):
return bytes([_escape_char + int(m.group(1))])
[docs]def gearsregex(gearspattern):
'''
:param gearspattern: regular-expression or substitution that escapes decimal-bytes written as: ``\g\d+``
with adding +128, eg::
\g124|\g7 --> unicode(128+124=252)|unicode(128+7=135)
'''
assert isinstance(gearspattern, bytes), 'Not bytes: %s' % gearspattern # For python-2 to work with __future__.unicode_literals.
regex = _regex_gears2regex.sub(dec_byte_repl, gearspattern)
return re.compile(regex)
def np2bytes(NUMS):
if (NUMS < 0).any() or (NUMS >= (256 - _escape_char)).any():
assert all(NUMS >= 0) and all(NUMS < (256 - _escape_char)), 'Outside byte-range: %s' % NUMS[(NUMS < 0) | (NUMS >= (256 - _escape_char))]
return (NUMS + _escape_char).astype('uint8').tostring()
def bytes2np(bytesarr):
assert isinstance(bytesarr, bytes), 'Not bytes: %s' % bytesarr
return np.fromstring(bytesarr, dtype='uint8') - _escape_char
def assert_regexp_unmatched(regex, string, msg):
assert not re.findall(regex, string), \
'%s: %s' % (msg, [(m.start(), m.group()) for m in re.finditer(regex, string)])
#=====================
# Driveability rules #
#=====================
def rule_checkSingletons(bV, GEARS, CLUTCH, driveability_issues, re_zeros):
re_singletons = gearsregex(b'(\g0)')
[docs]def rule_a(bV, GEARS, CLUTCH, driveability_issues, re_zeros):
"""Rule (a): Clutch & set to 1st-gear before accelerating from standstill.
Implemented with a regex, outside rules-loop:
Also ensures gear-0 always followed by gear-1.
NOTE: Rule(A) not inside x2 loop, and last to run.
"""
for m in re_zeros.finditer(bV):
t_accel = m.end()
# Exclude zeros at the end.
if (t_accel == len(bV)):
break
GEARS[t_accel - 1:t_accel] = 1
CLUTCH[t_accel - 1:t_accel - 2] = True
addDriveabilityMessage(t_accel-1, '(a: X-->0)', driveability_issues)
assert_regexp_unmatched(b'\x00[^\x00\x01]', GEARS.astype('uint8').tostring(), 'Jumped gears from standstill')
[docs]def step_rule_b1(t, pg, g, V, A, GEARS, driveability_issues):
"""Rule (b1): Do not skip gears while accelerating."""
if ((pg+1) < g and A[t-1] > 0):
pg = pg+1
GEARS[t] = pg
addDriveabilityMessage(t, '(b1: %i-->%i)' % (g, pg), driveability_issues)
return True
return False
[docs]def step_rule_b2(t, pg, g, V, A, GEARS, driveability_issues):
"""Rule (b2): Hold gears for at least 3sec when accelerating."""
if ((pg != GEARS[t-3:t-1]).any()): # A[t-1] > 0): NOTE: Not checking Accel on the final step of rule(b2)!
#assert g > pg, 'Rule e & g missed downshift(%i: %i-->%i) in acceleration!'%(t, pg, g)
if (g < pg):
addDriveabilityMessage(t, 'Rule e or g missed downshift(%i: %i-->%i) in acceleration?'%(t, pg, g), driveability_issues)
hold = False
if (pg == GEARS[t-2] and (A[t-3:t-1] > 0).all()):
hold = True; n = 2
elif (A[t-2] > 0):
hold = True; n = 1
if (hold):
GEARS[t] = pg
addDriveabilityMessage(t, '(b2(%i): %i-->%i)' % (n, g, pg), driveability_issues)
return True
return False
[docs]def step_rule_c1(t, pg, g, V, A, GEARS, driveability_issues):
"""Rule (c1): Skip gears <3sec when decelerating. """
if ((pg != GEARS[t-3:t-1]).any() and (A[t-2:t] < 0).all()):
pt = t - 2
while (pt >= t-3 and GEARS[pt] == pg):
pt -= 1
## Skip even further...
#
if (GEARS[t+1] < g):
t += 1
g = GEARS[t]
GEARS[pt+1:t] = g
for tt in range(pt+1, t):
addDriveabilityMessage(tt, '(c1: %i-->%i)' % (pg, g), driveability_issues)
return True
return False
[docs]def rule_c2(bV, A, GEARS, CLUTCH, driveability_issues, re_zeros):
"""Rule (c2): Skip 1st-gear while decelerating to standstill.
Implemented with a regex, outside rules-loop:
Search for zeros in _reversed_ V & GEAR profiles,
for as long Accel is negative.
NOTE: Rule(c2) is the last rule to run.
"""
nV = len(bV)
for m in re_zeros.finditer(bV[::-1]):
t_stop = m.end()
# Exclude zeros at the end.
if (t_stop == nV):
break
t = nV - t_stop - 1
while (A[t] < 0 and GEARS[t] == 1):
addDriveabilityMessage(t, '(c2: %i-->0)'% GEARS[t], driveability_issues)
GEARS[t] = 0
CLUTCH[t] = False
t -= 1
[docs]def step_rule_d(t, pg, g, V, A, GEARS, driveability_issues):
"""Rule (d): Cancel shifts after peak velocity."""
if (A[t-2] > 0 and A[t-1] < 0 and GEARS[t-2] == pg):
GEARS[t] = pg
addDriveabilityMessage(t, '(d: %i-->%i)' % (g, pg), driveability_issues)
return True
return False
[docs]def step_rule_e(t, pg, g, V, A, GEARS, driveability_issues):
"""Rule (e): Cancel shifts lasting 5secs or less."""
if (pg > g):
## Travel back in time for 5secs.
#
pt = t-2
while (pt >= t-5 and GEARS[pt] == pg):
pt -= 1
if (GEARS[pt] < pg): # NOTE: Apply rule(e) also for any LOWER initial/final gears (not just for i-1).
GEARS[pt+1:t] = g
for tt in range(pt+1, t):
addDriveabilityMessage(tt, '(e: %i-->%i)' % (pg, g), driveability_issues)
return True
return False
[docs]def step_rule_f(t, pg, g, V, A, GEARS, driveability_issues):
"""Rule(f): Cancel 1sec downshifts (under certain circumstances)."""
if (pg < g and GEARS[t-2] >= g):
# NOTE: Nowhere to apply it since rule(b2) would have eliminated 1-sec shifts. Moved before rule(b)!
# NOTE: Applying rule(f) also for i-2, i-3, ... signular-downshifts.
# FIXME: Rule(f) implement further constraints.
# NOTE: Rule(f): What if extra conditions unsatisfied? Allow shifting for 1 sec only??
GEARS[t-1] = min(g, GEARS[t-2])
addDriveabilityMessage(t-1, '(f: %i-->%i)' % (pg, g), driveability_issues)
return True
return False
[docs]def step_rule_g(t, pg, g, V, A, GEARS, driveability_issues):
"""Rule(g): Cancel upshift during acceleration if later downshifted for at least 2sec."""
if (pg > g and (GEARS[t:t+2] == g).all() and (A[t-1:t+2] > 0).all()):
## Travel back in time for as long accelerating and same gear.
#
pt = t-2
while (GEARS[pt] == pg and A[pt] > 0):
pt -= 1
GEARS[pt+1:t] = g
for tt in range(pt+1, t):
addDriveabilityMessage(tt, '(g: %i-->%i)' % (pg, g), driveability_issues)
return True
return False
[docs]def applyDriveabilityRules(V, A, GEARS, CLUTCH, driveability_issues):
'''
@note: Modifies GEARS & CLUTCH.
@see: Annex 2-4, p 72
'''
def apply_step_rules(rules, isStopOnFirstApplied):
for t in t_range:
if (GEARS[t-1] != GEARS[t]): ## All rules triggered by a gear-shift.
for rule in rules:
if (rule(t, GEARS[t-1], GEARS[t], V, A, GEARS, driveability_issues) and \
(isStopOnFirstApplied or GEARS[t-1] == GEARS[t])):
break
## V --> byte-array to search by regex.
#
V = V.copy(); V[V > (255 - _escape_char)] = (255 - _escape_char)
bV = np2bytes(V)
re_zeros = gearsregex(br'\g0+')
## NOTE: Extra_rule(1): Smooth-away INVALID-GEARS.
#
for t in range(2, len(GEARS)): # Start from 2nd element to accomodate rule(e)'s backtracking.
if (GEARS[t] < 0):
GEARS[t] = GEARS[t-1]
## Loop X 2 driveability-rules.
#
t_range = range(5, len(GEARS)) # Start from 5th element to accomodate rule(e)'s backtracking.
for _ in [0, 1]:
apply_step_rules([step_rule_g, step_rule_f], False) # NOTE: Rule-order and first-to-apply flag unimportant.
apply_step_rules([step_rule_e, step_rule_b1, step_rule_b2], False) # NOTE: Rule-order for b1 &b2 unimportant.
apply_step_rules([step_rule_c1], False)
rule_c2(bV, A, GEARS, CLUTCH, driveability_issues, re_zeros)
rule_a(bV, GEARS, CLUTCH, driveability_issues, re_zeros)
[docs]def run_cycle(V, A, P_REQ, gear_ratios,
n_idle, n_min_drive, n_rated,
p_rated, load_curve,
params):
'''Calculates gears, clutch and actual-velocity for the cycle (V).
Initial calculations happen on engine_revs for all gears, for all time-steps of the cycle (_N_GEARS array).
Driveability-rules are applied afterwards on the selected gear-sequence, for all steps.
:param V: the cycle, the velocity profile
:param A: acceleration of the cycle (diff over V) in m/sec^2
:return: CLUTCH: a (1 X #velocity) bool-array, eg. [3, 150] --> gear(3), time(150)
:rtype: array
'''
## A multimap to collect problems.
#
driveability_issues = np.empty_like(V, dtype='object')
driveability_issues[:] = ''
## Read and calc model parameters.
#
n_range = (n_rated - n_idle)
f_n_max = params.get('f_n_max', 1.2)
n_max = n_idle + f_n_max * n_range
if n_min_drive is None:
f_n_min = params.get('f_n_min', 0.125)
n_min_drive = n_idle + f_n_min * n_range
f_n_min_gear2 = params.get('f_n_min_gear2', 0.9)
n_min_gear2 = f_n_min_gear2 * n_idle
f_n_clutch_gear2 = params.get('f_n_clutch_gear2', [1.15, 0.03])
n_clutch_gear2 = max(f_n_clutch_gear2[0] * n_idle, f_n_clutch_gear2[1] * n_range + n_idle)
p_safety_margin = params.get('f_safety_margin', 0.9)
v_stopped_threshold = params.get('v_stopped_threshold', 1) # Km/h
(_N_GEARS, _GEARS, \
_GEAR_RATIOS) = calcEngineRevs_required(V, gear_ratios, n_idle, v_stopped_threshold)
(_G_BY_N, CLUTCH) = possibleGears_byEngineRevs(V, A, _N_GEARS,
len(gear_ratios), n_idle,
n_min_drive, n_min_gear2, n_max,
v_stopped_threshold,
driveability_issues)
(_G_BY_P, _P_AVAILS, _N_NORMS) = possibleGears_byPower(_N_GEARS, P_REQ,
n_idle, n_rated, p_rated, load_curve, p_safety_margin,
driveability_issues)
assert _GEAR_RATIOS.shape == _N_GEARS.shape == _P_AVAILS.shape == _N_NORMS.shape, \
_shapes(_GEAR_RATIOS, _N_GEARS, _P_AVAILS, _N_NORMS)
GEARS = selectGears(_GEARS, _G_BY_N, _G_BY_P, driveability_issues)
CLUTCH[(GEARS == 2) & (_N_GEARS[1, :] < n_clutch_gear2)] = True
assert V.shape == GEARS.shape, _shapes(V, GEARS)
assert GEARS.shape == CLUTCH.shape == driveability_issues.shape, \
_shapes(GEARS, CLUTCH.shape, driveability_issues)
assert 'i' == GEARS.dtype.kind, GEARS.dtype
assert ((GEARS >= -1) & (GEARS <= len(gear_ratios))).all(), (min(GEARS), max(GEARS))
return GEARS, CLUTCH, _GEAR_RATIOS, _N_GEARS, _P_AVAILS, _N_NORMS, driveability_issues
if __name__ == '__main__':
pass