fruit-fly-simulation / src /controller.js
Xenova's picture
Xenova HF Staff
Upload 227 files
cc38b25 verified
Raw History Blame Contribute Delete
5.74 kB
/** Engineered rate decoder. Body units: mm, seconds, radians; +yaw turns left. */
const clamp = (x, lo, hi) => Math.max(lo, Math.min(hi, x));
const relax = (value, target, dt, tau) => value + (target - value) * (1 - Math.exp(-dt / tau));
export function restPose() {
return {
x: 0,
z: 0,
y: 0,
yaw: 0,
velocity: 0,
yawRate: 0,
phase: 0,
time: 0,
pitch: 0,
bank: 0,
wingOpen: 0,
flightBlend: 0,
launch: 0,
landing: 0,
behavior: 'At rest',
};
}
export class FlyController {
constructor() {
this.reset();
}
reset() {
Object.assign(this, restPose());
this.rates = new Float64Array(6);
this.vy = 0;
this.distance = 0;
this.mode = 'ground';
this.escapeAge = 0;
this.landingAge = 0;
this.quietTime = 0;
this.cooldown = 0;
this.escapeArmed = true;
this.takeoffs = 0;
}
advance(input, dt) {
if (
input.length !== 6 ||
!Array.from(input).every(Number.isFinite) ||
!Number.isFinite(dt) ||
dt <= 0 ||
dt > 0.05
)
throw Error('Invalid neural controller input');
for (let i = 0; i < 6; i++)
this.rates[i] = relax(this.rates[i], Math.max(0, input[i]), dt, 0.08);
const [wl, wr, tl, tr, back, escape] = this.rates;
const walking = Math.max(0, (wl + wr) / 2 - 6),
reverse = Math.max(0, back - 25);
const turn = Math.max(0, tl - 15) - Math.max(0, tr - 15);
const groundSpeed = 8 * Math.tanh(walking / 30) - 4 * Math.tanh(reverse / 90);
this.yawRate = relax(this.yawRate, 3.8 * Math.tanh(turn / 45), dt, 0.04);
this.cooldown = Math.max(0, this.cooldown - dt);
if (this.mode === 'ground' && escape < 45) this.escapeArmed = true;
if (this.mode === 'ground' && escape > 100 && this.cooldown === 0 && this.escapeArmed) {
this.mode = 'escape';
this.escapeAge = 0;
this.quietTime = 0;
this.escapeArmed = false;
this.vy = 80;
this.takeoffs++;
}
if (this.mode === 'escape') {
this.escapeAge += dt;
this.quietTime = escape < 45 ? this.quietTime + dt : 0;
// Escape activity sustains flight for 0.35–0.9 neural seconds.
if ((this.escapeAge >= 0.35 && this.quietTime >= 0.06) || this.escapeAge >= 0.9) {
this.mode = 'landing';
this.landingAge = 0;
}
}
const airborne = this.mode !== 'ground';
if (airborne) {
if (this.mode === 'landing') this.landingAge += dt;
const landing = this.mode === 'landing';
const targetHeight = landing ? 0 : 2.2 + 0.8 * clamp(escape / 250, 0, 1);
this.velocity = relax(
this.velocity,
landing ? groundSpeed : 16 + 8 * clamp(escape / 250, 0, 1),
dt,
landing ? 0.12 : 0.06,
);
// Reduced-order wing lift: critically damped altitude control plus gravity.
// Substeps resolve takeoff/ground contact independently of the 10 ms readout.
const parts = Math.ceil(dt / 0.002),
h = dt / parts;
for (let k = 0; k < parts; k++) {
const omega = landing ? 16 : 24;
const lift = clamp(
9810 + omega * omega * (targetHeight - this.y) - 2 * omega * this.vy,
0,
16000,
);
const acceleration = lift - 9810;
this.y = Math.max(0, this.y + this.vy * h + 0.5 * acceleration * h * h);
this.vy += acceleration * h;
if (landing && this.y < 0.025 && this.vy <= 0) {
this.y = 0;
this.vy = 0;
this.mode = 'ground';
this.cooldown = 0.2;
break;
}
}
} else this.velocity = relax(this.velocity, groundSpeed, dt, 0.055);
this.flightBlend = clamp(this.y / 0.65, 0, 1);
this.launch = this.mode === 'escape' ? clamp(1 - this.escapeAge / 0.06, 0, 1) : 0;
this.landing = this.mode === 'landing' ? clamp(this.landingAge / 0.08, 0, 1) : 0;
this.wingOpen = relax(
this.wingOpen,
this.mode === 'ground' ? 0 : 1,
dt,
this.mode === 'ground' ? 0.065 : 0.022,
);
if (this.wingOpen < 0.0001) this.wingOpen = 0;
const pitchTarget =
(this.mode === 'landing' ? -0.13 : this.launch > 0 ? -0.24 : -0.06) * this.flightBlend;
this.pitch = relax(this.pitch, pitchTarget, dt, 0.045);
this.bank = relax(
this.bank,
clamp(-this.yawRate * 0.08, -0.22, 0.22) * this.flightBlend,
dt,
0.05,
);
this.yaw += this.yawRate * dt;
this.x += Math.sin(this.yaw) * this.velocity * dt;
this.z += Math.cos(this.yaw) * this.velocity * dt;
const gaitSpeed = Math.abs(this.velocity) + Math.abs(this.yawRate) * 0.5;
this.phase += (gaitSpeed > 0.06 ? Math.min(14, gaitSpeed / 1.4) * Math.PI * 2 : 0) * dt;
this.distance += Math.abs(this.velocity) * dt;
this.time += dt;
this.behavior =
this.mode === 'landing'
? 'Landing'
: this.mode === 'escape'
? this.escapeAge < 0.08
? 'Taking off'
: 'Escape flight'
: reverse > walking + 5
? 'Walking backward'
: Math.abs(this.yawRate) > 0.3
? this.yawRate > 0
? 'Turning left'
: 'Turning right'
: Math.abs(this.velocity) > 0.15
? 'Walking'
: 'At rest';
return this.pose();
}
pose() {
return {
x: this.x,
z: this.z,
y: this.y,
yaw: this.yaw,
velocity: this.velocity,
yawRate: this.yawRate,
phase: this.phase,
time: this.time,
pitch: this.pitch,
bank: this.bank,
wingOpen: this.wingOpen,
flightBlend: this.flightBlend,
launch: this.launch,
landing: this.landing,
behavior: this.behavior,
};
}
}