a
    lɀj'                     @   s   d Z ddlZddlZdZdZdddZd	d
 Zdd ZdddZ	dd Z
d ddZd!ddZdd ZG dd dZG dd dZG dd dZdS )"a)  The one implementation of Johnny 6's control laws.

Five copies of the balance law used to exist - three in serve.py, one in
learn_balance.py's episodes, one in assist_lab.py - and they drifted: the live
demo negated the foot-shift reflex the trainer had measured and fixed, the demo
dropped recover's brace slots, and drive mode re-derived balance with enough
small differences to produce a 1.9 Nm limit cycle while standing still. The fix
recorded in AUDIT.md and NEXT.md is structural: one module owns the law, and
the trainer, the scorer, the live demo and the drive mode all call it. A
divergence is now an import error, not a Saturday.

Everything here is deliberately plain: readable gains in, one command out,
state carried explicitly. These are the functions that would be transcribed
into firmware.
    Ng?g      ?              c              	   C   s   t d| t }t||}t|dt d }t dtd|| dtd   dtd   }|t| }t||ttt| tdt	|   }||fS )a_  Two-link leg: put the axle at height h, and dx ahead of the hip.

    dx=0 reproduces the simple case - hip +q/2, knee -q, axle directly under
    the hip. A non-zero dx moves the wheel fore or aft WITHOUT changing height,
    which is how the robot shifts its mass over the contact patch: sliding the
    feet forward under a forward dive is the same as throwing the body back.

    branch picks which way the knee folds. Both postures reach the same foot
    position, but each can only recover a fall one way, and swapping is a
    large, slow movement - choose by direction of travel, not mid-fall.
    g{Gz?g       @g-C6?r         ?   )
maxR_WHEELmathhypotminL_SEGacosatan2sincos)hdxbranchHrcZkneeZhip r   ./home/ghrups/robot-bench/trainer/controller.pystance_to_leg   s    ,r   c           
      C   s   t | }d|d  d|d   d|  d  }d}||d d krP|d d	 }n||d
 d krn|d
 d	 }nl|d d	 }t||d	d D ]L\\}}\}}||  kr|krn q|| ||  }	||	||   } qq|| S )a  The lean the robot must hold at a given stance height to stay put.

    REFITTED FOR THE R2 BODY (13 Aug): the r1 polynomial was not just stale,
    it was BACKWARDS - at a 380 crouch it commanded -6.1 degrees where the
    r2 body truly needs +7.0, opposite in sign at every height, because the
    knee-mounted drive motors flipped the crouch CoM geometry. That 13-degree
    error is why bob wandered (a wrong trim can only be "balanced" by
    accelerating), why gate changes rocked, and why look measured +50mm of
    feet-forward compensation (~6.6 degrees - the same error, absorbed
    geometrically). Solved kinematically on the r2 model: pose the body at
    each stance, find the pitch that puts the total CoM over the axle.
    g1]
ʾ   g=[m?r   ga?g4Op1Ui@))g     @u@g      )g      y@gܿ)g     |@gzG?r      N)floatzip)
	stance_mmsZkin_fixr   s0Zc0s1c1fr   r   r   lean_trim_deg1   s$    	"r%   c                 C   s*   t t| tt|| d| d|   S )a  The lean target: speed and odometry pick a small lean, clamped.

    The clamp is load-bearing, not incidental - flattening the law into a
    plain weighted sum was measured at 0.33, worse than all-zeros, because a
    0.66 m/s roll then asks for a 19 degree lean.
    g      ?皙?)r   absr
   )stationspeedodoclampr   r   r   lean_reference\   s    r,   c                 C   s(   t t|  || |  ||  ddS )zmThe inner balance law. Returns a normalised command in [-1, 1];
    the actuator maps 1.0 to its full 1.4 Nm.r   r   )r   npclip)kpkdleanratereftrimr   r   r   wheel_commandg   s    r5   c                 C   sB   ||  }t tt|}|dkr>| |t||| |   } | S )zMove all leg joints TOGETHER: one scale factor for the whole move so
    hip and knee arrive in ratio and the axle stays under the hip.g&.>)r   r-   r   r'   r
   )curwantr2   dtdfarr   r   r   slewm   s
    r;   r&   c                 C   s6   t |t| }|dur(||kr(tdS td| S )a  Leg slew rate in rad/s from a stage's leg-speed gain (sign-blind,
    floored - a negative half-line was an inescapable dead zone that froze
    the legs). Stages that may use the servos flat out (level, on the BNO
    reference) pass unlimited_at: above it the ramp opens to servo speed.

    Base is 150 deg/s per unit. The old 60 was the accidental-stabiliser
    era: the accel-polluted roll reference punished fast legs for their own
    motion, and the crawl was load-bearing. On the BNO reference with the
    r2 peak-torque legs it is only a brake - the searches now choose their
    speed on a scale where 1.0 means genuinely quick.Ng     j@g     b@)r   r'   r   radians)Z	leg_speedfloorunlimited_atZlsr   r   r   leg_ratew   s    
