Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Live indi tuning (#755) #116

Merged
merged 1 commit into from
Dec 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions cereal/car.capnp
Original file line number Diff line number Diff line change
Expand Up @@ -455,10 +455,12 @@ struct CarParams {
struct LateralINDITuning {
outerLoopGainV @0 :List(Float32);
outerLoopGainBP @1 :List(Float32);
innerLoopGain @2 :Float32;
timeConstantBP @3 :List(Float32);
timeConstantV @4:List(Float32);
actuatorEffectiveness @5 :Float32;
innerLoopGainBP @2 :List(Float32);
innerLoopGainV @3 :List(Float32);
timeConstantBP @4 :List(Float32);
timeConstantV @5:List(Float32);
actuatorEffectivenessBP @6 :List(Float32);
actuatorEffectivenessV @7 :List(Float32);
}

struct LateralLQRTuning {
Expand Down
3 changes: 2 additions & 1 deletion common/numpy_fast.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ def clip(x, lo, hi):

def interp(x, xp, fp):
N = len(xp)
T = len(fp)

def get_interp(xv):
hi = 0
while hi < N and xv > xp[hi]:
hi += 1
low = hi - 1
return fp[-1] if hi == N and xv > xp[low] else (
return fp[-1] if hi >= T or (hi == N and xv > xp[low]) or (N < T and xv == xp[-1]) else (
fp[0] if hi == 0 else
(xv - xp[low]) * (fp[hi] - fp[low]) / (xp[hi] - xp[low]) + fp[low])

Expand Down
62 changes: 49 additions & 13 deletions common/op_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,40 @@ class ValueTypes:


class Param:
def __init__(self, default=None, allowed_types=[], description=None, live=False, hidden=False):
def __init__(self, default=None, allowed_types=[], description=None, live=False, hidden=False, depends_on=None):
self.default = default
if not isinstance(allowed_types, list):
allowed_types = [allowed_types]
self.allowed_types = allowed_types
self.description = description
self.hidden = hidden
self.live = live
self.depends_on = depends_on
self.children = []
self._create_attrs()

def is_valid(self, value):
if not self.has_allowed_types:
return True
return type(value) in self.allowed_types
if self.is_list and isinstance(value, list):
for v in value:
if type(v) not in self.allowed_types:
return False
return True
else:
return type(value) in self.allowed_types or value in self.allowed_types

def _create_attrs(self): # Create attributes and check Param is valid
self.has_allowed_types = isinstance(self.allowed_types, list) and len(self.allowed_types) > 0
self.has_description = self.description is not None
self.is_list = list in self.allowed_types
self.is_bool = bool in self.allowed_types
if self.has_allowed_types:
assert type(self.default) in self.allowed_types, 'Default value type must be in specified allowed_types!'
if self.is_list:
self.allowed_types.remove(list)
assert type(self.default) in self.allowed_types or self.default in self.allowed_types, 'Default value type must be in specified allowed_types!'

if self.is_list and self.default:
for v in self.default:
assert type(v) in self.allowed_types, 'Default value type must be in specified allowed_types!'


class opParams:
Expand Down Expand Up @@ -103,21 +114,40 @@ def __init__(self):
#'min_TR': Param(None, VT.none_or_number, 'The minimum allowed following distance in seconds. Default is 0.9 seconds.\n'
#'The range is limited from 0.85 to 1.3. Set to None to disable', live=True),
#'use_virtual_middle_line': Param(False, bool, 'For roads over 4m wide, hug right. For roads under 2m wide, hug left.'),
'uniqueID': Param(None, [type(None), str], 'User\'s unique ID')
'uniqueID': Param(None, [type(None), str], 'User\'s unique ID'),
'enable_indi_live': Param(False, bool, live=True),
'indi_inner_gain_bp': Param([0, 255, 255], [list, float, int], live=True, depends_on='enable_indi_live'),
'indi_inner_gain_v': Param([6.0, 6.0, 6.0], [list, float, int], live=True, depends_on='enable_indi_live'),
'indi_outer_gain_bp': Param([0, 255, 255], [list, float, int], live=True, depends_on='enable_indi_live'),
'indi_outer_gain_v': Param([15, 15, 15], [list, float, int], live=True, depends_on='enable_indi_live'),
'indi_time_constant_bp': Param([0, 255, 255], [list, float, int], live=True, depends_on='enable_indi_live'),
'indi_time_constant_v': Param([5.5, 5.5, 5.5], [list, float, int], live=True, depends_on='enable_indi_live'),
'indi_actuator_effectiveness_bp': Param([0, 255, 255], [list, float, int], live=True, depends_on='enable_indi_live'),
'indi_actuator_effectiveness_v': Param([6, 6, 6], [list, float, int], live=True, depends_on='enable_indi_live'),
'steer_limit_timer': Param(0.4, VT.number, live=True, depends_on='enable_indi_live')
}

self._params_file = '/data/op_params.json'
self._backup_file = '/data/op_params_corrupt.json'
self._last_read_time = sec_since_boot()
self.read_frequency = 2.5 # max frequency to read with self.get(...) (sec)
self._to_delete = ['reset_integral', 'log_data'] # a list of params you want to delete (unused)
self._last_mod_time = 0.
self._run_init() # restores, reads, and updates params

def _run_init(self): # does first time initializing of default params
# Two required parameters for opEdit
self.fork_params['username'] = Param(None, [type(None), str, bool], 'Your identifier provided with any crash logs sent to Sentry.\nHelps the developer reach out to you if anything goes wrong')
self.fork_params['op_edit_live_mode'] = Param(False, bool, 'This parameter controls which mode opEdit starts in', hidden=True)
self.params = self._get_all_params(default=True) # in case file is corrupted

for k, p in self.fork_params.items():
d = p.depends_on
while d:
fp = self.fork_params[d]
fp.children.append(k)
d = fp.depends_on

if travis:
return

Expand Down Expand Up @@ -206,13 +236,19 @@ def _update_params(self, param_info, force_live):
self._last_read_time = sec_since_boot()

def _read(self):
try:
with open(self._params_file, "r") as f:
self.params = json.loads(f.read())
return True
except Exception as e:
error(e)
return False
if os.path.isfile(self._params_file):
try:
mod_time = os.path.getmtime(self._params_file)
if mod_time > self._last_mod_time:
with open(self._params_file, "r") as f:
self.params = json.loads(f.read())
self._last_mod_time = mod_time
return True
else:
return False
except Exception as e:
print("Unable to read file: " + str(e))
return False

def _write(self):
if not travis:
Expand Down
116 changes: 93 additions & 23 deletions op_edit.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
#!/usr/bin/env python3
import time
from common.op_params import opParams
import ast
import difflib

from common.op_params import opParams
from common.colors import COLORS
from collections import OrderedDict


class opEdit: # use by running `python /data/openpilot/op_edit.py`
Expand Down Expand Up @@ -57,9 +59,11 @@ def run_loop(self):
if self.live_tuning: # only display live tunable params
self.params = {k: v for k, v in self.params.items() if self.op_params.param_info(k).live}

self.params = OrderedDict(sorted(self.params.items(), key=self.sort_params))

values_list = []
for k, v in self.params.items():
if len(str(v)) < 20:
if len(str(v)) < 30 or len(str(v)) <= len('{} ... {}'.format(str(v)[:30], str(v)[-15:])):
v_color = ''
if type(v) in self.type_colors:
v_color = self.type_colors[type(v)]
Expand All @@ -74,14 +78,35 @@ def run_loop(self):

to_print = []
blue_gradient = [33, 39, 45, 51, 87]
last_key = ''
last_info = None
shown_dots = False
for idx, param in enumerate(self.params):
line = '{}. {}: {} {}'.format(idx + 1, param, values_list[idx], live[idx])
if idx == self.last_choice and self.last_choice is not None:
line = COLORS.OKGREEN + line
else:
_color = blue_gradient[min(round(idx / len(self.params) * len(blue_gradient)), len(blue_gradient) - 1)]
line = COLORS.BASE(_color) + line
to_print.append(line)
info = self.op_params.param_info(param)
indent = self.get_sort_key(param).count(',')
line = ''
if not info.depends_on or param in last_info.children and \
self.op_params.get(last_key) and self.op_params.get(info.depends_on):
line = '{}. {}: {} {}'.format(idx + 1, param, values_list[idx], live[idx])
line = indent * '.' + line
elif not shown_dots and last_info and param in last_info.children:
line = '...'
shown_dots = True
if line:
if idx == self.last_choice and self.last_choice is not None:
line = COLORS.OKGREEN + line
else:
_color = blue_gradient[min(round(idx / len(self.params) * len(blue_gradient)), len(blue_gradient) - 1)]
line = COLORS.BASE(_color) + line
if last_info and len(last_info.children) and indent == 0:
line = '\n' + line
shown_dots = False
to_print.append(line)

if indent == 0:
last_key = param
last_info = info


extras = {'a': ('Add new parameter', COLORS.OKGREEN),
'd': ('Delete parameter', COLORS.FAIL),
Expand All @@ -93,7 +118,7 @@ def run_loop(self):
self.prompt('\nChoose a parameter to edit (by index or name):')

choice = input('>> ').strip().lower()
parsed, choice = self.parse_choice(choice, len(to_print) - len(extras))
parsed, choice = self.parse_choice(choice, len(self.params) + len(extras))
if parsed == 'continue':
continue
elif parsed == 'add':
Expand Down Expand Up @@ -153,7 +178,7 @@ def change_parameter(self, choice):
if param_info.has_description:
to_print.append(COLORS.OKGREEN + '>> Description: {}'.format(param_info.description.replace('\n', '\n > ')) + COLORS.ENDC)
if param_info.has_allowed_types:
to_print.append(COLORS.RED + '>> Allowed types: {}'.format(', '.join([at.__name__ for at in param_info.allowed_types])) + COLORS.ENDC)
to_print.append(COLORS.RED + '>> Allowed types: {}'.format(', '.join([at.__name__ if isinstance(at, type) else at for at in param_info.allowed_types])) + COLORS.ENDC)
if param_info.live:
live_msg = '>> This parameter supports live tuning!'
if not self.live_tuning:
Expand All @@ -180,6 +205,10 @@ def change_parameter(self, choice):
return

new_value = self.str_eval(new_value)

if param_info.is_bool and type(new_value) is int:
new_value = bool(new_value)

if not param_info.is_valid(new_value):
self.error('The type of data you entered ({}) is not allowed with this parameter!'.format(type(new_value).__name__))
continue
Expand All @@ -201,31 +230,58 @@ def change_parameter(self, choice):
def change_param_list(self, old_value, param_info, chosen_key):
while True:
self.info('Current value: {} (type: {})'.format(old_value, type(old_value).__name__), sleep_time=0)
self.prompt('\nEnter index to edit (0 to {}):'.format(len(old_value) - 1))
choice_idx = self.str_eval(input('>> '))
self.prompt('\nEnter index to edit (0 to {}), or -i to remove index, or +value to append value:'.format(len(old_value) - 1))

append_val = False
remove_idx = False
choice_idx = input('>> ')

if choice_idx == '':
self.info('Exiting this parameter...', 0.5)
return

if not isinstance(choice_idx, int) or choice_idx not in range(len(old_value)):
if isinstance(choice_idx, str):
if choice_idx[0] == '-':
remove_idx = True
if choice_idx[0] == '+':
append_val = True

if append_val or remove_idx:
choice_idx = choice_idx[1::]

choice_idx = self.str_eval(choice_idx)
is_list = isinstance(choice_idx, list)

if not append_val and not (is_list or (isinstance(choice_idx, int) and choice_idx in range(len(old_value)))):
self.error('Must be an integar within list range!')
continue

while True:
self.info('Chosen index: {}'.format(choice_idx), sleep_time=0)
self.info('Value: {} (type: {})'.format(old_value[choice_idx], type(old_value[choice_idx]).__name__), sleep_time=0)
self.prompt('\nEnter your new value:')
new_value = input('>> ').strip()
if append_val or remove_idx or is_list:
new_value = choice_idx
else:
self.info('Chosen index: {}'.format(choice_idx), sleep_time=0)
self.info('Value: {} (type: {})'.format(old_value[choice_idx], type(old_value[choice_idx]).__name__), sleep_time=0)
self.prompt('\nEnter your new value:')
new_value = input('>> ').strip()
new_value = self.str_eval(new_value)

if new_value == '':
self.info('Exiting this list item...', 0.5)
break

new_value = self.str_eval(new_value)
if not param_info.is_valid(new_value):
self.error('The type of data you entered ({}) is not allowed with this parameter!'.format(type(new_value).__name__))
continue
break

old_value[choice_idx] = new_value
if append_val:
old_value.append(new_value)
elif remove_idx:
del old_value[choice_idx]
elif is_list:
old_value = new_value
else:
old_value[choice_idx] = new_value

self.op_params.put(chosen_key, old_value)
self.success('Saved {} with value: {}! (type: {})'.format(chosen_key, new_value, type(new_value).__name__), end='\n')
Expand Down Expand Up @@ -302,9 +358,9 @@ def str_eval(self, dat):
except:
if dat.lower() == 'none':
dat = None
elif dat.lower() == 'false':
elif dat.lower() in ['false', 'f']:
dat = False
elif dat.lower() == 'true': # else, assume string
elif dat.lower() in ['true', 't']: # else, assume string
dat = True
return dat

Expand Down Expand Up @@ -360,5 +416,19 @@ def add_parameter(self):
self.info('Not saved!')
return

def sort_params(self, kv):
return self.get_sort_key(kv[0])

def get_sort_key(self, k):
p = self.op_params.param_info(k)

if not p.depends_on:
return f'{1 if not len(p.children) else 0}{k}'
else:
s = ''
while p.depends_on:
s = f'{p.depends_on},{s}'
p = self.op_params.param_info(p.depends_on)
return f'{0}{s}{k}'

opEdit()
Loading