UNPKG

spot-sdk-js

Version:

Develop applications and payloads for Spot using the unofficial Boston Dynamics Spot Node.js SDK.

619 lines (517 loc) 21.5 kB
const geometry_pb = require('../bosdyn/api/geometry_pb'); const _ = require('underscore'); const {NdArray, array, identity, dot, multiply, zeros, sqrt} = require('numjs'); class ArithmeticError extends Error { constructor(msg){ super(msg); this.name = 'ArithmeticError'; } }; function angle_diff(a1, a2){ let v = a1 - a2; while(v > Math.PI){ v -= 2 * Math.PI; } while(v < -Math.PI){ v += 2 * Math.PI; } return v; } function radians_to_degrees(radians){ return radians * (180/Math.PI); } class SE2Pose { constructor(x, y, angle){ this.x = x; this.y = y; this.angle = angle; } toString(){ return `position -- X: ${this.x} Y: ${this.y} Yaw: ${radians_to_degrees(this.angle)} deg` } static flatten(se3pose){ const x = se3pose.x; const y = se3pose.y; const angle = se3pose.rot.to_yaw() return new SE2Pose(x, y, angle); } to_obj(proto){ proto.position = {x: this.x, y: this.y}; proto.angle = this.angle; } to_proto(){ return new geometry_pb.SE2Pose().setPosition(new geometry_pb.Vec2([this.x, this.y])).setAngle(this.angle); } inverse(){ const c = Math.cos(this.angle); const s = Math.sin(this.angle); return new SE2Pose(-this.x * c - this.y * s, this.x * s - this.y * c, -this.angle); } mult(se2pose){ const rotation_matrix = this.to_rot_matrix(); const rotated_pos = rotation_matrix.dot([se2pose.x, se2pose.y]); return new SE2Pose(this.x + rotated_pos[0], this.y + rotated_pos[1], this.angle + se2pose.angle); } to_rot_matrix(){ const c = Math.cos(this.angle); const s = Math.sin(this.angle); return [[c, -s], [s, c]]; } to_matrix(){ const c = Math.cos(this.angle); const s = Math.sin(this.angle); return [[c, -s, this.x], [s, c, this.y], [0, 0, 1]]; } to_adjoint_matrix(){ const a_R_b = this.to_rot_matrix(); const position_skew_mat = skew_matrix_2d(this.position); const a_adjoint_b = new NdArray(array([[a_R_b, position_skew_mat.T], [0, 0, 1]])); return a_adjoint_b; } get position(){ return new geometry_pb.Vec2([this.x, this.y]); } static from_matrix(mat){ const x = mat.get([0, 2]); const y = mat.get([1, 2]); const angle = Math.acos(mat.get([0, 0])); return new SE2Pose(x, y, angle); } static from_obj(tform){ return new SE2Pose(tform.position.x, tform.position.y, tform.angle); } get_closest_se3_transform(height_z=0.0){ return new SE3Pose(this.x, this.y, height_z, Quat.from_yaw(this.angle)); } }; class SE2Velocity { constructor(x, y, angular){ this.linear_velocity_x = x; this.linear_velocity_y = y; this.angular_velocity = angular; } toString(){ return `Linear velocity -- X: ${this.linear_velocity_x} Y: ${this.linear_velocity_y} Angular velocity -- ${this.angular_velocity}`; } to_obj(proto){ proto.linear = {x: this.linear_velocity_x, y: this.linear_velocity_y}; proto.angular = this.angular_velocity; } to_proto(){ return new geometry_pb.SE2Velocity().setLinear(new geometry_pb.Vec2([this.linear_velocity_x, this.linear_velocity_y])).setAngular(this.angular_velocity); } to_vector(){ return array([this.linear_velocity_x, this.linear_velocity_y, this.angular_velocity]).reshape(3,1); } static from_vector(se2_vel_vector){ if (Array.isArray(se2_vel_vector)){ if(se2_vel_vector.length != 3){ console.log(`[MATH HELPERS] Velocity list must have 3 elements. The input has the wrong dimension of: ${se2_vel_vector.length} elements`); return null; }else{ return new SE2Velocity(se2_vel_vector[0], se2_vel_vector[1], se2_vel_vector[2]); } } if(se2_vel_vector instanceof NdArray){ if(se2_vel_vector.shape[0].length != 3 || se2_vel_vector.shape[0] != 3){ console.log(`[MATH HELPERS] Velocity numjs array must have 3 elements. The input has the wrong dimension of: ${se2_vel_vector.shape[0]}`); return null; }else{ return new SE2Velocity(se2_vel_vector[0], se2_vel_vector[1], se2_vel_vector[2]); } } } get linear(){ return new geometry_pb.Vec2([this.linear_velocity_x, this.linear_velocity_y]); } get angular(){ return this.angular_velocity; } static from_obj(vel){ return new SE2Velocity(vel.linear.x, vel.linear.y, vel.angular); } }; class SE3Velocity { constructor(lin_x, lin_y, lin_z, ang_x, ang_y, ang_z){ this.linear_velocity_x = lin_x; this.linear_velocity_y = lin_y; this.linear_velocity_z = lin_z; this.angular_velocity_x = ang_x; this.angular_velocity_y = ang_y; this.angular_velocity_z = ang_z; } toString(){ return `Linear velocity -- X: ${this.linear_velocity_x} Y: ${this.linear_velocity_y} Z: ${this.linear_velocity_z} Angular velocity -- X: ${this.angular_velocity_x} Y: ${this.angular_velocity_y} Z: ${this.angular_velocity_z}`; } to_obj(proto){ proto.linear = {x: this.linear_velocity_x, y: this.linear_velocity_y, z: this.linear_velocity_z}; proto.angular = {x: this.angular_velocity_x, y: this.angular_velocity_y, z: this.angular_velocity_z}; } to_proto(){ return new geometry_pb.SE3Velocity() .setLinear(new geometry_pb.Vec3([this.linear_velocity_x, this.linear_velocity_y, this.linear_velocity_z])) .setAngular(new geometry_pb.Vec3([this.angular_velocity_x, this.angular_velocity_y, this.angular_velocity_z])); } to_vector(){ return array([ this.linear_velocity_x, this.linear_velocity_y, this.linear_velocity_z, this.angular_velocity_x, this.angular_velocity_y, this.angular_velocity_z ]).reshape(6,1); } get linear(){ return new geometry_pb.Vec3([this.linear_velocity_x, this.linear_velocity_y, this.linear_velocity_z]); } get angular(){ return new geometry_pb.Vec3([this.angular_velocity_x, this.angular_velocity_y, this.angular_velocity_z]); } static from_obj(vel){ return new SE3Velocity(vel.linear.x, vel.linear.y, vel.linear.z, vel.angular.x, vel.angular.y, vel.angular.z); } static from_vector(se3_vel_vector){ if(Array.isArray(se3_vel_vector)){ if(se3_vel_vector.length != 6){ console.log(`[MATH HELPERS] Velocity list must have 6 elements. The input has the wrong dimension of: ${se3_vel_vector.length}`); return null; }else{ return new SE3Velocity(se3_vel_vector[0], se3_vel_vector[1], se3_vel_vector[2], se3_vel_vector[3], se3_vel_vector[4], se3_vel_vector[5]); } } if(se3_vel_vector instanceof NdArray){ if(se3_vel_vector.shape[0] != 6){ console.log(`[MATH HELPERS] Velocity numjs array must have 6 elements. The input has the wrong dimension of: ${se3_vel_vector.shape[0]}`); return null; }else{ return new SE3Velocity(se3_vel_vector[0], se3_vel_vector[1], se3_vel_vector[2], se3_vel_vector[3], se3_vel_vector[4], se3_vel_vector[5]); } } } }; class SE3Pose { constructor(x, y, z, rot){ this.x = x; this.y = y; this.z = z; this.rot = (rot instanceof geometry_pb.Quaternion) ? Quat.from_obj(rot) : rot; } toString(){ return `position -- X: ${this.x} Y: ${this.z} Z: ${this.z} rotation -- ${this.rot}`; } static from_obj(tform){ let quat; if(tform.hasOwnProperty('rotation')){ quat = Quat.from_obj(tform.rotation); }else{ quat = new Quat(); } return new SE3Pose(tform.position.x, tform.position.y, tform.position.z, quat); } static from_se2(tform, z=0){ return new SE3Pose(tform.x, tform.y, z, Quat.from_yaw(tform.angle)); } to_obj(proto){ proto.setPosition(new geometry_pb.Vec3([this.x, this.y, this.z])); this.rot.to_obj(proto.getRotation()); } to_proto(){ return new geometry_pb.SE3Pose() .setPosition(new geometry_pb.Vec3([this.x, this.y, this.z])) .setRotation(new geometry_pb.Quaternion([this.rot.w, this.rot.x, this.rot.y, this.rot.z])); } inverse(){ const inv_rot = this.rot.inverse(); const [x, y, z] = inv_rot.transform_point(this.x, this.y, this.z); return new SE3Pose(-x, -y, -z, inv_rot); } transform_point(x, y, z){ const [out_x, out_y, out_z] = this.rot.transform_point(x, y, z); return [out_x + this.x, out_y + this.y, out_z + this.z]; } transform_cloud(points){ return SE3Pose.transform_cloud_from_matrix(this.to_matrix(), points); } static transform_cloud_from_matrix(transform, points){ // !!!!!!!! Code peut être faux à verifier en fonction du code python !!!!!!!! const rot = transform.get(0, 3); const trans = transform.get(3); return dot(points, rot.T).concat(trans); } to_matrix(){ const ret = identity(4); const matrix = this.rot.to_matrix(); // assignation pour chaque indices de la matrice de l'object "rot"; ret.set(0, 0, matrix.get(0, 0)); ret.set(0, 1, matrix.get(0, 1)); ret.set(0, 2, matrix.get(0, 2)); ret.set(1, 0, matrix.get(1, 0)); ret.set(1, 1, matrix.get(1, 1)); ret.set(1, 2, matrix.get(1, 2)); ret.set(2, 0, matrix.get(2, 0)); ret.set(2, 1, matrix.get(2, 1)); ret.set(2, 2, matrix.get(2, 2)); // assignation du "x", "y" et "z" ret.set(0, 3, this.x); ret.set(1, 3, this.y); ret.set(2, 3, this.z); return ret; } mult(se3pose){ const [x, y, z] = this.rot.transform_point(se3pose.x, se3pose.y, se3pose.z); return new SE3Pose(this.x + x, this.y + y, this.z + z, this.rot.mult(se3pose.rot)); } get position(){ return new geometry_pb.Vec3([this.x, this.y, this.z]); } get rotation(){ return this.rot; } static from_matrix(mat){ const [x, y, z] = mat.slice([0, 3], 3).tolist(); const rot = Quat.from_matrix(mat.slice([0, 3], [0, 3])); return new SE3Pose(x, y, z, rot); } static from_identity(){ return new SE3Pose(0, 0, 0, new Quat()); } get_translation(){ return array([this.x, this.y, this.z]) } to_adjoint_matrix(){ const a_R_b = this.rot.to_matrix(); const position_skew_mat = skew_matrix_3d(this.position); const mat = multiply(position_skew_mat, a_R_b).tolist(); a_adjoint_b = new NdArray(array([[a_R_b, mat], [zeros(3,3), a_R_b]])); return a_adjoint_b; } get_closest_se2_transform(){ const se2_angle = (this.rot.closest_yaw_only_quaternion()).to_yaw(); return new SE2Pose(this.x, this.y, se2_angle); } }; class Quat { constructor(w=1, x=0, y=0, z=0){ this.w = w; this.x = x; this.y = y; this.z = z; } toString(){ return `W: ${this.w} X: ${this.x} Y: ${this.y} Z: ${this.z}`; } inspect(){ return this.toString(); } inverse(){ return new Quat(this.w, -this.x, -this.y, -this.z); } transform_point(x, y, z){ const inv = this.inverse() let q = new Quat(0, x, y, z); q = q.mult(inv); q = this.mult(q); return [q.x, q.y, q.z]; } to_matrix(){ const ret = identity(3); ret.set(0, 0, 1.0 - 2.0 * this.y * this.y - 2.0 * this.z * this.z); ret.set(0, 1, 2.0 * this.x * this.y - 2.0 * this.z * this.w); ret.set(0, 2, 2.0 * this.x * this.z + 2.0 * this.y * this.w); ret.set(1, 0, 2.0 * this.x * this.y + 2.0 * this.z * this.w); ret.set(1, 1, 1.0 - 2.0 * this.x * this.x - 2.0 * this.z * this.z); ret.set(1, 2, 2.0 * this.y * this.z - 2.0 * this.x * this.w); ret.set(2, 0, 2.0 * this.x * this.z - 2.0 * this.y * this.w); ret.set(2, 1, 2.0 * this.y * this.z + 2.0 * this.x * this.w); ret.set(2, 2, 1.0 - 2.0 * this.x * this.x - 2.0 * this.y * this.y); return ret; } static from_matrix(rot){ const wt = 1 + rot.get(0, 0) + rot.get(1, 1) + rot.get(2, 2); if(wt > 0.1) return Quat._from_matrix_w(rot); const xt = 1 + rot.get(0, 0) - rot.get(1, 1) - rot.get(2, 2); const yt = 1 - rot.get(0, 0) + rot.get(1, 1) - rot.get(2, 2); const zt = 1 - rot.get(0, 0) - rot.get(1, 1) + rot.get(2, 2); const t = [[Quat._from_matrix_w, wt], [Quat._from_matrix_x, xt], [Quat._from_matrix_y, yt], [Quat._from_matrix_z, zt]]; const [from_matrix_coord, val] = _.max(t, e => e[1])[0]; if(val < 1e-6) throw new ArithmeticError(`[MATH HELPERS] Matrix cannot be converged to quaternion. Are you sure this is a valid rotation matrix? \n${JSON.stringify(rot)}`); return from_matrix_coord(rot); } static _from_matrix_w(rot){ const w = Math.sqrt(1 + rot.get(0, 0) + rot.get(1, 1) + rot.get(2, 2)) * 0.5; return new Quat(w, (rot.get(2, 1) - rot.get(1, 2)) / (4.0 * w), (rot.get(0, 2) - rot.get(2, 0)) / (4.0 * w), (rot.get(1, 0) - rot.get(0, 1)) / (4.0 * w)); } static _from_matrix_x(rot){ const x = Math.sqrt(1 + rot.get(0, 0) - rot.get(1, 1) - rot.get(2, 2)) * 0.5; return new Quat((rot.get(2, 1) - rot.get(1, 2)) / (4.0 * x), x, (rot.get(0, 1) + rot.get(1, 0)) / (4.0 * x), (rot.get(0, 2) + rot.get(2, 0)) / (4.0 * x)); } static _from_matrix_y(rot){ const y = Math.sqrt(1 - rot.get(0, 0) + rot.get(1, 1) - rot.get(2, 2)) * 0.5; return new Quat((rot.get(0, 2) - rot.get(2, 0)) / (4.0 * y), (rot.get(0, 1) + rot.get(1, 0)) / (4.0 * y), y, (rot.get(1, 2) + rot.get(2, 1)) / (4.0 * y)); } static _from_matrix_z(rot){ const z = Math.sqrt(1 - rot.get(0, 0) - rot.get(1, 1) + rot.get(2, 2)) * 0.5; return new Quat((rot.get(1, 0) - rot.get(0, 1)) / (4.0 * z), (rot.get(0, 2) + rot.get(2, 0)) / (4.0 * z), (rot.get(1, 2) + rot.get(2, 1)) / (4.0 * z), z); } static from_roll(angle){ return new Quat(Math.cos(angle / 2.0), Math.sin(angle / 2.0)); } static from_pitch(angle){ return new Quat(Math.cos(angle / 2.0), y = Math.sin(angle / 2.0)); } static from_yaw(angle){ return new Quat(Math.cos(angle / 2.0), z = Math.sin(angle / 2.0)); } to_roll(){ const d = this.w * this.w + this.x * this.x + this.y * this.y + this.z * this.z; if (d == 0.0 || d == 0) return 0.0; const t0 = 2.0 * (this.w * this.x + this.y * this.z); const t1 = 1.0 - 2.0 * (this.x * this.x + this.y * this.y); return Math.atan2(t0, t1); } to_pitch(){ const d = this.w * this.w + this.x * this.x + this.y * this.y + this.z * this.z; if (d == 0.0 || d == 0) return 0.0; let t2 = 2.0 * (this.w * this.y - this.z * this.x); if(t2 < -1.0) t2 = -1.0; if(t2 > 1.0) t2 = 1.0; return Math.asin(t2); } to_yaw(){ const yaw_only_quat = this.closest_yaw_only_quaternion(); return 2 * Math.atan2(yaw_only_quat.z, yaw_only_quat.w); } to_axis_angle(){ const d = this.w * this.w + this.x * this.x + this.y * this.y + this.z * this.z; if (d == 0.0 || d == 0) return [0.0, [0, 0, 1]]; const mag = 1.0 - (this.w * this.w); if(mag <= 1e-3) return [0.0, [0, 0, 1]]; const denom = Math.sqrt(mag); if(denom < 1e-3) return [0.0, [0, 0, 1]]; const angle = 2.0 * Math.acos(this.w); const axis = [this.x / denom, this.y / denom, this.z / denom]; return [angle, axis]; } static from_obj(proto){ return new Quat(proto.getW(), proto.getX(), proto.getY(), proto.getZ()); } to_obj(proto){ proto.setW(this.w).setX(this.x).setY(this.y).setZ(this.z); return proto; } to_proto(){ return new geometry_pb.Quaternion([this.x, this.y, this.z, this.w]); } mult(other_quat){ return new Quat(this.w * other_quat.w - this.x * other_quat.x - this.y * other_quat.y - this.z * other_quat.z, this.w * other_quat.x + this.x * other_quat.w + this.y * other_quat.z - this.z * other_quat.y, this.w * other_quat.y - this.x * other_quat.z + this.y * other_quat.w + this.z * other_quat.x, this.w * other_quat.z + this.x * other_quat.y - this.y * other_quat.x + this.z * other_quat.w); } normalize(){ let q = array([this.w, this.x, this.y, this.z]); const len = sqrt(dot(q.transpose(), q)); if(len < 1e-15){ q = [1.0, 0.0, 0.0, 0.0]; }else{ q /= len; } this.w = q[0]; this.x = q[1]; this.y = q[2]; this.z = q[3]; return this; } closest_yaw_only_quaternion(){ const mag = Math.sqrt(this.w * this.w + this.z * this.z); if(mag > 0){ return new Quat(this.w/mag, 0, 0, this.z/mag); }else{ return new Quat(0, 0, 1, 0); } } }; function pose_to_xyz_yaw(A_tform_B){ const yaw = Quat.from_obj(A_tform_B.rotation).to_yaw(); const x = A_tform_B.position.x; const y = A_tform_B.position.y; const z = A_tform_B.position.z; return [x, y, z, yaw]; } function is_within_threshold(pose_3d, max_translation_meters, max_yaw_degrees){ const delta = SE2Pose.flatten(SE3Pose.from_obj(pose_3d)); const dist_2d = Math.sqrt(delta.x * delta.x + delta.y * delta.y); const angle_deg = radians_to_degrees(Math.abs(delta.angle)); return (dist_2d < max_translation_meters) && (angle_deg < max_yaw_degrees); } function recenter_angle(q, lower_limit, upper_limit){ const recenter_range = upper_limit - lower_limit; while(q >= upper_limit){ q -= recenter_range; } while(q < lower_limit){ q += recenter_range }; return q; } function skew_matrix_3d(vec3_proto){ return array([[0, -vec3_proto.getZ(), vec3_proto.getY()], [vec3_proto.getZ(), 0, -vec3_proto.getX()], [-vec3_proto.getY(), vec3_proto.getX(), 0]]); } function skew_matrix_2d(vec2_proto){ return array([[vec2_proto.getY(), -vec2_proto.getX()]]); } function transform_se2velocity(a_adjoint_b_matrix, se2_velocity_in_b){ let se2_velocity_in_b_vector; if(se2_velocity_in_b instanceof geometry_pb.SE2Velocity){ se2_velocity_in_b_vector = (SE2Velocity.from_obj(se2_velocity_in_b)).to_vector(); }else if(se2_velocity_in_b instanceof SE2Velocity){ se2_velocity_in_b_vector = se2_velocity_in_b.to_vector(); }else{ return null; } const se2_velocity_in_a_vector = new NdArray(multiply(a_adjoint_b_matrix, se2_velocity_in_b_vector)); const se2_velocity_in_a = SE2Velocity.from_vector(se2_velocity_in_a_vector); return se2_velocity_in_a; } function transform_se3velocity(a_adjoint_b_matrix, se3_velocity_in_b){ let se3_velocity_in_b_vec; if(se3_velocity_in_b instanceof geometry_pb2.SE3Velocity){ se3_velocity_in_b_vec = (SE3Velocity.from_obj(se3_velocity_in_b)).to_vector(); }else if(se3_velocity_in_b instanceof SE3Velocity){ se3_velocity_in_b_vec = se3_velocity_in_b.to_vector(); }else{ return null; } const se3_velocity_in_a_vec = new NdArray(multiply(a_adjoint_b_matrix, se3_velocity_in_b_vec)); const se3_velocity_in_a = SE3Velocity.from_vector(se3_velocity_in_a_vec); return se3_velocity_in_a; } function quat_to_eulerZYX(q){ let pitch = Math.asin(-2 * (q.x * q.z - q.w * q.y)); let yaw, roll; if(pitch > 0.9999){ yaw = 2 * Math.atan2(q.z, q.w); pitch = Math.PI / 2; roll = 0; }else if(pitch < -0.9999){ yaw = 2 * Math.atan2(q.z, q.w); pitch = -Math.PI / 2; roll = 0; }else{ yaw = Math.atan2(2 * (q.x * q.y + q.w * q.z), q.w * q.w + q.x * q.x - q.y * q.y - q.z * q.z); roll = Math.atan2(2 * (q.y * q.z + q.w * q.x), q.w * q.w - q.x * q.x - q.y * q.y + q.z * q.z); } return {yaw, pitch, roll}; } module.exports = { SE2Pose, SE2Velocity, SE3Velocity, SE3Pose, Quat, pose_to_xyz_yaw, is_within_threshold, recenter_angle, skew_matrix_3d, skew_matrix_2d, transform_se2velocity, transform_se3velocity, quat_to_eulerZYX };