def add_header_section( report: reporting.Report, settings: dict, activity_phases: limb.Property ): """ :param report: :param settings: :param activity_phases: :return: """ activity_phases = activity_phases.values() support_phases = settings['support_phases'] activity_phases = ['{}%'.format(round(100 * x)) for x in activity_phases] support_phases = ['{}%'.format(round(100 * x)) for x in support_phases] report.add_template( paths.resource('trial', 'header.html'), title=settings.get('name'), summary=settings.get('summary'), duty_cycle=round(100.0 * settings['duty_cycle']), activity_phases=activity_phases, support_phases=support_phases, date=datetime.utcnow().strftime("%m-%d-%Y %H:%M") )
def strides( foot_positions: limb.Property, times: dict ) -> typing.Dict[str, list]: out = dict() for key, positions in foot_positions.items(): distances = [] out[key + '_strides'] = distances last_fixed = None for index, time in enumerate(times['cycles']): p = positions[index] if last_fixed is None: # Skip entries in motion until the first fixed position is # found if p.annotation == 'F': last_fixed = p continue if p.annotation == 'F' and not last_fixed.compare(p): distances.append(events.Event( time=time, index=index, value=last_fixed.distance_between(p) )) last_fixed = p return out
def make_cycle_data( foot_positions: limb.Property, times: dict ) -> limb.Property: """ Trial report pages contain a duty-cycle diagram, which is created using the data generated in this method. The returned limb.Property contains a list for each limb of the duty cycle regions for that limb where a region is a list with elements: - 0: The time at which this cycle ends - 1: The enumerated annotation for this cycle :param foot_positions: Simulation results :param times: Time step information """ gait_cycles = limb.Property().assign([], [], [], []) for i in range(times['count']): for key in limb.KEYS: cycles = gait_cycles.get(key) pos = foot_positions.get(key)[i] if not cycles or cycles[-1][1] != pos.annotation: cycles.append([1, pos.annotation]) else: cycles[-1][0] += 1 return gait_cycles
def unused_foot_prints( print_positions: limb.Property, foot_positions: limb.Property ): """ Trims the print positions lists to include only those positions found in the foot positions lists and the positions just before and after to provide context :param print_positions: :param foot_positions: :return: """ was_pruned = False def is_in(uid: str, items: list) -> bool: for item in items: if item.uid == uid: return True return False for limb_key, foot_prints in print_positions.items(): index = len(foot_prints) - 1 while index > 0: index -= 1 if is_in(foot_prints[index].uid, foot_positions.get(limb_key)): break foot_prints.pop() was_pruned = True while len(foot_prints) > 1: if is_in(foot_prints[1].uid, foot_positions.get(limb_key)): break foot_prints.pop(0) was_pruned = True return was_pruned
def invalid_positions( settings: dict, time_steps: list, foot_positions: limb.Property ): """ Iterates through the time_steps and foot_positions lists and removes values at the beginning and end where any invalid data is found. Such invalid data exists when some amount of time at the beginning or end of the simulation is valid for 1 or more of the limbs in the trackway, but not all 4. :param settings: Configuration for the simulation trial :param time_steps: A list of times at which the simulation calculated foot positions :param foot_positions: The calculated positions of each foot for each time step in the time_steps list """ was_culled = False start_time = settings.get('start_time', 0) end_time = settings.get('end_time', 1e8) values = list(foot_positions.values()) values.append(time_steps) index = 0 while index < len(values[0]): entries = [] for v in values: entries.append(v[index]) cull = ( None in entries or entries[-1] < start_time or entries[-1] > end_time ) if cull: was_culled = True for v in values: v[index:index + 1] = [] else: index += 1 return was_culled
def create_tangents(foot_positions: limb.Property) -> limb.Property: """ :param foot_positions: :return: """ def find_next_position(_positions, _i): reference_pos = _positions[_i] for pos in _positions[(i + 1) :]: identical = ( mstats.value.equivalent(pos.x.raw, reference_pos.x.raw, 0.1), mstats.value.equivalent(pos.y.raw, reference_pos.y.raw, 0.1), ) if not identical[0] or not identical[1]: return pos return None tangents = limb.Property().assign([], [], [], []) for key in limb.KEYS: positions = foot_positions.get(key) for i in range(len(positions)): p = positions[i] next_pos = find_next_position(positions, i) if not next_pos or i >= (len(positions) - 1): try: tan = tangents.get(key)[-1] except IndexError as err: print("Index:", i) print("Positions:", len(positions)) print("Next:", next_pos) raise err else: tan = geometry.LineSegment2D(p, next_pos) tan.post_extend_line(4) tan.pre_extend_line(4) tangents.get(key).append(tan) return tangents
def write_data( path: str, settings: dict, trackway_definition: trackway.TrackwayDefinition, foot_positions: limb.Property, times: dict, coupling_data: dict, advancement_data: dict, tangent_data: dict ): """ Writes a JSON serialized data file containing the results of the trial for later analysis :param path: :param settings: :param trackway_definition: :param foot_positions: :param times: :param coupling_data: :param advancement_data: :param tangent_data: :return: """ position_data = dict() for limb_id, positions in foot_positions.items(): position_data[limb_id] = [x.to_dict() for x in positions] track_data = dict() for limb_id, positions in trackway_definition.limb_positions.items(): track_data[limb_id] = [x.to_dict() for x in positions] reporting.write_json_results(path, dict( settings=settings, times=times, foot_positions=position_data, track_positions=track_data, couplings=coupling.serialize(coupling_data), advancement=advancement.serialize(advancement_data), tangent=tangent.serialize(tangent_data) ))
def save_positions_file(trackway_positions: limb.Property, path): """ Saves a limb positions property object to the specified path as a CSV file with columns: lp_x, lp_dx, lp_y, lp_dy, [lp_assumed], [lp_name], [lp_uid] rp_x, rp_dx, rp_y, rp_dy, [rp_assumed], [rp_name], [rp_uid] lm_x, lm_dx, lm_y, lm_dy, [lm_assumed], [lm_name], [lm_uid] rm_x, rm_dx, rm_y, rm_dy, [rm_assumed], [rm_name], [rm_uid] :param trackway_positions: The trackway positions to be saved :param path: The path to the positions file to be saved """ df = [] for prefix, limb_key in limb.LIMB_KEY_LOOKUP.items(): for index, position in enumerate(trackway_positions.get(limb_key)): while index >= len(df): df.append(dict()) row = df[index] row['{}_x'.format(prefix)] = position.x.value row['{}_dx'.format(prefix)] = position.x.uncertainty row['{}_y'.format(prefix)] = position.y.value row['{}_dy'.format(prefix)] = position.y.uncertainty row['{}_name'.format(prefix)] = position.name row['{}_uid'.format(prefix)] = position.uid row['{}_assumed'.format(prefix)] = 'x' if position.assumed else None df = pd.DataFrame(df) df.to_csv(path) return df
def make_animation_frame_data( foot_positions: limb.Property, times: dict, coupling_data: dict, tangent_data: dict ) -> typing.List[dict]: """ Creates a list of animation frame data from the results, which is used by the JavaScript report to animate the feet within the trackway. Each frame in the returned list contains: - time: The simulation time for the frame - positions: An ordered list of position dictionaries for each limb, where the order is defined by the limb.KEYS order. Each dictionary contains: - x: A list where x[0] is the position and x[1] is the uncertainty - y: A list where y[0] is the position and y[1] is the uncertainty - f: The enumerated annotation for the position :param coupling_data: :param tangent_data: :param foot_positions: The simulation results :param times: Time step information """ frames = [] for i in range(times['count']): positions = [] tangents = [] for key in limb.KEYS: pos = foot_positions.get(key)[i] tan = tangent_data['tangents'].get(key)[i] positions.append({ 'x': [pos.x.value, pos.x.uncertainty, pos.x.raw], 'y': [pos.y.value, pos.y.uncertainty, pos.y.raw], 'f': pos.annotation, 'tx0': [tan.start.x.raw, tan.start.x.uncertainty], 'ty0': [tan.start.y.raw, tan.start.y.uncertainty], 'tx1': [tan.end.x.raw, tan.end.x.uncertainty], 'ty1': [tan.end.y.raw, tan.end.y.uncertainty], }) rear = coupling_data['rear'][i] forward = coupling_data['forward'][i] midpoint = coupling_data['midpoints'][i] rear_box = tangent_data['rear_boxes'][i] forward_box = tangent_data['forward_boxes'][i] frames.append(dict( time=times['cycles'][i], support_time=times['support_cycles'][i], positions=positions, rear_coupler={ 'x': [rear.x.value, rear.x.uncertainty, rear.x.raw], 'y': [rear.y.value, rear.y.uncertainty, rear.y.raw] }, forward_coupler={ 'x': [forward.x.value, forward.x.uncertainty, forward.x.raw], 'y': [forward.y.value, forward.y.uncertainty, forward.y.raw] }, midpoint={ 'x': [midpoint.x.value, midpoint.x.uncertainty, midpoint.x.raw], 'y': [midpoint.y.value, midpoint.y.uncertainty, midpoint.y.raw] }, rear_support_box=[ { 'x': rear_box[0].x.raw, 'y': rear_box[0].y.raw }, { 'x': rear_box[1].x.raw, 'y': rear_box[1].y.raw }, { 'x': rear_box[2].x.raw, 'y': rear_box[2].y.raw }, { 'x': rear_box[3].x.raw, 'y': rear_box[3].y.raw } ], forward_support_box=[ { 'x': forward_box[0].x.raw, 'y': forward_box[0].y.raw }, { 'x': forward_box[1].x.raw, 'y': forward_box[1].y.raw }, { 'x': forward_box[2].x.raw, 'y': forward_box[2].y.raw }, { 'x': forward_box[3].x.raw, 'y': forward_box[3].y.raw } ] )) return frames
def load_trackway_positions( settings: dict, existing: limb.Property = None, save_as: str = None ) -> limb.Property: """ Loads the trackway positions for the trial from the information provided in the settings object, unless an existing trackway positions object has been specified :param settings: Configuration for the simulation trial :param existing: Optionally the already loaded trackway positions, which will be cloned and returned if present :param save_as: An optional path where the loaded trackway positions shold be saved """ if existing: # Ignore if already specified return existing.clone() data = settings.get('data') if isinstance(data, str): # Load from a specified file if not data.startswith('/'): data = os.path.join(settings['path'], data) if not os.path.exists(data): system.log( """ [ERROR]: No CSV source data exists at the path: {} """.format(data) ) raise FileNotFoundError('No CSV source file found') return trackway.load_positions_file(data) # Generate from configuration settings track_offsets = limb.Property().assign(*data['offsets']) out = generate.trackway_positions( cycle_count=data['count'], step_size=data['step_size'], track_offsets=track_offsets, lateral_displacement=data['lateral_displacement'], positional_uncertainty=data.get('uncertainty') ) if not save_as and data.get('save'): save_as = data.get('save') if save_as: trackway.save_positions_file( trackway_positions=out, path=os.path.join(settings['directory'], save_as) ) return out
def trackway_positions( limb_positions: limb.Property, drawer, positions=None ): """ :param limb_positions: :param drawer: :param positions: :return: """ limb_positions = create_positions(limb_positions, positions) bounds = [1e12, 1e12, -1e12, -1e12] for positions in limb_positions.values(): for pos in positions: bounds[0] = min(bounds[0], pos.x.raw, pos.x.value) bounds[1] = min(bounds[1], pos.y.raw, pos.y.value) bounds[2] = max(bounds[2], pos.x.raw, pos.x.value) bounds[3] = max(bounds[3], pos.y.raw, pos.y.value) scale = 2048.0 / max( abs(bounds[2] - bounds[0]), abs(bounds[3] - bounds[1]) ) drawer.add_style_definition('.left_pes', { 'fill': svg.SvgWriter.LIMB_COLORS.left_pes, 'opacity': '0.5' }) drawer.add_style_definition('.right_pes', { 'fill': svg.SvgWriter.LIMB_COLORS.right_pes, 'opacity': '0.5' }) drawer.add_style_definition('.left_manus', { 'fill': svg.SvgWriter.LIMB_COLORS.left_manus, 'opacity': '0.5' }) drawer.add_style_definition('.right_manus', { 'fill': svg.SvgWriter.LIMB_COLORS.right_manus, 'opacity': '0.5' }) for key, positions in limb_positions.items(): is_pes = key in [limb.LEFT_PES, limb.RIGHT_PES] for pos in positions: classes = [key, 'track-pos'] html_data = { 'x': '{}'.format(pos.x.value), 'x-unc': '{}'.format(pos.x.uncertainty), 'y': '{}'.format(pos.y.value), 'y-unc': '{}'.format(pos.y.uncertainty), 'color': svg.SvgWriter.LIMB_COLORS.get(key) } if pos.annotation: html_data['annotation'] = pos.annotation if pos.uid: html_data['uid'] = pos.uid if pos.name: html_data['name'] = pos.name if pos.assumed: html_data['assumed'] = '1' classes.append('assumed') drawer.draw_circle( x=scale*pos.x.raw, y=-scale*pos.y.raw, radius=PES_RADIUS if is_pes else RADIUS, classes=classes, data=html_data ) color = svg.SvgWriter.LIMB_COLORS.get(key) print_style_name = '.{}'.format(key) print_style = { 'fill': color, 'opacity': '0.5', 'stroke-width': '{}px'.format(STROKE), 'stroke': color } assumed_style_name = '.{}.assumed'.format(key) assumed_style = { 'fill': 'white', 'opacity': '0.33' } marker_style_name = '.{}-marker'.format(key) marker_style = { 'fill': 'transparent', 'stroke-width': '{}px'.format(STROKE), 'stroke': color } if not is_pes: dash_array = '{},{}'.format(4, 4) marker_style['stroke-dasharray'] = '{},{}'.format(8,4) print_style['stroke-dasharray'] = dash_array assumed_style['stroke-dasharray'] = dash_array drawer.add_style_definition(print_style_name, print_style) drawer.add_style_definition(assumed_style_name, assumed_style) drawer.add_style_definition(marker_style_name, marker_style) p0 = positions[0] drawer.draw_circle( x=scale*p0.x.raw, y=-scale*p0.y.raw, radius=(PES_RADIUS if is_pes else RADIUS) + 0.5 * STROKE, classes=marker_style_name[1:], name=key ) return { 'scale': scale, 'offset': (0, 0) }