moving to core/
[debian/openrocket] / core / src / net / sf / openrocket / motor / ThrustCurveMotor.java
1 package net.sf.openrocket.motor;
2
3 import java.io.Serializable;
4 import java.text.Collator;
5 import java.util.Arrays;
6 import java.util.Locale;
7
8 import net.sf.openrocket.logging.LogHelper;
9 import net.sf.openrocket.models.atmosphere.AtmosphericConditions;
10 import net.sf.openrocket.startup.Application;
11 import net.sf.openrocket.util.BugException;
12 import net.sf.openrocket.util.Coordinate;
13 import net.sf.openrocket.util.Inertia;
14 import net.sf.openrocket.util.MathUtil;
15
16
17 public class ThrustCurveMotor implements Motor, Comparable<ThrustCurveMotor> {
18         private static final LogHelper log = Application.getLogger();
19         
20         public static final double MAX_THRUST = 10e6;
21         
22         //  Comparators:
23         private static final Collator COLLATOR = Collator.getInstance(Locale.US);
24         static {
25                 COLLATOR.setStrength(Collator.PRIMARY);
26         }
27         private static final DesignationComparator DESIGNATION_COMPARATOR = new DesignationComparator();
28         
29
30
31         private final Manufacturer manufacturer;
32         private final String designation;
33         private final String description;
34         private final Motor.Type type;
35         private final double[] delays;
36         private final double diameter;
37         private final double length;
38         private final double[] time;
39         private final double[] thrust;
40         private final Coordinate[] cg;
41         
42         private double maxThrust;
43         private double burnTime;
44         private double averageThrust;
45         private double totalImpulse;
46         
47         
48         /**
49          * Sole constructor.  Sets all the properties of the motor.
50          * 
51          * @param manufacturer  the manufacturer of the motor.
52          * @param designation   the designation of the motor.
53          * @param description   extra description of the motor.
54          * @param type                  the motor type
55          * @param delays                the delays defined for this thrust curve
56          * @param diameter      diameter of the motor.
57          * @param length        length of the motor.
58          * @param time          the time points for the thrust curve.
59          * @param thrust        thrust at the time points.
60          * @param cg            cg at the time points.
61          */
62         public ThrustCurveMotor(Manufacturer manufacturer, String designation, String description,
63                         Motor.Type type, double[] delays, double diameter, double length,
64                         double[] time, double[] thrust, Coordinate[] cg) {
65                 
66                 // Check argument validity
67                 if ((time.length != thrust.length) || (time.length != cg.length)) {
68                         throw new IllegalArgumentException("Array lengths do not match, " +
69                                         "time:" + time.length + " thrust:" + thrust.length +
70                                         " cg:" + cg.length);
71                 }
72                 if (time.length < 2) {
73                         throw new IllegalArgumentException("Too short thrust-curve, length=" +
74                                         time.length);
75                 }
76                 for (int i = 0; i < time.length - 1; i++) {
77                         if (time[i + 1] < time[i]) {
78                                 throw new IllegalArgumentException("Time goes backwards, " +
79                                                 "time[" + i + "]=" + time[i] + " " +
80                                                 "time[" + (i + 1) + "]=" + time[i + 1]);
81                         }
82                 }
83                 if (!MathUtil.equals(time[0], 0)) {
84                         throw new IllegalArgumentException("Curve starts at time " + time[0]);
85                 }
86                 if (!MathUtil.equals(thrust[0], 0)) {
87                         throw new IllegalArgumentException("Curve starts at thrust " + thrust[0]);
88                 }
89                 if (!MathUtil.equals(thrust[thrust.length - 1], 0)) {
90                         throw new IllegalArgumentException("Curve ends at thrust " +
91                                         thrust[thrust.length - 1]);
92                 }
93                 for (double t : thrust) {
94                         if (t < 0) {
95                                 throw new IllegalArgumentException("Negative thrust.");
96                         }
97                         if (t > MAX_THRUST || Double.isNaN(t)) {
98                                 throw new IllegalArgumentException("Invalid thrust " + t);
99                         }
100                 }
101                 for (Coordinate c : cg) {
102                         if (c.isNaN()) {
103                                 throw new IllegalArgumentException("Invalid CG " + c);
104                         }
105                         if (c.x < 0 || c.x > length) {
106                                 throw new IllegalArgumentException("Invalid CG position " + c.x);
107                         }
108                         if (c.weight < 0) {
109                                 throw new IllegalArgumentException("Negative mass " + c.weight);
110                         }
111                 }
112                 
113                 if (type != Motor.Type.SINGLE && type != Motor.Type.RELOAD &&
114                                 type != Motor.Type.HYBRID && type != Motor.Type.UNKNOWN) {
115                         throw new IllegalArgumentException("Illegal motor type=" + type);
116                 }
117                 
118
119                 this.manufacturer = manufacturer;
120                 this.designation = designation;
121                 this.description = description;
122                 this.type = type;
123                 this.delays = delays.clone();
124                 this.diameter = diameter;
125                 this.length = length;
126                 this.time = time.clone();
127                 this.thrust = thrust.clone();
128                 this.cg = cg.clone();
129                 
130                 computeStatistics();
131         }
132         
133         
134
135         /**
136          * Get the manufacturer of this motor.
137          * 
138          * @return the manufacturer
139          */
140         public Manufacturer getManufacturer() {
141                 return manufacturer;
142         }
143         
144         
145         /**
146          * Return the array of time points for this thrust curve.
147          * @return      an array of time points where the thrust is sampled
148          */
149         public double[] getTimePoints() {
150                 return time.clone();
151         }
152         
153         /**
154          * Returns the array of thrust points for this thrust curve.
155          * @return      an array of thrust samples
156          */
157         public double[] getThrustPoints() {
158                 return thrust.clone();
159         }
160         
161         /**
162          * Returns the array of CG points for this thrust curve.
163          * @return      an array of CG samples
164          */
165         public Coordinate[] getCGPoints() {
166                 return cg.clone();
167         }
168         
169         /**
170          * Return a list of standard delays defined for this motor.
171          * @return      a list of standard delays
172          */
173         public double[] getStandardDelays() {
174                 return delays.clone();
175         }
176         
177         
178         /**
179          * {@inheritDoc}
180          * <p>
181          * NOTE: In most cases you want to examine the motor type of the ThrustCurveMotorSet,
182          * not the ThrustCurveMotor itself.
183          */
184         @Override
185         public Type getMotorType() {
186                 return type;
187         }
188         
189         
190         @Override
191         public String getDesignation() {
192                 return designation;
193         }
194         
195         @Override
196         public String getDesignation(double delay) {
197                 return designation + "-" + getDelayString(delay);
198         }
199         
200         
201         @Override
202         public String getDescription() {
203                 return description;
204         }
205         
206         @Override
207         public double getDiameter() {
208                 return diameter;
209         }
210         
211         @Override
212         public double getLength() {
213                 return length;
214         }
215         
216         
217         @Override
218         public MotorInstance getInstance() {
219                 return new ThrustCurveMotorInstance();
220         }
221         
222         
223         @Override
224         public Coordinate getLaunchCG() {
225                 return cg[0];
226         }
227         
228         @Override
229         public Coordinate getEmptyCG() {
230                 return cg[cg.length - 1];
231         }
232         
233         
234
235
236         @Override
237         public double getBurnTimeEstimate() {
238                 return burnTime;
239         }
240         
241         @Override
242         public double getAverageThrustEstimate() {
243                 return averageThrust;
244         }
245         
246         @Override
247         public double getMaxThrustEstimate() {
248                 return maxThrust;
249         }
250         
251         @Override
252         public double getTotalImpulseEstimate() {
253                 return totalImpulse;
254         }
255         
256         
257
258         /**
259          * Compute the general statistics of this motor.
260          */
261         private void computeStatistics() {
262                 
263                 // Maximum thrust
264                 maxThrust = 0;
265                 for (double t : thrust) {
266                         if (t > maxThrust)
267                                 maxThrust = t;
268                 }
269                 
270
271                 // Burn start time
272                 double thrustLimit = maxThrust * MARGINAL_THRUST;
273                 double burnStart, burnEnd;
274                 
275                 int pos;
276                 for (pos = 1; pos < thrust.length; pos++) {
277                         if (thrust[pos] >= thrustLimit)
278                                 break;
279                 }
280                 if (pos >= thrust.length) {
281                         throw new BugException("Could not compute burn start time, maxThrust=" + maxThrust +
282                                         " limit=" + thrustLimit + " thrust=" + Arrays.toString(thrust));
283                 }
284                 if (MathUtil.equals(thrust[pos - 1], thrust[pos])) {
285                         // For safety
286                         burnStart = (time[pos - 1] + time[pos]) / 2;
287                 } else {
288                         burnStart = MathUtil.map(thrustLimit, thrust[pos - 1], thrust[pos], time[pos - 1], time[pos]);
289                 }
290                 
291
292                 // Burn end time
293                 for (pos = thrust.length - 2; pos >= 0; pos--) {
294                         if (thrust[pos] >= thrustLimit)
295                                 break;
296                 }
297                 if (pos < 0) {
298                         throw new BugException("Could not compute burn end time, maxThrust=" + maxThrust +
299                                         " limit=" + thrustLimit + " thrust=" + Arrays.toString(thrust));
300                 }
301                 if (MathUtil.equals(thrust[pos], thrust[pos + 1])) {
302                         // For safety
303                         burnEnd = (time[pos] + time[pos + 1]) / 2;
304                 } else {
305                         burnEnd = MathUtil.map(thrustLimit, thrust[pos], thrust[pos + 1],
306                                         time[pos], time[pos + 1]);
307                 }
308                 
309
310                 // Burn time
311                 burnTime = Math.max(burnEnd - burnStart, 0);
312                 
313
314                 // Total impulse and average thrust
315                 totalImpulse = 0;
316                 averageThrust = 0;
317                 
318                 for (pos = 0; pos < time.length - 1; pos++) {
319                         double t0 = time[pos];
320                         double t1 = time[pos + 1];
321                         double f0 = thrust[pos];
322                         double f1 = thrust[pos + 1];
323                         
324                         totalImpulse += (t1 - t0) * (f0 + f1) / 2;
325                         
326                         if (t0 < burnStart && t1 > burnStart) {
327                                 double fStart = MathUtil.map(burnStart, t0, t1, f0, f1);
328                                 averageThrust += (fStart + f1) / 2 * (t1 - burnStart);
329                         } else if (t0 >= burnStart && t1 <= burnEnd) {
330                                 averageThrust += (f0 + f1) / 2 * (t1 - t0);
331                         } else if (t0 < burnEnd && t1 > burnEnd) {
332                                 double fEnd = MathUtil.map(burnEnd, t0, t1, f0, f1);
333                                 averageThrust += (f0 + fEnd) / 2 * (burnEnd - t0);
334                         }
335                 }
336                 
337                 if (burnTime > 0) {
338                         averageThrust /= burnTime;
339                 } else {
340                         averageThrust = 0;
341                 }
342                 
343         }
344         
345         
346         //////////  Static methods
347         
348         /**
349          * Return a String representation of a delay time.  If the delay is {@link #PLUGGED},
350          * returns "P".
351          *  
352          * @param delay         the delay time.
353          * @return                      the <code>String</code> representation.
354          */
355         public static String getDelayString(double delay) {
356                 return getDelayString(delay, "P");
357         }
358         
359         /**
360          * Return a String representation of a delay time.  If the delay is {@link #PLUGGED},
361          * <code>plugged</code> is returned.
362          *   
363          * @param delay         the delay time.
364          * @param plugged       the return value if there is no ejection charge.
365          * @return                      the String representation.
366          */
367         public static String getDelayString(double delay, String plugged) {
368                 if (delay == PLUGGED)
369                         return plugged;
370                 delay = Math.rint(delay * 10) / 10;
371                 if (MathUtil.equals(delay, Math.rint(delay)))
372                         return "" + ((int) delay);
373                 return "" + delay;
374         }
375         
376         
377
378         ////////  Motor instance implementation  ////////
379         private class ThrustCurveMotorInstance implements MotorInstance {
380                 
381                 private int position;
382                 
383                 // Previous time step value
384                 private double prevTime;
385                 
386                 // Average thrust during previous step
387                 private double stepThrust;
388                 // Instantaneous thrust at current time point
389                 private double instThrust;
390                 
391                 // Average CG during previous step
392                 private Coordinate stepCG;
393                 // Instantaneous CG at current time point
394                 private Coordinate instCG;
395                 
396                 private final double unitRotationalInertia;
397                 private final double unitLongitudinalInertia;
398                 
399                 private int modID = 0;
400                 
401                 public ThrustCurveMotorInstance() {
402                         log.debug("ThrustCurveMotor:  Creating motor instance of " + ThrustCurveMotor.this);
403                         position = 0;
404                         prevTime = 0;
405                         instThrust = 0;
406                         stepThrust = 0;
407                         instCG = cg[0];
408                         stepCG = cg[0];
409                         unitRotationalInertia = Inertia.filledCylinderRotational(getDiameter() / 2);
410                         unitLongitudinalInertia = Inertia.filledCylinderLongitudinal(getDiameter() / 2, getLength());
411                 }
412                 
413                 @Override
414                 public double getTime() {
415                         return prevTime;
416                 }
417                 
418                 @Override
419                 public Coordinate getCG() {
420                         return stepCG;
421                 }
422                 
423                 @Override
424                 public double getLongitudinalInertia() {
425                         return unitLongitudinalInertia * stepCG.weight;
426                 }
427                 
428                 @Override
429                 public double getRotationalInertia() {
430                         return unitRotationalInertia * stepCG.weight;
431                 }
432                 
433                 @Override
434                 public double getThrust() {
435                         return stepThrust;
436                 }
437                 
438                 @Override
439                 public boolean isActive() {
440                         return prevTime < time[time.length - 1];
441                 }
442                 
443                 @Override
444                 public void step(double nextTime, double acceleration, AtmosphericConditions cond) {
445                         
446                         if (!(nextTime >= prevTime)) {
447                                 // Also catches NaN
448                                 throw new IllegalArgumentException("Stepping backwards in time, current=" +
449                                                 prevTime + " new=" + nextTime);
450                         }
451                         if (MathUtil.equals(prevTime, nextTime)) {
452                                 return;
453                         }
454                         
455                         modID++;
456                         
457                         if (position >= time.length - 1) {
458                                 // Thrust has ended
459                                 prevTime = nextTime;
460                                 stepThrust = 0;
461                                 instThrust = 0;
462                                 stepCG = cg[cg.length - 1];
463                                 return;
464                         }
465                         
466
467                         // Compute average & instantaneous thrust
468                         if (nextTime < time[position + 1]) {
469                                 
470                                 // Time step between time points
471                                 double nextF = MathUtil.map(nextTime, time[position], time[position + 1],
472                                                 thrust[position], thrust[position + 1]);
473                                 stepThrust = (instThrust + nextF) / 2;
474                                 instThrust = nextF;
475                                 
476                         } else {
477                                 
478                                 // Portion of previous step
479                                 stepThrust = (instThrust + thrust[position + 1]) / 2 * (time[position + 1] - prevTime);
480                                 
481                                 // Whole steps
482                                 position++;
483                                 while ((position < time.length - 1) && (nextTime >= time[position + 1])) {
484                                         stepThrust += (thrust[position] + thrust[position + 1]) / 2 *
485                                                                         (time[position + 1] - time[position]);
486                                         position++;
487                                 }
488                                 
489                                 // End step
490                                 if (position < time.length - 1) {
491                                         instThrust = MathUtil.map(nextTime, time[position], time[position + 1],
492                                                         thrust[position], thrust[position + 1]);
493                                         stepThrust += (thrust[position] + instThrust) / 2 *
494                                                                         (nextTime - time[position]);
495                                 } else {
496                                         // Thrust ended during this step
497                                         instThrust = 0;
498                                 }
499                                 
500                                 stepThrust /= (nextTime - prevTime);
501                                 
502                         }
503                         
504                         // Compute average and instantaneous CG (simple average between points)
505                         Coordinate nextCG;
506                         if (position < time.length - 1) {
507                                 nextCG = MathUtil.map(nextTime, time[position], time[position + 1],
508                                                 cg[position], cg[position + 1]);
509                         } else {
510                                 nextCG = cg[cg.length - 1];
511                         }
512                         stepCG = instCG.add(nextCG).multiply(0.5);
513                         instCG = nextCG;
514                         
515                         // Update time
516                         prevTime = nextTime;
517                 }
518                 
519                 @Override
520                 public MotorInstance clone() {
521                         try {
522                                 return (MotorInstance) super.clone();
523                         } catch (CloneNotSupportedException e) {
524                                 throw new BugException("CloneNotSupportedException", e);
525                         }
526                 }
527                 
528                 @Override
529                 public int getModID() {
530                         return modID;
531                 }
532         }
533         
534         
535
536         @Override
537         public int compareTo(ThrustCurveMotor other) {
538                 
539                 int value;
540                 
541                 // 1. Manufacturer
542                 value = COLLATOR.compare(this.manufacturer.getDisplayName(),
543                                 ((ThrustCurveMotor) other).manufacturer.getDisplayName());
544                 if (value != 0)
545                         return value;
546                 
547                 // 2. Designation
548                 value = DESIGNATION_COMPARATOR.compare(this.getDesignation(), other.getDesignation());
549                 if (value != 0)
550                         return value;
551                 
552                 // 3. Diameter
553                 value = (int) ((this.getDiameter() - other.getDiameter()) * 1000000);
554                 if (value != 0)
555                         return value;
556                 
557                 // 4. Length
558                 value = (int) ((this.getLength() - other.getLength()) * 1000000);
559                 return value;
560                 
561         }
562         
563
564 }