package net.sf.openrocket.motor;
import java.io.Serializable;
import java.text.Collator;
import java.util.Arrays;
import java.util.Locale;
import net.sf.openrocket.models.atmosphere.AtmosphericConditions;
import net.sf.openrocket.util.ArrayUtils;
import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.Coordinate;
import net.sf.openrocket.util.Inertia;
import net.sf.openrocket.util.MathUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ThrustCurveMotor implements Motor, Comparable<ThrustCurveMotor>, Serializable {
/**
*
*/
private static final long serialVersionUID = -1490333207132694479L;
private static final Logger log = LoggerFactory.getLogger(ThrustCurveMotor.class);
public static final double MAX_THRUST = 10e6;
// Comparators:
private static final Collator COLLATOR = Collator.getInstance(Locale.US);
static {
COLLATOR.setStrength(Collator.PRIMARY);
}
private static final DesignationComparator DESIGNATION_COMPARATOR = new DesignationComparator();
private final String digest;
private final Manufacturer manufacturer;
private final String designation;
private final String description;
private final Motor.Type type;
private final double[] delays;
private final double diameter;
private final double length;
private final double[] time;
private final double[] thrust;
private final Coordinate[] cg;
private double maxThrust;
private double burnTime;
private double averageThrust;
private double totalImpulse;
/**
* Deep copy constructor.
* Constructs a new ThrustCurveMotor from an existing ThrustCurveMotor.
* @param m
*/
protected ThrustCurveMotor(ThrustCurveMotor m) {
this.digest = m.digest;
this.manufacturer = m.manufacturer;
this.designation = m.designation;
this.description = m.description;
this.type = m.type;
this.delays = ArrayUtils.copyOf(m.delays, m.delays.length);
this.diameter = m.diameter;
this.length = m.length;
this.time = ArrayUtils.copyOf(m.time, m.time.length);
this.thrust = ArrayUtils.copyOf(m.thrust, m.thrust.length);
this.cg = new Coordinate[m.cg.length];
for (int i = 0; i < cg.length; i++) {
this.cg[i] = m.cg[i].clone();
}
this.maxThrust = m.maxThrust;
this.burnTime = m.burnTime;
this.averageThrust = m.averageThrust;
this.totalImpulse = m.totalImpulse;
}
/**
* Sole constructor. Sets all the properties of the motor.
*
* @param manufacturer the manufacturer of the motor.
* @param designation the designation of the motor.
* @param description extra description of the motor.
* @param type the motor type
* @param delays the delays defined for this thrust curve
* @param diameter diameter of the motor.
* @param length length of the motor.
* @param time the time points for the thrust curve.
* @param thrust thrust at the time points.
* @param cg cg at the time points.
*/
public ThrustCurveMotor(Manufacturer manufacturer, String designation, String description,
Motor.Type type, double[] delays, double diameter, double length,
double[] time, double[] thrust, Coordinate[] cg, String digest) {
this.digest = digest;
// Check argument validity
if ((time.length != thrust.length) || (time.length != cg.length)) {
throw new IllegalArgumentException("Array lengths do not match, " +
"time:" + time.length + " thrust:" + thrust.length +
" cg:" + cg.length);
}
if (time.length < 2) {
throw new IllegalArgumentException("Too short thrust-curve, length=" +
time.length);
}
for (int i = 0; i < time.length - 1; i++) {
if (time[i + 1] < time[i]) {
throw new IllegalArgumentException("Time goes backwards, " +
"time[" + i + "]=" + time[i] + " " +
"time[" + (i + 1) + "]=" + time[i + 1]);
}
}
if (!MathUtil.equals(time[0], 0)) {
throw new IllegalArgumentException("Curve starts at time " + time[0]);
}
if (!MathUtil.equals(thrust[0], 0)) {
throw new IllegalArgumentException("Curve starts at thrust " + thrust[0]);
}
if (!MathUtil.equals(thrust[thrust.length - 1], 0)) {
throw new IllegalArgumentException("Curve ends at thrust " +
thrust[thrust.length - 1]);
}
for (double t : thrust) {
if (t < 0) {
throw new IllegalArgumentException("Negative thrust.");
}
if (t > MAX_THRUST || Double.isNaN(t)) {
throw new IllegalArgumentException("Invalid thrust " + t);
}
}
for (Coordinate c : cg) {
if (c.isNaN()) {
throw new IllegalArgumentException("Invalid CG " + c);
}
if (c.x < 0) {
throw new IllegalArgumentException("Invalid CG position " + String.format("%f", c.x) + ": CG is below the start of the motor.");
}
if (c.x > length) {
throw new IllegalArgumentException("Invalid CG position: " + String.format("%f", c.x) + ": CG is above the end of the motor.");
}
if (c.weight < 0) {
throw new IllegalArgumentException("Negative mass " + c.weight + "at time=" + time[Arrays.asList(cg).indexOf(c)]);
}
}
if (type != Motor.Type.SINGLE && type != Motor.Type.RELOAD &&
type != Motor.Type.HYBRID && type != Motor.Type.UNKNOWN) {
throw new IllegalArgumentException("Illegal motor type=" + type);
}
this.manufacturer = manufacturer;
this.designation = designation;
this.description = description;
this.type = type;
this.delays = delays.clone();
this.diameter = diameter;
this.length = length;
this.time = time.clone();
this.thrust = thrust.clone();
this.cg = cg.clone();
computeStatistics();
}
/**
* Get the manufacturer of this motor.
*
* @return the manufacturer
*/
public Manufacturer getManufacturer() {
return manufacturer;
}
/**
* Return the array of time points for this thrust curve.
* @return an array of time points where the thrust is sampled
*/
public double[] getTimePoints() {
return time.clone();
}
/**
* Returns the array of thrust points for this thrust curve.
* @return an array of thrust samples
*/
public double[] getThrustPoints() {
return thrust.clone();
}
/**
* Returns the array of CG points for this thrust curve.
* @return an array of CG samples
*/
public Coordinate[] getCGPoints() {
return cg.clone();
}
/**
* Return a list of standard delays defined for this motor.
* @return a list of standard delays
*/
public double[] getStandardDelays() {
return delays.clone();
}
/**
* {@inheritDoc}
* <p>
* NOTE: In most cases you want to examine the motor type of the ThrustCurveMotorSet,
* not the ThrustCurveMotor itself.
*/
@Override
public Type getMotorType() {
return type;
}
@Override
public String getDesignation() {
return designation;
}
@Override
public String getDesignation(double delay) {
return designation + "-" + getDelayString(delay);
}
@Override
public String getDescription() {
return description;
}
@Override
public double getDiameter() {
return diameter;
}
@Override
public double getLength() {
return length;
}
@Override
public MotorInstance getInstance() {
return new ThrustCurveMotorInstance();
}
@Override
public Coordinate getLaunchCG() {
return cg[0];
}
@Override
public Coordinate getEmptyCG() {
return cg[cg.length - 1];
}
@Override
public double getBurnTimeEstimate() {
return burnTime;
}
@Override
public double getAverageThrustEstimate() {
return averageThrust;
}
@Override
public double getMaxThrustEstimate() {
return maxThrust;
}
@Override
public double getTotalImpulseEstimate() {
return totalImpulse;
}
@Override
public String getDigest() {
return digest;
}
/**
* Compute the general statistics of this motor.
*/
private void computeStatistics() {
// Maximum thrust
maxThrust = 0;
for (double t : thrust) {
if (t > maxThrust)
maxThrust = t;
}
// Burn start time
double thrustLimit = maxThrust * MARGINAL_THRUST;
double burnStart, burnEnd;
int pos;
for (pos = 1; pos < thrust.length; pos++) {
if (thrust[pos] >= thrustLimit)
break;
}
if (pos >= thrust.length) {
throw new BugException("Could not compute burn start time, maxThrust=" + maxThrust +
" limit=" + thrustLimit + " thrust=" + Arrays.toString(thrust));
}
if (MathUtil.equals(thrust[pos - 1], thrust[pos])) {
// For safety
burnStart = (time[pos - 1] + time[pos]) / 2;
} else {
burnStart = MathUtil.map(thrustLimit, thrust[pos - 1], thrust[pos], time[pos - 1], time[pos]);
}
// Burn end time
for (pos = thrust.length - 2; pos >= 0; pos--) {
if (thrust[pos] >= thrustLimit)
break;
}
if (pos < 0) {
throw new BugException("Could not compute burn end time, maxThrust=" + maxThrust +
" limit=" + thrustLimit + " thrust=" + Arrays.toString(thrust));
}
if (MathUtil.equals(thrust[pos], thrust[pos + 1])) {
// For safety
burnEnd = (time[pos] + time[pos + 1]) / 2;
} else {
burnEnd = MathUtil.map(thrustLimit, thrust[pos], thrust[pos + 1],
time[pos], time[pos + 1]);
}
// Burn time
burnTime = Math.max(burnEnd - burnStart, 0);
// Total impulse and average thrust
totalImpulse = 0;
averageThrust = 0;
for (pos = 0; pos < time.length - 1; pos++) {
double t0 = time[pos];
double t1 = time[pos + 1];
double f0 = thrust[pos];
double f1 = thrust[pos + 1];
totalImpulse += (t1 - t0) * (f0 + f1) / 2;
if (t0 < burnStart && t1 > burnStart) {
double fStart = MathUtil.map(burnStart, t0, t1, f0, f1);
averageThrust += (fStart + f1) / 2 * (t1 - burnStart);
} else if (t0 >= burnStart && t1 <= burnEnd) {
averageThrust += (f0 + f1) / 2 * (t1 - t0);
} else if (t0 < burnEnd && t1 > burnEnd) {
double fEnd = MathUtil.map(burnEnd, t0, t1, f0, f1);
averageThrust += (f0 + fEnd) / 2 * (burnEnd - t0);
}
}
if (burnTime > 0) {
averageThrust /= burnTime;
} else {
averageThrust = 0;
}
}
////////// Static methods
/**
* Return a String representation of a delay time. If the delay is {@link #PLUGGED},
* returns "P".
*
* @param delay the delay time.
* @return the <code>String</code> representation.
*/
public static String getDelayString(double delay) {
return getDelayString(delay, "P");
}
/**
* Return a String representation of a delay time. If the delay is {@link #PLUGGED},
* <code>plugged</code> is returned.
*
* @param delay the delay time.
* @param plugged the return value if there is no ejection charge.
* @return the String representation.
*/
public static String getDelayString(double delay, String plugged) {
if (delay == PLUGGED)
return plugged;
delay = Math.rint(delay * 10) / 10;
if (MathUtil.equals(delay, Math.rint(delay)))
return "" + ((int) delay);
return "" + delay;
}
//////// Motor instance implementation ////////
private class ThrustCurveMotorInstance implements MotorInstance {
private int position;
// Previous time step value
private double prevTime;
// Average thrust during previous step
private double stepThrust;
// Instantaneous thrust at current time point
private double instThrust;
// Average CG during previous step
private Coordinate stepCG;
// Instantaneous CG at current time point
private Coordinate instCG;
private final double unitRotationalInertia;
private final double unitLongitudinalInertia;
private final Motor parentMotor;
private int modID = 0;
public ThrustCurveMotorInstance() {
log.debug("ThrustCurveMotor: Creating motor instance of " + ThrustCurveMotor.this);
position = 0;
prevTime = 0;
instThrust = 0;
stepThrust = 0;
instCG = cg[0];
stepCG = cg[0];
unitRotationalInertia = Inertia.filledCylinderRotational(getDiameter() / 2);
unitLongitudinalInertia = Inertia.filledCylinderLongitudinal(getDiameter() / 2, getLength());
parentMotor = ThrustCurveMotor.this;
}
@Override
public Motor getParentMotor() {
return parentMotor;
}
@Override
public double getTime() {
return prevTime;
}
@Override
public Coordinate getCG() {
return stepCG;
}
@Override
public double getLongitudinalInertia() {
return unitLongitudinalInertia * stepCG.weight;
}
@Override
public double getRotationalInertia() {
return unitRotationalInertia * stepCG.weight;
}
@Override
public double getThrust() {
return stepThrust;
}
@Override
public boolean isActive() {
return prevTime < time[time.length - 1];
}
@Override
public void step(double nextTime, double acceleration, AtmosphericConditions cond) {
if (!(nextTime >= prevTime)) {
// Also catches NaN
throw new IllegalArgumentException("Stepping backwards in time, current=" +
prevTime + " new=" + nextTime);
}
if (MathUtil.equals(prevTime, nextTime)) {
return;
}
modID++;
if (position >= time.length - 1) {
// Thrust has ended
prevTime = nextTime;
stepThrust = 0;
instThrust = 0;
stepCG = cg[cg.length - 1];
return;
}
// Compute average & instantaneous thrust
if (nextTime < time[position + 1]) {
// Time step between time points
double nextF = MathUtil.map(nextTime, time[position], time[position + 1],
thrust[position], thrust[position + 1]);
stepThrust = (instThrust + nextF) / 2;
instThrust = nextF;
} else {
// Portion of previous step
stepThrust = (instThrust + thrust[position + 1]) / 2 * (time[position + 1] - prevTime);
// Whole steps
position++;
while ((position < time.length - 1) && (nextTime >= time[position + 1])) {
stepThrust += (thrust[position] + thrust[position + 1]) / 2 *
(time[position + 1] - time[position]);
position++;
}
// End step
if (position < time.length - 1) {
instThrust = MathUtil.map(nextTime, time[position], time[position + 1],
thrust[position], thrust[position + 1]);
stepThrust += (thrust[position] + instThrust) / 2 *
(nextTime - time[position]);
} else {
// Thrust ended during this step
instThrust = 0;
}
stepThrust /= (nextTime - prevTime);
}
// Compute average and instantaneous CG (simple average between points)
Coordinate nextCG;
if (position < time.length - 1) {
nextCG = MathUtil.map(nextTime, time[position], time[position + 1],
cg[position], cg[position + 1]);
} else {
nextCG = cg[cg.length - 1];
}
stepCG = instCG.add(nextCG).multiply(0.5);
instCG = nextCG;
// Update time
prevTime = nextTime;
}
@Override
public MotorInstance clone() {
try {
return (MotorInstance) super.clone();
} catch (CloneNotSupportedException e) {
throw new BugException("CloneNotSupportedException", e);
}
}
@Override
public int getModID() {
return modID;
}
}
@Override
public int compareTo(ThrustCurveMotor other) {
int value;
// 1. Manufacturer
value = COLLATOR.compare(this.manufacturer.getDisplayName(),
((ThrustCurveMotor) other).manufacturer.getDisplayName());
if (value != 0)
return value;
// 2. Designation
value = DESIGNATION_COMPARATOR.compare(this.getDesignation(), other.getDesignation());
if (value != 0)
return value;
// 3. Diameter
value = (int) ((this.getDiameter() - other.getDiameter()) * 1000000);
if (value != 0)
return value;
// 4. Length
value = (int) ((this.getLength() - other.getLength()) * 1000000);
return value;
}
}