Turn auto trim into a fly-by-wire flight computer
Pitch input becomes a rate command; the computer measures the pitching torque without its own surfaces (airframe stability, thrust line, weight offset) and cancels it with dynamic inversion, so relaxed-stability craft stay controllable. Replaces trimAuthority with flightComputer* config values. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YVREx3uo7nz1FEhzveWTUh
This commit is contained in:
6 files changed
+114
-47
No files matched your search
@@ -123,12 +123,31 @@ public final class Config {
|
||||
.comment("Improved flight model: multiplier for the extra lift the vortices make at high angles of attack")
|
||||
.defineInRange("vortexLiftStrength", 1.0D, 0.0D, 3.0D);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue TRIM_AUTHORITY = BUILDER
|
||||
.comment("Auto trim: largest pitch input (fraction of full deflection) the trim may hold")
|
||||
.defineInRange("trimAuthority", 0.5D, 0.0D, 1.0D);
|
||||
public static final ForgeConfigSpec.DoubleValue FBW_AUTHORITY = BUILDER
|
||||
.comment("Flight computer (auto trim): largest total pitch input, as a fraction of full deflection, the computer may use.",
|
||||
"A craft with strongly relaxed stability needs close to 1")
|
||||
.defineInRange("flightComputerAuthority", 1.0D, 0.0D, 1.0D);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue FBW_RATE_GAIN = BUILDER
|
||||
.comment("Flight computer: how hard it corrects a pitch rate that differs from the command, in rad/s² of angular",
|
||||
"acceleration per rad/s of error. Higher holds the nose tighter, too high makes slow surfaces oscillate")
|
||||
.defineInRange("flightComputerRateGain", 8.0D, 0.5D, 40.0D);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue FBW_INTEGRAL_GAIN = BUILDER
|
||||
.comment("Flight computer: integral gain that removes the steady error left by the torque model")
|
||||
.defineInRange("flightComputerIntegralGain", 4.0D, 0.0D, 40.0D);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue FBW_STICK_RATE_DEGREES = BUILDER
|
||||
.comment("Flight computer: pitch rate in degrees per second the pilot commands with full pitch input")
|
||||
.defineInRange("flightComputerStickRateDegrees", 40.0D, 5.0D, 120.0D);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue FBW_ACTUATOR_RATE = BUILDER
|
||||
.comment("Flight computer: fastest the computer moves the pitch surfaces, in fraction of full deflection per second.",
|
||||
"Unstable craft need it high enough to catch a divergence, 4 or more")
|
||||
.defineInRange("flightComputerActuatorRate", 6.0D, 0.5D, 50.0D);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue TRIM_MIN_SPEED = BUILDER
|
||||
.comment("Auto trim: below this airspeed (blocks per second) the trim holds its current value")
|
||||
.comment("Flight computer (auto trim): below this airspeed (blocks per second) the computer holds its current output")
|
||||
.defineInRange("trimMinSpeed", 10.0D, 0.0D, 100.0D);
|
||||
|
||||
static {
|
||||
|
||||
@@ -50,7 +50,11 @@ public final class ConfigCommands {
|
||||
new Option<>("vortex_lift", Config.VORTEX_LIFT, BoolArgumentType.bool(), Boolean.class),
|
||||
new Option<>("vortex_stall_bonus", Config.VORTEX_STALL_BONUS, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("vortex_lift_strength", Config.VORTEX_LIFT_STRENGTH, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("trim_authority", Config.TRIM_AUTHORITY, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("flight_computer_authority", Config.FBW_AUTHORITY, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("flight_computer_rate_gain", Config.FBW_RATE_GAIN, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("flight_computer_integral_gain", Config.FBW_INTEGRAL_GAIN, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("flight_computer_stick_rate_degrees", Config.FBW_STICK_RATE_DEGREES, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("flight_computer_actuator_rate", Config.FBW_ACTUATOR_RATE, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("trim_min_speed", Config.TRIM_MIN_SPEED, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("air_placeholder", Config.ENABLE_AIR_PLACEHOLDER, BoolArgumentType.bool(), Boolean.class),
|
||||
new Option<>("nozzle_max_deflection", Config.NOZZLE_MAX_DEFLECTION, DoubleArgumentType.doubleArg(), Double.class),
|
||||
|
||||
@@ -19,7 +19,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
* to; at the end of the tick that set is handed to the physics thread, which computes the forces from the ship's
|
||||
* motion in each substep until the next tick arrives.
|
||||
* <p>
|
||||
* The physics side also smooths pilot input, runs auto trim, and integrates damping implicitly so stiff damping
|
||||
* The physics side also smooths pilot input, runs the flight computer (auto trim), and integrates damping implicitly so stiff damping
|
||||
* cannot overshoot.
|
||||
*/
|
||||
public final class AeroState implements PhysicsBridge.Publishable {
|
||||
@@ -55,6 +55,8 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
double roll;
|
||||
double trimIntegral;
|
||||
double trimOutput;
|
||||
// Total pitch input the flight computer applied last substep, pilot and trim together
|
||||
double computerOutput;
|
||||
// Torque (ship axes) one unit of pitch input on this node's pitch surfaces makes, measured every substep
|
||||
final Vector3d pitchResponse = new Vector3d();
|
||||
final Vector3d pitchResponseBuilding = new Vector3d();
|
||||
@@ -79,8 +81,9 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
yaw = data.yaw();
|
||||
roll = data.roll();
|
||||
trimIntegral = data.autoTrim() ? data.storedTrim() : 0.0;
|
||||
trimOutput = trimIntegral;
|
||||
trimOutput = data.autoTrim() ? 0.0 : trimIntegral;
|
||||
savedTrim = trimIntegral;
|
||||
computerOutput = pitch + trimOutput;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,6 +316,8 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
private void updateNodes(Frame frame, Vector3dc worldVelocity, double altitude, Quaterniondc rotation, Matrix3dc inertia,
|
||||
double[] w, double dt, boolean improved) {
|
||||
double step = Config.CONTROL_SMOOTHING.get() ? INPUT_RATE_PER_TICK * clock.weight() : Double.POSITIVE_INFINITY;
|
||||
// Torque of the last substep, in ship axes, which the flight computer separates from its own surfaces' share
|
||||
double[] torque = {wrench.totalTorque(0), wrench.totalTorque(1), wrench.totalTorque(2)};
|
||||
for (NodeData node : frame.nodes.values()) {
|
||||
NodeState state = nodeStates.get(node.key());
|
||||
if (state == null) {
|
||||
@@ -323,7 +328,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
state.pitch = moveTowards(state.pitch, effective.pitch(), step);
|
||||
state.yaw = moveTowards(state.yaw, effective.yaw(), step);
|
||||
state.roll = moveTowards(state.roll, effective.roll(), step);
|
||||
AutoTrim.update(state, effective, worldVelocity, altitude, rotation, inertia, w, dt, improved && wingStalled);
|
||||
AutoTrim.update(state, effective, worldVelocity, altitude, rotation, inertia, w, torque, dt, improved && wingStalled);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,19 +7,28 @@ import org.joml.Vector3d;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
/**
|
||||
* Auto trim for the pitch control surfaces of one control node: while the pilot gives no pitch input it holds the
|
||||
* altitude the craft had when the pilot let go, cancelling whatever pitching moment the craft has (off-center weight,
|
||||
* thrust line, speed changes).
|
||||
* The flight computer of one control node, switched on and off with the auto trim key. It is a fly-by-wire pitch
|
||||
* system: the pilot's pitch input is a pitch rate command, not a surface deflection, and the computer moves the
|
||||
* surfaces to make the craft follow it, whatever the airframe does on its own.
|
||||
* <p>
|
||||
* Three nested loops: altitude error sets a gentle climb rate, climb rate error sets a nose attitude (never more than
|
||||
* {@link #MAX_ATTITUDE} from level), attitude error sets a pitch rate, and a PI controller moves the trim to reach
|
||||
* that rate. Bounding the attitude matters: in Warium's physics the flight path follows the nose only slowly, so
|
||||
* steering the flight path directly with pitch rate keeps rotating the nose until the craft dives.
|
||||
* Every physics substep it measures the pitching torque the craft makes with the pitch surfaces taken out
|
||||
* ({@code external}: weight offset, thrust line, and above all the airframe's own stability or instability), and the
|
||||
* torque one unit of pitch input makes ({@code response}). It then picks the input that makes the craft's total
|
||||
* pitch torque equal the angular acceleration it wants, {@code rateGain * (rate command - pitch rate)} plus a small
|
||||
* integral for what the model gets wrong. That cancels a craft that is statically unstable, or trimmed badly, as
|
||||
* fast as the surfaces can move, so a jet built with relaxed stability can be flown like a stable one.
|
||||
* <p>
|
||||
* Attitude is measured from the ship's own up axis, so it assumes the craft is built level, as Warium craft are. The
|
||||
* trim's effect is measured every substep (torque per unit of pitch input), which gives the pitch axis and lets the
|
||||
* inner gains be expressed as angular acceleration, so the same tuning works for any craft, speed and altitude.
|
||||
* The trim holds still while the pilot pitches, when too slow, and when wings stall, so it never fights maneuvers.
|
||||
* Without pilot input the rate command comes from three nested loops that hold the altitude the craft had when the
|
||||
* pilot let go: altitude error sets a gentle climb rate, climb rate error sets a nose attitude (never more than
|
||||
* {@link #MAX_ATTITUDE} from level), attitude error sets a pitch rate. Bounding the attitude matters: in Warium's
|
||||
* physics the flight path follows the nose only slowly, so steering the flight path directly with pitch rate keeps
|
||||
* rotating the nose until the craft dives.
|
||||
* <p>
|
||||
* Attitude is measured from the ship's own up axis, so it assumes the craft is built level, as Warium craft are.
|
||||
* Torques are measured per unit of input, so the same tuning works for any craft, speed and altitude. Below the
|
||||
* minimum speed the computer holds its output, the surfaces have no authority there anyway. Stalled wings cut the
|
||||
* pilot's rate command and the altitude hold, so the computer only keeps the nose where it is. While cursor aim flies
|
||||
* the node, that is in charge instead.
|
||||
*/
|
||||
final class AutoTrim {
|
||||
// Climb rate (blocks/s) per block of altitude error, and its limit
|
||||
@@ -32,12 +41,16 @@ final class AutoTrim {
|
||||
// Pitch rate (rad/s) per radian of attitude error, and its limit
|
||||
private static final double ATTITUDE_GAIN = 1.0;
|
||||
private static final double MAX_PITCH_RATE = 0.15;
|
||||
// Angular acceleration per rad/s of pitch rate error, and integral gain
|
||||
private static final double PROPORTIONAL_GAIN = 3.0;
|
||||
private static final double INTEGRAL_GAIN = 1.5;
|
||||
// Fastest the trim moves, in input per second
|
||||
// Slow trim movement when the computer is switched off
|
||||
private static final double TRIM_RATE = 1.0;
|
||||
private static final double PILOT_DEADBAND = 0.01;
|
||||
// The computer assumes the surfaces do this much of what was measured, so a response measured too high (stalled
|
||||
// surfaces, clamped inputs) makes it command a little too much rather than too little, which stays stable
|
||||
private static final double RESPONSE_MARGIN = 0.7;
|
||||
// Pitch rate command while stalled, as a fraction of what the pilot asked
|
||||
private static final double STALLED_COMMAND = 0.3;
|
||||
// The integral may make up for this much rate error worth of acceleration
|
||||
private static final double MAX_ERROR_FOR_INTEGRAL = 0.5;
|
||||
|
||||
private AutoTrim() {
|
||||
}
|
||||
@@ -49,29 +62,29 @@ final class AutoTrim {
|
||||
}
|
||||
|
||||
static void update(AeroState.NodeState state, NodeData node, Vector3dc worldVelocity, double altitude,
|
||||
Quaterniondc rotation, Matrix3dc inertia, double[] w, double dt, boolean stalled) {
|
||||
Quaterniondc rotation, Matrix3dc inertia, double[] w, double[] torque, double dt, boolean stalled) {
|
||||
Targets targets = state.trimTargets;
|
||||
boolean aimed = CursorAimRegistry.isActive(node.key());
|
||||
if (!node.autoTrim()) {
|
||||
state.trimIntegral = 0.0;
|
||||
state.trimOutput = AeroState.moveTowards(state.trimOutput, 0.0, TRIM_RATE * dt);
|
||||
state.savedTrim = 0.0;
|
||||
state.computerOutput = state.pitch + state.trimOutput;
|
||||
targets.altitude = Double.NaN;
|
||||
targets.attitudeIntegral = 0.0;
|
||||
return;
|
||||
}
|
||||
double authority = Config.TRIM_AUTHORITY.get();
|
||||
double response = state.pitchResponse.length();
|
||||
double speed = worldVelocity.length();
|
||||
boolean hold = Math.abs(node.pitch()) > PILOT_DEADBAND || speed < Config.TRIM_MIN_SPEED.get() || stalled
|
||||
|| response < 1.0E-6;
|
||||
if (hold || Double.isNaN(targets.altitude)) {
|
||||
// Hold whatever altitude the craft is at once the pilot lets go
|
||||
targets.altitude = altitude;
|
||||
if (aimed || speed < Config.TRIM_MIN_SPEED.get() || response < 1.0E-6) {
|
||||
// Cursor aim, no airflow or no pitch surfaces: the computer holds what it has
|
||||
targets.altitude = Double.NaN;
|
||||
targets.attitudeIntegral = 0.0;
|
||||
state.computerOutput = state.pitch + state.trimOutput;
|
||||
state.savedTrim = state.trimIntegral;
|
||||
return;
|
||||
}
|
||||
|
||||
double target = state.trimIntegral;
|
||||
if (!hold) {
|
||||
// Pitch axis in ship axes, oriented so that positive rotation is what more pitch input does
|
||||
Vector3d axis = state.pitchResponse.div(response, new Vector3d());
|
||||
double axisInertia = axis.dot(inertia.transform(axis, new Vector3d()));
|
||||
@@ -82,24 +95,50 @@ final class AutoTrim {
|
||||
Vector3d direction = worldVelocity.normalize(new Vector3d());
|
||||
double noseUp = Math.signum(worldAxis.cross(direction, new Vector3d()).y);
|
||||
double attitude = attitude(axis, rotation);
|
||||
if (noseUp == 0.0 || Double.isNaN(attitude)) {
|
||||
return;
|
||||
}
|
||||
|
||||
double pilot = node.pitch();
|
||||
boolean pilotActive = Math.abs(pilot) > PILOT_DEADBAND;
|
||||
double rateCommand;
|
||||
if (pilotActive || stalled || noseUp == 0.0 || Double.isNaN(attitude)) {
|
||||
// Hold whatever altitude the craft is at once the pilot lets go
|
||||
targets.altitude = altitude;
|
||||
targets.attitudeIntegral = 0.0;
|
||||
rateCommand = pilotActive
|
||||
? pilot * Math.toRadians(Config.FBW_STICK_RATE_DEGREES.get()) * (stalled ? STALLED_COMMAND : 1.0)
|
||||
: 0.0;
|
||||
} else {
|
||||
if (Double.isNaN(targets.altitude)) {
|
||||
targets.altitude = altitude;
|
||||
}
|
||||
double climbTarget = clamp(ALTITUDE_GAIN * (targets.altitude - altitude), MAX_CLIMB_RATE);
|
||||
double climbError = climbTarget - worldVelocity.y();
|
||||
targets.attitudeIntegral = clamp(targets.attitudeIntegral + CLIMB_INTEGRAL_GAIN * climbError * dt, MAX_ATTITUDE);
|
||||
// Nose elevation wanted, turned into rotation about the axis
|
||||
double attitudeTarget = noseUp * clamp(targets.attitudeIntegral + CLIMB_GAIN * climbError, MAX_ATTITUDE);
|
||||
double rateTarget = clamp(ATTITUDE_GAIN * (attitudeTarget - attitude), MAX_PITCH_RATE);
|
||||
|
||||
double error = rateTarget - pitchRate;
|
||||
// Pitch input needed per rad/s² of angular acceleration
|
||||
double gain = axisInertia / response;
|
||||
state.trimIntegral = clamp(state.trimIntegral + INTEGRAL_GAIN * error * dt * gain, authority);
|
||||
target = clamp(state.trimIntegral + PROPORTIONAL_GAIN * error * gain, authority);
|
||||
rateCommand = clamp(ATTITUDE_GAIN * (attitudeTarget - attitude), MAX_PITCH_RATE);
|
||||
}
|
||||
state.trimOutput = AeroState.moveTowards(state.trimOutput, target, TRIM_RATE * dt);
|
||||
|
||||
// Pitching torque about the axis the pitch surfaces are not making: total of the last substep minus what the
|
||||
// input of that substep made
|
||||
double total = axis.x * torque[0] + axis.y * torque[1] + axis.z * torque[2];
|
||||
double external = total - response * state.computerOutput;
|
||||
|
||||
double authority = Config.FBW_AUTHORITY.get();
|
||||
double rateError = rateCommand - pitchRate;
|
||||
double acceleration = Config.FBW_RATE_GAIN.get() * rateError + state.trimIntegral;
|
||||
double wanted = (axisInertia * acceleration - external) / (response * RESPONSE_MARGIN);
|
||||
boolean saturated = Math.abs(wanted) > authority;
|
||||
wanted = clamp(wanted, authority);
|
||||
if (!saturated) {
|
||||
// Integral of the acceleration, bounded so it cannot run away if the model is far off
|
||||
double limit = Config.FBW_RATE_GAIN.get() * MAX_ERROR_FOR_INTEGRAL;
|
||||
state.trimIntegral = clamp(state.trimIntegral + Config.FBW_INTEGRAL_GAIN.get() * rateError * dt, limit);
|
||||
}
|
||||
|
||||
double applied = AeroState.moveTowards(state.computerOutput, wanted, Config.FBW_ACTUATOR_RATE.get() * dt);
|
||||
state.computerOutput = applied;
|
||||
// The surfaces get the pilot's smoothed input plus the trim, so the trim is what makes up the difference
|
||||
state.trimOutput = applied - state.pitch;
|
||||
state.savedTrim = state.trimIntegral;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"key.categories.improved_aerodynamics": "Improved Aerodynamics",
|
||||
"key.improved_aerodynamics.toggle_auto_trim": "Automatische Trimmung umschalten",
|
||||
"message.improved_aerodynamics.auto_trim.on": "Automatische Trimmung an",
|
||||
"message.improved_aerodynamics.auto_trim.off": "Automatische Trimmung aus",
|
||||
"message.improved_aerodynamics.auto_trim.on": "Flugcomputer an (automatische Trimmung)",
|
||||
"message.improved_aerodynamics.auto_trim.off": "Flugcomputer aus (automatische Trimmung)",
|
||||
"message.improved_aerodynamics.auto_trim.unavailable": "Automatische Trimmung an, sie braucht aber physicsRateAerodynamics in der Serverkonfiguration",
|
||||
"message.improved_aerodynamics.auto_trim.no_seat": "Setz dich in einen Warium-Steuersitz, um die automatische Trimmung zu nutzen",
|
||||
"message.improved_aerodynamics.auto_trim.no_node": "Dieser Sitz ist mit keinem Vehicle Control Node verbunden",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"key.categories.improved_aerodynamics": "Improved Aerodynamics",
|
||||
"key.improved_aerodynamics.toggle_auto_trim": "Toggle auto trim",
|
||||
"message.improved_aerodynamics.auto_trim.on": "Auto trim on",
|
||||
"message.improved_aerodynamics.auto_trim.off": "Auto trim off",
|
||||
"message.improved_aerodynamics.auto_trim.on": "Flight computer on (auto trim)",
|
||||
"message.improved_aerodynamics.auto_trim.off": "Flight computer off (auto trim)",
|
||||
"message.improved_aerodynamics.auto_trim.unavailable": "Auto trim on, but it needs physicsRateAerodynamics enabled in the server config",
|
||||
"message.improved_aerodynamics.auto_trim.no_seat": "Sit in a Warium control seat to use auto trim",
|
||||
"message.improved_aerodynamics.auto_trim.no_node": "This seat is not linked to a vehicle control node",
|
||||
|
||||
Reference in new issue
Block a user