version 1.0.0
[debian/openrocket] / src / net / sf / openrocket / motor / Motor.java
1 package net.sf.openrocket.motor;
2
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;
9
10 import net.sf.openrocket.util.BugException;
11 import net.sf.openrocket.util.Coordinate;
12 import net.sf.openrocket.util.MathUtil;
13
14
15
16 /**
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.
20  * <p>
21  * 
22  * NOTE:  The current implementation of {@link #getAverageTime()} and 
23  * {@link #getAverageThrust()} assume that the class is immutable!
24  * 
25  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
26  */
27 public abstract class Motor implements Comparable<Motor> {
28         
29         /**
30          * Enum of rocket motor types.
31          * 
32          * @author Sampo Niskanen <sampo.niskanen@iki.fi>
33          */
34         public enum Type {
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");
39                 
40                 private final String name;
41                 private final String description;
42                 
43                 Type(String name, String description) {
44                         this.name = name;
45                         this.description = description;
46                 }
47
48                 /**
49                  * Return a short name of this motor type.
50                  * @return  a short name of the motor type.
51                  */
52                 public String getName() {
53                         return name;
54                 }
55                 
56                 /**
57                  * Return a long description of this motor type.
58                  * @return  a description of the motor type.
59                  */
60                 public String getDescription() {
61                         return description;
62                 }
63                 
64                 @Override
65                 public String toString() {
66                         return name;
67                 }
68         }
69         
70         
71         /**
72          * Ejection charge delay value signifying a "plugged" motor with no ejection charge.
73          * The value is that of <code>Double.POSITIVE_INFINITY</code>.
74          */
75         public static final double PLUGGED = Double.POSITIVE_INFINITY;
76         
77         
78         /**
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.
82          */
83         public static final double AVERAGE_MARGINAL = 0.05;
84         
85         /* All data is cached, so divisions can be very tight. */
86         private static final int DIVISIONS = 1000;
87
88         
89         //  Comparators:
90         private static final Collator COLLATOR = Collator.getInstance(Locale.US);
91         static {
92                 COLLATOR.setStrength(Collator.PRIMARY);
93         }
94         private static DesignationComparator DESIGNATION_COMPARATOR = new DesignationComparator();
95         
96         
97         
98         
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;
104         
105         private final double[] delays;
106         
107         private final double diameter;
108         private final double length;
109         
110         /* Cached data */
111         private double maxThrust = -1;
112         private double avgTime = -1;
113         private double avgThrust = -1;
114         private double totalImpulse = -1;
115         
116         
117         
118         /**
119          * Sole constructor.  None of the parameters may be <code>null</code>.
120          * 
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
130          */
131         protected Motor(Manufacturer manufacturer, String designation, String description, 
132                         Type type, double[] delays, double diameter, double length, String digest) {
133
134                 if (manufacturer == null || designation == null || description == null ||
135                                 type == null || delays == null) {
136                         throw new IllegalArgumentException("Parameters cannot be null.");
137                 }
138                 
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;
148         }
149
150
151         
152         /**
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.
155          * 
156          * @return  the total burn time of the motor.
157          */
158         public abstract double getTotalTime();
159
160
161         /**
162          * Return the thrust of the motor at the specified time.
163          * 
164          * @param time  time since the ignition of the motor.
165          * @return      the thrust at the specified time.
166          */
167         public abstract double getThrust(double time);
168         
169         
170         /**
171          * Return the average thrust of the motor between times t1 and t2.
172          * 
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.
176          */
177         /* TODO: MEDIUM: Implement better method in subclass */
178         public double getThrust(double t1, double t2) {
179                 double f = 0;
180                 f += getThrust(t1);
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);
185                 f += getThrust(t2);
186                 return f/6;
187         }
188
189         
190         /**
191          * Return the mass and CG of the motor at the specified time.
192          * 
193          * @param time  time since the ignition of the motor.
194          * @return      the mass and CG of the motor.
195          */
196         public abstract Coordinate getCG(double time);
197         
198         
199         
200         /**
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>.
204          * 
205          * @param time  time since the ignition of the motor.
206          * @return      the mass of the motor.
207          */
208         public double getMass(double time) {
209                 return getCG(time).weight;
210         }
211         
212         
213         /**
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.
217          * 
218          * @param time  time since the ignition of the motor.
219          * @return              the longitudal moment of inertia of the motor.
220          */
221         public double getLongitudalInertia(double time) {
222                 return getMass(time) * (3.0*MathUtil.pow2(diameter/2) + MathUtil.pow2(length))/12;
223         }
224         
225         
226         
227         /**
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.
231          * 
232          * @param time  time since the ignition of the motor.
233          * @return              the rotational moment of inertia of the motor.
234          */
235         public double getRotationalInertia(double time) {
236                 return getMass(time) * MathUtil.pow2(diameter) / 8;
237         }
238         
239         
240         
241         
242         /**
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.
246          * 
247          * @return  the maximum thrust of the motor
248          */
249         public double getMaxThrust() {
250                 if (maxThrust < 0) {
251                         double time = getTotalTime();
252                         maxThrust = 0;
253                         
254                         for (int i=0; i < DIVISIONS; i++) {
255                                 double t = time * i / DIVISIONS;
256                                 double thrust = getThrust(t);
257                                 
258                                 if (thrust > maxThrust)
259                                         maxThrust = thrust;
260                         }
261                 }
262                 return maxThrust;
263         }
264         
265         
266         /**
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.
270          * 
271          * @return  the nominal burn time.
272          */
273         public double getAverageTime() {
274                 // Compute average time lazily
275                 if (avgTime < 0) {
276                         double max = getMaxThrust();
277                         double time = getTotalTime();
278                         
279                         avgTime = 0;
280                         for (int i=0; i <= DIVISIONS; i++) {
281                                 double t = i*time/DIVISIONS;
282                                 if (getThrust(t) >= max*AVERAGE_MARGINAL)
283                                         avgTime++;
284                         }
285                         avgTime *= time/(DIVISIONS+1);
286                         
287                         if (Double.isNaN(avgTime))
288                                 throw new BugException("Calculated avg. time is NaN for motor "+this);
289
290                 }
291                 return avgTime;
292         }
293         
294         
295         /**
296          * Return the calculated average thrust during the time the motor produces
297          * over 5% ({@link #AVERAGE_MARGINAL}) of its thrust.
298          * 
299          * @return  the nominal average thrust.
300          */
301         public double getAverageThrust() {
302                 // Compute average thrust lazily
303                 if (avgThrust < 0) {
304                         double max = getMaxThrust();
305                         double time = getTotalTime();
306                         int points = 0;
307                         
308                         avgThrust = 0;
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) {
313                                         avgThrust += thrust;
314                                         points++;
315                                 }
316                         }
317                         if (points > 0)
318                                 avgThrust /= points;
319                         
320                         if (Double.isNaN(avgThrust))
321                                 throw new BugException("Calculated average thrust is NaN for motor "+this);
322                 }
323                 return avgThrust;
324         }
325         
326         
327         /**
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.
331          * 
332          * @return  the total impulse of the motor.
333          */
334         public double getTotalImpulse() {
335                 // Compute total impulse lazily
336                 if (totalImpulse < 0) {
337                         double time = getTotalTime();
338                         double f0, t0;
339                         
340                         totalImpulse = 0;
341                         t0 = 0;
342                         f0 = getThrust(0);
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);
347                                 t0 = t1;
348                                 f0 = f1;
349                         }
350                         
351                         if (Double.isNaN(totalImpulse))
352                                 throw new BugException("Calculated total impulse is NaN for motor "+this);
353                 }
354                 return totalImpulse;
355         }
356         
357
358         /**
359          * Return the manufacturer of the motor.
360          * 
361          * @return the manufacturer
362          */
363         public Manufacturer getManufacturer() {
364                 return manufacturer;
365         }
366         
367         /**
368          * Return the designation of the motor.
369          * 
370          * @return the designation
371          */
372         public String getDesignation() {
373                 return designation;
374         }
375         
376         /**
377          * Return the designation of the motor, including a delay.
378          * 
379          * @param delay  the delay of the motor.
380          * @return               designation with delay.
381          */
382         public String getDesignation(double delay) {
383                 return getDesignation() + "-" + getDelayString(delay);
384         }
385
386         
387         /**
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.
391          * 
392          * @return the description
393          */
394         public String getDescription() {
395                 return description;
396         }
397         
398         
399         /**
400          * Return the motor type.
401          * 
402          * @return  the motorType
403          */
404         public Type getMotorType() {
405                 return motorType;
406         }
407
408
409
410         /**
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>).
414          * 
415          * @return  the list of standard ejection charge delays, which may be empty.
416          */
417         public double[] getStandardDelays() {
418                 return delays.clone();
419         }
420
421         /**
422          * Return the maximum diameter of the motor.
423          * 
424          * @return the diameter
425          */
426         public double getDiameter() {
427                 return diameter;
428         }
429
430         /**
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.
435          * 
436          * @return the length
437          */
438         public double getLength() {
439                 return length;
440         }
441         
442         
443         /**
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.
449          * 
450          * @return      a string digest of this motor (0-60 chars)
451          */
452         public String getDigestString() {
453                 return digest;
454         }
455         
456         
457         public boolean similar(Motor other) {
458                 // TODO: HIGH:  Merge with equals / cleanup
459
460                 // Tests manufacturer, designation, diameter and length
461                 if (this.compareTo(other) != 0)
462                         return false;
463                 
464                 // Compare total time
465                 if (Math.abs(this.getTotalTime() - other.getTotalTime()) > 0.5) {
466                         return false;
467                 }
468                 
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)) {
472                         return false;
473                 }
474
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) {
478                                 return false;
479                         }
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) {
483                                         return false;
484                                 }
485                         }
486                 }
487                 
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) {
492                                 return false;
493                         }
494                 }
495
496                 return true;
497         }
498         
499         /**
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).
504          * <p>
505          * The comment field is ignored when comparing equality.
506          * <p>
507          * TODO: HIGH: Check for identical contents instead
508          */
509         @Override
510         public boolean equals(Object o) {
511                 if (!(o instanceof Motor))
512                         return false;
513                 
514                 Motor other = (Motor) o;
515                 
516                 // Tests manufacturer, designation, diameter and length
517                 if (this.compareTo(other) != 0)
518                         return false;
519                 
520                 // Compare total time
521                 if (Math.abs(this.getTotalTime() - other.getTotalTime()) > 0.5) {
522                         return false;
523                 }
524                 
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)) {
528                         return false;
529                 }
530
531                 // Compare delays
532                 if (this.delays.length != other.delays.length) {
533                         return false;
534                 }
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) {
538                                 return false;
539                         }
540                 }
541                 
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) {
546                                 return false;
547                         }
548                 }
549
550                 return true;
551         }
552         
553         /**
554          * A <code>hashCode</code> method compatible with the <code>equals</code>
555          * method.
556          */
557         @Override
558         public int hashCode() {
559                 return (manufacturer.hashCode() + designation.hashCode() + 
560                                 ((int)(length*1000)) + ((int)(diameter*1000)));
561         }
562         
563         
564         
565         @Override
566         public String toString() {
567                 return manufacturer + " " + designation;
568         }
569         
570         
571         //////////  Static methods
572
573         
574         /**
575          * Return a String representation of a delay time.  If the delay is {@link #PLUGGED},
576          * returns "P".
577          *  
578          * @param delay         the delay time.
579          * @return                      the <code>String</code> representation.
580          */
581         public static String getDelayString(double delay) {
582                 return getDelayString(delay,"P");
583         }
584         
585         /**
586          * Return a String representation of a delay time.  If the delay is {@link #PLUGGED},
587          * <code>plugged</code> is returned.
588          *   
589          * @param delay         the delay time.
590          * @param plugged       the return value if there is no ejection charge.
591          * @return                      the String representation.
592          */
593         public static String getDelayString(double delay, String plugged) {
594                 if (delay == PLUGGED)
595                         return plugged;
596                 delay = Math.rint(delay*10)/10;
597                 if (MathUtil.equals(delay, Math.rint(delay)))
598                         return "" + ((int)delay);
599                 return "" + delay;
600         }
601         
602
603         
604         
605         ////////////  Comparation
606         
607         
608
609         @Override
610         public int compareTo(Motor other) {
611                 int value;
612                 
613                 // 1. Manufacturer
614                 value = COLLATOR.compare(this.manufacturer.getDisplayName(), 
615                                 other.manufacturer.getDisplayName());
616                 if (value != 0)
617                         return value;
618                 
619                 // 2. Designation
620                 value = DESIGNATION_COMPARATOR.compare(this.designation, other.designation);
621                 if (value != 0)
622                         return value;
623                 
624                 // 3. Diameter
625                 value = (int)((this.diameter - other.diameter)*1000000);
626                 if (value != 0)
627                         return value;
628                                 
629                 // 4. Length
630                 value = (int)((this.length - other.length)*1000000);
631                 if (value != 0)
632                         return value;
633                 
634                 // 5. Total impulse
635                 value = (int)((this.getTotalImpulse() - other.getTotalImpulse())*1000);
636                 return value;
637         }
638         
639         
640         
641         public static Comparator<String> getDesignationComparator() {
642                 return DESIGNATION_COMPARATOR;
643         }
644         
645         
646         /**
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.
650          * 
651          * @author Sampo Niskanen <sampo.niskanen@iki.fi>
652          */
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]+)(.*?)$");
656                 
657                 @Override
658                 public int compare(String o1, String o2) {
659                         int value;
660                         Matcher m1, m2;
661                         
662                         m1 = pattern.matcher(o1);
663                         m2 = pattern.matcher(o2);
664                         
665                         if (m1.find() && m2.find()) {
666
667                                 String o1Class = m1.group(3);
668                                 int o1Thrust = Integer.parseInt(m1.group(4));
669                                 String o1Extra = m1.group(5);
670                                 
671                                 String o2Class = m2.group(3);
672                                 int o2Thrust = Integer.parseInt(m2.group(4));
673                                 String o2Extra = m2.group(5);
674                                 
675                                 // 1. Motor class
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);
680
681                                         if (sub1 != null || sub2 != null) {
682                                                 if (sub1 == null)
683                                                         sub1 = "1";
684                                                 if (sub2 == null)
685                                                         sub2 = "1";
686                                                 value = -COLLATOR.compare(sub1,sub2);
687                                                 if (value != 0)
688                                                         return value;
689                                         }
690                                 }
691                                 value = COLLATOR.compare(o1Class,o2Class);
692                                 if (value != 0)
693                                         return value;
694                                 
695                                 // 2. Average thrust
696                                 if (o1Thrust != o2Thrust)
697                                         return o1Thrust - o2Thrust;
698                                 
699                                 // 3. Extra modifier
700                                 return COLLATOR.compare(o1Extra, o2Extra);
701                                 
702                         } else {
703                                 
704                                 // Not understandable designation, simply compare strings
705                                 return COLLATOR.compare(o1, o2);
706                         }
707                 }
708         }
709 }