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