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();
28 private final String digest;
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;
47 * Deep copy constructor.
48 * Constructs a new ThrustCurveMotor from an existing ThrustCurveMotor.
51 protected ThrustCurveMotor( ThrustCurveMotor m ) {
52 this.digest = m.digest;
53 this.manufacturer = m.manufacturer;
54 this.designation = m.designation;
55 this.description = m.description;
57 this.delays = Arrays.copyOf(m.delays, m.delays.length);
58 this.diameter = m.diameter;
59 this.length = m.length;
60 this.time = Arrays.copyOf(m.time, m.time.length);
61 this.thrust = Arrays.copyOf(m.thrust, m.thrust.length);
62 this.cg = new Coordinate[ m.cg.length ];
63 for( int i = 0; i< cg.length; i++ ) {
64 this.cg[i] = m.cg[i].clone();
66 this.maxThrust = m.maxThrust;
67 this.burnTime = m.burnTime;
68 this.averageThrust = m.averageThrust;
69 this.totalImpulse = m.totalImpulse;
73 * Sole constructor. Sets all the properties of the motor.
75 * @param manufacturer the manufacturer of the motor.
76 * @param designation the designation of the motor.
77 * @param description extra description of the motor.
78 * @param type the motor type
79 * @param delays the delays defined for this thrust curve
80 * @param diameter diameter of the motor.
81 * @param length length of the motor.
82 * @param time the time points for the thrust curve.
83 * @param thrust thrust at the time points.
84 * @param cg cg at the time points.
86 public ThrustCurveMotor(Manufacturer manufacturer, String designation, String description,
87 Motor.Type type, double[] delays, double diameter, double length,
88 double[] time, double[] thrust, Coordinate[] cg, String digest) {
90 // Check argument validity
91 if ((time.length != thrust.length) || (time.length != cg.length)) {
92 throw new IllegalArgumentException("Array lengths do not match, " +
93 "time:" + time.length + " thrust:" + thrust.length +
96 if (time.length < 2) {
97 throw new IllegalArgumentException("Too short thrust-curve, length=" +
100 for (int i = 0; i < time.length - 1; i++) {
101 if (time[i + 1] < time[i]) {
102 throw new IllegalArgumentException("Time goes backwards, " +
103 "time[" + i + "]=" + time[i] + " " +
104 "time[" + (i + 1) + "]=" + time[i + 1]);
107 if (!MathUtil.equals(time[0], 0)) {
108 throw new IllegalArgumentException("Curve starts at time " + time[0]);
110 if (!MathUtil.equals(thrust[0], 0)) {
111 throw new IllegalArgumentException("Curve starts at thrust " + thrust[0]);
113 if (!MathUtil.equals(thrust[thrust.length - 1], 0)) {
114 throw new IllegalArgumentException("Curve ends at thrust " +
115 thrust[thrust.length - 1]);
117 for (double t : thrust) {
119 throw new IllegalArgumentException("Negative thrust.");
121 if (t > MAX_THRUST || Double.isNaN(t)) {
122 throw new IllegalArgumentException("Invalid thrust " + t);
125 for (Coordinate c : cg) {
127 throw new IllegalArgumentException("Invalid CG " + c);
129 if (c.x < 0 || c.x > length) {
130 throw new IllegalArgumentException("Invalid CG position " + c.x);
133 throw new IllegalArgumentException("Negative mass " + c.weight);
137 if (type != Motor.Type.SINGLE && type != Motor.Type.RELOAD &&
138 type != Motor.Type.HYBRID && type != Motor.Type.UNKNOWN) {
139 throw new IllegalArgumentException("Illegal motor type=" + type);
143 this.manufacturer = manufacturer;
144 this.designation = designation;
145 this.description = description;
147 this.delays = delays.clone();
148 this.diameter = diameter;
149 this.length = length;
150 this.time = time.clone();
151 this.thrust = thrust.clone();
152 this.cg = cg.clone();
160 * Get the manufacturer of this motor.
162 * @return the manufacturer
164 public Manufacturer getManufacturer() {
170 * Return the array of time points for this thrust curve.
171 * @return an array of time points where the thrust is sampled
173 public double[] getTimePoints() {
178 * Returns the array of thrust points for this thrust curve.
179 * @return an array of thrust samples
181 public double[] getThrustPoints() {
182 return thrust.clone();
186 * Returns the array of CG points for this thrust curve.
187 * @return an array of CG samples
189 public Coordinate[] getCGPoints() {
194 * Return a list of standard delays defined for this motor.
195 * @return a list of standard delays
197 public double[] getStandardDelays() {
198 return delays.clone();
205 * NOTE: In most cases you want to examine the motor type of the ThrustCurveMotorSet,
206 * not the ThrustCurveMotor itself.
209 public Type getMotorType() {
215 public String getDesignation() {
220 public String getDesignation(double delay) {
221 return designation + "-" + getDelayString(delay);
226 public String getDescription() {
231 public double getDiameter() {
236 public double getLength() {
242 public MotorInstance getInstance() {
243 return new ThrustCurveMotorInstance();
248 public Coordinate getLaunchCG() {
253 public Coordinate getEmptyCG() {
254 return cg[cg.length - 1];
261 public double getBurnTimeEstimate() {
266 public double getAverageThrustEstimate() {
267 return averageThrust;
271 public double getMaxThrustEstimate() {
276 public double getTotalImpulseEstimate() {
281 public String getDigest() {
287 * Compute the general statistics of this motor.
289 private void computeStatistics() {
293 for (double t : thrust) {
300 double thrustLimit = maxThrust * MARGINAL_THRUST;
301 double burnStart, burnEnd;
304 for (pos = 1; pos < thrust.length; pos++) {
305 if (thrust[pos] >= thrustLimit)
308 if (pos >= thrust.length) {
309 throw new BugException("Could not compute burn start time, maxThrust=" + maxThrust +
310 " limit=" + thrustLimit + " thrust=" + Arrays.toString(thrust));
312 if (MathUtil.equals(thrust[pos - 1], thrust[pos])) {
314 burnStart = (time[pos - 1] + time[pos]) / 2;
316 burnStart = MathUtil.map(thrustLimit, thrust[pos - 1], thrust[pos], time[pos - 1], time[pos]);
321 for (pos = thrust.length - 2; pos >= 0; pos--) {
322 if (thrust[pos] >= thrustLimit)
326 throw new BugException("Could not compute burn end time, maxThrust=" + maxThrust +
327 " limit=" + thrustLimit + " thrust=" + Arrays.toString(thrust));
329 if (MathUtil.equals(thrust[pos], thrust[pos + 1])) {
331 burnEnd = (time[pos] + time[pos + 1]) / 2;
333 burnEnd = MathUtil.map(thrustLimit, thrust[pos], thrust[pos + 1],
334 time[pos], time[pos + 1]);
339 burnTime = Math.max(burnEnd - burnStart, 0);
342 // Total impulse and average thrust
346 for (pos = 0; pos < time.length - 1; pos++) {
347 double t0 = time[pos];
348 double t1 = time[pos + 1];
349 double f0 = thrust[pos];
350 double f1 = thrust[pos + 1];
352 totalImpulse += (t1 - t0) * (f0 + f1) / 2;
354 if (t0 < burnStart && t1 > burnStart) {
355 double fStart = MathUtil.map(burnStart, t0, t1, f0, f1);
356 averageThrust += (fStart + f1) / 2 * (t1 - burnStart);
357 } else if (t0 >= burnStart && t1 <= burnEnd) {
358 averageThrust += (f0 + f1) / 2 * (t1 - t0);
359 } else if (t0 < burnEnd && t1 > burnEnd) {
360 double fEnd = MathUtil.map(burnEnd, t0, t1, f0, f1);
361 averageThrust += (f0 + fEnd) / 2 * (burnEnd - t0);
366 averageThrust /= burnTime;
374 ////////// Static methods
377 * Return a String representation of a delay time. If the delay is {@link #PLUGGED},
380 * @param delay the delay time.
381 * @return the <code>String</code> representation.
383 public static String getDelayString(double delay) {
384 return getDelayString(delay, "P");
388 * Return a String representation of a delay time. If the delay is {@link #PLUGGED},
389 * <code>plugged</code> is returned.
391 * @param delay the delay time.
392 * @param plugged the return value if there is no ejection charge.
393 * @return the String representation.
395 public static String getDelayString(double delay, String plugged) {
396 if (delay == PLUGGED)
398 delay = Math.rint(delay * 10) / 10;
399 if (MathUtil.equals(delay, Math.rint(delay)))
400 return "" + ((int) delay);
406 //////// Motor instance implementation ////////
407 private class ThrustCurveMotorInstance implements MotorInstance {
409 private int position;
411 // Previous time step value
412 private double prevTime;
414 // Average thrust during previous step
415 private double stepThrust;
416 // Instantaneous thrust at current time point
417 private double instThrust;
419 // Average CG during previous step
420 private Coordinate stepCG;
421 // Instantaneous CG at current time point
422 private Coordinate instCG;
424 private final double unitRotationalInertia;
425 private final double unitLongitudinalInertia;
427 private int modID = 0;
429 public ThrustCurveMotorInstance() {
430 log.debug("ThrustCurveMotor: Creating motor instance of " + ThrustCurveMotor.this);
437 unitRotationalInertia = Inertia.filledCylinderRotational(getDiameter() / 2);
438 unitLongitudinalInertia = Inertia.filledCylinderLongitudinal(getDiameter() / 2, getLength());
442 public double getTime() {
447 public Coordinate getCG() {
452 public double getLongitudinalInertia() {
453 return unitLongitudinalInertia * stepCG.weight;
457 public double getRotationalInertia() {
458 return unitRotationalInertia * stepCG.weight;
462 public double getThrust() {
467 public boolean isActive() {
468 return prevTime < time[time.length - 1];
472 public void step(double nextTime, double acceleration, AtmosphericConditions cond) {
474 if (!(nextTime >= prevTime)) {
476 throw new IllegalArgumentException("Stepping backwards in time, current=" +
477 prevTime + " new=" + nextTime);
479 if (MathUtil.equals(prevTime, nextTime)) {
485 if (position >= time.length - 1) {
490 stepCG = cg[cg.length - 1];
495 // Compute average & instantaneous thrust
496 if (nextTime < time[position + 1]) {
498 // Time step between time points
499 double nextF = MathUtil.map(nextTime, time[position], time[position + 1],
500 thrust[position], thrust[position + 1]);
501 stepThrust = (instThrust + nextF) / 2;
506 // Portion of previous step
507 stepThrust = (instThrust + thrust[position + 1]) / 2 * (time[position + 1] - prevTime);
511 while ((position < time.length - 1) && (nextTime >= time[position + 1])) {
512 stepThrust += (thrust[position] + thrust[position + 1]) / 2 *
513 (time[position + 1] - time[position]);
518 if (position < time.length - 1) {
519 instThrust = MathUtil.map(nextTime, time[position], time[position + 1],
520 thrust[position], thrust[position + 1]);
521 stepThrust += (thrust[position] + instThrust) / 2 *
522 (nextTime - time[position]);
524 // Thrust ended during this step
528 stepThrust /= (nextTime - prevTime);
532 // Compute average and instantaneous CG (simple average between points)
534 if (position < time.length - 1) {
535 nextCG = MathUtil.map(nextTime, time[position], time[position + 1],
536 cg[position], cg[position + 1]);
538 nextCG = cg[cg.length - 1];
540 stepCG = instCG.add(nextCG).multiply(0.5);
548 public MotorInstance clone() {
550 return (MotorInstance) super.clone();
551 } catch (CloneNotSupportedException e) {
552 throw new BugException("CloneNotSupportedException", e);
557 public int getModID() {
565 public int compareTo(ThrustCurveMotor other) {
570 value = COLLATOR.compare(this.manufacturer.getDisplayName(),
571 ((ThrustCurveMotor) other).manufacturer.getDisplayName());
576 value = DESIGNATION_COMPARATOR.compare(this.getDesignation(), other.getDesignation());
581 value = (int) ((this.getDiameter() - other.getDiameter()) * 1000000);
586 value = (int) ((this.getLength() - other.getLength()) * 1000000);