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