1 package net.sf.openrocket.simulation;
3 import java.util.ArrayList;
4 import java.util.HashSet;
5 import java.util.Iterator;
8 import net.sf.openrocket.aerodynamics.FlightConditions;
9 import net.sf.openrocket.aerodynamics.Warning;
10 import net.sf.openrocket.logging.LogHelper;
11 import net.sf.openrocket.motor.Motor;
12 import net.sf.openrocket.motor.MotorId;
13 import net.sf.openrocket.motor.MotorInstance;
14 import net.sf.openrocket.motor.MotorInstanceConfiguration;
15 import net.sf.openrocket.rocketcomponent.Configuration;
16 import net.sf.openrocket.rocketcomponent.LaunchLug;
17 import net.sf.openrocket.rocketcomponent.MotorMount;
18 import net.sf.openrocket.rocketcomponent.RecoveryDevice;
19 import net.sf.openrocket.rocketcomponent.RocketComponent;
20 import net.sf.openrocket.rocketcomponent.Stage;
21 import net.sf.openrocket.simulation.customexpression.CustomExpression;
22 import net.sf.openrocket.simulation.exception.MotorIgnitionException;
23 import net.sf.openrocket.simulation.exception.SimulationException;
24 import net.sf.openrocket.simulation.exception.SimulationLaunchException;
25 import net.sf.openrocket.simulation.listeners.SimulationListenerHelper;
26 import net.sf.openrocket.startup.Application;
27 import net.sf.openrocket.unit.UnitGroup;
28 import net.sf.openrocket.util.Coordinate;
29 import net.sf.openrocket.util.MathUtil;
30 import net.sf.openrocket.util.Pair;
31 import net.sf.openrocket.util.Quaternion;
34 public class BasicEventSimulationEngine implements SimulationEngine {
36 private static final LogHelper log = Application.getLogger();
38 // TODO: MEDIUM: Allow selecting steppers
39 private SimulationStepper flightStepper = new RK4SimulationStepper();
40 private SimulationStepper landingStepper = new BasicLandingStepper();
42 private SimulationStepper currentStepper;
44 private SimulationStatus status;
48 public FlightData simulate(SimulationConditions simulationConditions) throws SimulationException {
49 Set<MotorId> motorBurntOut = new HashSet<MotorId>();
52 FlightData flightData = new FlightData();
54 // Set up rocket configuration
55 Configuration configuration = setupConfiguration(simulationConditions);
56 MotorInstanceConfiguration motorConfiguration = setupMotorConfiguration(configuration);
57 if (motorConfiguration.getMotorIDs().isEmpty()) {
58 throw new MotorIgnitionException("No motors defined in the simulation.");
61 // Initialize the simulation
62 currentStepper = flightStepper;
63 status = initialStatus(configuration, motorConfiguration, simulationConditions, flightData);
64 status = currentStepper.initialize(status);
67 SimulationListenerHelper.fireStartSimulation(status);
68 // Get originating position (in case listener has modified launch position)
69 Coordinate origin = status.getRocketPosition();
70 Coordinate originVelocity = status.getRocketVelocity();
73 double maxAlt = Double.NEGATIVE_INFINITY;
75 // Start the simulation
76 while (handleEvents()) {
79 double oldAlt = status.getRocketPosition().z;
81 if (SimulationListenerHelper.firePreStep(status)) {
82 // Step at most to the next event
83 double maxStepTime = Double.MAX_VALUE;
84 FlightEvent nextEvent = status.getEventQueue().peek();
85 if (nextEvent != null) {
86 maxStepTime = MathUtil.max(nextEvent.getTime() - status.getSimulationTime(), 0.001);
88 log.verbose("BasicEventSimulationEngine: Taking simulation step at t=" + status.getSimulationTime());
89 currentStepper.step(status, maxStepTime);
91 SimulationListenerHelper.firePostStep(status);
93 // Calculate values for custom expressions
94 FlightDataBranch data = status.getFlightData();
95 ArrayList<CustomExpression> allExpressions = status.getSimulationConditions().getSimulation().getDocument().getCustomExpressions();
96 for (CustomExpression expression : allExpressions ) {
97 data.setValue(expression.getType(), expression.evaluateDouble(status));
100 // Check for NaN values in the simulation status
103 // Add altitude event
104 addEvent(new FlightEvent(FlightEvent.Type.ALTITUDE, status.getSimulationTime(),
105 status.getConfiguration().getRocket(),
106 new Pair<Double, Double>(oldAlt, status.getRocketPosition().z)));
108 if (status.getRocketPosition().z > maxAlt) {
109 maxAlt = status.getRocketPosition().z;
113 // Position relative to start location
114 Coordinate relativePosition = status.getRocketPosition().sub(origin);
116 // Add appropriate events
117 if (!status.isLiftoff()) {
119 // Avoid sinking into ground before liftoff
120 if (relativePosition.z < 0) {
121 status.setRocketPosition(origin);
122 status.setRocketVelocity(originVelocity);
125 if (relativePosition.z > 0.02) {
126 addEvent(new FlightEvent(FlightEvent.Type.LIFTOFF, status.getSimulationTime()));
131 // Check ground hit after liftoff
132 if (status.getRocketPosition().z < 0) {
133 status.setRocketPosition(status.getRocketPosition().setZ(0));
134 addEvent(new FlightEvent(FlightEvent.Type.GROUND_HIT, status.getSimulationTime()));
135 addEvent(new FlightEvent(FlightEvent.Type.SIMULATION_END, status.getSimulationTime()));
140 // Check for launch guide clearance
141 if (!status.isLaunchRodCleared() &&
142 relativePosition.length() > status.getSimulationConditions().getLaunchRodLength()) {
143 addEvent(new FlightEvent(FlightEvent.Type.LAUNCHROD, status.getSimulationTime(), null));
148 if (!status.isApogeeReached() && status.getRocketPosition().z < maxAlt - 0.01) {
149 addEvent(new FlightEvent(FlightEvent.Type.APOGEE, status.getSimulationTime(),
150 status.getConfiguration().getRocket()));
154 // Check for burnt out motors
155 for (MotorId motorId : status.getMotorConfiguration().getMotorIDs()) {
156 MotorInstance motor = status.getMotorConfiguration().getMotorInstance(motorId);
157 if (!motor.isActive() && motorBurntOut.add(motorId)) {
158 addEvent(new FlightEvent(FlightEvent.Type.BURNOUT, status.getSimulationTime(),
159 (RocketComponent) status.getMotorConfiguration().getMotorMount(motorId), motorId));
165 } catch (SimulationException e) {
166 SimulationListenerHelper.fireEndSimulation(status, e);
170 SimulationListenerHelper.fireEndSimulation(status, null);
172 flightData.addBranch(status.getFlightData());
174 if (!flightData.getWarningSet().isEmpty()) {
175 log.info("Warnings at the end of simulation: " + flightData.getWarningSet());
178 // TODO: HIGH: Simulate branches
184 private SimulationStatus initialStatus(Configuration configuration,
185 MotorInstanceConfiguration motorConfiguration,
186 SimulationConditions simulationConditions, FlightData flightData) {
188 SimulationStatus init = new SimulationStatus();
189 init.setSimulationConditions(simulationConditions);
190 init.setConfiguration(configuration);
191 init.setMotorConfiguration(motorConfiguration);
193 init.setSimulationTime(0);
194 init.setPreviousTimeStep(simulationConditions.getTimeStep());
195 init.setRocketPosition(Coordinate.NUL);
196 init.setRocketVelocity(Coordinate.NUL);
197 init.setRocketWorldPosition(simulationConditions.getLaunchSite());
199 // Initialize to roll angle with least stability w.r.t. the wind
201 FlightConditions cond = new FlightConditions(configuration);
202 simulationConditions.getAerodynamicCalculator().getWorstCP(configuration, cond, null);
203 double angle = -cond.getTheta() - simulationConditions.getLaunchRodDirection();
204 o = Quaternion.rotation(new Coordinate(0, 0, angle));
206 // Launch rod angle and direction
207 o = o.multiplyLeft(Quaternion.rotation(new Coordinate(0, simulationConditions.getLaunchRodAngle(), 0)));
208 o = o.multiplyLeft(Quaternion.rotation(new Coordinate(0, 0, simulationConditions.getLaunchRodDirection())));
210 init.setRocketOrientationQuaternion(o);
211 init.setRocketRotationVelocity(Coordinate.NUL);
215 * Calculate the effective launch rod length taking into account launch lugs.
216 * If no lugs are found, assume a tower launcher of full length.
218 double length = simulationConditions.getLaunchRodLength();
219 double lugPosition = Double.NaN;
220 for (RocketComponent c : configuration) {
221 if (c instanceof LaunchLug) {
222 double pos = c.toAbsolute(new Coordinate(c.getLength()))[0].x;
223 if (Double.isNaN(lugPosition) || pos > lugPosition) {
228 if (!Double.isNaN(lugPosition)) {
230 for (Coordinate c : configuration.getBounds()) {
234 if (maxX >= lugPosition) {
235 length = Math.max(0, length - (maxX - lugPosition));
238 init.setEffectiveLaunchRodLength(length);
242 init.setSimulationStartWallTime(System.nanoTime());
244 init.setMotorIgnited(false);
245 init.setLiftoff(false);
246 init.setLaunchRodCleared(false);
247 init.setApogeeReached(false);
249 init.getEventQueue().add(new FlightEvent(FlightEvent.Type.LAUNCH, 0, simulationConditions.getRocket()));
251 init.setFlightData(new FlightDataBranch("MAIN", FlightDataType.TYPE_TIME));
252 init.setWarnings(flightData.getWarningSet());
260 * Create a rocket configuration from the launch conditions.
262 * @param simulation the launch conditions.
263 * @return a rocket configuration with all stages attached.
265 private Configuration setupConfiguration(SimulationConditions simulation) {
266 Configuration configuration = new Configuration(simulation.getRocket());
267 configuration.setAllStages();
268 configuration.setMotorConfigurationID(simulation.getMotorConfigurationID());
270 return configuration;
276 * Create a new motor instance configuration for the rocket configuration.
278 * @param configuration the rocket configuration.
279 * @return a new motor instance configuration with all motors in place.
281 private MotorInstanceConfiguration setupMotorConfiguration(Configuration configuration) {
282 MotorInstanceConfiguration motors = new MotorInstanceConfiguration();
283 final String motorId = configuration.getMotorConfigurationID();
285 Iterator<MotorMount> iterator = configuration.motorIterator();
286 while (iterator.hasNext()) {
287 MotorMount mount = iterator.next();
288 RocketComponent component = (RocketComponent) mount;
289 Motor motor = mount.getMotor(motorId);
292 Coordinate[] positions = component.toAbsolute(mount.getMotorPosition(motorId));
293 for (int i = 0; i < positions.length; i++) {
294 Coordinate position = positions[i];
295 MotorId id = new MotorId(component.getID(), i + 1);
296 motors.addMotor(id, motor.getInstance(), mount, position);
304 * Handles events occurring during the flight from the event queue.
305 * Each event that has occurred before or at the current simulation time is
306 * processed. Suitable events are also added to the flight data.
308 private boolean handleEvents() throws SimulationException {
312 for (event = nextEvent(); event != null; event = nextEvent()) {
314 // Ignore events for components that are no longer attached to the rocket
315 if (event.getSource() != null && event.getSource().getParent() != null &&
316 !status.getConfiguration().isStageActive(event.getSource().getStageNumber())) {
320 // Call simulation listeners, allow aborting event handling
321 if (!SimulationListenerHelper.fireHandleFlightEvent(status, event)) {
325 if (event.getType() != FlightEvent.Type.ALTITUDE) {
326 log.verbose("BasicEventSimulationEngine: Handling event " + event);
329 if (event.getType() == FlightEvent.Type.IGNITION) {
330 MotorMount mount = (MotorMount) event.getSource();
331 MotorId motorId = (MotorId) event.getData();
332 MotorInstance instance = status.getMotorConfiguration().getMotorInstance(motorId);
333 if (!SimulationListenerHelper.fireMotorIgnition(status, motorId, mount, instance)) {
338 if (event.getType() == FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT) {
339 RecoveryDevice device = (RecoveryDevice) event.getSource();
340 if (!SimulationListenerHelper.fireRecoveryDeviceDeployment(status, device)) {
347 // Check for motor ignition events, add ignition events to queue
348 for (MotorId id : status.getMotorConfiguration().getMotorIDs()) {
349 MotorMount mount = status.getMotorConfiguration().getMotorMount(id);
350 RocketComponent component = (RocketComponent) mount;
352 if (mount.getIgnitionEvent().isActivationEvent(event, component)) {
353 addEvent(new FlightEvent(FlightEvent.Type.IGNITION,
354 status.getSimulationTime() + mount.getIgnitionDelay(),
360 // Check for stage separation event
361 for (int stageNo : status.getConfiguration().getActiveStages()) {
365 Stage stage = (Stage) status.getConfiguration().getRocket().getChild(stageNo);
366 if (stage.getSeparationEvent().isSeparationEvent(event, stage)) {
367 addEvent(new FlightEvent(FlightEvent.Type.STAGE_SEPARATION,
368 event.getTime() + stage.getSeparationDelay(), stage));
373 // Check for recovery device deployment, add events to queue
374 Iterator<RocketComponent> rci = status.getConfiguration().iterator();
375 while (rci.hasNext()) {
376 RocketComponent c = rci.next();
377 if (!(c instanceof RecoveryDevice))
379 if (((RecoveryDevice) c).getDeployEvent().isActivationEvent(event, c)) {
380 // Delay event by at least 1ms to allow stage separation to occur first
381 addEvent(new FlightEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT,
382 event.getTime() + Math.max(0.001, ((RecoveryDevice) c).getDeployDelay()), c));
388 switch (event.getType()) {
391 status.getFlightData().addEvent(event);
397 MotorMount mount = (MotorMount) event.getSource();
398 RocketComponent component = (RocketComponent) mount;
399 MotorId motorId = (MotorId) event.getData();
400 MotorInstanceConfiguration config = status.getMotorConfiguration();
401 config.setMotorIgnitionTime(motorId, event.getTime());
402 status.setMotorIgnited(true);
403 status.getFlightData().addEvent(event);
409 // Mark lift-off as occurred
410 status.setLiftoff(true);
411 status.getFlightData().addEvent(event);
416 // Mark launch rod as cleared
417 status.setLaunchRodCleared(true);
418 status.getFlightData().addEvent(event);
423 // If motor burnout occurs without lift-off, abort
424 if (!status.isLiftoff()) {
425 throw new SimulationLaunchException("Motor burnout without liftoff.");
427 // Add ejection charge event
428 String id = status.getConfiguration().getMotorConfigurationID();
429 MotorMount mount = (MotorMount) event.getSource();
430 double delay = mount.getMotorDelay(id);
431 if (delay != Motor.PLUGGED) {
432 addEvent(new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, status.getSimulationTime() + delay,
433 event.getSource(), event.getData()));
435 status.getFlightData().addEvent(event);
439 case EJECTION_CHARGE: {
440 status.getFlightData().addEvent(event);
444 case STAGE_SEPARATION: {
445 // TODO: HIGH: Store lower stages to be simulated later
446 RocketComponent stage = event.getSource();
447 int n = stage.getStageNumber();
448 status.getConfiguration().setToStage(n - 1);
449 status.getFlightData().addEvent(event);
454 // Mark apogee as reached
455 status.setApogeeReached(true);
456 status.getFlightData().addEvent(event);
459 case RECOVERY_DEVICE_DEPLOYMENT:
460 RocketComponent c = event.getSource();
461 int n = c.getStageNumber();
462 // Ignore event if stage not active
463 if (status.getConfiguration().isStageActive(n)) {
464 // TODO: HIGH: Check stage activeness for other events as well?
466 // Check whether any motor in the active stages is active anymore
467 for (MotorId motorId : status.getMotorConfiguration().getMotorIDs()) {
468 int stage = ((RocketComponent) status.getMotorConfiguration().
469 getMotorMount(motorId)).getStageNumber();
470 if (!status.getConfiguration().isStageActive(stage))
472 if (!status.getMotorConfiguration().getMotorInstance(motorId).isActive())
474 status.getWarnings().add(Warning.RECOVERY_DEPLOYMENT_WHILE_BURNING);
477 // Check for launch rod
478 if (!status.isLaunchRodCleared()) {
479 status.getWarnings().add(Warning.fromString("Recovery device device deployed while on " +
480 "the launch guide."));
483 // Check current velocity
484 if (status.getRocketVelocity().length() > 20) {
485 // TODO: LOW: Custom warning.
486 status.getWarnings().add(Warning.fromString("Recovery device deployment at high " +
488 + UnitGroup.UNITS_VELOCITY.toStringUnit(status.getRocketVelocity().length())
492 status.setLiftoff(true);
493 status.getDeployedRecoveryDevices().add((RecoveryDevice) c);
495 this.currentStepper = this.landingStepper;
496 this.status = currentStepper.initialize(status);
498 status.getFlightData().addEvent(event);
503 status.getFlightData().addEvent(event);
508 status.getFlightData().addEvent(event);
518 // If no motor has ignited, abort
519 if (!status.isMotorIgnited()) {
520 throw new MotorIgnitionException("No motors ignited.");
527 * Add a flight event to the event queue unless a listener aborts adding it.
529 * @param event the event to add to the queue.
531 private void addEvent(FlightEvent event) throws SimulationException {
532 if (SimulationListenerHelper.fireAddFlightEvent(status, event)) {
533 status.getEventQueue().add(event);
540 * Return the next flight event to handle, or null if no more events should be handled.
541 * This method jumps the simulation time forward in case no motors have been ignited.
542 * The flight event is removed from the event queue.
544 * @param status the simulation status
545 * @return the flight event to handle, or null
547 private FlightEvent nextEvent() {
548 EventQueue queue = status.getEventQueue();
549 FlightEvent event = queue.peek();
553 // Jump to event if no motors have been ignited
554 if (!status.isMotorIgnited() && event.getTime() > status.getSimulationTime()) {
555 status.setSimulationTime(event.getTime());
557 if (event.getTime() <= status.getSimulationTime()) {
566 private void checkNaN() throws SimulationException {
569 d += status.getSimulationTime();
570 d += status.getPreviousTimeStep();
571 b |= status.getRocketPosition().isNaN();
572 b |= status.getRocketVelocity().isNaN();
573 b |= status.getRocketOrientationQuaternion().isNaN();
574 b |= status.getRocketRotationVelocity().isNaN();
575 d += status.getEffectiveLaunchRodLength();
577 if (Double.isNaN(d) || b) {
578 log.error("Simulation resulted in NaN value:" +
579 " simulationTime=" + status.getSimulationTime() +
580 " previousTimeStep=" + status.getPreviousTimeStep() +
581 " rocketPosition=" + status.getRocketPosition() +
582 " rocketVelocity=" + status.getRocketVelocity() +
583 " rocketOrientationQuaternion=" + status.getRocketOrientationQuaternion() +
584 " rocketRotationVelocity=" + status.getRocketRotationVelocity() +
585 " effectiveLaunchRodLength=" + status.getEffectiveLaunchRodLength());
586 throw new SimulationException("Simulation resulted in not-a-number (NaN) value, please report a bug.");