305516ad78b0a8ff122064086258ae245f642250
[debian/openrocket] / core / src / net / sf / openrocket / simulation / BasicEventSimulationEngine.java
1 package net.sf.openrocket.simulation;
2
3 import java.util.HashSet;
4 import java.util.Iterator;
5 import java.util.Set;
6
7 import net.sf.openrocket.aerodynamics.FlightConditions;
8 import net.sf.openrocket.aerodynamics.Warning;
9 import net.sf.openrocket.logging.LogHelper;
10 import net.sf.openrocket.motor.Motor;
11 import net.sf.openrocket.motor.MotorId;
12 import net.sf.openrocket.motor.MotorInstance;
13 import net.sf.openrocket.motor.MotorInstanceConfiguration;
14 import net.sf.openrocket.rocketcomponent.Configuration;
15 import net.sf.openrocket.rocketcomponent.LaunchLug;
16 import net.sf.openrocket.rocketcomponent.MotorMount;
17 import net.sf.openrocket.rocketcomponent.RecoveryDevice;
18 import net.sf.openrocket.rocketcomponent.RocketComponent;
19 import net.sf.openrocket.rocketcomponent.Stage;
20 import net.sf.openrocket.simulation.exception.MotorIgnitionException;
21 import net.sf.openrocket.simulation.exception.SimulationException;
22 import net.sf.openrocket.simulation.exception.SimulationLaunchException;
23 import net.sf.openrocket.simulation.listeners.SimulationListenerHelper;
24 import net.sf.openrocket.startup.Application;
25 import net.sf.openrocket.unit.UnitGroup;
26 import net.sf.openrocket.util.Coordinate;
27 import net.sf.openrocket.util.MathUtil;
28 import net.sf.openrocket.util.Pair;
29 import net.sf.openrocket.util.Quaternion;
30
31
32 public class BasicEventSimulationEngine implements SimulationEngine {
33         
34         private static final LogHelper log = Application.getLogger();
35         
36         // TODO: MEDIUM: Allow selecting steppers
37         private SimulationStepper flightStepper = new RK4SimulationStepper();
38         private SimulationStepper landingStepper = new BasicLandingStepper();
39         
40         private SimulationStepper currentStepper;
41         
42         private SimulationStatus status;
43         
44         
45         @Override
46         public FlightData simulate(SimulationConditions simulationConditions) throws SimulationException {
47                 Set<MotorId> motorBurntOut = new HashSet<MotorId>();
48                 
49                 // Set up flight data
50                 FlightData flightData = new FlightData();
51                 
52                 // Set up rocket configuration
53                 Configuration configuration = setupConfiguration(simulationConditions);
54                 MotorInstanceConfiguration motorConfiguration = setupMotorConfiguration(configuration);
55                 if (motorConfiguration.getMotorIDs().isEmpty()) {
56                         throw new MotorIgnitionException("No motors defined in the simulation.");
57                 }
58                 
59                 // Initialize the simulation
60                 currentStepper = flightStepper;
61                 status = initialStatus(configuration, motorConfiguration, simulationConditions, flightData);
62                 status = currentStepper.initialize(status);
63                 
64                 
65                 SimulationListenerHelper.fireStartSimulation(status);
66                 // Get originating position (in case listener has modified launch position)
67                 Coordinate origin = status.getRocketPosition();
68                 Coordinate originVelocity = status.getRocketVelocity();
69                 
70                 try {
71                         double maxAlt = Double.NEGATIVE_INFINITY;
72                         
73                         // Start the simulation
74                         while (handleEvents()) {
75                                 
76                                 // Take the step
77                                 double oldAlt = status.getRocketPosition().z;
78                                 
79                                 if (SimulationListenerHelper.firePreStep(status)) {
80                                         // Step at most to the next event
81                                         double maxStepTime = Double.MAX_VALUE;
82                                         FlightEvent nextEvent = status.getEventQueue().peek();
83                                         if (nextEvent != null) {
84                                                 maxStepTime = MathUtil.max(nextEvent.getTime() - status.getSimulationTime(), 0.001);
85                                         }
86                                         log.verbose("BasicEventSimulationEngine: Taking simulation step at t=" + status.getSimulationTime());
87                                         currentStepper.step(status, maxStepTime);
88                                 }
89                                 SimulationListenerHelper.firePostStep(status);
90                                 
91                                 
92                                 // Check for NaN values in the simulation status
93                                 checkNaN();
94                                 
95                                 // Add altitude event
96                                 addEvent(new FlightEvent(FlightEvent.Type.ALTITUDE, status.getSimulationTime(),
97                                                 status.getConfiguration().getRocket(),
98                                                 new Pair<Double, Double>(oldAlt, status.getRocketPosition().z)));
99                                 
100                                 if (status.getRocketPosition().z > maxAlt) {
101                                         maxAlt = status.getRocketPosition().z;
102                                 }
103                                 
104                                 
105                                 // Position relative to start location
106                                 Coordinate relativePosition = status.getRocketPosition().sub(origin);
107                                 
108                                 // Add appropriate events
109                                 if (!status.isLiftoff()) {
110                                         
111                                         // Avoid sinking into ground before liftoff
112                                         if (relativePosition.z < 0) {
113                                                 status.setRocketPosition(origin);
114                                                 status.setRocketVelocity(originVelocity);
115                                         }
116                                         // Detect lift-off
117                                         if (relativePosition.z > 0.02) {
118                                                 addEvent(new FlightEvent(FlightEvent.Type.LIFTOFF, status.getSimulationTime()));
119                                         }
120                                         
121                                 } else {
122                                         
123                                         // Check ground hit after liftoff
124                                         if (status.getRocketPosition().z < 0) {
125                                                 status.setRocketPosition(status.getRocketPosition().setZ(0));
126                                                 addEvent(new FlightEvent(FlightEvent.Type.GROUND_HIT, status.getSimulationTime()));
127                                                 addEvent(new FlightEvent(FlightEvent.Type.SIMULATION_END, status.getSimulationTime()));
128                                         }
129                                         
130                                 }
131                                 
132                                 // Check for launch guide clearance
133                                 if (!status.isLaunchRodCleared() &&
134                                                 relativePosition.length() > status.getSimulationConditions().getLaunchRodLength()) {
135                                         addEvent(new FlightEvent(FlightEvent.Type.LAUNCHROD, status.getSimulationTime(), null));
136                                 }
137                                 
138                                 
139                                 // Check for apogee
140                                 if (!status.isApogeeReached() && status.getRocketPosition().z < maxAlt - 0.01) {
141                                         addEvent(new FlightEvent(FlightEvent.Type.APOGEE, status.getSimulationTime(),
142                                                         status.getConfiguration().getRocket()));
143                                 }
144                                 
145                                 
146                                 // Check for burnt out motors
147                                 for (MotorId motorId : status.getMotorConfiguration().getMotorIDs()) {
148                                         MotorInstance motor = status.getMotorConfiguration().getMotorInstance(motorId);
149                                         if (!motor.isActive() && motorBurntOut.add(motorId)) {
150                                                 addEvent(new FlightEvent(FlightEvent.Type.BURNOUT, status.getSimulationTime(),
151                                                                 (RocketComponent) status.getMotorConfiguration().getMotorMount(motorId), motorId));
152                                         }
153                                 }
154                                 
155                         }
156                         
157                 } catch (SimulationException e) {
158                         SimulationListenerHelper.fireEndSimulation(status, e);
159                         throw e;
160                 }
161                 
162                 SimulationListenerHelper.fireEndSimulation(status, null);
163                 
164                 flightData.addBranch(status.getFlightData());
165                 
166                 if (!flightData.getWarningSet().isEmpty()) {
167                         log.info("Warnings at the end of simulation:  " + flightData.getWarningSet());
168                 }
169                 
170                 // TODO: HIGH: Simulate branches
171                 return flightData;
172         }
173         
174         
175         
176         private SimulationStatus initialStatus(Configuration configuration,
177                         MotorInstanceConfiguration motorConfiguration,
178                         SimulationConditions simulationConditions, FlightData flightData) {
179                 
180                 SimulationStatus init = new SimulationStatus();
181                 init.setSimulationConditions(simulationConditions);
182                 init.setConfiguration(configuration);
183                 init.setMotorConfiguration(motorConfiguration);
184                 
185                 init.setSimulationTime(0);
186                 init.setPreviousTimeStep(simulationConditions.getTimeStep());
187                 init.setRocketPosition(Coordinate.NUL);
188                 init.setRocketVelocity(Coordinate.NUL);
189                 init.setRocketWorldPosition(simulationConditions.getLaunchSite());
190                 
191                 // Initialize to roll angle with least stability w.r.t. the wind
192                 Quaternion o;
193                 FlightConditions cond = new FlightConditions(configuration);
194                 simulationConditions.getAerodynamicCalculator().getWorstCP(configuration, cond, null);
195                 double angle = -cond.getTheta() - simulationConditions.getLaunchRodDirection();
196                 o = Quaternion.rotation(new Coordinate(0, 0, angle));
197                 
198                 // Launch rod angle and direction
199                 o = o.multiplyLeft(Quaternion.rotation(new Coordinate(0, simulationConditions.getLaunchRodAngle(), 0)));
200                 o = o.multiplyLeft(Quaternion.rotation(new Coordinate(0, 0, simulationConditions.getLaunchRodDirection())));
201                 
202                 init.setRocketOrientationQuaternion(o);
203                 init.setRocketRotationVelocity(Coordinate.NUL);
204                 
205                 
206                 /*
207                  * Calculate the effective launch rod length taking into account launch lugs.
208                  * If no lugs are found, assume a tower launcher of full length.
209                  */
210                 double length = simulationConditions.getLaunchRodLength();
211                 double lugPosition = Double.NaN;
212                 for (RocketComponent c : configuration) {
213                         if (c instanceof LaunchLug) {
214                                 double pos = c.toAbsolute(new Coordinate(c.getLength()))[0].x;
215                                 if (Double.isNaN(lugPosition) || pos > lugPosition) {
216                                         lugPosition = pos;
217                                 }
218                         }
219                 }
220                 if (!Double.isNaN(lugPosition)) {
221                         double maxX = 0;
222                         for (Coordinate c : configuration.getBounds()) {
223                                 if (c.x > maxX)
224                                         maxX = c.x;
225                         }
226                         if (maxX >= lugPosition) {
227                                 length = Math.max(0, length - (maxX - lugPosition));
228                         }
229                 }
230                 init.setEffectiveLaunchRodLength(length);
231                 
232                 
233                 
234                 init.setSimulationStartWallTime(System.nanoTime());
235                 
236                 init.setMotorIgnited(false);
237                 init.setLiftoff(false);
238                 init.setLaunchRodCleared(false);
239                 init.setApogeeReached(false);
240                 
241                 init.getEventQueue().add(new FlightEvent(FlightEvent.Type.LAUNCH, 0, simulationConditions.getRocket()));
242                 
243                 init.setFlightData(new FlightDataBranch("MAIN", FlightDataType.TYPE_TIME));
244                 init.setWarnings(flightData.getWarningSet());
245                 
246                 return init;
247         }
248         
249         
250         
251         /**
252          * Create a rocket configuration from the launch conditions.
253          * 
254          * @param simulation    the launch conditions.
255          * @return                              a rocket configuration with all stages attached.
256          */
257         private Configuration setupConfiguration(SimulationConditions simulation) {
258                 Configuration configuration = new Configuration(simulation.getRocket());
259                 configuration.setAllStages();
260                 configuration.setMotorConfigurationID(simulation.getMotorConfigurationID());
261                 
262                 return configuration;
263         }
264         
265         
266         
267         /**
268          * Create a new motor instance configuration for the rocket configuration.
269          * 
270          * @param configuration         the rocket configuration.
271          * @return                                      a new motor instance configuration with all motors in place.
272          */
273         private MotorInstanceConfiguration setupMotorConfiguration(Configuration configuration) {
274                 MotorInstanceConfiguration motors = new MotorInstanceConfiguration();
275                 final String motorId = configuration.getMotorConfigurationID();
276                 
277                 Iterator<MotorMount> iterator = configuration.motorIterator();
278                 while (iterator.hasNext()) {
279                         MotorMount mount = iterator.next();
280                         RocketComponent component = (RocketComponent) mount;
281                         Motor motor = mount.getMotor(motorId);
282                         
283                         if (motor != null) {
284                                 Coordinate[] positions = component.toAbsolute(mount.getMotorPosition(motorId));
285                                 for (int i = 0; i < positions.length; i++) {
286                                         Coordinate position = positions[i];
287                                         MotorId id = new MotorId(component.getID(), i + 1);
288                                         motors.addMotor(id, motor.getInstance(), mount, position);
289                                 }
290                         }
291                 }
292                 return motors;
293         }
294         
295         /**
296          * Handles events occurring during the flight from the event queue.
297          * Each event that has occurred before or at the current simulation time is
298          * processed.  Suitable events are also added to the flight data.
299          */
300         private boolean handleEvents() throws SimulationException {
301                 boolean ret = true;
302                 FlightEvent event;
303                 
304                 for (event = nextEvent(); event != null; event = nextEvent()) {
305                         
306                         // Ignore events for components that are no longer attached to the rocket
307                         if (event.getSource() != null && event.getSource().getParent() != null &&
308                                         !status.getConfiguration().isStageActive(event.getSource().getStageNumber())) {
309                                 continue;
310                         }
311                         
312                         // Call simulation listeners, allow aborting event handling
313                         if (!SimulationListenerHelper.fireHandleFlightEvent(status, event)) {
314                                 continue;
315                         }
316                         
317                         if (event.getType() != FlightEvent.Type.ALTITUDE) {
318                                 log.verbose("BasicEventSimulationEngine:  Handling event " + event);
319                         }
320                         
321                         if (event.getType() == FlightEvent.Type.IGNITION) {
322                                 MotorMount mount = (MotorMount) event.getSource();
323                                 MotorId motorId = (MotorId) event.getData();
324                                 MotorInstance instance = status.getMotorConfiguration().getMotorInstance(motorId);
325                                 if (!SimulationListenerHelper.fireMotorIgnition(status, motorId, mount, instance)) {
326                                         continue;
327                                 }
328                         }
329                         
330                         if (event.getType() == FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT) {
331                                 RecoveryDevice device = (RecoveryDevice) event.getSource();
332                                 if (!SimulationListenerHelper.fireRecoveryDeviceDeployment(status, device)) {
333                                         continue;
334                                 }
335                         }
336                         
337                         
338                         
339                         // Check for motor ignition events, add ignition events to queue
340                         for (MotorId id : status.getMotorConfiguration().getMotorIDs()) {
341                                 MotorMount mount = status.getMotorConfiguration().getMotorMount(id);
342                                 RocketComponent component = (RocketComponent) mount;
343                                 
344                                 if (mount.getIgnitionEvent().isActivationEvent(event, component)) {
345                                         addEvent(new FlightEvent(FlightEvent.Type.IGNITION,
346                                                         status.getSimulationTime() + mount.getIgnitionDelay(),
347                                                         component, id));
348                                 }
349                         }
350                         
351                         
352                         // Check for stage separation event
353                         for (int stageNo : status.getConfiguration().getActiveStages()) {
354                                 if (stageNo == 0)
355                                         continue;
356                                 
357                                 Stage stage = (Stage) status.getConfiguration().getRocket().getChild(stageNo);
358                                 if (stage.getSeparationEvent().isSeparationEvent(event, stage)) {
359                                         addEvent(new FlightEvent(FlightEvent.Type.STAGE_SEPARATION,
360                                                         event.getTime() + stage.getSeparationDelay(), stage));
361                                 }
362                         }
363                         
364                         
365                         // Check for recovery device deployment, add events to queue
366                         Iterator<RocketComponent> rci = status.getConfiguration().iterator();
367                         while (rci.hasNext()) {
368                                 RocketComponent c = rci.next();
369                                 if (!(c instanceof RecoveryDevice))
370                                         continue;
371                                 if (((RecoveryDevice) c).getDeployEvent().isActivationEvent(event, c)) {
372                                         // Delay event by at least 1ms to allow stage separation to occur first
373                                         addEvent(new FlightEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT,
374                                                         event.getTime() + Math.max(0.001, ((RecoveryDevice) c).getDeployDelay()), c));
375                                 }
376                         }
377                         
378                         
379                         // Handle event
380                         switch (event.getType()) {
381                         
382                         case LAUNCH: {
383                                 status.getFlightData().addEvent(event);
384                                 break;
385                         }
386                         
387                         case IGNITION: {
388                                 // Ignite the motor
389                                 MotorMount mount = (MotorMount) event.getSource();
390                                 RocketComponent component = (RocketComponent) mount;
391                                 MotorId motorId = (MotorId) event.getData();
392                                 MotorInstanceConfiguration config = status.getMotorConfiguration();
393                                 config.setMotorIgnitionTime(motorId, event.getTime());
394                                 status.setMotorIgnited(true);
395                                 status.getFlightData().addEvent(event);
396                                 
397                                 break;
398                         }
399                         
400                         case LIFTOFF: {
401                                 // Mark lift-off as occurred
402                                 status.setLiftoff(true);
403                                 status.getFlightData().addEvent(event);
404                                 break;
405                         }
406                         
407                         case LAUNCHROD: {
408                                 // Mark launch rod as cleared
409                                 status.setLaunchRodCleared(true);
410                                 status.getFlightData().addEvent(event);
411                                 break;
412                         }
413                         
414                         case BURNOUT: {
415                                 // If motor burnout occurs without lift-off, abort
416                                 if (!status.isLiftoff()) {
417                                         throw new SimulationLaunchException("Motor burnout without liftoff.");
418                                 }
419                                 // Add ejection charge event
420                                 String id = status.getConfiguration().getMotorConfigurationID();
421                                 MotorMount mount = (MotorMount) event.getSource();
422                                 double delay = mount.getMotorDelay(id);
423                                 if (delay != Motor.PLUGGED) {
424                                         addEvent(new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, status.getSimulationTime() + delay,
425                                                         event.getSource(), event.getData()));
426                                 }
427                                 status.getFlightData().addEvent(event);
428                                 break;
429                         }
430                         
431                         case EJECTION_CHARGE: {
432                                 status.getFlightData().addEvent(event);
433                                 break;
434                         }
435                         
436                         case STAGE_SEPARATION: {
437                                 // TODO: HIGH: Store lower stages to be simulated later
438                                 RocketComponent stage = event.getSource();
439                                 int n = stage.getStageNumber();
440                                 status.getConfiguration().setToStage(n - 1);
441                                 status.getFlightData().addEvent(event);
442                                 break;
443                         }
444                         
445                         case APOGEE:
446                                 // Mark apogee as reached
447                                 status.setApogeeReached(true);
448                                 status.getFlightData().addEvent(event);
449                                 break;
450                         
451                         case RECOVERY_DEVICE_DEPLOYMENT:
452                                 RocketComponent c = event.getSource();
453                                 int n = c.getStageNumber();
454                                 // Ignore event if stage not active
455                                 if (status.getConfiguration().isStageActive(n)) {
456                                         // TODO: HIGH: Check stage activeness for other events as well?
457                                         
458                                         // Check whether any motor in the active stages is active anymore
459                                         for (MotorId motorId : status.getMotorConfiguration().getMotorIDs()) {
460                                                 int stage = ((RocketComponent) status.getMotorConfiguration().
461                                                                 getMotorMount(motorId)).getStageNumber();
462                                                 if (!status.getConfiguration().isStageActive(stage))
463                                                         continue;
464                                                 if (!status.getMotorConfiguration().getMotorInstance(motorId).isActive())
465                                                         continue;
466                                                 status.getWarnings().add(Warning.RECOVERY_DEPLOYMENT_WHILE_BURNING);
467                                         }
468                                         
469                                         // Check for launch rod
470                                         if (!status.isLaunchRodCleared()) {
471                                                 status.getWarnings().add(Warning.fromString("Recovery device device deployed while on " +
472                                                                 "the launch guide."));
473                                         }
474                                         
475                                         // Check current velocity
476                                         if (status.getRocketVelocity().length() > 20) {
477                                                 // TODO: LOW: Custom warning.
478                                                 status.getWarnings().add(Warning.fromString("Recovery device deployment at high " +
479                                                                 "speed ("
480                                                                 + UnitGroup.UNITS_VELOCITY.toStringUnit(status.getRocketVelocity().length())
481                                                                 + ")."));
482                                         }
483                                         
484                                         status.setLiftoff(true);
485                                         status.getDeployedRecoveryDevices().add((RecoveryDevice) c);
486                                         
487                                         this.currentStepper = this.landingStepper;
488                                         this.status = currentStepper.initialize(status);
489                                         
490                                         status.getFlightData().addEvent(event);
491                                 }
492                                 break;
493                         
494                         case GROUND_HIT:
495                                 status.getFlightData().addEvent(event);
496                                 break;
497                         
498                         case SIMULATION_END:
499                                 ret = false;
500                                 status.getFlightData().addEvent(event);
501                                 break;
502                         
503                         case ALTITUDE:
504                                 break;
505                         }
506                         
507                 }
508                 
509                 
510                 // If no motor has ignited, abort
511                 if (!status.isMotorIgnited()) {
512                         throw new MotorIgnitionException("No motors ignited.");
513                 }
514                 
515                 return ret;
516         }
517         
518         /**
519          * Add a flight event to the event queue unless a listener aborts adding it.
520          * 
521          * @param event         the event to add to the queue.
522          */
523         private void addEvent(FlightEvent event) throws SimulationException {
524                 if (SimulationListenerHelper.fireAddFlightEvent(status, event)) {
525                         status.getEventQueue().add(event);
526                 }
527         }
528         
529         
530         
531         /**
532          * Return the next flight event to handle, or null if no more events should be handled.
533          * This method jumps the simulation time forward in case no motors have been ignited.
534          * The flight event is removed from the event queue.
535          * 
536          * @param status        the simulation status
537          * @return                      the flight event to handle, or null
538          */
539         private FlightEvent nextEvent() {
540                 EventQueue queue = status.getEventQueue();
541                 FlightEvent event = queue.peek();
542                 if (event == null)
543                         return null;
544                 
545                 // Jump to event if no motors have been ignited
546                 if (!status.isMotorIgnited() && event.getTime() > status.getSimulationTime()) {
547                         status.setSimulationTime(event.getTime());
548                 }
549                 if (event.getTime() <= status.getSimulationTime()) {
550                         return queue.poll();
551                 } else {
552                         return null;
553                 }
554         }
555         
556         
557         
558         private void checkNaN() throws SimulationException {
559                 double d = 0;
560                 boolean b = false;
561                 d += status.getSimulationTime();
562                 d += status.getPreviousTimeStep();
563                 b |= status.getRocketPosition().isNaN();
564                 b |= status.getRocketVelocity().isNaN();
565                 b |= status.getRocketOrientationQuaternion().isNaN();
566                 b |= status.getRocketRotationVelocity().isNaN();
567                 d += status.getEffectiveLaunchRodLength();
568                 
569                 if (Double.isNaN(d) || b) {
570                         log.error("Simulation resulted in NaN value:" +
571                                         " simulationTime=" + status.getSimulationTime() +
572                                         " previousTimeStep=" + status.getPreviousTimeStep() +
573                                         " rocketPosition=" + status.getRocketPosition() +
574                                         " rocketVelocity=" + status.getRocketVelocity() +
575                                         " rocketOrientationQuaternion=" + status.getRocketOrientationQuaternion() +
576                                         " rocketRotationVelocity=" + status.getRocketRotationVelocity() +
577                                         " effectiveLaunchRodLength=" + status.getEffectiveLaunchRodLength());
578                         throw new SimulationException("Simulation resulted in not-a-number (NaN) value, please report a bug.");
579                 }
580         }
581         
582         
583 }