1 package net.sf.openrocket.motor;
3 import java.text.Collator;
4 import java.util.Arrays;
5 import java.util.Locale;
7 import net.sf.openrocket.logging.LogHelper;
8 import net.sf.openrocket.models.atmosphere.AtmosphericConditions;
9 import net.sf.openrocket.startup.Application;
10 import net.sf.openrocket.util.BugException;
11 import net.sf.openrocket.util.Coordinate;
12 import net.sf.openrocket.util.Inertia;
13 import net.sf.openrocket.util.MathUtil;
16 public class ThrustCurveMotor implements Motor, Comparable<ThrustCurveMotor> {
17 private static final LogHelper log = Application.getLogger();
19 public static final double MAX_THRUST = 10e6;
22 private static final Collator COLLATOR = Collator.getInstance(Locale.US);
24 COLLATOR.setStrength(Collator.PRIMARY);
26 private static final DesignationComparator DESIGNATION_COMPARATOR = new DesignationComparator();
30 private final Manufacturer manufacturer;
31 private final String designation;
32 private final String description;
33 private final Motor.Type type;
34 private final double[] delays;
35 private final double diameter;
36 private final double length;
37 private final double[] time;
38 private final double[] thrust;
39 private final Coordinate[] cg;
41 private double maxThrust;
42 private double burnTime;
43 private double averageThrust;
44 private double totalImpulse;
48 * Sole constructor. Sets all the properties of the motor.
50 * @param manufacturer the manufacturer of the motor.
51 * @param designation the designation of the motor.
52 * @param description extra description of the motor.
53 * @param type the motor type
54 * @param delays the delays defined for this thrust curve
55 * @param diameter diameter of the motor.
56 * @param length length of the motor.
57 * @param time the time points for the thrust curve.
58 * @param thrust thrust at the time points.
59 * @param cg cg at the time points.
61 public ThrustCurveMotor(Manufacturer manufacturer, String designation, String description,
62 Motor.Type type, double[] delays, double diameter, double length,
63 double[] time, double[] thrust, Coordinate[] cg) {
65 // Check argument validity
66 if ((time.length != thrust.length) || (time.length != cg.length)) {
67 throw new IllegalArgumentException("Array lengths do not match, " +
68 "time:" + time.length + " thrust:" + thrust.length +
71 if (time.length < 2) {
72 throw new IllegalArgumentException("Too short thrust-curve, length=" +
75 for (int i = 0; i < time.length - 1; i++) {
76 if (time[i + 1] < time[i]) {
77 throw new IllegalArgumentException("Time goes backwards, " +
78 "time[" + i + "]=" + time[i] + " " +
79 "time[" + (i + 1) + "]=" + time[i + 1]);
82 if (!MathUtil.equals(time[0], 0)) {
83 throw new IllegalArgumentException("Curve starts at time " + time[0]);
85 if (!MathUtil.equals(thrust[0], 0)) {
86 throw new IllegalArgumentException("Curve starts at thrust " + thrust[0]);
88 if (!MathUtil.equals(thrust[thrust.length - 1], 0)) {
89 throw new IllegalArgumentException("Curve ends at thrust " +
90 thrust[thrust.length - 1]);
92 for (double t : thrust) {
94 throw new IllegalArgumentException("Negative thrust.");
96 if (t > MAX_THRUST || Double.isNaN(t)) {
97 throw new IllegalArgumentException("Invalid thrust " + t);
100 for (Coordinate c : cg) {
102 throw new IllegalArgumentException("Invalid CG " + c);
104 if (c.x < 0 || c.x > length) {
105 throw new IllegalArgumentException("Invalid CG position " + c.x);
108 throw new IllegalArgumentException("Negative mass " + c.weight);
112 if (type != Motor.Type.SINGLE && type != Motor.Type.RELOAD &&
113 type != Motor.Type.HYBRID && type != Motor.Type.UNKNOWN) {
114 throw new IllegalArgumentException("Illegal motor type=" + type);
118 this.manufacturer = manufacturer;
119 this.designation = designation;
120 this.description = description;
122 this.delays = delays.clone();
123 this.diameter = diameter;
124 this.length = length;
125 this.time = time.clone();
126 this.thrust = thrust.clone();
127 this.cg = cg.clone();
135 * Get the manufacturer of this motor.
137 * @return the manufacturer
139 public Manufacturer getManufacturer() {
145 * Return the array of time points for this thrust curve.
146 * @return an array of time points where the thrust is sampled
148 public double[] getTimePoints() {
153 * Returns the array of thrust points for this thrust curve.
154 * @return an array of thrust samples
156 public double[] getThrustPoints() {
157 return thrust.clone();
161 * Returns the array of CG points for this thrust curve.
162 * @return an array of CG samples
164 public Coordinate[] getCGPoints() {
169 * Return a list of standard delays defined for this motor.
170 * @return a list of standard delays
172 public double[] getStandardDelays() {
173 return delays.clone();
180 * NOTE: In most cases you want to examine the motor type of the ThrustCurveMotorSet,
181 * not the ThrustCurveMotor itself.
184 public Type getMotorType() {
190 public String getDesignation() {
195 public String getDesignation(double delay) {
196 return designation + "-" + getDelayString(delay);
201 public String getDescription() {
206 public double getDiameter() {
211 public double getLength() {
217 public MotorInstance getInstance() {
218 return new ThrustCurveMotorInstance();
223 public Coordinate getLaunchCG() {
228 public Coordinate getEmptyCG() {
229 return cg[cg.length - 1];
236 public double getBurnTimeEstimate() {
241 public double getAverageThrustEstimate() {
242 return averageThrust;
246 public double getMaxThrustEstimate() {
251 public double getTotalImpulseEstimate() {
258 * Compute the general statistics of this motor.
260 private void computeStatistics() {
264 for (double t : thrust) {
271 double thrustLimit = maxThrust * MARGINAL_THRUST;
272 double burnStart, burnEnd;
275 for (pos = 1; pos < thrust.length; pos++) {
276 if (thrust[pos] >= thrustLimit)
279 if (pos >= thrust.length) {
280 throw new BugException("Could not compute burn start time, maxThrust=" + maxThrust +
281 " limit=" + thrustLimit + " thrust=" + Arrays.toString(thrust));
283 if (MathUtil.equals(thrust[pos - 1], thrust[pos])) {
285 burnStart = (time[pos - 1] + time[pos]) / 2;
287 burnStart = MathUtil.map(thrustLimit, thrust[pos - 1], thrust[pos], time[pos - 1], time[pos]);
292 for (pos = thrust.length - 2; pos >= 0; pos--) {
293 if (thrust[pos] >= thrustLimit)
297 throw new BugException("Could not compute burn end time, maxThrust=" + maxThrust +
298 " limit=" + thrustLimit + " thrust=" + Arrays.toString(thrust));
300 if (MathUtil.equals(thrust[pos], thrust[pos + 1])) {
302 burnEnd = (time[pos] + time[pos + 1]) / 2;
304 burnEnd = MathUtil.map(thrustLimit, thrust[pos], thrust[pos + 1],
305 time[pos], time[pos + 1]);
310 burnTime = Math.max(burnEnd - burnStart, 0);
313 // Total impulse and average thrust
317 for (pos = 0; pos < time.length - 1; pos++) {
318 double t0 = time[pos];
319 double t1 = time[pos + 1];
320 double f0 = thrust[pos];
321 double f1 = thrust[pos + 1];
323 totalImpulse += (t1 - t0) * (f0 + f1) / 2;
325 if (t0 < burnStart && t1 > burnStart) {
326 double fStart = MathUtil.map(burnStart, t0, t1, f0, f1);
327 averageThrust += (fStart + f1) / 2 * (t1 - burnStart);
328 } else if (t0 >= burnStart && t1 <= burnEnd) {
329 averageThrust += (f0 + f1) / 2 * (t1 - t0);
330 } else if (t0 < burnEnd && t1 > burnEnd) {
331 double fEnd = MathUtil.map(burnEnd, t0, t1, f0, f1);
332 averageThrust += (f0 + fEnd) / 2 * (burnEnd - t0);
337 averageThrust /= burnTime;
345 ////////// Static methods
348 * Return a String representation of a delay time. If the delay is {@link #PLUGGED},
351 * @param delay the delay time.
352 * @return the <code>String</code> representation.
354 public static String getDelayString(double delay) {
355 return getDelayString(delay, "P");
359 * Return a String representation of a delay time. If the delay is {@link #PLUGGED},
360 * <code>plugged</code> is returned.
362 * @param delay the delay time.
363 * @param plugged the return value if there is no ejection charge.
364 * @return the String representation.
366 public static String getDelayString(double delay, String plugged) {
367 if (delay == PLUGGED)
369 delay = Math.rint(delay * 10) / 10;
370 if (MathUtil.equals(delay, Math.rint(delay)))
371 return "" + ((int) delay);
377 //////// Motor instance implementation ////////
378 private class ThrustCurveMotorInstance implements MotorInstance {
380 private int position;
382 // Previous time step value
383 private double prevTime;
385 // Average thrust during previous step
386 private double stepThrust;
387 // Instantaneous thrust at current time point
388 private double instThrust;
390 // Average CG during previous step
391 private Coordinate stepCG;
392 // Instantaneous CG at current time point
393 private Coordinate instCG;
395 private final double unitRotationalInertia;
396 private final double unitLongitudalInertia;
398 private int modID = 0;
400 public ThrustCurveMotorInstance() {
401 log.debug("ThrustCurveMotor: Creating motor instance of " + ThrustCurveMotor.this);
408 unitRotationalInertia = Inertia.filledCylinderRotational(getDiameter() / 2);
409 unitLongitudalInertia = Inertia.filledCylinderLongitudal(getDiameter() / 2, getLength());
413 public double getTime() {
418 public Coordinate getCG() {
423 public double getLongitudalInertia() {
424 return unitLongitudalInertia * stepCG.weight;
428 public double getRotationalInertia() {
429 return unitRotationalInertia * stepCG.weight;
433 public double getThrust() {
438 public boolean isActive() {
439 return prevTime < time[time.length - 1];
443 public void step(double nextTime, double acceleration, AtmosphericConditions cond) {
445 if (!(nextTime >= prevTime)) {
447 throw new IllegalArgumentException("Stepping backwards in time, current=" +
448 prevTime + " new=" + nextTime);
450 if (MathUtil.equals(prevTime, nextTime)) {
456 if (position >= time.length - 1) {
461 stepCG = cg[cg.length - 1];
466 // Compute average & instantaneous thrust
467 if (nextTime < time[position + 1]) {
469 // Time step between time points
470 double nextF = MathUtil.map(nextTime, time[position], time[position + 1],
471 thrust[position], thrust[position + 1]);
472 stepThrust = (instThrust + nextF) / 2;
477 // Portion of previous step
478 stepThrust = (instThrust + thrust[position + 1]) / 2 * (time[position + 1] - prevTime);
482 while ((position < time.length - 1) && (nextTime >= time[position + 1])) {
483 stepThrust += (thrust[position] + thrust[position + 1]) / 2 *
484 (time[position + 1] - time[position]);
489 if (position < time.length - 1) {
490 instThrust = MathUtil.map(nextTime, time[position], time[position + 1],
491 thrust[position], thrust[position + 1]);
492 stepThrust += (thrust[position] + instThrust) / 2 *
493 (nextTime - time[position]);
495 // Thrust ended during this step
499 stepThrust /= (nextTime - prevTime);
503 // Compute average and instantaneous CG (simple average between points)
505 if (position < time.length - 1) {
506 nextCG = MathUtil.map(nextTime, time[position], time[position + 1],
507 cg[position], cg[position + 1]);
509 nextCG = cg[cg.length - 1];
511 stepCG = instCG.add(nextCG).multiply(0.5);
519 public MotorInstance clone() {
521 return (MotorInstance) super.clone();
522 } catch (CloneNotSupportedException e) {
523 throw new BugException("CloneNotSupportedException", e);
528 public int getModID() {
536 public int compareTo(ThrustCurveMotor other) {
541 value = COLLATOR.compare(this.manufacturer.getDisplayName(),
542 ((ThrustCurveMotor) other).manufacturer.getDisplayName());
547 value = DESIGNATION_COMPARATOR.compare(this.getDesignation(), other.getDesignation());
552 value = (int) ((this.getDiameter() - other.getDiameter()) * 1000000);
557 value = (int) ((this.getLength() - other.getLength()) * 1000000);