r?   c                 C   sV   t d| t }|t| |t|  }|t| |t|  }t|t ||S )a  Leg IK for a PITCHED chassis: hips and knees move together so the
    wheel stays planted at dx_w (world) while the chassis rotates by phi.

    The naive version - adding the gaze to the hip joint alone - swings the
    whole leg and translates the wheel (the leg is the long lever, the
    chassis the heavy part); measured at 5m of drift. Rotating the leg
    TARGET into the pitched chassis frame is the coordinated pose the
    operator specified: gaze through hips AND knees, wheels balancing only.
    皙?)r   r   r   r   r   r   )Zstance_mZdx_wphir   ZHhZdx_cZHh_cr   r   r   	gaze_pose   s    
rB   c                 C   s    t t| | t| t|S )a  Feet slide TOWARD the fall - dx has the SAME sign as lean. Measured in
    the assist lab: toward the fall cuts a 2 m/s shove from 24.9 deg peak lean
    to 6.7; the inverted sign (feet fleeing) closed an unstable pitch loop
    that fell in 5s unshoved. That inversion has now happened twice, once in
    the trainer and once in serve.py's copies - which is why the sign lives
    here and nowhere else.)r   r-   r.   r'   )shift_kr1   	shift_maxr   r   r   
foot_shift   s    rE   c                   @   s(   e Zd ZdZdZdZdd Zdd ZdS )	
ShoveBracea\  Detect a shove and lean into it, fading afterwards.

    Fires on acceleration the robot did not command for itself:
        shove_felt = filtered_accel - (commanded_speed_change / dt)
    Raw wheel-speed derivative carries the balance loop's own activity
    (measured 17 m/s^2 peak just standing, against a threshold of 3 - the
    brace became a random 12 degree injection). Through a 0.10s low-pass the
    same standing noise peaks at 3.7 while a real 2.5 m/s shove reads 10.5,
    so 5.0 separates cleanly. A driving stage must feed its own v_cmd or it
    will brace against its own throttle.
    r&   g      @c                 C   s(   || _ || _d| _d| _d| _d| _d S Nr   )gainfadev_filt
prev_speed
prev_v_cmdbrace)selfrH   rI   r   r   r   __init__   s    zShoveBrace.__init__c              	   C   s   |  j |t|| j || j   7  _ | j | j | }| j | _|| j | }|| _|| }t|| jkrr| j| | _|  jt	
| tdt| j 9  _| jS )Nr@   )rJ   r   TAUrK   rL   r'   	THRESHOLDrH   rM   r   exprI   )rN   r)   v_cmdr8   accelexpectedZfeltr   r   r   update   s    $&zShoveBrace.updateN)__name__
__module____qualname____doc__rP   rQ   rO   rV   r   r   r   r   rF      s
   rF   c                   @   s"   e Zd ZdZdddZdd ZdS )	SpeedFiltera  Low-pass on wheel speed before it drives the lean target.

    Raw wheel speed contains the limit cycle itself, so feeding it back
    unfiltered closes a positive loop: the lean target saturates at its
    clamp, flips sign every step, and the wheels chatter at full torque while
    the robot stands perfectly still - measured at 2.22 Nm mean where holding
    still needs about 0.05. tau=0 disables (raw).r&   c                 C   s   || _ d| _d S rG   )tauvrN   r\   r   r   r   rO      s    zSpeedFilter.__init__c                 C   s.   |t || j }|  j||| j  7  _| jS )N)r   r\   r]   )rN   v_rawr8   ar   r   r   rV      s    zSpeedFilter.updateN)r&   rW   rX   rY   rZ   rO   rV   r   r   r   r   r[      s   
r[   c                   @   s"   e Zd ZdZdddZdd ZdS )	ComplementaryTilta  Gyro-fast tilt with slow accel correction, for the REAL robot.

    In the sim the fused estimator (est_up) already plays the BNO086 and is
    the reference every stage controls and scores on; this class is the
    firmware-portable equivalent for hardware without a fused IMU. The gyro
    carries the fast path (immune to the linear acceleration the legs
    inject - the loop that made accel-only references punish fast legs);
    the accelerometer corrects drift with time constant tau.r   c                 C   s   || _ d| _d S rG   r\   angler^   r   r   r   rO      s    zComplementaryTilt.__init__c                 C   s6   | j | j |  }|| j||   d| |  | _| jS )Nr   rc   )rN   Z	gyro_rateZaccel_angler8   r`   r   r   r   rV      s     zComplementaryTilt.updateN)r   ra   r   r   r   r   rb      s   	
rb   )r   r   )r   r   )r&   N)r   )rZ   r   numpyr-   r   r   r   r%   r,   r5   r;   r?   rB   rE   rF   r[   rb   r   r   r   r   <module>   s   
+




%