diff --git a/core/src/net/sf/openrocket/simulation/AbstractEulerStepper.java b/core/src/net/sf/openrocket/simulation/AbstractEulerStepper.java index 703ad0d72..71739e9d4 100644 --- a/core/src/net/sf/openrocket/simulation/AbstractEulerStepper.java +++ b/core/src/net/sf/openrocket/simulation/AbstractEulerStepper.java @@ -4,6 +4,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.SimulationAbort; import net.sf.openrocket.models.atmosphere.AtmosphericConditions; import net.sf.openrocket.rocketcomponent.InstanceMap; import net.sf.openrocket.rocketcomponent.RecoveryDevice; @@ -38,51 +39,55 @@ public abstract class AbstractEulerStepper extends AbstractSimulationStepper { public void step(SimulationStatus status, double maxTimeStep) throws SimulationException { // Get the atmospheric conditions - AtmosphericConditions atmosphere = modelAtmosphericConditions(status); + final AtmosphericConditions atmosphere = modelAtmosphericConditions(status); //// Local wind speed and direction - Coordinate windSpeed = modelWindVelocity(status); + final Coordinate windSpeed = modelWindVelocity(status); Coordinate airSpeed = status.getRocketVelocity().add(windSpeed); // Compute drag force - double mach = airSpeed.length() / atmosphere.getMachSpeed(); - double dynP = (0.5 * atmosphere.getDensity() * airSpeed.length2()); - double dragForce = getCD() * dynP * status.getConfiguration().getReferenceArea(); + final double mach = airSpeed.length() / atmosphere.getMachSpeed(); + final double CdA = getCD() * status.getConfiguration().getReferenceArea(); + final double dragForce = 0.5 * CdA * atmosphere.getDensity() * airSpeed.length2(); - double rocketMass = calculateStructureMass(status).getMass(); - double motorMass = calculateMotorMass(status).getMass(); + final double rocketMass = calculateStructureMass(status).getMass(); + final double motorMass = calculateMotorMass(status).getMass(); - double mass = rocketMass + motorMass; - + final double mass = rocketMass + motorMass; if (mass < MathUtil.EPSILON) { - throw new SimulationException(trans.get("SimulationStepper.error.totalMassZero")); + status.abortSimulation(SimulationAbort.Cause.ACTIVE_MASS_ZERO); } // Compute drag acceleration - Coordinate linearAcceleration; - if (airSpeed.length() > 0.001) { - linearAcceleration = airSpeed.normalize().multiply(-dragForce / mass); - } else { - linearAcceleration = Coordinate.NUL; - } + Coordinate linearAcceleration = airSpeed.normalize().multiply(-dragForce / mass); // Add effect of gravity - double gravity = modelGravity(status); + final double gravity = modelGravity(status); linearAcceleration = linearAcceleration.sub(0, 0, gravity); // Add coriolis acceleration - Coordinate coriolisAcceleration = status.getSimulationConditions().getGeodeticComputation().getCoriolisAcceleration( + final Coordinate coriolisAcceleration = status.getSimulationConditions().getGeodeticComputation().getCoriolisAcceleration( status.getRocketWorldPosition(), status.getRocketVelocity()); linearAcceleration = linearAcceleration.add(coriolisAcceleration); // Select tentative time step double timeStep = RECOVERY_TIME_STEP; - // adjust based on change in acceleration (ie jerk) - final double jerk = Math.abs(linearAcceleration.sub(status.getRocketAcceleration()).multiply(1.0/status.getPreviousTimeStep()).length()); - if (jerk > MathUtil.EPSILON) { - timeStep = Math.min(timeStep, 1.0/jerk); + // adjust based on acceleration + final double absAccel = linearAcceleration.length(); + if (absAccel > MathUtil.EPSILON) { + timeStep = Math.min(timeStep, 1.0/absAccel); + } + + // Honor max step size passed in. If the time to next time step is greater than our minimum + // we'll set our next step to just before it in order to better capture discontinuities in things like chute opening + if (maxTimeStep < timeStep) { + if (maxTimeStep > MIN_TIME_STEP) { + timeStep = maxTimeStep - MIN_TIME_STEP; + } else { + timeStep = maxTimeStep; + } } // but don't let it get *too* small @@ -90,33 +95,72 @@ public abstract class AbstractEulerStepper extends AbstractSimulationStepper { log.trace("timeStep is " + timeStep); // Perform Euler integration - Coordinate newPosition = status.getRocketPosition().add(status.getRocketVelocity().multiply(timeStep)). - add(linearAcceleration.multiply(MathUtil.pow2(timeStep) / 2)); + EulerValues newVals = eulerIntegrate(status.getRocketPosition(), status.getRocketVelocity(), linearAcceleration, timeStep); - // If I've hit the ground, recalculate time step and position - if (newPosition.z < 0) { + // Check to see if z or either of its first two derivatives have changed sign and recalculate + // time step to point of change if so + // z -- ground hit + // z' -- apogee + // z'' -- possible oscillation building up in descent rate + // Note that it's virtually impossible for apogee to occur on the same + // step as either ground hit or descent rate inflection, and if we get a ground hit + // any descent rate inflection won't matter + final double a = linearAcceleration.z; + final double v = status.getRocketVelocity().z; + final double z = status.getRocketPosition().z; + double t = timeStep; + if (newVals.pos.z < 0) { + // If I've hit the ground, the new timestep is the solution of + // 1/2 at^2 + vt + z = 0 + t = (-v - Math.sqrt(v*v - 2*a*z))/a; + log.trace("ground hit changes timeStep to " + t); + } else if (v * newVals.vel.z < 0) { + // If I've got apogee, the new timestep is the solution of + // v + at = 0 + t = Math.abs(v / a); + log.trace("apogee changes timeStep to " + t); + } else { + // Use jerk to estimate accleration at end of time step. Don't really need to redo all the atmospheric + // calculations to get it "right"; this will be close enough for our purposes. + // use chain rule to compute jerk + // dA/dT = dA/dV * dV/dT + final double dFdV = CdA * atmosphere.getDensity() * airSpeed.length(); + final Coordinate dAdV = airSpeed.normalize().multiply(dFdV / mass); + final Coordinate jerk = linearAcceleration.multiply(dAdV); + final Coordinate newAcceleration = linearAcceleration.add(jerk.multiply(timeStep)); - final double a = linearAcceleration.z; - final double v = status.getRocketVelocity().z; - final double z0 = status.getRocketPosition().z; + // Only do this one if acceleration is appreciably different from 0 + if (newAcceleration.z * linearAcceleration.z < -MathUtil.EPSILON) { + // If acceleration oscillation is building up, the new timestep is the solution of + // a + j*t = 0 + t = Math.abs(a / jerk.z); + log.trace("oscillation avoidance changes timeStep to " + t); + } + } + + // once again, make sure new timestep isn't *too* small + t = Math.max(t, MIN_TIME_STEP); - // The new timestep is the solution of - // 1/2 at^2 + vt + z0 = 0 - timeStep = (-v - Math.sqrt(v*v - 2*a*z0))/a; - log.trace("ground hit changes timeStep to " + timeStep); + // recalculate Euler integration for position and velocity if necessary. + if (Math.abs(t - timeStep) > MathUtil.EPSILON) { + timeStep = t; - newPosition = status.getRocketPosition().add(status.getRocketVelocity().multiply(timeStep)). - add(linearAcceleration.multiply(MathUtil.pow2(timeStep) / 2)); + if (maxTimeStep - timeStep < MIN_TIME_STEP) { + timeStep = maxTimeStep; + } - // avoid rounding error in new altitude - newPosition = newPosition.setZ(0); + newVals = eulerIntegrate(status.getRocketPosition(), status.getRocketVelocity(), linearAcceleration, timeStep); + + // If we just landed chop off rounding error + if (Math.abs(newVals.pos.z) < MathUtil.EPSILON) { + newVals.pos = newVals.pos.setZ(0); + } } status.setSimulationTime(status.getSimulationTime() + timeStep); - status.setPreviousTimeStep(timeStep); - status.setRocketPosition(newPosition); - status.setRocketVelocity(status.getRocketVelocity().add(linearAcceleration.multiply(timeStep))); + status.setRocketPosition(newVals.pos); + status.setRocketVelocity(newVals.vel); status.setRocketAcceleration(linearAcceleration); // Update the world coordinate @@ -125,64 +169,84 @@ public abstract class AbstractEulerStepper extends AbstractSimulationStepper { status.setRocketWorldPosition(w); // Store data - FlightDataBranch data = status.getFlightData(); - data.addPoint(); + final FlightDataBranch data = status.getFlightData(); + + // Values looked up or calculated at start of time step + data.setValue(FlightDataType.TYPE_REFERENCE_LENGTH, status.getConfiguration().getReferenceLength()); + data.setValue(FlightDataType.TYPE_REFERENCE_AREA, status.getConfiguration().getReferenceArea()); + data.setValue(FlightDataType.TYPE_WIND_VELOCITY, windSpeed.length()); + data.setValue(FlightDataType.TYPE_AIR_TEMPERATURE, atmosphere.getTemperature()); + data.setValue(FlightDataType.TYPE_AIR_PRESSURE, atmosphere.getPressure()); + data.setValue(FlightDataType.TYPE_SPEED_OF_SOUND, atmosphere.getMachSpeed()); + data.setValue(FlightDataType.TYPE_MACH_NUMBER, mach); + if (status.getSimulationConditions().getGeodeticComputation() != GeodeticComputationStrategy.FLAT) { + data.setValue(FlightDataType.TYPE_CORIOLIS_ACCELERATION, coriolisAcceleration.length()); + } + data.setValue(FlightDataType.TYPE_GRAVITY, gravity); + + data.setValue(FlightDataType.TYPE_DRAG_COEFF, getCD()); + data.setValue(FlightDataType.TYPE_PRESSURE_DRAG_COEFF, getCD()); + data.setValue(FlightDataType.TYPE_FRICTION_DRAG_COEFF, 0); + data.setValue(FlightDataType.TYPE_BASE_DRAG_COEFF, 0); + data.setValue(FlightDataType.TYPE_AXIAL_DRAG_COEFF, getCD()); + data.setValue(FlightDataType.TYPE_THRUST_FORCE, 0); + data.setValue(FlightDataType.TYPE_DRAG_FORCE, dragForce); + + data.setValue(FlightDataType.TYPE_MASS, mass); + data.setValue(FlightDataType.TYPE_MOTOR_MASS, motorMass); + data.setValue(FlightDataType.TYPE_THRUST_WEIGHT_RATIO, 0); + + data.setValue(FlightDataType.TYPE_ACCELERATION_XY, + MathUtil.hypot(linearAcceleration.x, linearAcceleration.y)); + data.setValue(FlightDataType.TYPE_ACCELERATION_Z, linearAcceleration.z); + data.setValue(FlightDataType.TYPE_ACCELERATION_TOTAL, linearAcceleration.length()); + + data.setValue(FlightDataType.TYPE_TIME_STEP, timeStep); + + // Values calculated on this step + data.addPoint(); data.setValue(FlightDataType.TYPE_TIME, status.getSimulationTime()); data.setValue(FlightDataType.TYPE_ALTITUDE, status.getRocketPosition().z); data.setValue(FlightDataType.TYPE_POSITION_X, status.getRocketPosition().x); data.setValue(FlightDataType.TYPE_POSITION_Y, status.getRocketPosition().y); - airSpeed = status.getRocketVelocity().add(windSpeed); - data.setValue(FlightDataType.TYPE_POSITION_XY, MathUtil.hypot(status.getRocketPosition().x, status.getRocketPosition().y)); data.setValue(FlightDataType.TYPE_POSITION_DIRECTION, Math.atan2(status.getRocketPosition().y, status.getRocketPosition().x)); + data.setValue(FlightDataType.TYPE_LATITUDE, status.getRocketWorldPosition().getLatitudeRad()); + data.setValue(FlightDataType.TYPE_LONGITUDE, status.getRocketWorldPosition().getLongitudeRad()); data.setValue(FlightDataType.TYPE_VELOCITY_XY, MathUtil.hypot(status.getRocketVelocity().x, status.getRocketVelocity().y)); - data.setValue(FlightDataType.TYPE_ACCELERATION_XY, - MathUtil.hypot(linearAcceleration.x, linearAcceleration.y)); + data.setValue(FlightDataType.TYPE_VELOCITY_Z, status.getRocketVelocity().z); + data.setValue(FlightDataType.TYPE_VELOCITY_TOTAL, airSpeed.length()); - data.setValue(FlightDataType.TYPE_ACCELERATION_TOTAL, linearAcceleration.length()); - - double Re = airSpeed.length() * + airSpeed = status.getRocketVelocity().add(windSpeed); + final double Re = airSpeed.length() * status.getConfiguration().getLengthAerodynamic() / atmosphere.getKinematicViscosity(); data.setValue(FlightDataType.TYPE_REYNOLDS_NUMBER, Re); - - data.setValue(FlightDataType.TYPE_LATITUDE, status.getRocketWorldPosition().getLatitudeRad()); - data.setValue(FlightDataType.TYPE_LONGITUDE, status.getRocketWorldPosition().getLongitudeRad()); - data.setValue(FlightDataType.TYPE_GRAVITY, gravity); - - if (status.getSimulationConditions().getGeodeticComputation() != GeodeticComputationStrategy.FLAT) { - data.setValue(FlightDataType.TYPE_CORIOLIS_ACCELERATION, coriolisAcceleration.length()); - } - - - data.setValue(FlightDataType.TYPE_VELOCITY_Z, status.getRocketVelocity().z); - data.setValue(FlightDataType.TYPE_ACCELERATION_Z, linearAcceleration.z); - - data.setValue(FlightDataType.TYPE_VELOCITY_TOTAL, airSpeed.length()); - data.setValue(FlightDataType.TYPE_MACH_NUMBER, mach); - - data.setValue(FlightDataType.TYPE_MASS, mass); - data.setValue(FlightDataType.TYPE_MOTOR_MASS, motorMass); - - data.setValue(FlightDataType.TYPE_THRUST_FORCE, 0); - data.setValue(FlightDataType.TYPE_DRAG_FORCE, dragForce); - - data.setValue(FlightDataType.TYPE_WIND_VELOCITY, windSpeed.length()); - data.setValue(FlightDataType.TYPE_AIR_TEMPERATURE, atmosphere.getTemperature()); - data.setValue(FlightDataType.TYPE_AIR_PRESSURE, atmosphere.getPressure()); - data.setValue(FlightDataType.TYPE_SPEED_OF_SOUND, atmosphere.getMachSpeed()); - - data.setValue(FlightDataType.TYPE_TIME_STEP, timeStep); data.setValue(FlightDataType.TYPE_COMPUTATION_TIME, (System.nanoTime() - status.getSimulationStartWallTime()) / 1000000000.0); log.trace("time " + data.getLast(FlightDataType.TYPE_TIME) + ", altitude " + data.getLast(FlightDataType.TYPE_ALTITUDE) + ", velocity " + data.getLast(FlightDataType.TYPE_VELOCITY_Z)); } - + + private static class EulerValues { + /** linear velocity */ + public Coordinate vel; + /** position */ + public Coordinate pos; + } + + private EulerValues eulerIntegrate (Coordinate pos, Coordinate v, Coordinate a, double timeStep) { + EulerValues result = new EulerValues(); + + result.vel = v.add(a.multiply(timeStep)); + result.pos = pos.add(v.multiply(timeStep)).add(a.multiply(MathUtil.pow2(timeStep) / 2.0)); + + return result; + } } diff --git a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java index 821bab500..7bb429012 100644 --- a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java +++ b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java @@ -706,7 +706,6 @@ public class BasicEventSimulationEngine implements SimulationEngine { double d = 0; boolean b = false; d += currentStatus.getSimulationTime(); - d += currentStatus.getPreviousTimeStep(); b |= currentStatus.getRocketPosition().isNaN(); b |= currentStatus.getRocketVelocity().isNaN(); b |= currentStatus.getRocketOrientationQuaternion().isNaN(); @@ -716,7 +715,6 @@ public class BasicEventSimulationEngine implements SimulationEngine { if (Double.isNaN(d) || b) { log.error("Simulation resulted in NaN value:" + " simulationTime=" + currentStatus.getSimulationTime() + - " previousTimeStep=" + currentStatus.getPreviousTimeStep() + " rocketPosition=" + currentStatus.getRocketPosition() + " rocketVelocity=" + currentStatus.getRocketVelocity() + " rocketOrientationQuaternion=" + currentStatus.getRocketOrientationQuaternion() + diff --git a/core/src/net/sf/openrocket/simulation/SimulationStatus.java b/core/src/net/sf/openrocket/simulation/SimulationStatus.java index 0aba10023..0db480fd6 100644 --- a/core/src/net/sf/openrocket/simulation/SimulationStatus.java +++ b/core/src/net/sf/openrocket/simulation/SimulationStatus.java @@ -44,8 +44,6 @@ public class SimulationStatus implements Monitorable { private double time; - private double previousTimeStep; - private Coordinate position; private WorldCoordinate worldPosition; private Coordinate velocity; @@ -105,7 +103,6 @@ public class SimulationStatus implements Monitorable { this.configuration = configuration; this.time = 0; - this.previousTimeStep = this.simulationConditions.getTimeStep(); this.position = this.simulationConditions.getLaunchPosition(); this.velocity = this.simulationConditions.getLaunchVelocity(); this.worldPosition = this.simulationConditions.getLaunchSite(); @@ -178,7 +175,6 @@ public class SimulationStatus implements Monitorable { // FlightData is not cloned. this.flightData = orig.flightData; this.time = orig.time; - this.previousTimeStep = orig.previousTimeStep; this.position = orig.position; this.acceleration = orig.acceleration; this.worldPosition = orig.worldPosition; @@ -267,17 +263,6 @@ public class SimulationStatus implements Monitorable { return flightData; } - public double getPreviousTimeStep() { - return previousTimeStep; - } - - - public void setPreviousTimeStep(double previousTimeStep) { - this.previousTimeStep = previousTimeStep; - this.modID++; - } - - public void setRocketPosition(Coordinate position) { this.position = position; this.modID++; diff --git a/core/src/net/sf/openrocket/util/Coordinate.java b/core/src/net/sf/openrocket/util/Coordinate.java index c6a9f247a..5b87bbccf 100644 --- a/core/src/net/sf/openrocket/util/Coordinate.java +++ b/core/src/net/sf/openrocket/util/Coordinate.java @@ -190,6 +190,16 @@ public final class Coordinate implements Cloneable, Serializable { return new Coordinate(this.x * m, this.y * m, this.z * m, this.weight * m); } + /** + * Multiply the Coordinate with another component-by-component + * + * @param other the other Coordinate + * @return The product. + */ + public Coordinate multiply(Coordinate other) { + return new Coordinate(this.x * other.x, this.y * other.y, this.z * other.z, this.weight * other.weight); + } + /** * Dot product of two Coordinates, taken as vectors. Equal to * x1*x2+y1*y2+z1*z2 diff --git a/core/test/net/sf/openrocket/simulation/FlightEventsTest.java b/core/test/net/sf/openrocket/simulation/FlightEventsTest.java index 4a6818bc4..638e73eae 100644 --- a/core/test/net/sf/openrocket/simulation/FlightEventsTest.java +++ b/core/test/net/sf/openrocket/simulation/FlightEventsTest.java @@ -55,9 +55,9 @@ public class FlightEventsTest extends BaseTestCase { new FlightEvent(FlightEvent.Type.BURNOUT, 2.0, motorMountTube), new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, 2.0, stage), new FlightEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, 2.001, parachute), - new FlightEvent(FlightEvent.Type.APOGEE, 2.48338, rocket), - new FlightEvent(FlightEvent.Type.GROUND_HIT, 43.076, null), - new FlightEvent(FlightEvent.Type.SIMULATION_END, 43.076, null) + new FlightEvent(FlightEvent.Type.APOGEE, 2.48, rocket), + new FlightEvent(FlightEvent.Type.GROUND_HIT, 42.97, null), + new FlightEvent(FlightEvent.Type.SIMULATION_END, 42.97, null) }; checkEvents(expectedEvents, sim, 0); @@ -138,9 +138,9 @@ public class FlightEventsTest extends BaseTestCase { new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, 2.01, centerBooster), new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, 2.01, centerBooster), new FlightEvent(FlightEvent.Type.TUMBLE, 2.85, null), - new FlightEvent(FlightEvent.Type.APOGEE, 1200, rocket), - new FlightEvent(FlightEvent.Type.GROUND_HIT, 1200, null), - new FlightEvent(FlightEvent.Type.SIMULATION_END, 1200, null) + new FlightEvent(FlightEvent.Type.APOGEE, 3.78, rocket), + new FlightEvent(FlightEvent.Type.GROUND_HIT, 9.0, null), + new FlightEvent(FlightEvent.Type.SIMULATION_END, 9.0, null) }; break;