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