1 package net.sf.openrocket.simulation;
3 import java.util.ArrayList;
5 import java.util.Random;
7 import javax.swing.event.ChangeEvent;
8 import javax.swing.event.ChangeListener;
10 import net.sf.openrocket.aerodynamics.BarrowmanCalculator;
11 import net.sf.openrocket.masscalc.BasicMassCalculator;
12 import net.sf.openrocket.models.atmosphere.AtmosphericModel;
13 import net.sf.openrocket.models.atmosphere.ExtendedISAModel;
14 import net.sf.openrocket.models.gravity.GravityModel;
15 import net.sf.openrocket.models.gravity.WGSGravityModel;
16 import net.sf.openrocket.models.wind.PinkNoiseWindModel;
17 import net.sf.openrocket.rocketcomponent.Rocket;
18 import net.sf.openrocket.util.BugException;
19 import net.sf.openrocket.util.ChangeSource;
20 import net.sf.openrocket.util.GeodeticComputationStrategy;
21 import net.sf.openrocket.util.MathUtil;
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;
66 * SimulationOptions maintains the launch site parameters as separate double values,
67 * and converts them into a WorldCoordinate when converting to SimulationConditions.
69 private double launchAltitude = 0;
70 private double launchLatitude = 45;
71 private double launchLongitude = 0;
72 private GeodeticComputationStrategy geodeticComputation = GeodeticComputationStrategy.SPHERICAL;
74 private boolean useISA = true;
75 private double launchTemperature = ExtendedISAModel.STANDARD_TEMPERATURE;
76 private double launchPressure = ExtendedISAModel.STANDARD_PRESSURE;
79 private double timeStep = RK4SimulationStepper.RECOMMENDED_TIME_STEP;
80 private double maximumAngle = RK4SimulationStepper.RECOMMENDED_ANGLE_STEP;
82 private int randomSeed = new Random().nextInt();
84 private boolean calculateExtras = true;
87 private List<ChangeListener> listeners = new ArrayList<ChangeListener>();
91 public SimulationOptions(Rocket rocket) {
96 public Rocket getRocket() {
101 public String getMotorConfigurationID() {
106 * Set the motor configuration ID. This must be a valid motor configuration ID of
107 * the rocket, otherwise the configuration is set to <code>null</code>.
109 * @param id the configuration to set.
111 public void setMotorConfigurationID(String id) {
114 if (!rocket.isMotorConfigurationID(id))
123 public double getLaunchRodLength() {
124 return launchRodLength;
127 public void setLaunchRodLength(double launchRodLength) {
128 if (MathUtil.equals(this.launchRodLength, launchRodLength))
130 this.launchRodLength = launchRodLength;
135 public double getLaunchRodAngle() {
136 return launchRodAngle;
139 public void setLaunchRodAngle(double launchRodAngle) {
140 launchRodAngle = MathUtil.clamp(launchRodAngle, 0, MAX_LAUNCH_ROD_ANGLE);
141 if (MathUtil.equals(this.launchRodAngle, launchRodAngle))
143 this.launchRodAngle = launchRodAngle;
148 public double getLaunchRodDirection() {
149 return launchRodDirection;
152 public void setLaunchRodDirection(double launchRodDirection) {
153 launchRodDirection = MathUtil.reduce180(launchRodDirection);
154 if (MathUtil.equals(this.launchRodDirection, launchRodDirection))
156 this.launchRodDirection = launchRodDirection;
162 public double getWindSpeedAverage() {
166 public void setWindSpeedAverage(double windAverage) {
167 if (MathUtil.equals(this.windAverage, windAverage))
169 this.windAverage = MathUtil.max(windAverage, 0);
174 public double getWindSpeedDeviation() {
175 return windAverage * windTurbulence;
178 public void setWindSpeedDeviation(double windDeviation) {
179 if (windAverage < 0.1) {
182 setWindTurbulenceIntensity(windDeviation / windAverage);
187 * Return the wind turbulence intensity (standard deviation / average).
189 * @return the turbulence intensity
191 public double getWindTurbulenceIntensity() {
192 return windTurbulence;
196 * Set the wind standard deviation to match the given turbulence intensity.
198 * @param intensity the turbulence intensity
200 public void setWindTurbulenceIntensity(double intensity) {
201 // Does not check equality so that setWindSpeedDeviation can be sure of event firing
202 this.windTurbulence = intensity;
210 public double getLaunchAltitude() {
211 return launchAltitude;
214 public void setLaunchAltitude(double altitude) {
215 if (MathUtil.equals(this.launchAltitude, altitude))
217 this.launchAltitude = altitude;
222 public double getLaunchLatitude() {
223 return launchLatitude;
226 public void setLaunchLatitude(double launchLatitude) {
227 launchLatitude = MathUtil.clamp(launchLatitude, -90, 90);
228 if (MathUtil.equals(this.launchLatitude, launchLatitude))
230 this.launchLatitude = launchLatitude;
234 public double getLaunchLongitude() {
235 return launchLongitude;
238 public void setLaunchLongitude(double launchLongitude) {
239 launchLongitude = MathUtil.clamp(launchLongitude, -180, 180);
240 if (MathUtil.equals(this.launchLongitude, launchLongitude))
242 this.launchLongitude = launchLongitude;
247 public GeodeticComputationStrategy getGeodeticComputation() {
248 return geodeticComputation;
251 public void setGeodeticComputation(GeodeticComputationStrategy geodeticComputation) {
252 if (this.geodeticComputation == geodeticComputation)
254 if (geodeticComputation == null) {
255 throw new IllegalArgumentException("strategy cannot be null");
257 this.geodeticComputation = geodeticComputation;
262 public boolean isISAAtmosphere() {
266 public void setISAAtmosphere(boolean isa) {
274 public double getLaunchTemperature() {
275 return launchTemperature;
280 public void setLaunchTemperature(double launchTemperature) {
281 if (MathUtil.equals(this.launchTemperature, launchTemperature))
283 this.launchTemperature = launchTemperature;
289 public double getLaunchPressure() {
290 return launchPressure;
295 public void setLaunchPressure(double launchPressure) {
296 if (MathUtil.equals(this.launchPressure, launchPressure))
298 this.launchPressure = launchPressure;
304 * Returns an atmospheric model corresponding to the launch conditions. The
305 * atmospheric models may be shared between different calls.
307 * @return an AtmosphericModel object.
309 private AtmosphericModel getAtmosphericModel() {
311 return ISA_ATMOSPHERIC_MODEL;
313 return new ExtendedISAModel(getLaunchAltitude(), launchTemperature, launchPressure);
317 public double getTimeStep() {
321 public void setTimeStep(double timeStep) {
322 if (MathUtil.equals(this.timeStep, timeStep))
324 this.timeStep = timeStep;
328 public double getMaximumStepAngle() {
332 public void setMaximumStepAngle(double maximumAngle) {
333 maximumAngle = MathUtil.clamp(maximumAngle, 1 * Math.PI / 180, 20 * Math.PI / 180);
334 if (MathUtil.equals(this.maximumAngle, maximumAngle))
336 this.maximumAngle = maximumAngle;
342 public boolean getCalculateExtras() {
343 return calculateExtras;
348 public void setCalculateExtras(boolean calculateExtras) {
349 if (this.calculateExtras == calculateExtras)
351 this.calculateExtras = calculateExtras;
357 public int getRandomSeed() {
361 public void setRandomSeed(int randomSeed) {
362 if (this.randomSeed == randomSeed) {
365 this.randomSeed = randomSeed;
367 * This does not fire an event since we don't want to invalidate simulation results
368 * due to changing the seed value. This needs to be revisited if the user is ever
369 * allowed to select the seed value.
371 // fireChangeEvent();
375 * Randomize the random seed value.
377 public void randomizeSeed() {
378 this.randomSeed = new Random().nextInt();
379 // fireChangeEvent();
385 public SimulationOptions clone() {
387 SimulationOptions copy = (SimulationOptions) super.clone();
388 copy.listeners = new ArrayList<ChangeListener>();
390 } catch (CloneNotSupportedException e) {
391 throw new BugException(e);
396 public void copyFrom(SimulationOptions src) {
398 if (this.rocket == src.rocket) {
400 this.motorID = src.motorID;
404 if (src.rocket.hasMotors(src.motorID)) {
405 // Try to find a matching motor ID
406 String motorDesc = src.rocket.getMotorConfigurationDescription(src.motorID);
407 String matchID = null;
409 for (String id : this.rocket.getMotorConfigurationIDs()) {
410 if (motorDesc.equals(this.rocket.getMotorConfigurationDescription(id))) {
416 this.motorID = matchID;
422 this.launchAltitude = src.launchAltitude;
423 this.launchLatitude = src.launchLatitude;
424 this.launchLongitude = src.launchLongitude;
425 this.launchPressure = src.launchPressure;
426 this.launchRodAngle = src.launchRodAngle;
427 this.launchRodDirection = src.launchRodDirection;
428 this.launchRodLength = src.launchRodLength;
429 this.launchTemperature = src.launchTemperature;
430 this.maximumAngle = src.maximumAngle;
431 this.timeStep = src.timeStep;
432 this.windAverage = src.windAverage;
433 this.windTurbulence = src.windTurbulence;
434 this.calculateExtras = src.calculateExtras;
435 this.randomSeed = src.randomSeed;
443 * Compares whether the two simulation conditions are equal. The two are considered
444 * equal if the rocket, motor id and all variables are equal.
447 public boolean equals(Object other) {
448 if (!(other instanceof SimulationOptions))
450 SimulationOptions o = (SimulationOptions) other;
451 return ((this.rocket == o.rocket) &&
452 Utils.equals(this.motorID, o.motorID) &&
453 MathUtil.equals(this.launchAltitude, o.launchAltitude) &&
454 MathUtil.equals(this.launchLatitude, o.launchLatitude) &&
455 MathUtil.equals(this.launchLongitude, o.launchLongitude) &&
456 MathUtil.equals(this.launchPressure, o.launchPressure) &&
457 MathUtil.equals(this.launchRodAngle, o.launchRodAngle) &&
458 MathUtil.equals(this.launchRodDirection, o.launchRodDirection) &&
459 MathUtil.equals(this.launchRodLength, o.launchRodLength) &&
460 MathUtil.equals(this.launchTemperature, o.launchTemperature) &&
461 MathUtil.equals(this.maximumAngle, o.maximumAngle) &&
462 MathUtil.equals(this.timeStep, o.timeStep) &&
463 MathUtil.equals(this.windAverage, o.windAverage) &&
464 MathUtil.equals(this.windTurbulence, o.windTurbulence) &&
465 this.calculateExtras == o.calculateExtras && this.randomSeed == o.randomSeed);
469 * Hashcode method compatible with {@link #equals(Object)}.
472 public int hashCode() {
474 return rocket.hashCode();
475 return rocket.hashCode() + motorID.hashCode();
479 public void addChangeListener(ChangeListener listener) {
480 listeners.add(listener);
484 public void removeChangeListener(ChangeListener listener) {
485 listeners.remove(listener);
488 private final ChangeEvent event = new ChangeEvent(this);
490 private void fireChangeEvent() {
491 ChangeListener[] array = listeners.toArray(new ChangeListener[0]);
493 for (int i = array.length - 1; i >= 0; i--) {
494 array[i].stateChanged(event);
499 // TODO: HIGH: Clean up
500 public SimulationConditions toSimulationConditions() {
501 SimulationConditions conditions = new SimulationConditions();
503 conditions.setRocket((Rocket) getRocket().copy());
504 conditions.setMotorConfigurationID(getMotorConfigurationID());
505 conditions.setLaunchRodLength(getLaunchRodLength());
506 conditions.setLaunchRodAngle(getLaunchRodAngle());
507 conditions.setLaunchRodDirection(getLaunchRodDirection());
508 conditions.setLaunchSite(new WorldCoordinate(getLaunchLatitude(), getLaunchLongitude(), getLaunchAltitude()));
509 conditions.setGeodeticComputation(getGeodeticComputation());
510 conditions.setRandomSeed(randomSeed);
512 PinkNoiseWindModel windModel = new PinkNoiseWindModel(randomSeed);
513 windModel.setAverage(getWindSpeedAverage());
514 windModel.setStandardDeviation(getWindSpeedDeviation());
515 conditions.setWindModel(windModel);
517 conditions.setAtmosphericModel(getAtmosphericModel());
519 GravityModel gravityModel = new WGSGravityModel();
521 conditions.setGravityModel(gravityModel);
523 conditions.setAerodynamicCalculator(new BarrowmanCalculator());
524 conditions.setMassCalculator(new BasicMassCalculator());
526 conditions.setTimeStep(getTimeStep());
527 conditions.setMaximumAngleStep(getMaximumStepAngle());
529 conditions.setCalculateExtras(getCalculateExtras());