1 package net.sf.openrocket.motor;
3 import java.io.Serializable;
4 import java.text.Collator;
5 import java.util.Arrays;
6 import java.util.Locale;
8 import net.sf.openrocket.logging.LogHelper;
9 import net.sf.openrocket.models.atmosphere.AtmosphericConditions;
10 import net.sf.openrocket.startup.Application;
11 import net.sf.openrocket.util.BugException;
12 import net.sf.openrocket.util.Coordinate;
13 import net.sf.openrocket.util.Inertia;
14 import net.sf.openrocket.util.MathUtil;
17 public class ThrustCurveMotor implements Motor, Comparable<ThrustCurveMotor> {
18 private static final LogHelper log = Application.getLogger();
20 public static final double MAX_THRUST = 10e6;
23 private static final Collator COLLATOR = Collator.getInstance(Locale.US);
25 COLLATOR.setStrength(Collator.PRIMARY);
27 private static final DesignationComparator DESIGNATION_COMPARATOR = new DesignationComparator();
31 private final Manufacturer manufacturer;
32 private final String designation;
33 private final String description;
34 private final Motor.Type type;
35 private final double[] delays;
36 private final double diameter;
37 private final double length;
38 private final double[] time;
39 private final double[] thrust;
40 private final Coordinate[] cg;
42 private double maxThrust;
43 private double burnTime;
44 private double averageThrust;
45 private double totalImpulse;
49 * Sole constructor. Sets all the properties of the motor.
51 * @param manufacturer the manufacturer of the motor.
52 * @param designation the designation of the motor.
53 * @param description extra description of the motor.
54 * @param type the motor type
55 * @param delays the delays defined for this thrust curve
56 * @param diameter diameter of the motor.
57 * @param length length of the motor.
58 * @param time the time points for the thrust curve.
59 * @param thrust thrust at the time points.
60 * @param cg cg at the time points.
62 public ThrustCurveMotor(Manufacturer manufacturer, String designation, String description,
63 Motor.Type type, double[] delays, double diameter, double length,
64 double[] time, double[] thrust, Coordinate[] cg) {
66 // Check argument validity
67 if ((time.length != thrust.length) || (time.length != cg.length)) {
68 throw new IllegalArgumentException("Array lengths do not match, " +
69 "time:" + time.length + " thrust:" + thrust.length +
72 if (time.length < 2) {
73 throw new IllegalArgumentException("Too short thrust-curve, length=" +
76 for (int i = 0; i < time.length - 1; i++) {
77 if (time[i + 1] < time[i]) {
78 throw new IllegalArgumentException("Time goes backwards, " +
79 "time[" + i + "]=" + time[i] + " " +
80 "time[" + (i + 1) + "]=" + time[i + 1]);
83 if (!MathUtil.equals(time[0], 0)) {
84 throw new IllegalArgumentException("Curve starts at time " + time[0]);
86 if (!MathUtil.equals(thrust[0], 0)) {
87 throw new IllegalArgumentException("Curve starts at thrust " + thrust[0]);
89 if (!MathUtil.equals(thrust[thrust.length - 1], 0)) {
90 throw new IllegalArgumentException("Curve ends at thrust " +
91 thrust[thrust.length - 1]);
93 for (double t : thrust) {
95 throw new IllegalArgumentException("Negative thrust.");
97 if (t > MAX_THRUST || Double.isNaN(t)) {
98 throw new IllegalArgumentException("Invalid thrust " + t);
101 for (Coordinate c : cg) {
103 throw new IllegalArgumentException("Invalid CG " + c);
105 if (c.x < 0 || c.x > length) {
106 throw new IllegalArgumentException("Invalid CG position " + c.x);
109 throw new IllegalArgumentException("Negative mass " + c.weight);
113 if (type != Motor.Type.SINGLE && type != Motor.Type.RELOAD &&
114 type != Motor.Type.HYBRID && type != Motor.Type.UNKNOWN) {
115 throw new IllegalArgumentException("Illegal motor type=" + type);
119 this.manufacturer = manufacturer;
120 this.designation = designation;
121 this.description = description;
123 this.delays = delays.clone();
124 this.diameter = diameter;
125 this.length = length;
126 this.time = time.clone();
127 this.thrust = thrust.clone();
128 this.cg = cg.clone();
136 * Get the manufacturer of this motor.
138 * @return the manufacturer
140 public Manufacturer getManufacturer() {
146 * Return the array of time points for this thrust curve.
147 * @return an array of time points where the thrust is sampled
149 public double[] getTimePoints() {
154 * Returns the array of thrust points for this thrust curve.
155 * @return an array of thrust samples
157 public double[] getThrustPoints() {
158 return thrust.clone();
162 * Returns the array of CG points for this thrust curve.
163 * @return an array of CG samples
165 public Coordinate[] getCGPoints() {
170 * Return a list of standard delays defined for this motor.
171 * @return a list of standard delays
173 public double[] getStandardDelays() {
174 return delays.clone();
181 * NOTE: In most cases you want to examine the motor type of the ThrustCurveMotorSet,
182 * not the ThrustCurveMotor itself.
185 public Type getMotorType() {
191 public String getDesignation() {
196 public String getDesignation(double delay) {
197 return designation + "-" + getDelayString(delay);
202 public String getDescription() {
207 public double getDiameter() {
212 public double getLength() {
218 public MotorInstance getInstance() {
219 return new ThrustCurveMotorInstance();
224 public Coordinate getLaunchCG() {
229 public Coordinate getEmptyCG() {
230 return cg[cg.length - 1];
237 public double getBurnTimeEstimate() {
242 public double getAverageThrustEstimate() {
243 return averageThrust;
247 public double getMaxThrustEstimate() {
252 public double getTotalImpulseEstimate() {
259 * Compute the general statistics of this motor.
261 private void computeStatistics() {
265 for (double t : thrust) {
272 double thrustLimit = maxThrust * MARGINAL_THRUST;
273 double burnStart, burnEnd;
276 for (pos = 1; pos < thrust.length; pos++) {
277 if (thrust[pos] >= thrustLimit)
280 if (pos >= thrust.length) {
281 throw new BugException("Could not compute burn start time, maxThrust=" + maxThrust +
282 " limit=" + thrustLimit + " thrust=" + Arrays.toString(thrust));
284 if (MathUtil.equals(thrust[pos - 1], thrust[pos])) {
286 burnStart = (time[pos - 1] + time[pos]) / 2;
288 burnStart = MathUtil.map(thrustLimit, thrust[pos - 1], thrust[pos], time[pos - 1], time[pos]);
293 for (pos = thrust.length - 2; pos >= 0; pos--) {
294 if (thrust[pos] >= thrustLimit)
298 throw new BugException("Could not compute burn end time, maxThrust=" + maxThrust +
299 " limit=" + thrustLimit + " thrust=" + Arrays.toString(thrust));
301 if (MathUtil.equals(thrust[pos], thrust[pos + 1])) {
303 burnEnd = (time[pos] + time[pos + 1]) / 2;
305 burnEnd = MathUtil.map(thrustLimit, thrust[pos], thrust[pos + 1],
306 time[pos], time[pos + 1]);
311 burnTime = Math.max(burnEnd - burnStart, 0);
314 // Total impulse and average thrust
318 for (pos = 0; pos < time.length - 1; pos++) {
319 double t0 = time[pos];
320 double t1 = time[pos + 1];
321 double f0 = thrust[pos];
322 double f1 = thrust[pos + 1];
324 totalImpulse += (t1 - t0) * (f0 + f1) / 2;
326 if (t0 < burnStart && t1 > burnStart) {
327 double fStart = MathUtil.map(burnStart, t0, t1, f0, f1);
328 averageThrust += (fStart + f1) / 2 * (t1 - burnStart);
329 } else if (t0 >= burnStart && t1 <= burnEnd) {
330 averageThrust += (f0 + f1) / 2 * (t1 - t0);
331 } else if (t0 < burnEnd && t1 > burnEnd) {
332 double fEnd = MathUtil.map(burnEnd, t0, t1, f0, f1);
333 averageThrust += (f0 + fEnd) / 2 * (burnEnd - t0);
338 averageThrust /= burnTime;
346 ////////// Static methods
349 * Return a String representation of a delay time. If the delay is {@link #PLUGGED},
352 * @param delay the delay time.
353 * @return the <code>String</code> representation.
355 public static String getDelayString(double delay) {
356 return getDelayString(delay, "P");
360 * Return a String representation of a delay time. If the delay is {@link #PLUGGED},
361 * <code>plugged</code> is returned.
363 * @param delay the delay time.
364 * @param plugged the return value if there is no ejection charge.
365 * @return the String representation.
367 public static String getDelayString(double delay, String plugged) {
368 if (delay == PLUGGED)
370 delay = Math.rint(delay * 10) / 10;
371 if (MathUtil.equals(delay, Math.rint(delay)))
372 return "" + ((int) delay);
378 //////// Motor instance implementation ////////
379 private class ThrustCurveMotorInstance implements MotorInstance {
381 private int position;
383 // Previous time step value
384 private double prevTime;
386 // Average thrust during previous step
387 private double stepThrust;
388 // Instantaneous thrust at current time point
389 private double instThrust;
391 // Average CG during previous step
392 private Coordinate stepCG;
393 // Instantaneous CG at current time point
394 private Coordinate instCG;
396 private final double unitRotationalInertia;
397 private final double unitLongitudinalInertia;
399 private int modID = 0;
401 public ThrustCurveMotorInstance() {
402 log.debug("ThrustCurveMotor: Creating motor instance of " + ThrustCurveMotor.this);
409 unitRotationalInertia = Inertia.filledCylinderRotational(getDiameter() / 2);
410 unitLongitudinalInertia = Inertia.filledCylinderLongitudinal(getDiameter() / 2, getLength());
414 public double getTime() {
419 public Coordinate getCG() {
424 public double getLongitudinalInertia() {
425 return unitLongitudinalInertia * stepCG.weight;
429 public double getRotationalInertia() {
430 return unitRotationalInertia * stepCG.weight;
434 public double getThrust() {
439 public boolean isActive() {
440 return prevTime < time[time.length - 1];
444 public void step(double nextTime, double acceleration, AtmosphericConditions cond) {
446 if (!(nextTime >= prevTime)) {
448 throw new IllegalArgumentException("Stepping backwards in time, current=" +
449 prevTime + " new=" + nextTime);
451 if (MathUtil.equals(prevTime, nextTime)) {
457 if (position >= time.length - 1) {
462 stepCG = cg[cg.length - 1];
467 // Compute average & instantaneous thrust
468 if (nextTime < time[position + 1]) {
470 // Time step between time points
471 double nextF = MathUtil.map(nextTime, time[position], time[position + 1],
472 thrust[position], thrust[position + 1]);
473 stepThrust = (instThrust + nextF) / 2;
478 // Portion of previous step
479 stepThrust = (instThrust + thrust[position + 1]) / 2 * (time[position + 1] - prevTime);
483 while ((position < time.length - 1) && (nextTime >= time[position + 1])) {
484 stepThrust += (thrust[position] + thrust[position + 1]) / 2 *
485 (time[position + 1] - time[position]);
490 if (position < time.length - 1) {
491 instThrust = MathUtil.map(nextTime, time[position], time[position + 1],
492 thrust[position], thrust[position + 1]);
493 stepThrust += (thrust[position] + instThrust) / 2 *
494 (nextTime - time[position]);
496 // Thrust ended during this step
500 stepThrust /= (nextTime - prevTime);
504 // Compute average and instantaneous CG (simple average between points)
506 if (position < time.length - 1) {
507 nextCG = MathUtil.map(nextTime, time[position], time[position + 1],
508 cg[position], cg[position + 1]);
510 nextCG = cg[cg.length - 1];
512 stepCG = instCG.add(nextCG).multiply(0.5);
520 public MotorInstance clone() {
522 return (MotorInstance) super.clone();
523 } catch (CloneNotSupportedException e) {
524 throw new BugException("CloneNotSupportedException", e);
529 public int getModID() {
537 public int compareTo(ThrustCurveMotor other) {
542 value = COLLATOR.compare(this.manufacturer.getDisplayName(),
543 ((ThrustCurveMotor) other).manufacturer.getDisplayName());
548 value = DESIGNATION_COMPARATOR.compare(this.getDesignation(), other.getDesignation());
553 value = (int) ((this.getDiameter() - other.getDiameter()) * 1000000);
558 value = (int) ((this.getLength() - other.getLength()) * 1000000);