diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties index 6826a2b15..a8f7f1f3c 100644 --- a/core/resources/l10n/messages.properties +++ b/core/resources/l10n/messages.properties @@ -1371,6 +1371,7 @@ FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = Recovery device deployment FlightEvent.Type.GROUND_HIT = Ground hit FlightEvent.Type.SIMULATION_END = Simulation end FlightEvent.Type.ALTITUDE = Altitude change +FlightEvent.Type.TUMBLE = Tumbling ! ThrustCurveMotorColumns TCurveMotorCol.MANUFACTURER = Manufacturer diff --git a/core/src/net/sf/openrocket/gui/plot/PlotConfiguration.java b/core/src/net/sf/openrocket/gui/plot/PlotConfiguration.java index fc0b5b8f0..db604a1f2 100644 --- a/core/src/net/sf/openrocket/gui/plot/PlotConfiguration.java +++ b/core/src/net/sf/openrocket/gui/plot/PlotConfiguration.java @@ -36,6 +36,7 @@ public class PlotConfiguration implements Cloneable { config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true); + config.setEvent(FlightEvent.Type.TUMBLE, true); configs.add(config); //// Total motion vs. time @@ -49,6 +50,7 @@ public class PlotConfiguration implements Cloneable { config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true); + config.setEvent(FlightEvent.Type.TUMBLE, true); configs.add(config); //// Flight side profile @@ -60,6 +62,7 @@ public class PlotConfiguration implements Cloneable { config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true); + config.setEvent(FlightEvent.Type.TUMBLE, true); configs.add(config); //// Stability vs. time @@ -73,6 +76,7 @@ public class PlotConfiguration implements Cloneable { config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true); + config.setEvent(FlightEvent.Type.TUMBLE, true); configs.add(config); //// Drag coefficients vs. Mach number @@ -97,6 +101,7 @@ public class PlotConfiguration implements Cloneable { config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true); + config.setEvent(FlightEvent.Type.TUMBLE, true); configs.add(config); //// Angle of attack and orientation vs. time @@ -110,6 +115,7 @@ public class PlotConfiguration implements Cloneable { config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true); + config.setEvent(FlightEvent.Type.TUMBLE, true); configs.add(config); //// Simulation time step and computation time @@ -122,6 +128,7 @@ public class PlotConfiguration implements Cloneable { config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true); + config.setEvent(FlightEvent.Type.TUMBLE, true); configs.add(config); DEFAULT_CONFIGURATIONS = configs.toArray(new PlotConfiguration[0]); diff --git a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java index ff9f4985a..5d6ec0b64 100644 --- a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java +++ b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java @@ -37,6 +37,11 @@ public class BasicEventSimulationEngine implements SimulationEngine { // TODO: MEDIUM: Allow selecting steppers private SimulationStepper flightStepper = new RK4SimulationStepper(); private SimulationStepper landingStepper = new BasicLandingStepper(); + private SimulationStepper tumbleStepper = new BasicTumbleStepper(); + + // Constant holding 30 degress in radians. This is the AOA condition + // necessary to transistion to tumbling. + private final static double AOA_TUMBLE_CONDITION = Math.PI / 3.0; private SimulationStepper currentStepper; @@ -192,6 +197,24 @@ public class BasicEventSimulationEngine implements SimulationEngine { } } + // Check for Tumbling + // Conditions for transision are: + // apogee reached + // and is not already tumbling + // and not stable (cg > cp) + // and aoa > 30 + + if (status.isApogeeReached() && !status.isTumbling()) { + double cp = status.getFlightData().getLast(FlightDataType.TYPE_CP_LOCATION); + double cg = status.getFlightData().getLast(FlightDataType.TYPE_CG_LOCATION); + double aoa = status.getFlightData().getLast(FlightDataType.TYPE_AOA); + if (cg > cp && aoa > AOA_TUMBLE_CONDITION) { + addEvent(new FlightEvent(FlightEvent.Type.TUMBLE, status.getSimulationTime())); + status.setTumbling(true); + } + + } + } } catch (SimulationException e) { @@ -479,6 +502,12 @@ public class BasicEventSimulationEngine implements SimulationEngine { case ALTITUDE: break; + + case TUMBLE: + this.currentStepper = this.tumbleStepper; + this.status = currentStepper.initialize(status); + status.getFlightData().addEvent(event); + break; } } diff --git a/core/src/net/sf/openrocket/simulation/BasicTumbleStatus.java b/core/src/net/sf/openrocket/simulation/BasicTumbleStatus.java new file mode 100644 index 000000000..db2d97469 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/BasicTumbleStatus.java @@ -0,0 +1,77 @@ +package net.sf.openrocket.simulation; + +import java.util.Iterator; + +import net.sf.openrocket.aerodynamics.AerodynamicForces; +import net.sf.openrocket.aerodynamics.FlightConditions; +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.motor.MotorInstanceConfiguration; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.SymmetricComponent; + +public class BasicTumbleStatus extends SimulationStatus { + + // Magic constants from techdoc.pdf + private final static double cDFin = 1.42; + private final static double cDBt = 0.56; + // Fin efficiency. Index is number of fins. The 0th entry is arbitrary and used to + // offset the indexes so finEff[1] is the coefficient for one fin from the table in techdoc.pdf + private final static double[] finEff = { 0.0, 0.5, 1.0, 1.41, 1.81, 1.73, 1.90, 1.85 }; + + private double drag; + + public BasicTumbleStatus(Configuration configuration, + MotorInstanceConfiguration motorConfiguration, + SimulationConditions simulationConditions) { + super(configuration, motorConfiguration, simulationConditions); + computeTumbleDrag(); + } + + public BasicTumbleStatus(SimulationStatus orig) { + super(orig); + if ( orig instanceof BasicTumbleStatus ) { + this.drag = ((BasicTumbleStatus) orig).drag; + } + } + + public double getTumbleDrag( ) { + return drag; + } + + + public void computeTumbleDrag() { + + // Computed based on Sampo's experimentation as documented in the pdf. + + // compute the fin and body tube projected areas + double aFins = 0.0; + double aBt = 0.0; + Rocket r = this.getConfiguration().getRocket(); + Iterator componentIterator = r.iterator(); + while ( componentIterator.hasNext() ) { + RocketComponent component = componentIterator.next(); + if ( ! component.isAerodynamic() ) { + continue; + } + if ( component instanceof FinSet ) { + + double finComponent = ((FinSet)component).getFinArea(); + int finCount = ((FinSet)component).getFinCount(); + // check bounds on finCount. + if ( finCount >= finEff.length ) { + finCount = finEff.length-1; + } + + aFins += finComponent * finEff[finCount]; + + } else if (component instanceof SymmetricComponent ){ + aBt += ((SymmetricComponent) component) .getComponentPlanformArea(); + } + } + + drag = ( cDFin * aFins + cDBt * aBt ); + } +} diff --git a/core/src/net/sf/openrocket/simulation/BasicTumbleStepper.java b/core/src/net/sf/openrocket/simulation/BasicTumbleStepper.java new file mode 100644 index 000000000..d0ec8da67 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/BasicTumbleStepper.java @@ -0,0 +1,137 @@ +package net.sf.openrocket.simulation; + +import net.sf.openrocket.models.atmosphere.AtmosphericConditions; +import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.GeodeticComputationStrategy; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.WorldCoordinate; + +public class BasicTumbleStepper extends AbstractSimulationStepper { + + private static final double RECOVERY_TIME_STEP = 0.5; + + @Override + public SimulationStatus initialize(SimulationStatus status) throws SimulationException { + return new BasicTumbleStatus(status); + } + + @Override + public void step(SimulationStatus status, double maxTimeStep) throws SimulationException { + + // Get the atmospheric conditions + AtmosphericConditions atmosphere = modelAtmosphericConditions(status); + + //// Local wind speed and direction + Coordinate windSpeed = modelWindVelocity(status); + Coordinate airSpeed = status.getRocketVelocity().add(windSpeed); + + // Get total CD + double mach = airSpeed.length() / atmosphere.getMachSpeed(); + + double tumbleDrag = ((BasicTumbleStatus)status).getTumbleDrag(); + + // Compute drag force + double dynP = (0.5 * atmosphere.getDensity() * airSpeed.length2()); + double dragForce = tumbleDrag * dynP; + MassData massData = calculateMassData(status); + double mass = massData.getCG().weight; + + + // Compute drag acceleration + Coordinate linearAcceleration; + if (airSpeed.length() > 0.001) { + linearAcceleration = airSpeed.normalize().multiply(-dragForce / mass); + } else { + linearAcceleration = Coordinate.NUL; + } + + // Add effect of gravity + double gravity = modelGravity(status); + linearAcceleration = linearAcceleration.sub(0, 0, gravity); + + + // Add coriolis acceleration + Coordinate coriolisAcceleration = status.getSimulationConditions().getGeodeticComputation().getCoriolisAcceleration( + status.getRocketWorldPosition(), status.getRocketVelocity()); + linearAcceleration = linearAcceleration.add(coriolisAcceleration); + + + + // Select time step + double timeStep = MathUtil.min(0.5 / linearAcceleration.length(), RECOVERY_TIME_STEP); + + // Perform Euler integration + status.setRocketPosition(status.getRocketPosition().add(status.getRocketVelocity().multiply(timeStep)). + add(linearAcceleration.multiply(MathUtil.pow2(timeStep) / 2))); + status.setRocketVelocity(status.getRocketVelocity().add(linearAcceleration.multiply(timeStep))); + status.setSimulationTime(status.getSimulationTime() + timeStep); + + + // Update the world coordinate + WorldCoordinate w = status.getSimulationConditions().getLaunchSite(); + w = status.getSimulationConditions().getGeodeticComputation().addCoordinate(w, status.getRocketPosition()); + status.setRocketWorldPosition(w); + + + // Store data + FlightDataBranch data = status.getFlightData(); + boolean extra = status.getSimulationConditions().isCalculateExtras(); + 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); + if (extra) { + 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_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_ACCELERATION_TOTAL, linearAcceleration.length()); + + double Re = airSpeed.length() * + status.getConfiguration().getLength() / + 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_PROPELLANT_MASS, 0.0); // Is this a reasonable assumption? Probably. + + 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); + } + +} diff --git a/core/src/net/sf/openrocket/simulation/FlightEvent.java b/core/src/net/sf/openrocket/simulation/FlightEvent.java index 92f125c67..a997f49ee 100644 --- a/core/src/net/sf/openrocket/simulation/FlightEvent.java +++ b/core/src/net/sf/openrocket/simulation/FlightEvent.java @@ -72,7 +72,12 @@ public class FlightEvent implements Comparable { * A change in altitude has occurred. Data is a Pair * which contains the old and new altitudes. */ - ALTITUDE(trans.get("FlightEvent.Type.ALTITUDE")); + ALTITUDE(trans.get("FlightEvent.Type.ALTITUDE")), + + /** + * The rocket begins to tumble. + */ + TUMBLE(trans.get("FlightEvent.Type.TUMBLE")); private final String name; diff --git a/core/src/net/sf/openrocket/simulation/SimulationStatus.java b/core/src/net/sf/openrocket/simulation/SimulationStatus.java index 444ed4330..30fce5772 100644 --- a/core/src/net/sf/openrocket/simulation/SimulationStatus.java +++ b/core/src/net/sf/openrocket/simulation/SimulationStatus.java @@ -69,6 +69,9 @@ public class SimulationStatus implements Monitorable { /** Set to true when apogee has been detected. */ private boolean apogeeReached = false; + /** Set to true to indicate the rocket is tumbling. */ + private boolean tumbling = false; + /** Contains a list of deployed recovery devices. */ private MonitorableSet deployedRecoveryDevices = new MonitorableSet(); @@ -179,6 +182,7 @@ public class SimulationStatus implements Monitorable { this.liftoff = orig.liftoff; this.launchRodCleared = orig.launchRodCleared; this.apogeeReached = orig.apogeeReached; + this.tumbling = orig.tumbling; this.motorBurntOut = orig.motorBurntOut; this.deployedRecoveryDevices.clear(); @@ -380,6 +384,15 @@ public class SimulationStatus implements Monitorable { } + public void setTumbling( boolean tumbling ) { + this.tumbling = tumbling; + this.modID++; + } + + public boolean isTumbling() { + return tumbling; + } + public Set getDeployedRecoveryDevices() { return deployedRecoveryDevices; }