diff --git a/inc/cvc/nav/detail/dyn_bike.h b/inc/cvc/nav/detail/dyn_bike.h index 687863b13..ef3a58a9e 100644 --- a/inc/cvc/nav/detail/dyn_bike.h +++ b/inc/cvc/nav/detail/dyn_bike.h @@ -364,6 +364,197 @@ CVC_HD inline void dyn_plant_vjp(const dyn_bike_params &p, const bike_veh &v, co } } +// ── The plant in port-Hamiltonian form ──────────────────────────────────────────────────── +// Doc 20's rungs 2 and 3, at the plant. The body is a planar rigid body: the pose q = (x, y, psi) +// on SE(2) and the body momenta p = M v, with v = (vx, vy, r) the body twist at the centre of +// gravity and M = diag(m, m, Iz). In the linear-tire regime dyn_plant integrates +// +// H(q, p) = 1/2 p^T M^-1 p + V(q), dH/dp = v +// p' = (J(p) - D(v)) v - dV + G u, u = (F_x, delta), y = G^T v +// +// J(p) [[0, 0, p_y], [0, 0, -p_x], [-p_y, p_x, 0]]: SE(2)'s coadjoint action, J(p) v = ad*_v p. +// It gives dyn_plant's + r vy in vx' and - r vx in vy' (and, with equal masses, nothing in +// r'). Skew, so it moves no energy. +// D(v) diag(D_x, D_lat). D_x = drag u + c_rr m g / u: the drag and rolling resistance, each +// opposing vx (u = sqrt(vx^2 + eps^2), as the plant smooths them). D_lat = (C_f a a^T + +// C_r b b^T) / u on (vy, r), a = (1, lf), b = (1, -lr): the velocity parts of the +// linearized tires, F_yf = C_f (delta - (vy + lf r) / u), F_yr = -C_r (vy - lr r) / u. +// Positive semi-definite for every u > 0. +// G [[1, 0], [0, C_f], [0, lf C_f]]: the drive and brake force on p_x; the steering's C_f +// delta on p_y and lf C_f delta on p_r. +// dV (m g grade, 0, 0) in the body frame: gravity along the heading (dyn_plant's grade). +// C_f, C_r the tires' cornering stiffness at zero slip, D C B, D the lateral grip the axle has +// left beside its drive or brake force (dyn_lateral_cap). +// +// So dH/dt = -v^T D v + y^T u - m g grade vx: supplied power y^T u, dissipated v^T D v. The +// inputs are the actuators' outputs (the state's delta and a): their lags are outside the body. +// dyn_plant departs from this by the tires' and the small steering angle's linearization and by +// its own step; nav_dyn_bike_test's DynBikePH gates measure both. Past a tire's peak its force +// falls as slip grows, and the tangent dissipation (dyn_ph_tangent_D) loses definiteness: the +// vehicle slides, which a positive-definite D cannot represent. These are evaluations only; +// dyn_plant does not call them. + +// The kinetic energy 1/2 v^T M v. +template +CVC_HD inline S dyn_ph_hamiltonian(const dyn_bike_params &p, const dyn_state &s) { + return S(0.5) * (S(p.m) * (s.vx * s.vx + s.vy * s.vy) + S(p.Iz) * s.r * s.r); +} + +// p = M v. +template +CVC_HD inline void dyn_ph_momentum(const dyn_bike_params &p, const dyn_state &s, S pm[3]) { + pm[0] = S(p.m) * s.vx; + pm[1] = S(p.m) * s.vy; + pm[2] = S(p.Iz) * s.r; +} + +// J(p), the coadjoint structure. +template CVC_HD inline void dyn_ph_J(const S pm[3], S J[3][3]) { + const S z = S(0); + J[0][0] = z; + J[0][1] = z; + J[0][2] = pm[1]; + J[1][0] = z; + J[1][1] = z; + J[1][2] = -pm[0]; + J[2][0] = -pm[1]; + J[2][1] = pm[0]; + J[2][2] = z; +} + +// The lateral grip each axle has left (dyn_plant's step 2 at the state's own a): Dyf, Dyr. +template +CVC_HD inline void dyn_ph_grip(const dyn_bike_params &p, const dyn_state &s, S &Dyf, S &Dyr) { + const S m = S(p.m), g = S(p.g), lf = S(p.lf), lr = S(p.lr), h = S(p.h), L = lf + lr; + const S fz_floor = S(p.fz_min) * m * g; + S Fzf = m * (g * lr - s.a * h) / L, Fzr = m * (g * lf + s.a * h) / L; + if (s_val(Fzf) < s_val(fz_floor)) + Fzf = fz_floor; + if (s_val(Fzr) < s_val(fz_floor)) + Fzr = fz_floor; + const S cap_f = S(p.mu) * Fzf, cap_r = S(p.mu) * Fzr, F_drive = m * s.a; + const S share = S(s_val(s.a) >= 0.0 ? p.front_drive : p.front_brake); + Dyf = dyn_lateral_cap(dyn_sat(F_drive * share, cap_f), cap_f); + Dyr = dyn_lateral_cap(dyn_sat(F_drive * (S(1) - share), cap_r), cap_r); +} + +// The cornering stiffnesses at zero slip, D C B. +template +CVC_HD inline void dyn_ph_cornering(const dyn_bike_params &p, const dyn_state &s, S &Cf, S &Cr) { + S Dyf, Dyr; + dyn_ph_grip(p, s, Dyf, Dyr); + Cf = Dyf * S(p.C) * S(p.B_f); + Cr = Dyr * S(p.C) * S(p.B_r); +} + +// D(v), the linear-tire dissipation (with drag and rolling resistance on vx). +template +CVC_HD inline void dyn_ph_D(const dyn_bike_params &p, const dyn_state &s, S D[3][3]) { + S Cf, Cr; + dyn_ph_cornering(p, s, Cf, Cr); + const S u = s_sqrt(s.vx * s.vx + S(p.eps) * S(p.eps)); + const S lf = S(p.lf), lr = S(p.lr), z = S(0); + D[0][0] = S(p.drag) * u + S(p.c_rr) * S(p.m) * S(p.g) / u; + D[0][1] = z; + D[0][2] = z; + D[1][0] = z; + D[2][0] = z; + D[1][1] = (Cf + Cr) / u; + D[1][2] = (Cf * lf - Cr * lr) / u; + D[2][1] = D[1][2]; + D[2][2] = (Cf * lf * lf + Cr * lr * lr) / u; +} + +// G, the input map for u = (F_x, delta). +template +CVC_HD inline void dyn_ph_G(const dyn_bike_params &p, const dyn_state &s, S G[3][2]) { + S Cf, Cr; + dyn_ph_cornering(p, s, Cf, Cr); + const S z = S(0); + G[0][0] = S(1); + G[0][1] = z; + G[1][0] = z; + G[1][1] = Cf; + G[2][0] = z; + G[2][1] = S(p.lf) * Cf; +} + +// u = (F_x, delta) from the actuators' state: the drive force m a and the road-wheel angle. +template +CVC_HD inline void dyn_ph_input(const dyn_bike_params &p, const dyn_state &s, S u[2]) { + u[0] = S(p.m) * s.a; + u[1] = s.delta; +} + +// p' = (J - D) v - dV + G u at the state. +template +CVC_HD inline void dyn_ph_rhs(const dyn_bike_params &p, const dyn_state &s, S grade, S pdot[3]) { + S pm[3], J[3][3], D[3][3], G[3][2], u[2]; + dyn_ph_momentum(p, s, pm); + dyn_ph_J(pm, J); + dyn_ph_D(p, s, D); + dyn_ph_G(p, s, G); + dyn_ph_input(p, s, u); + const S v[3] = {s.vx, s.vy, s.r}; + for (int i = 0; i < 3; ++i) { + S acc = G[i][0] * u[0] + G[i][1] * u[1]; + for (int j = 0; j < 3; ++j) + acc = acc + (J[i][j] - D[i][j]) * v[j]; + pdot[i] = acc; + } + pdot[0] = pdot[0] - S(p.m) * S(p.g) * grade; +} + +// The power balance at the state: dissipated v^T D v and supplied y^T u (y = G^T v). +template +CVC_HD inline void dyn_ph_power(const dyn_bike_params &p, const dyn_state &s, S &dissipated, + S &supplied) { + S D[3][3], G[3][2], u[2]; + dyn_ph_D(p, s, D); + dyn_ph_G(p, s, G); + dyn_ph_input(p, s, u); + const S v[3] = {s.vx, s.vy, s.r}; + dissipated = S(0); + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + dissipated = dissipated + v[i] * D[i][j] * v[j]; + supplied = S(0); + for (int k = 0; k < 2; ++k) { + S y = S(0); + for (int i = 0; i < 3; ++i) + y = y + G[i][k] * v[i]; + supplied = supplied + y * u[k]; + } +} + +// The tangent dissipation of dyn_plant's Pacejka tires at the state: Dt = -d(Q_y, Q_r)/d(vy, r) +// with Q the tires' lateral force and yaw moment, = k_f a a^T + k_r b b^T where k is each tire's +// dF/dalpha times its slip's sensitivity to its lateral velocity (and the front's cos delta). +// With a and b independent, Dt is positive semi-definite exactly when both k are >= 0, i.e. both +// tires are below their peak; kf and kr are returned with it. alpha_f and alpha_r are the slips. +template +CVC_HD inline void dyn_ph_tangent_D(const dyn_bike_params &p, const dyn_state &s, S Dt[2][2], + S &kf, S &kr, S &alpha_f, S &alpha_r) { + S Dyf, Dyr; + dyn_ph_grip(p, s, Dyf, Dyr); + const S lf = S(p.lf), lr = S(p.lr), C = S(p.C), Bf = S(p.B_f), Br = S(p.B_r); + const S u = s_sqrt(s.vx * s.vx + S(p.eps) * S(p.eps)), sg = s.vx / u; + const S wf = s.vy + lf * s.r, wr = s.vy - lr * s.r; + alpha_f = sg * s.delta - s_atan2(wf, u); + alpha_r = -s_atan2(wr, u); + // dF/dalpha of D sin(C atan(B alpha)). + const S bf = Bf * alpha_f, br = Br * alpha_r; + const S dFf = Dyf * C * Bf * s_cos(C * s_atan(bf)) / (S(1) + bf * bf); + const S dFr = Dyr * C * Br * s_cos(C * s_atan(br)) / (S(1) + br * br); + // dalpha/dw = -u / (u^2 + w^2); the front force reaches (vy, r) through cos delta. + kf = dFf * s_cos(s.delta) * u / (u * u + wf * wf); + kr = dFr * u / (u * u + wr * wr); + Dt[0][0] = kf + kr; + Dt[0][1] = kf * lf - kr * lr; + Dt[1][0] = Dt[0][1]; + Dt[1][1] = kf * lf * lf + kr * lr * lr; +} + // ── The vehicle under the trained controller ────────────────────────────────────────────── // One step: bike_control's commands from the rear axle's pose and the speed vx, through the diff --git a/src/cvc/tests/nav_dyn_bike_test.cpp b/src/cvc/tests/nav_dyn_bike_test.cpp index d2a0ae7aa..bca3a3d36 100644 --- a/src/cvc/tests/nav_dyn_bike_test.cpp +++ b/src/cvc/tests/nav_dyn_bike_test.cpp @@ -681,3 +681,255 @@ TEST(DynBike, Golden) { 1e-9 * std::max(1.0, std::max(std::fabs(now[k]), std::fabs(golden[k])))) << "value " << k << " (case " << k / 18 << "): golden " << golden[k] << ", now " << now[k]; } + +// ── The plant in port-Hamiltonian form (dyn_ph_*, doc 20's rungs 2 and 3) ───────────────────── +// H = 1/2 p^T M^-1 p + V, p' = (J - D) v - dV + G u, u = (F_x, delta). These gates hold dyn_plant +// to that form in the linear-tire regime, and report where the tires' tangent dissipation stops +// being positive semi-definite past the Pacejka peak. dyn_plant itself is unchanged (DynBike.Golden +// still pins it). + +namespace { + +constexpr double kTwoPi = 6.283185307179586; // M_PI is not portable (MSVC) + +// A Humvee state in the linear-tire regime: some speed, a small steer, small slip. +dyn_state linear_state(std::mt19937 &rng, const dyn_bike_params &p) { + std::uniform_real_distribution u(-1.0, 1.0); + dyn_state s{}; + s.vx = 11.0 + 6.0 * u(rng); + s.delta = 0.02 * u(rng); + s.r = s.vx * s.delta / (p.lf + p.lr) * (1.0 + 0.2 * u(rng)); + s.vy = p.lr * s.r + 0.03 * u(rng); + s.a = 1.5 * u(rng); + s.psi = u(rng); + return s; +} + +// The tires' lateral force and the body-frame lateral acceleration Q_y / m at a state (dyn_plant's +// step 4 at the state, its own a and delta): a_y = vy' + r vx. +double lateral_g(const dyn_bike_params &p, const dyn_state &s) { + double Dyf, Dyr; + diff::dyn_ph_grip(p, s, Dyf, Dyr); + const double u = std::sqrt(s.vx * s.vx + double(p.eps) * p.eps), sg = s.vx / u; + const double af = sg * s.delta - std::atan2(s.vy + p.lf * s.r, u); + const double ar = -std::atan2(s.vy - p.lr * s.r, u); + const double Fyf = diff::dyn_lateral(af, p.B_f, p.C, Dyf); + const double Fyr = diff::dyn_lateral(ar, p.B_r, p.C, Dyr); + return (Fyf * std::cos(s.delta) + Fyr) / (p.m * p.g); +} + +} // namespace + +TEST(DynBikePH, TheStructureIsPortHamiltonian) { + const dyn_bike_params p = humvee(); + std::mt19937 rng(3); + std::uniform_real_distribution u(-1.0, 1.0); + for (int k = 0; k < 200; ++k) { + dyn_state s{}; + s.vx = 20.0 * u(rng); + s.vy = 3.0 * u(rng); + s.r = u(rng); + s.delta = 0.5 * u(rng); + s.a = 4.0 * u(rng); + double pm[3], J[3][3], D[3][3]; + diff::dyn_ph_momentum(p, s, pm); + diff::dyn_ph_J(pm, J); + diff::dyn_ph_D(p, s, D); + const double v[3] = {s.vx, s.vy, s.r}; + double vJv = 0.0; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) { + EXPECT_EQ(J[i][j], -J[j][i]); + EXPECT_EQ(D[i][j], D[j][i]); + vJv += v[i] * J[i][j] * v[j]; + } + EXPECT_NEAR(vJv, 0.0, 1e-9 * (1.0 + pm[0] * pm[0])); + // D is PSD: its 1x1 block is positive and its 2x2 block has a nonnegative trace and det. + EXPECT_GT(D[0][0], 0.0); + EXPECT_GE(D[1][1] + D[2][2], 0.0); + EXPECT_GE(D[1][1] * D[2][2] - D[1][2] * D[2][1], -1e-9 * D[1][1] * D[2][2]); + // dH/dp = v: the Hamiltonian's gradient in the momenta is the twist. + EXPECT_NEAR(diff::dyn_ph_hamiltonian(p, s), 0.5 * (pm[0] * s.vx + pm[1] * s.vy + pm[2] * s.r), + 1e-9 * (1 + pm[0] * s.vx)); + } +} + +// One short dyn_plant step's momentum change against the port-Hamiltonian vector field, with the +// actuators holding (commands equal to their state) so u is the state's own. +TEST(DynBikePH, TheVectorFieldIsThePlantsInTheLinearRegime) { + const dyn_bike_params p = humvee(); + bike_veh lim = plant_limits(p); + lim.hdt = 1e-4f; + std::mt19937 rng(5); + double worst = 0.0; + for (int k = 0; k < 300; ++k) { + const dyn_state s = linear_state(rng, p); + const double grade = (k % 3) * 0.02; + dyn_state o; + diff::dyn_plant(p, lim, s, s.delta, s.a, grade, o); + double p0[3], p1[3], f[3]; + diff::dyn_ph_momentum(p, s, p0); + diff::dyn_ph_momentum(p, o, p1); + diff::dyn_ph_rhs(p, s, grade, f); + // Relative to the forces' scale on each axis (m g on the momenta, m g L on the yaw). + const double scale[3] = {p.m * p.g, p.m * p.g, p.m * p.g * (p.lf + p.lr)}; + for (int i = 0; i < 3; ++i) { + const double fd = (p1[i] - p0[i]) / lim.hdt; + const double rel = std::fabs(fd - f[i]) / scale[i]; + worst = std::max(worst, rel); + EXPECT_LT(rel, 0.01) << "axis " << i << ": plant " << fd << ", pH " << f[i] << " at vx " + << s.vx << ", delta " << s.delta; + } + } + std::printf(" vector field vs dyn_plant, linear regime: worst |dp/dt - pH| = %.2e m g\n", worst); +} + +// The gate: along dyn_plant's own trajectories in the linear-tire regime, the change in H is the +// supplied energy minus the dissipated one, Delta H = -int v^T D v + int y^T u (- int m g grade +// vx). The actuators lag as they do in training; u is their output. Drive, brake and a weaving +// steer at about 0.1-0.15 g, flat and on a 3% grade. The residual is the linearization's (the +// Pacejka curve against its slope at zero slip, sin delta and cos delta against 0 and 1, which +// put F_y sin delta on vx): it falls with the steering amplitude, and the integrator's step adds +// nothing measurable to it (implicit Euler's own dissipation is on the small lateral energy). +TEST(DynBikePH, EnergyBalanceInTheLinearRegime) { + const dyn_bike_params p = humvee(); + const auto worst_at = [&](double hdt, double steer, bool verbose) { + bike_veh lim = plant_limits(p); + lim.hdt = static_cast(hdt); + double worst = 0.0; + for (int run = 0; run < 4; ++run) { + const double grade = run == 3 ? 0.03 : 0.0; + dyn_state s = at_rest(10.0 + 2.0 * run); + const int n = static_cast(std::lround(8.0 / hdt)); + double supplied = 0.0, dissipated = 0.0, gravity = 0.0, flow = 0.0; + const double h0 = diff::dyn_ph_hamiltonian(p, s); + for (int t = 0; t < n; ++t) { + const double time = t * hdt; + const double dcmd = steer * (1.0 + 0.25 * run) * std::sin(kTwoPi * time / (2.5 + run)); + const double acmd = 1.2 * std::sin(kTwoPi * time / 4.0 + run); + dyn_state o; + diff::dyn_plant(p, lim, s, dcmd, acmd, grade, o); + // The step's power by the trapezoid between its ends, with the input it applied (the + // actuators' output, o's delta and a) at both. + dyn_state s_in = s; + s_in.delta = o.delta; + s_in.a = o.a; + double d0, u0, d1, u1; + diff::dyn_ph_power(p, s_in, d0, u0); + diff::dyn_ph_power(p, o, d1, u1); + const double ds = 0.5 * hdt * (d0 + d1), us = 0.5 * hdt * (u0 + u1); + const double gs = 0.5 * hdt * p.m * p.g * grade * (s.vx + o.vx); + dissipated += ds; + supplied += us; + gravity += gs; + flow += std::fabs(ds) + std::fabs(us) + std::fabs(gs); + s = o; + } + const double dH = diff::dyn_ph_hamiltonian(p, s) - h0; + const double resid = std::fabs(dH - (supplied - dissipated - gravity)) / flow; + worst = std::max(worst, resid); + if (verbose) + std::printf(" run %d: dH %.4g J, supplied %.4g, dissipated %.4g, gravity %.4g, " + "residual %.2e of the flow\n", + run, dH, supplied, dissipated, gravity, resid); + } + return worst; + }; + for (const double hdt : {0.06, 0.02, 0.01, 0.005}) { + const double w = worst_at(hdt, 0.02, hdt == 0.01); + std::printf(" energy balance at hdt %.3f s, steer 0.02-0.035 rad: worst |dH - (supplied - " + "dissipated)| / flow = %.3e\n", + hdt, w); + EXPECT_LT(w, 0.02) << "hdt " << hdt; + } + double prev = 0.0; + for (const double steer : {0.04, 0.02, 0.01, 0.005}) { + const double w = worst_at(0.01, steer, false); + std::printf(" energy balance at hdt 0.010 s, steer x%.3f: %.3e\n", steer, w); + if (prev > 0.0) + EXPECT_LT(w, 0.6 * prev) << "the residual does not fall with the steering amplitude"; + prev = w; + } +} + +// The report: the tires' tangent dissipation (dyn_ph_tangent_D) along steer ramps to the lock and +// swerves at several speeds, binned by lateral g like the plant's drift (D14). Below the peak it +// is positive semi-definite; past the Pacejka peak a tire's force falls as its slip grows and the +// tangent D loses definiteness. The bins say where. With the tires fitted to Jolt's (B C = +// kJoltTireStiffness / tire_peak_slip_angle) the Humvee understeers: its front tire passes the +// peak first, from about half its grip, and the ramps plateau short of mu g. +TEST(DynBikePH, WhereTheTangentDissipationStopsBeingPSD) { + for (const double mu : {0.9, 0.6}) { + const dyn_bike_params p = humvee(mu); + bike_veh lim = plant_limits(p); + lim.hdt = 0.01f; + const double edges[] = {0.0, 0.1, 0.2, 0.3, 0.5, 0.7, 0.8, 0.9, 1e9}; + const int nb = 8; + std::vector count(nb, 0), indef(nb, 0), front(nb, 0), rear(nb, 0); + std::vector lam_min(nb, 1e300); + const auto record = [&](const dyn_state &s) { + const double g = std::fabs(lateral_g(p, s)); + int b = 0; + while (b + 1 < nb && g >= edges[b + 1]) + ++b; + double Dt[2][2], kf, kr, af, ar; + diff::dyn_ph_tangent_D(p, s, Dt, kf, kr, af, ar); + const double tr = Dt[0][0] + Dt[1][1], det = Dt[0][0] * Dt[1][1] - Dt[0][1] * Dt[1][0]; + const double lmin = 0.5 * tr - std::sqrt(std::max(0.0, 0.25 * tr * tr - det)); + ++count[b]; + indef[b] += lmin < 0.0; + front[b] += kf < 0.0; + rear[b] += kr < 0.0; + lam_min[b] = std::min(lam_min[b], lmin / (p.m / 1.0)); + }; + for (const double v : {8.0, 12.0, 16.0, 20.0}) { + // A steady ramp of the steer to the lock, the speed held by the throttle: past the limit. + dyn_state s = at_rest(v); + const int n = static_cast(25.0 / lim.hdt); + for (int t = 0; t < n; ++t) { + const double dcmd = lim.delta_max * t / double(n); + const double acmd = 2.0 * (v - s.vx) + p.drag * s.vx * s.vx / p.m + p.c_rr * p.g; + dyn_state o; + diff::dyn_plant(p, lim, s, dcmd, acmd, 0.0, o); + s = o; + record(s); + } + // A swerve: a 0.2 rad steer sine at 0.6 Hz, coasting. + s = at_rest(v); + for (int t = 0; t < static_cast(6.0 / lim.hdt); ++t) { + const double dcmd = 0.2 * std::sin(kTwoPi * 0.6 * t * lim.hdt); + dyn_state o; + diff::dyn_plant(p, lim, s, dcmd, 0.0, 0.0, o); + s = o; + record(s); + } + } + std::printf(" tangent dissipation by lateral g (Humvee, mu %.1f):\n", mu); + std::printf(" %-10s %8s %10s %8s %8s %14s\n", "g", "samples", "indefinite", "front", "rear", + "min lambda/m"); + for (int b = 0; b < nb; ++b) { + if (!count[b]) + continue; + char lab[32]; + if (edges[b + 1] > 1e8) + std::snprintf(lab, sizeof lab, ">= %.1f", edges[b]); + else + std::snprintf(lab, sizeof lab, "%.1f-%.1f", edges[b], edges[b + 1]); + std::printf(" %-10s %8d %9.1f%% %7.1f%% %7.1f%% %14.3g\n", lab, count[b], + 100.0 * indef[b] / count[b], 100.0 * front[b] / count[b], + 100.0 * rear[b] / count[b], lam_min[b]); + // Below half the grip both tires are short of their peak: PSD everywhere. + if (edges[b + 1] <= 0.5 * mu + 1e-9) + EXPECT_EQ(indef[b], 0) << "mu " << mu << ", bin " << lab; + // Understeer: the rear tire never passes its peak. + EXPECT_EQ(rear[b], 0) << "mu " << mu << ", bin " << lab; + } + // Above half the grip the ramps take the front tire past its peak: the tangent D is + // indefinite there. + int top = 0; + for (int b = 0; b < nb; ++b) + if (edges[b] >= 0.5 * mu - 1e-9) + top += indef[b]; + EXPECT_GT(top, 0) << "mu " << mu << ": the ramps never passed the peak"; + } +}