1 package net.sf.openrocket.motor;
3 import java.text.Collator;
4 import java.util.Arrays;
5 import java.util.Comparator;
6 import java.util.Locale;
7 import java.util.regex.Matcher;
8 import java.util.regex.Pattern;
10 import net.sf.openrocket.util.BugException;
11 import net.sf.openrocket.util.Coordinate;
12 import net.sf.openrocket.util.MathUtil;
17 * Abstract base class for motors. The methods that must be implemented are
18 * {@link #getTotalTime()}, {@link #getThrust(double)} and {@link #getCG(double)}.
19 * Additionally the method {@link #getMaxThrust()} may be overridden for efficiency.
22 * NOTE: The current implementation of {@link #getAverageTime()} and
23 * {@link #getAverageThrust()} assume that the class is immutable!
25 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
27 public abstract class Motor implements Comparable<Motor> {
30 * Enum of rocket motor types.
32 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
35 SINGLE("Single-use", "Single-use solid propellant motor"),
36 RELOAD("Reloadable", "Reloadable solid propellant motor"),
37 HYBRID("Hybrid", "Hybrid rocket motor engine"),
38 UNKNOWN("Unknown", "Unknown motor type");
40 private final String name;
41 private final String description;
43 Type(String name, String description) {
45 this.description = description;
49 * Return a short name of this motor type.
50 * @return a short name of the motor type.
52 public String getName() {
57 * Return a long description of this motor type.
58 * @return a description of the motor type.
60 public String getDescription() {
65 public String toString() {
72 * Ejection charge delay value signifying a "plugged" motor with no ejection charge.
73 * The value is that of <code>Double.POSITIVE_INFINITY</code>.
75 public static final double PLUGGED = Double.POSITIVE_INFINITY;
79 * Below what portion of maximum thrust is the motor chosen to be off when
80 * calculating average thrust and burn time. NFPA 1125 defines the "official"
81 * burn time to be the time which the motor produces over 5% of its maximum thrust.
83 public static final double AVERAGE_MARGINAL = 0.05;
85 /* All data is cached, so divisions can be very tight. */
86 private static final int DIVISIONS = 1000;
90 private static final Collator COLLATOR = Collator.getInstance(Locale.US);
92 COLLATOR.setStrength(Collator.PRIMARY);
94 private static DesignationComparator DESIGNATION_COMPARATOR = new DesignationComparator();
99 private final Manufacturer manufacturer;
100 private final String designation;
101 private final String description;
102 private final Type motorType;
103 private final String digest;
105 private final double[] delays;
107 private final double diameter;
108 private final double length;
111 private double maxThrust = -1;
112 private double avgTime = -1;
113 private double avgThrust = -1;
114 private double totalImpulse = -1;
119 * Sole constructor. None of the parameters may be <code>null</code>.
121 * @param manufacturer the manufacturer of the motor.
122 * @param designation the motor designation.
123 * @param description further description, including any comments on the origin
124 * of the thrust curve.
125 * @param delays an array of the standard ejection charge delays. A plugged
126 * motor (no ejection charge) is specified by a delay of
127 * {@link #PLUGGED} (<code>Double.POSITIVE_INFINITY</code>).
128 * @param diameter maximum diameter of the motor
129 * @param length length of the motor
131 protected Motor(Manufacturer manufacturer, String designation, String description,
132 Type type, double[] delays, double diameter, double length, String digest) {
134 if (manufacturer == null || designation == null || description == null ||
135 type == null || delays == null) {
136 throw new IllegalArgumentException("Parameters cannot be null.");
139 this.manufacturer = manufacturer;
140 this.designation = designation;
141 this.description = description.trim();
142 this.motorType = type;
143 this.delays = delays.clone();
144 Arrays.sort(this.delays);
145 this.diameter = diameter;
146 this.length = length;
147 this.digest = digest;
153 * Return the total burn time of the motor. The method {@link #getThrust(double)}
154 * must return zero for time values greater than the return value.
156 * @return the total burn time of the motor.
158 public abstract double getTotalTime();
162 * Return the thrust of the motor at the specified time.
164 * @param time time since the ignition of the motor.
165 * @return the thrust at the specified time.
167 public abstract double getThrust(double time);
171 * Return the average thrust of the motor between times t1 and t2.
173 * @param t1 starting time since the ignition of the motor.
174 * @param t2 end time since the ignition of the motor.
175 * @return the average thrust during the time period.
177 /* TODO: MEDIUM: Implement better method in subclass */
178 public double getThrust(double t1, double t2) {
181 f += getThrust(0.8*t1 + 0.2*t2);
182 f += getThrust(0.6*t1 + 0.4*t2);
183 f += getThrust(0.4*t1 + 0.6*t2);
184 f += getThrust(0.2*t1 + 0.8*t2);
191 * Return the mass and CG of the motor at the specified time.
193 * @param time time since the ignition of the motor.
194 * @return the mass and CG of the motor.
196 public abstract Coordinate getCG(double time);
201 * Return the mass of the motor at the specified time. The original mass
202 * of the motor can be queried by <code>getMass(0)</code> and the burnt mass
203 * by <code>getMass(Double.MAX_VALUE)</code>.
205 * @param time time since the ignition of the motor.
206 * @return the mass of the motor.
208 public double getMass(double time) {
209 return getCG(time).weight;
214 * Return the longitudal moment of inertia of the motor at the specified time.
215 * This default method assumes that the mass of the motor is evenly distributed
216 * in a cylinder with the diameter and length of the motor.
218 * @param time time since the ignition of the motor.
219 * @return the longitudal moment of inertia of the motor.
221 public double getLongitudalInertia(double time) {
222 return getMass(time) * (3.0*MathUtil.pow2(diameter/2) + MathUtil.pow2(length))/12;
228 * Return the rotational moment of inertia of the motor at the specified time.
229 * This default method assumes that the mass of the motor is evenly distributed
230 * in a cylinder with the diameter and length of the motor.
232 * @param time time since the ignition of the motor.
233 * @return the rotational moment of inertia of the motor.
235 public double getRotationalInertia(double time) {
236 return getMass(time) * MathUtil.pow2(diameter) / 8;
243 * Return the maximum thrust. This implementation slices through the thrust curve
244 * searching for the maximum thrust. Subclasses may wish to override this with a
245 * more efficient method.
247 * @return the maximum thrust of the motor
249 public double getMaxThrust() {
251 double time = getTotalTime();
254 for (int i=0; i < DIVISIONS; i++) {
255 double t = time * i / DIVISIONS;
256 double thrust = getThrust(t);
258 if (thrust > maxThrust)
267 * Return the time used in calculating the average thrust. The time is the
268 * length of time that the motor produces over 5% ({@link #AVERAGE_MARGINAL})
269 * of its maximum thrust.
271 * @return the nominal burn time.
273 public double getAverageTime() {
274 // Compute average time lazily
276 double max = getMaxThrust();
277 double time = getTotalTime();
280 for (int i=0; i <= DIVISIONS; i++) {
281 double t = i*time/DIVISIONS;
282 if (getThrust(t) >= max*AVERAGE_MARGINAL)
285 avgTime *= time/(DIVISIONS+1);
287 if (Double.isNaN(avgTime))
288 throw new BugException("Calculated avg. time is NaN for motor "+this);
296 * Return the calculated average thrust during the time the motor produces
297 * over 5% ({@link #AVERAGE_MARGINAL}) of its thrust.
299 * @return the nominal average thrust.
301 public double getAverageThrust() {
302 // Compute average thrust lazily
304 double max = getMaxThrust();
305 double time = getTotalTime();
309 for (int i=0; i <= DIVISIONS; i++) {
310 double t = i*time/DIVISIONS;
311 double thrust = getThrust(t);
312 if (thrust >= max*AVERAGE_MARGINAL) {
320 if (Double.isNaN(avgThrust))
321 throw new BugException("Calculated average thrust is NaN for motor "+this);
328 * Return the total impulse of the motor. This is calculated from the entire
329 * burn time, and therefore may differ from the value of {@link #getAverageTime()}
330 * and {@link #getAverageThrust()} multiplied together.
332 * @return the total impulse of the motor.
334 public double getTotalImpulse() {
335 // Compute total impulse lazily
336 if (totalImpulse < 0) {
337 double time = getTotalTime();
343 for (int i=1; i < DIVISIONS; i++) {
344 double t1 = time * i / DIVISIONS;
345 double f1 = getThrust(t1);
346 totalImpulse += 0.5*(f0+f1)*(t1-t0);
351 if (Double.isNaN(totalImpulse))
352 throw new BugException("Calculated total impulse is NaN for motor "+this);
359 * Return the manufacturer of the motor.
361 * @return the manufacturer
363 public Manufacturer getManufacturer() {
368 * Return the designation of the motor.
370 * @return the designation
372 public String getDesignation() {
377 * Return the designation of the motor, including a delay.
379 * @param delay the delay of the motor.
380 * @return designation with delay.
382 public String getDesignation(double delay) {
383 return getDesignation() + "-" + getDelayString(delay);
388 * Return extra description for the motor. This may include for example
389 * comments on the source of the thrust curve. The returned <code>String</code>
390 * may include new-lines.
392 * @return the description
394 public String getDescription() {
400 * Return the motor type.
402 * @return the motorType
404 public Type getMotorType() {
411 * Return the standard ejection charge delays for the motor. "Plugged" motors
412 * with no ejection charge are signified by the value {@link #PLUGGED}
413 * (<code>Double.POSITIVE_INFINITY</code>).
415 * @return the list of standard ejection charge delays, which may be empty.
417 public double[] getStandardDelays() {
418 return delays.clone();
422 * Return the maximum diameter of the motor.
424 * @return the diameter
426 public double getDiameter() {
431 * Return the length of the motor. This should be a "characteristic" length,
432 * and the exact definition may depend on the motor type. Typically this should
433 * be the length from the bottom of the motor to the end of the maximum diameter
434 * portion, ignoring any smaller ejection charge compartments.
438 public double getLength() {
444 * Return a digest string of this motor. This digest should be computed from all
445 * flight-affecting data. For example for thrust curve motors the thrust curve
446 * should be digested using suitable precision. The intention is that the combination
447 * of motor type, manufacturer, designation, diameter, length and digest uniquely
448 * identify any particular motor data file.
450 * @return a string digest of this motor (0-60 chars)
452 public String getDigestString() {
457 public boolean similar(Motor other) {
458 // TODO: HIGH: Merge with equals / cleanup
460 // Tests manufacturer, designation, diameter and length
461 if (this.compareTo(other) != 0)
464 // Compare total time
465 if (Math.abs(this.getTotalTime() - other.getTotalTime()) > 0.5) {
469 // Consider type only if neither is of unknown type
470 if ((this.motorType != Type.UNKNOWN) && (other.motorType != Type.UNKNOWN) &&
471 (this.motorType != other.motorType)) {
475 // Compare delays if both have some delays defined
476 if (this.delays.length != 0 && other.delays.length != 0) {
477 if (this.delays.length != other.delays.length) {
480 for (int i=0; i < delays.length; i++) {
481 // INF - INF == NaN, which produces false when compared
482 if (Math.abs(this.delays[i] - other.delays[i]) > 0.5) {
488 double time = getTotalTime();
489 for (int i=0; i < 10; i++) {
490 double t = time * i/10;
491 if (Math.abs(this.getThrust(t) - other.getThrust(t)) > 1) {
500 * Compares two <code>Motor</code> objects. The motors are considered equal
501 * if they have identical manufacturers, designations and types, near-identical
502 * dimensions, burn times and delays and near-identical thrust curves
503 * (sampled at 10 equidistant points).
505 * The comment field is ignored when comparing equality.
507 * TODO: HIGH: Check for identical contents instead
510 public boolean equals(Object o) {
511 if (!(o instanceof Motor))
514 Motor other = (Motor) o;
516 // Tests manufacturer, designation, diameter and length
517 if (this.compareTo(other) != 0)
520 // Compare total time
521 if (Math.abs(this.getTotalTime() - other.getTotalTime()) > 0.5) {
525 // Consider type only if neither is of unknown type
526 if ((this.motorType != Type.UNKNOWN) && (other.motorType != Type.UNKNOWN) &&
527 (this.motorType != other.motorType)) {
532 if (this.delays.length != other.delays.length) {
535 for (int i=0; i < delays.length; i++) {
536 // INF - INF == NaN, which produces false when compared
537 if (Math.abs(this.delays[i] - other.delays[i]) > 0.5) {
542 double time = getTotalTime();
543 for (int i=0; i < 10; i++) {
544 double t = time * i/10;
545 if (Math.abs(this.getThrust(t) - other.getThrust(t)) > 1) {
554 * A <code>hashCode</code> method compatible with the <code>equals</code>
558 public int hashCode() {
559 return (manufacturer.hashCode() + designation.hashCode() +
560 ((int)(length*1000)) + ((int)(diameter*1000)));
566 public String toString() {
567 return manufacturer + " " + designation;
571 ////////// Static methods
575 * Return a String representation of a delay time. If the delay is {@link #PLUGGED},
578 * @param delay the delay time.
579 * @return the <code>String</code> representation.
581 public static String getDelayString(double delay) {
582 return getDelayString(delay,"P");
586 * Return a String representation of a delay time. If the delay is {@link #PLUGGED},
587 * <code>plugged</code> is returned.
589 * @param delay the delay time.
590 * @param plugged the return value if there is no ejection charge.
591 * @return the String representation.
593 public static String getDelayString(double delay, String plugged) {
594 if (delay == PLUGGED)
596 delay = Math.rint(delay*10)/10;
597 if (MathUtil.equals(delay, Math.rint(delay)))
598 return "" + ((int)delay);
605 //////////// Comparation
610 public int compareTo(Motor other) {
614 value = COLLATOR.compare(this.manufacturer.getDisplayName(),
615 other.manufacturer.getDisplayName());
620 value = DESIGNATION_COMPARATOR.compare(this.designation, other.designation);
625 value = (int)((this.diameter - other.diameter)*1000000);
630 value = (int)((this.length - other.length)*1000000);
635 value = (int)((this.getTotalImpulse() - other.getTotalImpulse())*1000);
641 public static Comparator<String> getDesignationComparator() {
642 return DESIGNATION_COMPARATOR;
647 * Compares two motors by their designations. The motors are ordered first
648 * by their motor class, second by their average thrust and lastly by any
649 * extra modifiers at the end of the designation.
651 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
653 private static class DesignationComparator implements Comparator<String> {
654 private Pattern pattern =
655 Pattern.compile("^([0-9][0-9]+|1/([1-8]))?([a-zA-Z])([0-9]+)(.*?)$");
658 public int compare(String o1, String o2) {
662 m1 = pattern.matcher(o1);
663 m2 = pattern.matcher(o2);
665 if (m1.find() && m2.find()) {
667 String o1Class = m1.group(3);
668 int o1Thrust = Integer.parseInt(m1.group(4));
669 String o1Extra = m1.group(5);
671 String o2Class = m2.group(3);
672 int o2Thrust = Integer.parseInt(m2.group(4));
673 String o2Extra = m2.group(5);
676 if (o1Class.equalsIgnoreCase("A") && o2Class.equalsIgnoreCase("A")) {
677 // 1/2A and 1/4A comparison
678 String sub1 = m1.group(2);
679 String sub2 = m2.group(2);
681 if (sub1 != null || sub2 != null) {
686 value = -COLLATOR.compare(sub1,sub2);
691 value = COLLATOR.compare(o1Class,o2Class);
696 if (o1Thrust != o2Thrust)
697 return o1Thrust - o2Thrust;
700 return COLLATOR.compare(o1Extra, o2Extra);
704 // Not understandable designation, simply compare strings
705 return COLLATOR.compare(o1, o2);