1 package net.sf.openrocket.simulation;
3 import java.util.ArrayList;
4 import java.util.EventListener;
5 import java.util.EventObject;
7 import java.util.Random;
9 import net.sf.openrocket.aerodynamics.BarrowmanCalculator;
10 import net.sf.openrocket.masscalc.BasicMassCalculator;
11 import net.sf.openrocket.models.atmosphere.AtmosphericModel;
12 import net.sf.openrocket.models.atmosphere.ExtendedISAModel;
13 import net.sf.openrocket.models.gravity.GravityModel;
14 import net.sf.openrocket.models.gravity.WGSGravityModel;
15 import net.sf.openrocket.models.wind.PinkNoiseWindModel;
16 import net.sf.openrocket.rocketcomponent.Rocket;
17 import net.sf.openrocket.util.BugException;
18 import net.sf.openrocket.util.ChangeSource;
19 import net.sf.openrocket.util.GeodeticComputationStrategy;
20 import net.sf.openrocket.util.MathUtil;
21 import net.sf.openrocket.util.StateChangeListener;
22 import net.sf.openrocket.util.Utils;
23 import net.sf.openrocket.util.WorldCoordinate;
26 * A class holding simulation options in basic parameter form and which functions
27 * as a ChangeSource. A SimulationConditions instance is generated from this class
28 * using {@link #toSimulationConditions()}.
30 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
32 public class SimulationOptions implements ChangeSource, Cloneable {
34 public static final double MAX_LAUNCH_ROD_ANGLE = Math.PI / 3;
37 * The ISA standard atmosphere.
39 private static final AtmosphericModel ISA_ATMOSPHERIC_MODEL = new ExtendedISAModel();
42 private final Rocket rocket;
43 private String motorID = null;
47 * NOTE: When adding/modifying parameters, they must also be added to the
48 * equals and copyFrom methods!!
51 // TODO: HIGH: Fetch default values from Prefs!
53 private double launchRodLength = 1;
55 /** Launch rod angle > 0, radians from vertical */
56 private double launchRodAngle = 0;
58 /** Launch rod direction, 0 = upwind, PI = downwind. */
59 private double launchRodDirection = 0;
62 private double windAverage = 2.0;
63 private double windTurbulence = 0.1;
67 * SimulationOptions maintains the launch site parameters as separate double values,
68 * and converts them into a WorldCoordinate when converting to SimulationConditions.
70 private double launchAltitude = 0;
71 private double launchLatitude = 45;
72 private double launchLongitude = 0;
73 private GeodeticComputationStrategy geodeticComputation = GeodeticComputationStrategy.SPHERICAL;
75 private boolean useISA = true;
76 private double launchTemperature = ExtendedISAModel.STANDARD_TEMPERATURE;
77 private double launchPressure = ExtendedISAModel.STANDARD_PRESSURE;
80 private double timeStep = RK4SimulationStepper.RECOMMENDED_TIME_STEP;
81 private double maximumAngle = RK4SimulationStepper.RECOMMENDED_ANGLE_STEP;
83 private int randomSeed = new Random().nextInt();
85 private boolean calculateExtras = true;
88 private List<EventListener> listeners = new ArrayList<EventListener>();
92 public SimulationOptions(Rocket rocket) {
97 public Rocket getRocket() {
102 public String getMotorConfigurationID() {
107 * Set the motor configuration ID. This must be a valid motor configuration ID of
108 * the rocket, otherwise the configuration is set to <code>null</code>.
110 * @param id the configuration to set.
112 public void setMotorConfigurationID(String id) {
115 if (!rocket.isMotorConfigurationID(id))
124 public double getLaunchRodLength() {
125 return launchRodLength;
128 public void setLaunchRodLength(double launchRodLength) {
129 if (MathUtil.equals(this.launchRodLength, launchRodLength))
131 this.launchRodLength = launchRodLength;
136 public double getLaunchRodAngle() {
137 return launchRodAngle;
140 public void setLaunchRodAngle(double launchRodAngle) {
141 launchRodAngle = MathUtil.clamp(launchRodAngle, 0, MAX_LAUNCH_ROD_ANGLE);
142 if (MathUtil.equals(this.launchRodAngle, launchRodAngle))
144 this.launchRodAngle = launchRodAngle;
149 public double getLaunchRodDirection() {
150 return launchRodDirection;
153 public void setLaunchRodDirection(double launchRodDirection) {
154 launchRodDirection = MathUtil.reduce180(launchRodDirection);
155 if (MathUtil.equals(this.launchRodDirection, launchRodDirection))
157 this.launchRodDirection = launchRodDirection;
163 public double getWindSpeedAverage() {
167 public void setWindSpeedAverage(double windAverage) {
168 if (MathUtil.equals(this.windAverage, windAverage))
170 this.windAverage = MathUtil.max(windAverage, 0);
175 public double getWindSpeedDeviation() {
176 return windAverage * windTurbulence;
179 public void setWindSpeedDeviation(double windDeviation) {
180 if (windAverage < 0.1) {
183 setWindTurbulenceIntensity(windDeviation / windAverage);
188 * Return the wind turbulence intensity (standard deviation / average).
190 * @return the turbulence intensity
192 public double getWindTurbulenceIntensity() {
193 return windTurbulence;
197 * Set the wind standard deviation to match the given turbulence intensity.
199 * @param intensity the turbulence intensity
201 public void setWindTurbulenceIntensity(double intensity) {
202 // Does not check equality so that setWindSpeedDeviation can be sure of event firing
203 this.windTurbulence = intensity;
211 public double getLaunchAltitude() {
212 return launchAltitude;
215 public void setLaunchAltitude(double altitude) {
216 if (MathUtil.equals(this.launchAltitude, altitude))
218 this.launchAltitude = altitude;
223 public double getLaunchLatitude() {
224 return launchLatitude;
227 public void setLaunchLatitude(double launchLatitude) {
228 launchLatitude = MathUtil.clamp(launchLatitude, -90, 90);
229 if (MathUtil.equals(this.launchLatitude, launchLatitude))
231 this.launchLatitude = launchLatitude;
235 public double getLaunchLongitude() {
236 return launchLongitude;
239 public void setLaunchLongitude(double launchLongitude) {
240 launchLongitude = MathUtil.clamp(launchLongitude, -180, 180);
241 if (MathUtil.equals(this.launchLongitude, launchLongitude))
243 this.launchLongitude = launchLongitude;
248 public GeodeticComputationStrategy getGeodeticComputation() {
249 return geodeticComputation;
252 public void setGeodeticComputation(GeodeticComputationStrategy geodeticComputation) {
253 if (this.geodeticComputation == geodeticComputation)
255 if (geodeticComputation == null) {
256 throw new IllegalArgumentException("strategy cannot be null");
258 this.geodeticComputation = geodeticComputation;
263 public boolean isISAAtmosphere() {
267 public void setISAAtmosphere(boolean isa) {
275 public double getLaunchTemperature() {
276 return launchTemperature;
281 public void setLaunchTemperature(double launchTemperature) {
282 if (MathUtil.equals(this.launchTemperature, launchTemperature))
284 this.launchTemperature = launchTemperature;
290 public double getLaunchPressure() {
291 return launchPressure;
296 public void setLaunchPressure(double launchPressure) {
297 if (MathUtil.equals(this.launchPressure, launchPressure))
299 this.launchPressure = launchPressure;
305 * Returns an atmospheric model corresponding to the launch conditions. The
306 * atmospheric models may be shared between different calls.
308 * @return an AtmosphericModel object.
310 private AtmosphericModel getAtmosphericModel() {
312 return ISA_ATMOSPHERIC_MODEL;
314 return new ExtendedISAModel(getLaunchAltitude(), launchTemperature, launchPressure);
318 public double getTimeStep() {
322 public void setTimeStep(double timeStep) {
323 if (MathUtil.equals(this.timeStep, timeStep))
325 this.timeStep = timeStep;
329 public double getMaximumStepAngle() {
333 public void setMaximumStepAngle(double maximumAngle) {
334 maximumAngle = MathUtil.clamp(maximumAngle, 1 * Math.PI / 180, 20 * Math.PI / 180);
335 if (MathUtil.equals(this.maximumAngle, maximumAngle))
337 this.maximumAngle = maximumAngle;
343 public boolean getCalculateExtras() {
344 return calculateExtras;
349 public void setCalculateExtras(boolean calculateExtras) {
350 if (this.calculateExtras == calculateExtras)
352 this.calculateExtras = calculateExtras;
358 public int getRandomSeed() {
362 public void setRandomSeed(int randomSeed) {
363 if (this.randomSeed == randomSeed) {
366 this.randomSeed = randomSeed;
368 * This does not fire an event since we don't want to invalidate simulation results
369 * due to changing the seed value. This needs to be revisited if the user is ever
370 * allowed to select the seed value.
372 // fireChangeEvent();
376 * Randomize the random seed value.
378 public void randomizeSeed() {
379 this.randomSeed = new Random().nextInt();
380 // fireChangeEvent();
386 public SimulationOptions clone() {
388 SimulationOptions copy = (SimulationOptions) super.clone();
389 copy.listeners = new ArrayList<EventListener>();
391 } catch (CloneNotSupportedException e) {
392 throw new BugException(e);
397 public void copyFrom(SimulationOptions src) {
399 if (this.rocket == src.rocket) {
401 this.motorID = src.motorID;
405 if (src.rocket.hasMotors(src.motorID)) {
406 // Try to find a matching motor ID
407 String motorDesc = src.rocket.getMotorConfigurationDescription(src.motorID);
408 String matchID = null;
410 for (String id : this.rocket.getMotorConfigurationIDs()) {
411 if (motorDesc.equals(this.rocket.getMotorConfigurationDescription(id))) {
417 this.motorID = matchID;
423 this.launchAltitude = src.launchAltitude;
424 this.launchLatitude = src.launchLatitude;
425 this.launchLongitude = src.launchLongitude;
426 this.launchPressure = src.launchPressure;
427 this.launchRodAngle = src.launchRodAngle;
428 this.launchRodDirection = src.launchRodDirection;
429 this.launchRodLength = src.launchRodLength;
430 this.launchTemperature = src.launchTemperature;
431 this.maximumAngle = src.maximumAngle;
432 this.timeStep = src.timeStep;
433 this.windAverage = src.windAverage;
434 this.windTurbulence = src.windTurbulence;
435 this.calculateExtras = src.calculateExtras;
436 this.randomSeed = src.randomSeed;
444 * Compares whether the two simulation conditions are equal. The two are considered
445 * equal if the rocket, motor id and all variables are equal.
448 public boolean equals(Object other) {
449 if (!(other instanceof SimulationOptions))
451 SimulationOptions o = (SimulationOptions) other;
452 return ((this.rocket == o.rocket) &&
453 Utils.equals(this.motorID, o.motorID) &&
454 MathUtil.equals(this.launchAltitude, o.launchAltitude) &&
455 MathUtil.equals(this.launchLatitude, o.launchLatitude) &&
456 MathUtil.equals(this.launchLongitude, o.launchLongitude) &&
457 MathUtil.equals(this.launchPressure, o.launchPressure) &&
458 MathUtil.equals(this.launchRodAngle, o.launchRodAngle) &&
459 MathUtil.equals(this.launchRodDirection, o.launchRodDirection) &&
460 MathUtil.equals(this.launchRodLength, o.launchRodLength) &&
461 MathUtil.equals(this.launchTemperature, o.launchTemperature) &&
462 MathUtil.equals(this.maximumAngle, o.maximumAngle) &&
463 MathUtil.equals(this.timeStep, o.timeStep) &&
464 MathUtil.equals(this.windAverage, o.windAverage) &&
465 MathUtil.equals(this.windTurbulence, o.windTurbulence) &&
466 this.calculateExtras == o.calculateExtras && this.randomSeed == o.randomSeed);
470 * Hashcode method compatible with {@link #equals(Object)}.
473 public int hashCode() {
475 return rocket.hashCode();
476 return rocket.hashCode() + motorID.hashCode();
480 public void addChangeListener(EventListener listener) {
481 listeners.add(listener);
485 public void removeChangeListener(EventListener listener) {
486 listeners.remove(listener);
489 private final EventObject event = new EventObject(this);
491 private void fireChangeEvent() {
493 // Copy the list before iterating to prevent concurrent modification exceptions.
494 EventListener[] list = listeners.toArray(new EventListener[0]);
495 for (EventListener l : list) {
496 if ( l instanceof StateChangeListener ) {
497 ((StateChangeListener)l).stateChanged(event);
503 // TODO: HIGH: Clean up
504 public SimulationConditions toSimulationConditions() {
505 SimulationConditions conditions = new SimulationConditions();
507 conditions.setRocket((Rocket) getRocket().copy());
508 conditions.setMotorConfigurationID(getMotorConfigurationID());
509 conditions.setLaunchRodLength(getLaunchRodLength());
510 conditions.setLaunchRodAngle(getLaunchRodAngle());
511 conditions.setLaunchRodDirection(getLaunchRodDirection());
512 conditions.setLaunchSite(new WorldCoordinate(getLaunchLatitude(), getLaunchLongitude(), getLaunchAltitude()));
513 conditions.setGeodeticComputation(getGeodeticComputation());
514 conditions.setRandomSeed(randomSeed);
516 PinkNoiseWindModel windModel = new PinkNoiseWindModel(randomSeed);
517 windModel.setAverage(getWindSpeedAverage());
518 windModel.setStandardDeviation(getWindSpeedDeviation());
519 conditions.setWindModel(windModel);
521 conditions.setAtmosphericModel(getAtmosphericModel());
523 GravityModel gravityModel = new WGSGravityModel();
525 conditions.setGravityModel(gravityModel);
527 conditions.setAerodynamicCalculator(new BarrowmanCalculator());
528 conditions.setMassCalculator(new BasicMassCalculator());
530 conditions.setTimeStep(getTimeStep());
531 conditions.setMaximumAngleStep(getMaximumStepAngle());
533 conditions.setCalculateExtras(getCalculateExtras());