ce746d332ab24b434557bd1f28f8fb14ffdb0c77
[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.Comparator;
5 import java.util.Locale;
6 import java.util.regex.Matcher;
7 import java.util.regex.Pattern;
8
9 import net.sf.openrocket.util.Coordinate;
10 import net.sf.openrocket.util.MathUtil;
11
12
13
14 /**
15  * Abstract base class for motors.  The methods that must be implemented are
16  * {@link #getTotalTime()}, {@link #getThrust(double)} and {@link #getCG(double)}.
17  * Additionally the method {@link #getMaxThrust()} may be overridden for efficiency.
18  * <p>
19  * 
20  * NOTE:  The current implementation of {@link #getAverageTime()} and 
21  * {@link #getAverageThrust()} assume that the class is immutable!
22  * 
23  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
24  */
25 public abstract class Motor implements Comparable<Motor> {
26         
27         /**
28          * Enum of rocket motor types.
29          * 
30          * @author Sampo Niskanen <sampo.niskanen@iki.fi>
31          */
32         public enum Type {
33                 SINGLE("Single-use", "Single-use solid propellant motor"), 
34                 RELOAD("Reloadable", "Reloadable solid propellant motor"), 
35                 HYBRID("Hybrid", "Hybrid rocket motor engine"), 
36                 UNKNOWN("Unknown", "Unknown motor type");
37                 
38                 private final String name;
39                 private final String description;
40                 
41                 Type(String name, String description) {
42                         this.name = name;
43                         this.description = description;
44                 }
45
46                 /**
47                  * Return a short name of this motor type.
48                  * @return  a short name of the motor type.
49                  */
50                 public String getName() {
51                         return name;
52                 }
53                 
54                 /**
55                  * Return a long description of this motor type.
56                  * @return  a description of the motor type.
57                  */
58                 public String getDescription() {
59                         return description;
60                 }
61                 
62                 @Override
63                 public String toString() {
64                         return name;
65                 }
66         }
67         
68         
69         /**
70          * Ejection charge delay value signifying a "plugged" motor with no ejection charge.
71          * The value is that of <code>Double.POSITIVE_INFINITY</code>.
72          */
73         public static final double PLUGGED = Double.POSITIVE_INFINITY;
74         
75         
76         /**
77          * Below what portion of maximum thrust is the motor chosen to be off when
78          * calculating average thrust and burn time.  NFPA 1125 defines the "official"
79          * burn time to be the time which the motor produces over 5% of its maximum thrust.
80          */
81         public static final double AVERAGE_MARGINAL = 0.05;
82         
83         /* All data is cached, so divisions can be very tight. */
84         private static final int DIVISIONS = 1000;
85
86         
87         //  Comparators:
88         private static final Collator COLLATOR = Collator.getInstance(Locale.US);
89         static {
90                 COLLATOR.setStrength(Collator.PRIMARY);
91         }
92         private static DesignationComparator DESIGNATION_COMPARATOR = new DesignationComparator();
93         
94         
95         
96         
97         private final Manufacturer manufacturer;
98         private final String designation;
99         private final String description;
100         private final Type motorType;
101         private final String digest;
102         
103         private final double[] delays;
104         
105         private final double diameter;
106         private final double length;
107         
108         /* Cached data */
109         private double maxThrust = -1;
110         private double avgTime = -1;
111         private double avgThrust = -1;
112         private double totalImpulse = -1;
113         
114         
115         
116         /**
117          * Sole constructor.  None of the parameters may be <code>null</code>.
118          * 
119          * @param manufacturer  the manufacturer of the motor.
120          * @param designation   the motor designation.
121          * @param description   further description, including any comments on the origin
122          *                                              of the thrust curve.
123          * @param delays                an array of the standard ejection charge delays.  A plugged
124          *                                              motor (no ejection charge) is specified by a delay of
125          *                                              {@link #PLUGGED} (<code>Double.POSITIVE_INFINITY</code>).
126          * @param diameter              maximum diameter of the motor
127          * @param length                length of the motor
128          */
129         protected Motor(Manufacturer manufacturer, String designation, String description, 
130                         Type type, double[] delays, double diameter, double length, String digest) {
131
132                 if (manufacturer == null || designation == null || description == null ||
133                                 type == null || delays == null) {
134                         throw new IllegalArgumentException("Parameters cannot be null.");
135                 }
136                 
137                 this.manufacturer = manufacturer;
138                 this.designation = designation;
139                 this.description = description.trim();
140                 this.motorType = type;
141                 this.delays = delays.clone();
142                 this.diameter = diameter;
143                 this.length = length;
144                 this.digest = digest;
145         }
146
147
148         
149         /**
150          * Return the total burn time of the motor.  The method {@link #getThrust(double)}
151          * must return zero for time values greater than the return value.
152          * 
153          * @return  the total burn time of the motor.
154          */
155         public abstract double getTotalTime();
156
157
158         /**
159          * Return the thrust of the motor at the specified time.
160          * 
161          * @param time  time since the ignition of the motor.
162          * @return      the thrust at the specified time.
163          */
164         public abstract double getThrust(double time);
165         
166         
167         /**
168          * Return the average thrust of the motor between times t1 and t2.
169          * 
170          * @param t1    starting time since the ignition of the motor.
171          * @param t2    end time since the ignition of the motor.
172          * @return              the average thrust during the time period.
173          */
174         /* TODO: MEDIUM: Implement better method in subclass */
175         public double getThrust(double t1, double t2) {
176                 double f = 0;
177                 f += getThrust(t1);
178                 f += getThrust(0.8*t1 + 0.2*t2);
179                 f += getThrust(0.6*t1 + 0.4*t2);
180                 f += getThrust(0.4*t1 + 0.6*t2);
181                 f += getThrust(0.2*t1 + 0.8*t2);
182                 f += getThrust(t2);
183                 return f/6;
184         }
185
186         
187         /**
188          * Return the mass and CG of the motor at the specified time.
189          * 
190          * @param time  time since the ignition of the motor.
191          * @return      the mass and CG of the motor.
192          */
193         public abstract Coordinate getCG(double time);
194         
195         
196         
197         /**
198          * Return the mass of the motor at the specified time.  The original mass
199          * of the motor can be queried by <code>getMass(0)</code> and the burnt mass
200          * by <code>getMass(Double.MAX_VALUE)</code>.
201          * 
202          * @param time  time since the ignition of the motor.
203          * @return      the mass of the motor.
204          */
205         public double getMass(double time) {
206                 return getCG(time).weight;
207         }
208         
209         
210         /**
211          * Return the longitudal moment of inertia of the motor at the specified time.
212          * This default method assumes that the mass of the motor is evenly distributed
213          * in a cylinder with the diameter and length of the motor.
214          * 
215          * @param time  time since the ignition of the motor.
216          * @return              the longitudal moment of inertia of the motor.
217          */
218         public double getLongitudalInertia(double time) {
219                 return getMass(time) * (3.0*MathUtil.pow2(diameter/2) + MathUtil.pow2(length))/12;
220         }
221         
222         
223         
224         /**
225          * Return the rotational moment of inertia of the motor at the specified time.
226          * This default method assumes that the mass of the motor is evenly distributed
227          * in a cylinder with the diameter and length of the motor.
228          * 
229          * @param time  time since the ignition of the motor.
230          * @return              the rotational moment of inertia of the motor.
231          */
232         public double getRotationalInertia(double time) {
233                 return getMass(time) * MathUtil.pow2(diameter) / 8;
234         }
235         
236         
237         
238         
239         /**
240          * Return the maximum thrust.  This implementation slices through the thrust curve
241          * searching for the maximum thrust.  Subclasses may wish to override this with a
242          * more efficient method.
243          * 
244          * @return  the maximum thrust of the motor
245          */
246         public double getMaxThrust() {
247                 if (maxThrust < 0) {
248                         double time = getTotalTime();
249                         maxThrust = 0;
250                         
251                         for (int i=0; i < DIVISIONS; i++) {
252                                 double t = time * i / DIVISIONS;
253                                 double thrust = getThrust(t);
254                                 
255                                 if (thrust > maxThrust)
256                                         maxThrust = thrust;
257                         }
258                 }
259                 return maxThrust;
260         }
261         
262         
263         /**
264          * Return the time used in calculating the average thrust.  The time is the
265          * length of time that the motor produces over 5% ({@link #AVERAGE_MARGINAL})
266          * of its maximum thrust.
267          * 
268          * @return  the nominal burn time.
269          */
270         public double getAverageTime() {
271                 // Compute average time lazily
272                 if (avgTime < 0) {
273                         double max = getMaxThrust();
274                         double time = getTotalTime();
275                         
276                         avgTime = 0;
277                         for (int i=0; i <= DIVISIONS; i++) {
278                                 double t = i*time/DIVISIONS;
279                                 if (getThrust(t) >= max*AVERAGE_MARGINAL)
280                                         avgTime++;
281                         }
282                         avgTime *= time/(DIVISIONS+1);
283                         
284                         if (Double.isNaN(avgTime))
285                                 throw new RuntimeException("Calculated avg. time is NaN for motor "+this);
286
287                 }
288                 return avgTime;
289         }
290         
291         
292         /**
293          * Return the calculated average thrust during the time the motor produces
294          * over 5% ({@link #AVERAGE_MARGINAL}) of its thrust.
295          * 
296          * @return  the nominal average thrust.
297          */
298         public double getAverageThrust() {
299                 // Compute average thrust lazily
300                 if (avgThrust < 0) {
301                         double max = getMaxThrust();
302                         double time = getTotalTime();
303                         int points = 0;
304                         
305                         avgThrust = 0;
306                         for (int i=0; i <= DIVISIONS; i++) {
307                                 double t = i*time/DIVISIONS;
308                                 double thrust = getThrust(t);
309                                 if (thrust >= max*AVERAGE_MARGINAL) {
310                                         avgThrust += thrust;
311                                         points++;
312                                 }
313                         }
314                         if (points > 0)
315                                 avgThrust /= points;
316                         
317                         if (Double.isNaN(avgThrust))
318                                 throw new RuntimeException("Calculated average thrust is NaN for motor "+this);
319                 }
320                 return avgThrust;
321         }
322         
323         
324         /**
325          * Return the total impulse of the motor.  This is calculated from the entire
326          * burn time, and therefore may differ from the value of {@link #getAverageTime()}
327          * and {@link #getAverageThrust()} multiplied together.
328          * 
329          * @return  the total impulse of the motor.
330          */
331         public double getTotalImpulse() {
332                 // Compute total impulse lazily
333                 if (totalImpulse < 0) {
334                         double time = getTotalTime();
335                         double f0, t0;
336                         
337                         totalImpulse = 0;
338                         t0 = 0;
339                         f0 = getThrust(0);
340                         for (int i=1; i < DIVISIONS; i++) {
341                                 double t1 = time * i / DIVISIONS;
342                                 double f1 = getThrust(t1); 
343                                 totalImpulse += 0.5*(f0+f1)*(t1-t0);
344                                 t0 = t1;
345                                 f0 = f1;
346                         }
347                         
348                         if (Double.isNaN(totalImpulse))
349                                 throw new RuntimeException("Calculated total impulse is NaN for motor "+this);
350                 }
351                 return totalImpulse;
352         }
353         
354
355         /**
356          * Return the manufacturer of the motor.
357          * 
358          * @return the manufacturer
359          */
360         public Manufacturer getManufacturer() {
361                 return manufacturer;
362         }
363         
364         /**
365          * Return the designation of the motor.
366          * 
367          * @return the designation
368          */
369         public String getDesignation() {
370                 return designation;
371         }
372         
373         /**
374          * Return the designation of the motor, including a delay.
375          * 
376          * @param delay  the delay of the motor.
377          * @return               designation with delay.
378          */
379         public String getDesignation(double delay) {
380                 return getDesignation() + "-" + getDelayString(delay);
381         }
382
383         
384         /**
385          * Return extra description for the motor.  This may include for example 
386          * comments on the source of the thrust curve.  The returned <code>String</code>
387          * may include new-lines.
388          * 
389          * @return the description
390          */
391         public String getDescription() {
392                 return description;
393         }
394         
395         
396         /**
397          * Return the motor type.
398          * 
399          * @return  the motorType
400          */
401         public Type getMotorType() {
402                 return motorType;
403         }
404
405
406
407         /**
408          * Return the standard ejection charge delays for the motor.  "Plugged" motors
409          * with no ejection charge are signified by the value {@link #PLUGGED}
410          * (<code>Double.POSITIVE_INFINITY</code>).
411          * 
412          * @return  the list of standard ejection charge delays, which may be empty.
413          */
414         public double[] getStandardDelays() {
415                 return delays.clone();
416         }
417
418         /**
419          * Return the maximum diameter of the motor.
420          * 
421          * @return the diameter
422          */
423         public double getDiameter() {
424                 return diameter;
425         }
426
427         /**
428          * Return the length of the motor.  This should be a "characteristic" length,
429          * and the exact definition may depend on the motor type.  Typically this should
430          * be the length from the bottom of the motor to the end of the maximum diameter
431          * portion, ignoring any smaller ejection charge compartments.
432          * 
433          * @return the length
434          */
435         public double getLength() {
436                 return length;
437         }
438         
439         
440         /**
441          * Return a digest string of this motor.  This digest should be computed from all
442          * flight-affecting data.  For example for thrust curve motors the thrust curve
443          * should be digested using suitable precision.  The intention is that the combination
444          * of motor type, manufacturer, designation, diameter, length and digest uniquely
445          * identify any particular motor data file.
446          * 
447          * @return      a string digest of this motor (0-60 chars)
448          */
449         public String getDigestString() {
450                 return digest;
451         }
452         
453         
454         /**
455          * Compares two <code>Motor</code> objects.  The motors are considered equal
456          * if they have identical manufacturers, designations and types, near-identical
457          * dimensions, burn times and delays and near-identical thrust curves
458          * (sampled at 10 equidistant points).
459          * <p>
460          * The comment field is ignored when comparing equality.
461          */
462         @Override
463         public boolean equals(Object o) {
464                 if (!(o instanceof Motor))
465                         return false;
466                 
467                 Motor other = (Motor) o;
468                 
469                 // Tests manufacturer, designation, diameter and length
470                 if (this.compareTo(other) != 0)
471                         return false;
472                 
473                 if (Math.abs(this.getTotalTime() - other.getTotalTime()) > 0.5 ||
474                                 this.motorType != other.motorType ||
475                                 this.delays.length != other.delays.length) {
476                         
477                         return false;
478                 }
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                 double time = getTotalTime();
488                 for (int i=0; i < 10; i++) {
489                         double t = time * i/10;
490                         if (Math.abs(this.getThrust(t) - other.getThrust(t)) > 1) {
491                                 return false;
492                         }
493                 }
494                 return true;
495         }
496         
497         /**
498          * A <code>hashCode</code> method compatible with the <code>equals</code>
499          * method.
500          */
501         @Override
502         public int hashCode() {
503                 return (manufacturer.hashCode() + designation.hashCode() + 
504                                 ((int)(length*1000)) + ((int)(diameter*1000)));
505         }
506         
507         
508         
509         @Override
510         public String toString() {
511                 return manufacturer + " " + designation;
512         }
513         
514         
515         //////////  Static methods
516
517         
518         /**
519          * Return a String representation of a delay time.  If the delay is {@link #PLUGGED},
520          * returns "P".
521          *  
522          * @param delay         the delay time.
523          * @return                      the <code>String</code> representation.
524          */
525         public static String getDelayString(double delay) {
526                 return getDelayString(delay,"P");
527         }
528         
529         /**
530          * Return a String representation of a delay time.  If the delay is {@link #PLUGGED},
531          * <code>plugged</code> is returned.
532          *   
533          * @param delay         the delay time.
534          * @param plugged       the return value if there is no ejection charge.
535          * @return                      the String representation.
536          */
537         public static String getDelayString(double delay, String plugged) {
538                 if (delay == PLUGGED)
539                         return plugged;
540                 delay = Math.rint(delay*10)/10;
541                 if (MathUtil.equals(delay, Math.rint(delay)))
542                         return "" + ((int)delay);
543                 return "" + delay;
544         }
545         
546
547         
548         
549         ////////////  Comparation
550         
551         
552
553         @Override
554         public int compareTo(Motor other) {
555                 int value;
556                 
557                 // 1. Manufacturer
558                 value = COLLATOR.compare(this.manufacturer.getDisplayName(), 
559                                 other.manufacturer.getDisplayName());
560                 if (value != 0)
561                         return value;
562                 
563                 // 2. Designation
564                 value = DESIGNATION_COMPARATOR.compare(this.designation, other.designation);
565                 if (value != 0)
566                         return value;
567                 
568                 // 3. Diameter
569                 value = (int)((this.diameter - other.diameter)*1000000);
570                 if (value != 0)
571                         return value;
572                                 
573                 // 4. Length
574                 value = (int)((this.length - other.length)*1000000);
575                 if (value != 0)
576                         return value;
577                 
578                 // 5. Total impulse
579                 value = (int)((this.getTotalImpulse() - other.getTotalImpulse())*1000);
580                 return value;
581         }
582         
583         
584         
585         public static Comparator<String> getDesignationComparator() {
586                 return DESIGNATION_COMPARATOR;
587         }
588         
589         
590         /**
591          * Compares two motors by their designations.  The motors are ordered first
592          * by their motor class, second by their average thrust and lastly by any
593          * extra modifiers at the end of the designation.
594          * 
595          * @author Sampo Niskanen <sampo.niskanen@iki.fi>
596          */
597         private static class DesignationComparator implements Comparator<String> {
598                 private Pattern pattern = 
599                         Pattern.compile("^([0-9][0-9]+|1/([1-8]))?([a-zA-Z])([0-9]+)(.*?)$");
600                 
601                 @Override
602                 public int compare(String o1, String o2) {
603                         int value;
604                         Matcher m1, m2;
605                         
606                         m1 = pattern.matcher(o1);
607                         m2 = pattern.matcher(o2);
608                         
609                         if (m1.find() && m2.find()) {
610
611                                 String o1Class = m1.group(3);
612                                 int o1Thrust = Integer.parseInt(m1.group(4));
613                                 String o1Extra = m1.group(5);
614                                 
615                                 String o2Class = m2.group(3);
616                                 int o2Thrust = Integer.parseInt(m2.group(4));
617                                 String o2Extra = m2.group(5);
618                                 
619                                 // 1. Motor class
620                                 if (o1Class.equalsIgnoreCase("A") && o2Class.equalsIgnoreCase("A")) {
621                                         //  1/2A and 1/4A comparison
622                                         String sub1 = m1.group(2);
623                                         String sub2 = m2.group(2);
624
625                                         if (sub1 != null || sub2 != null) {
626                                                 if (sub1 == null)
627                                                         sub1 = "1";
628                                                 if (sub2 == null)
629                                                         sub2 = "1";
630                                                 value = -COLLATOR.compare(sub1,sub2);
631                                                 if (value != 0)
632                                                         return value;
633                                         }
634                                 }
635                                 value = COLLATOR.compare(o1Class,o2Class);
636                                 if (value != 0)
637                                         return value;
638                                 
639                                 // 2. Average thrust
640                                 if (o1Thrust != o2Thrust)
641                                         return o1Thrust - o2Thrust;
642                                 
643                                 // 3. Extra modifier
644                                 return COLLATOR.compare(o1Extra, o2Extra);
645                                 
646                         } else {
647                                 
648                                 System.out.println("Falling back");
649                                 System.out.println("o1:"+o1 + " o2:"+o2);
650                                 
651                                 // Not understandable designation, simply compare strings
652                                 return COLLATOR.compare(o1, o2);
653                         }
654                 }
655         }
656 }